Skip to content

Commit 0052671

Browse files
committed
fix(analytics): coerce all number params from js double to native integral
previously quantity was handled, and a new PR proposed to handle index as well, but there was a significant list of params that needed JS double -> native integral handling additionally the level end "success" param could never have worked as it was a string in JS level when firebase-js-sdk has it as a boolean, and in native it is also an integral value - so attempting to use a string natively as before would never register in analytics
1 parent fcdf425 commit 0052671

7 files changed

Lines changed: 105 additions & 22 deletions

File tree

packages/analytics/__tests__/analytics.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1390,7 +1390,7 @@ describe('Analytics', function () {
13901390
() =>
13911391
analytics.logLevelEnd({
13921392
level: 12,
1393-
success: 'true',
1393+
success: true,
13941394
}),
13951395
'logLevelEnd',
13961396
);
@@ -1401,7 +1401,7 @@ describe('Analytics', function () {
14011401
() =>
14021402
logLevelEnd(analytics, {
14031403
level: 12,
1404-
success: 'true',
1404+
success: true,
14051405
}),
14061406
'logLevelEnd',
14071407
);

packages/analytics/android/src/reactnative/java/io/invertase/firebase/analytics/ReactNativeFirebaseAnalyticsModule.java

Lines changed: 48 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,27 @@
2626
import com.google.firebase.analytics.FirebaseAnalytics;
2727
import io.invertase.firebase.common.ReactNativeFirebaseModule;
2828
import java.util.ArrayList;
29+
import java.util.Locale;
2930
import javax.annotation.Nullable;
3031

3132
public class ReactNativeFirebaseAnalyticsModule extends ReactNativeFirebaseModule {
3233
private static final String SERVICE_NAME = "Analytics";
34+
35+
/**
36+
* GA4 parameters that must be sent as long values. React Native's bridge stores JS numbers as
37+
* doubles in {@link Bundle}; Firebase Analytics expects integral types for these keys.
38+
*/
39+
private static final String[] LONG_NUMERIC_PARAM_KEYS =
40+
new String[] {
41+
FirebaseAnalytics.Param.QUANTITY,
42+
FirebaseAnalytics.Param.INDEX,
43+
FirebaseAnalytics.Param.LEVEL,
44+
FirebaseAnalytics.Param.NUMBER_OF_NIGHTS,
45+
FirebaseAnalytics.Param.NUMBER_OF_PASSENGERS,
46+
FirebaseAnalytics.Param.NUMBER_OF_ROOMS,
47+
FirebaseAnalytics.Param.SCORE,
48+
};
49+
3350
private final UniversalFirebaseAnalyticsModule module;
3451

3552
ReactNativeFirebaseAnalyticsModule(ReactApplicationContext reactContext) {
@@ -207,14 +224,7 @@ private Bundle toBundle(ReadableMap readableMap) {
207224
for (Object item : itemsArray) {
208225
if (item instanceof Bundle) {
209226
Bundle itemBundle = (Bundle) item;
210-
if (itemBundle.containsKey(FirebaseAnalytics.Param.QUANTITY)) {
211-
double number = itemBundle.getDouble(FirebaseAnalytics.Param.QUANTITY);
212-
itemBundle.putInt(FirebaseAnalytics.Param.QUANTITY, (int) number);
213-
}
214-
if (itemBundle.containsKey(FirebaseAnalytics.Param.INDEX)) {
215-
double number = itemBundle.getDouble(FirebaseAnalytics.Param.INDEX);
216-
itemBundle.putLong(FirebaseAnalytics.Param.INDEX, (long) number);
217-
}
227+
coerceLongNumericParams(itemBundle);
218228
validBundles.add(itemBundle);
219229
}
220230
}
@@ -223,10 +233,40 @@ private Bundle toBundle(ReadableMap readableMap) {
223233
}
224234
}
225235

236+
coerceLongNumericParams(bundle);
237+
coerceSuccessParamToLong(bundle);
238+
226239
if (bundle.containsKey(FirebaseAnalytics.Param.EXTEND_SESSION)) {
227240
double number = bundle.getDouble(FirebaseAnalytics.Param.EXTEND_SESSION);
228241
bundle.putLong(FirebaseAnalytics.Param.EXTEND_SESSION, (long) number);
229242
}
230243
return bundle;
231244
}
245+
246+
private static void coerceLongNumericParams(Bundle bundle) {
247+
for (String key : LONG_NUMERIC_PARAM_KEYS) {
248+
if (bundle.containsKey(key)) {
249+
double number = bundle.getDouble(key);
250+
bundle.putLong(key, (long) number);
251+
}
252+
}
253+
}
254+
255+
private static void coerceSuccessParamToLong(Bundle bundle) {
256+
if (!bundle.containsKey(FirebaseAnalytics.Param.SUCCESS)) {
257+
return;
258+
}
259+
Object value = bundle.get(FirebaseAnalytics.Param.SUCCESS);
260+
bundle.remove(FirebaseAnalytics.Param.SUCCESS);
261+
long asLong = 0L;
262+
if (value instanceof Boolean) {
263+
asLong = (Boolean) value ? 1L : 0L;
264+
} else if (value instanceof Number) {
265+
asLong = ((Number) value).longValue() != 0L ? 1L : 0L;
266+
} else if (value instanceof String) {
267+
String s = ((String) value).trim().toLowerCase(Locale.ROOT);
268+
asLong = ("1".equals(s) || "true".equals(s) || "yes".equals(s)) ? 1L : 0L;
269+
}
270+
bundle.putLong(FirebaseAnalytics.Param.SUCCESS, asLong);
271+
}
232272
}

packages/analytics/e2e/analytics.e2e.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,7 @@ describe('analytics()', function () {
264264
it('calls logLevelEnd', async function () {
265265
await firebase.analytics().logLevelEnd({
266266
level: 123,
267-
success: 'yes',
267+
success: true,
268268
});
269269
});
270270
});
@@ -753,7 +753,7 @@ describe('analytics()', function () {
753753
const { getAnalytics, logLevelEnd } = analyticsModular;
754754
await logLevelEnd(getAnalytics(), {
755755
level: 123,
756-
success: 'yes',
756+
success: true,
757757
});
758758
});
759759
});

packages/analytics/ios/RNFBAnalytics/RNFBAnalyticsModule.m

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,24 @@
3232
#import <RNFBApp/RNFBSharedUtils.h>
3333
#import "RNFBAnalyticsModule.h"
3434

35+
/** GA4 parameters that must be sent as integer NSNumber values (not doubles from JS). */
36+
static NSArray<NSString *> *RNFBAnalyticsLongNumericParameterKeys(void) {
37+
static NSArray<NSString *> *keys;
38+
static dispatch_once_t onceToken;
39+
dispatch_once(&onceToken, ^{
40+
keys = @[
41+
kFIRParameterQuantity,
42+
kFIRParameterIndex,
43+
kFIRParameterLevel,
44+
kFIRParameterNumberOfNights,
45+
kFIRParameterNumberOfPassengers,
46+
kFIRParameterNumberOfRooms,
47+
kFIRParameterScore,
48+
];
49+
});
50+
return keys;
51+
}
52+
3553
@implementation RNFBAnalyticsModule
3654
#pragma mark -
3755
#pragma mark Module Setup
@@ -268,23 +286,47 @@ - (NSDictionary *)cleanJavascriptParams:(NSDictionary *)params {
268286
[(NSArray *)newParams[kFIRParameterItems]
269287
enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL *_Nonnull stop) {
270288
NSMutableDictionary *item = [obj mutableCopy];
271-
if (item[kFIRParameterQuantity]) {
272-
item[kFIRParameterQuantity] = @([item[kFIRParameterQuantity] integerValue]);
273-
}
274-
if (item[kFIRParameterIndex]) {
275-
item[kFIRParameterIndex] = @([item[kFIRParameterIndex] integerValue]);
276-
}
289+
[self rnfb_coerceLongNumericParametersInMutableDictionary:item];
277290
[newItems addObject:[item copy]];
278291
}];
279292
newParams[kFIRParameterItems] = [newItems copy];
280293
}
294+
[self rnfb_coerceLongNumericParametersInMutableDictionary:newParams];
295+
[self rnfb_coerceSuccessParameterInMutableDictionary:newParams];
281296
NSNumber *extendSession = [newParams valueForKey:kFIRParameterExtendSession];
282297
if ([extendSession isEqualToNumber:@1]) {
283298
newParams[kFIRParameterExtendSession] = @YES;
284299
}
285300
return [newParams copy];
286301
}
287302

303+
- (void)rnfb_coerceLongNumericParametersInMutableDictionary:(NSMutableDictionary *)dict {
304+
for (NSString *key in RNFBAnalyticsLongNumericParameterKeys()) {
305+
id value = dict[key];
306+
if (value != nil && value != [NSNull null]) {
307+
dict[key] = @([value integerValue]);
308+
}
309+
}
310+
}
311+
312+
- (void)rnfb_coerceSuccessParameterInMutableDictionary:(NSMutableDictionary *)dict {
313+
id value = dict[kFIRParameterSuccess];
314+
if (value == nil || value == [NSNull null]) {
315+
return;
316+
}
317+
int success = 0;
318+
if ([value isKindOfClass:[NSString class]]) {
319+
NSString *lower = [(NSString *)value lowercaseString];
320+
if ([lower isEqualToString:@"true"] || [lower isEqualToString:@"yes"] ||
321+
[lower isEqualToString:@"1"]) {
322+
success = 1;
323+
}
324+
} else {
325+
success = [value boolValue] ? 1 : 0;
326+
}
327+
dict[kFIRParameterSuccess] = @(success);
328+
}
329+
288330
/// Converts null values received over the bridge from NSNull to nil
289331
/// @param value Nullable string value
290332
- (NSString *)convertNSNullToNil:(NSString *)value {

packages/analytics/lib/structs.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { object, string, number, array, optional, define, type } from 'superstruct';
17+
import { object, string, number, boolean, array, optional, define, type } from 'superstruct';
1818

1919
const ShortDate = define(
2020
'ShortDate',
@@ -36,6 +36,7 @@ const Item = type({
3636
item_variant: optional(string()),
3737
quantity: optional(number()),
3838
price: optional(number()),
39+
index: optional(number()),
3940
});
4041

4142
export const ScreenView = type({
@@ -104,7 +105,7 @@ export const JoinGroup = object({
104105

105106
export const LevelEnd = object({
106107
level: number(),
107-
success: optional(string()),
108+
success: optional(boolean()),
108109
});
109110

110111
export const LevelStart = object({

packages/analytics/lib/types/analytics.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -282,7 +282,7 @@ export interface LevelEndEventParameters {
282282
/**
283283
* The result of an operation.
284284
*/
285-
success?: string;
285+
success?: boolean;
286286
}
287287

288288
export interface LevelStartEventParameters {

packages/analytics/type-test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ const earnVirtualCurrencyParams: EarnVirtualCurrencyEventParameters = {
189189
};
190190
const generateLeadParams: GenerateLeadEventParameters = { value: 123, currency: 'USD' };
191191
const joinGroupParams: JoinGroupEventParameters = { group_id: 'group1' };
192-
const levelEndParams: LevelEndEventParameters = { level: 1, success: 'true' };
192+
const levelEndParams: LevelEndEventParameters = { level: 1, success: true };
193193
const levelStartParams: LevelStartEventParameters = { level: 1 };
194194
const levelUpParams: LevelUpEventParameters = { level: 5, character: 'character1' };
195195
const loginParams: LoginEventParameters = { method: 'email' };

0 commit comments

Comments
 (0)