Skip to content

Commit d738ebb

Browse files
committed
Fix API signature inconsistencies and code quality issues for web SDK parity
- Make signature() chainId optional to match web SDK (chain-agnostic signatures) - Add function_name and function_args to transaction() for contract call tracking - Add optional category parameter to screen() matching web page() API - Consolidate FormoAnalyticsProviderProps into single complete type - Replace require() with static import in setTrafficSourceFromUrl - Add errorHandler option to Options for global error handling - Add ReferralOptions for configurable referral query parameter parsing - Update tests for new optional chainId behavior https://claude.ai/code/session_0117WwRELH1nzVbxRj7Kmm5N
1 parent b8f3be5 commit d738ebb

8 files changed

Lines changed: 106 additions & 51 deletions

File tree

src/FormoAnalytics.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import {
3535
TransactionStatus,
3636
} from "./types";
3737
import { toChecksumAddress, getValidAddress } from "./utils";
38+
import { parseTrafficSource, storeTrafficSource } from "./utils/trafficSource";
3839

3940
export class FormoAnalytics implements IFormoAnalytics {
4041
private session: FormoAnalyticsSession;
@@ -138,14 +139,15 @@ export class FormoAnalytics implements IFormoAnalytics {
138139
*/
139140
public async screen(
140141
name: string,
142+
category?: string,
141143
properties?: IFormoEventProperties,
142144
context?: IFormoEventContext,
143145
callback?: (...args: unknown[]) => void
144146
): Promise<void> {
145147
// Note: shouldTrack() is called in trackEvent() - no need to check here
146148
await this.trackEvent(
147149
EventType.SCREEN,
148-
{ name },
150+
{ name, ...(category && { category }) },
149151
properties,
150152
context,
151153
callback
@@ -175,8 +177,7 @@ export class FormoAnalytics implements IFormoAnalytics {
175177
* ```
176178
*/
177179
public setTrafficSourceFromUrl(url: string): void {
178-
const { parseTrafficSource, storeTrafficSource } = require("./utils/trafficSource");
179-
const trafficSource = parseTrafficSource(url);
180+
const trafficSource = parseTrafficSource(url, this.options.referral?.queryParams);
180181
storeTrafficSource(trafficSource);
181182
logger.debug("Traffic source set from URL:", trafficSource);
182183
}
@@ -325,7 +326,7 @@ export class FormoAnalytics implements IFormoAnalytics {
325326
signatureHash,
326327
}: {
327328
status: SignatureStatus;
328-
chainId: ChainID;
329+
chainId?: ChainID;
329330
address: Address;
330331
message: string;
331332
signatureHash?: string;
@@ -334,10 +335,6 @@ export class FormoAnalytics implements IFormoAnalytics {
334335
context?: IFormoEventContext,
335336
callback?: (...args: unknown[]) => void
336337
): Promise<void> {
337-
if (chainId === null || chainId === undefined || Number(chainId) === 0) {
338-
logger.warn("Signature: Chain ID cannot be null, undefined, or 0");
339-
return;
340-
}
341338
if (!address) {
342339
logger.warn("Signature: Address cannot be empty");
343340
return;
@@ -346,7 +343,7 @@ export class FormoAnalytics implements IFormoAnalytics {
346343
EventType.SIGNATURE,
347344
{
348345
status,
349-
chainId,
346+
...(chainId !== undefined && chainId !== null && { chainId }),
350347
address,
351348
message,
352349
...(signatureHash && { signatureHash }),
@@ -369,6 +366,8 @@ export class FormoAnalytics implements IFormoAnalytics {
369366
to,
370367
value,
371368
transactionHash,
369+
function_name,
370+
function_args,
372371
}: {
373372
status: TransactionStatus;
374373
chainId: ChainID;
@@ -377,6 +376,8 @@ export class FormoAnalytics implements IFormoAnalytics {
377376
to?: string;
378377
value?: string;
379378
transactionHash?: string;
379+
function_name?: string;
380+
function_args?: Record<string, unknown>;
380381
},
381382
properties?: IFormoEventProperties,
382383
context?: IFormoEventContext,
@@ -400,6 +401,8 @@ export class FormoAnalytics implements IFormoAnalytics {
400401
to,
401402
value,
402403
...(transactionHash && { transactionHash }),
404+
...(function_name && { function_name }),
405+
...(function_args && { function_args }),
403406
},
404407
properties,
405408
context,
@@ -596,6 +599,9 @@ export class FormoAnalytics implements IFormoAnalytics {
596599
);
597600
} catch (error) {
598601
logger.error("Error tracking event:", error);
602+
if (this.options.errorHandler) {
603+
this.options.errorHandler(error instanceof Error ? error : new Error(String(error)));
604+
}
599605
}
600606
}
601607

src/FormoAnalyticsProvider.tsx

Lines changed: 3 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import React, {
88
FC,
99
} from "react";
1010
import { FormoAnalytics } from "./FormoAnalytics";
11-
import { initStorageManager, AsyncStorageInterface } from "./lib/storage";
11+
import { initStorageManager } from "./lib/storage";
1212
import { logger } from "./lib/logger";
1313
import { FormoAnalyticsProviderProps, IFormoAnalytics } from "./types";
1414

@@ -34,25 +34,6 @@ const defaultContext: IFormoAnalytics = {
3434
export const FormoAnalyticsContext =
3535
createContext<IFormoAnalytics>(defaultContext);
3636

37-
export interface FormoAnalyticsProviderPropsWithStorage
38-
extends FormoAnalyticsProviderProps {
39-
/**
40-
* AsyncStorage instance from @react-native-async-storage/async-storage
41-
* Required for persistent storage
42-
*/
43-
asyncStorage?: AsyncStorageInterface;
44-
/**
45-
* Callback when SDK is ready
46-
* Note: Use useCallback to avoid re-initialization on every render
47-
*/
48-
onReady?: (sdk: IFormoAnalytics) => void;
49-
/**
50-
* Callback when SDK initialization fails
51-
* Note: Use useCallback to avoid re-initialization on every render
52-
*/
53-
onError?: (error: Error) => void;
54-
}
55-
5637
/**
5738
* Formo Analytics Provider for React Native
5839
*
@@ -76,7 +57,7 @@ export interface FormoAnalyticsProviderPropsWithStorage
7657
* }
7758
* ```
7859
*/
79-
export const FormoAnalyticsProvider: FC<FormoAnalyticsProviderPropsWithStorage> = (
60+
export const FormoAnalyticsProvider: FC<FormoAnalyticsProviderProps> = (
8061
props
8162
) => {
8263
const { writeKey, disabled = false, children } = props;
@@ -102,7 +83,7 @@ export const FormoAnalyticsProvider: FC<FormoAnalyticsProviderPropsWithStorage>
10283
return <InitializedAnalytics {...props} />;
10384
};
10485

105-
const InitializedAnalytics: FC<FormoAnalyticsProviderPropsWithStorage> = ({
86+
const InitializedAnalytics: FC<FormoAnalyticsProviderProps> = ({
10687
writeKey,
10788
options,
10889
asyncStorage,

src/__tests__/FormoAnalytics.test.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -260,15 +260,25 @@ describe('FormoAnalytics', () => {
260260
});
261261

262262
describe('signature()', () => {
263-
it('should not track if chainId is invalid', async () => {
263+
it('should track signature with chainId 0 (chainId is optional)', async () => {
264264
await analytics.signature({
265265
status: SignatureStatus.REQUESTED,
266266
chainId: 0,
267267
address: '0x742d35cc6634c0532925a3b844bc9e7595f3f6d2',
268268
message: 'test message',
269269
});
270270

271-
expect(mockEventManager.addEvent).not.toHaveBeenCalled();
271+
expect(mockEventManager.addEvent).toHaveBeenCalled();
272+
});
273+
274+
it('should track signature without chainId', async () => {
275+
await analytics.signature({
276+
status: SignatureStatus.REQUESTED,
277+
address: '0x742d35cc6634c0532925a3b844bc9e7595f3f6d2',
278+
message: 'test message',
279+
});
280+
281+
expect(mockEventManager.addEvent).toHaveBeenCalled();
272282
});
273283

274284
it('should not track if address is empty', async () => {

src/lib/event/EventFactory.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -367,10 +367,11 @@ class EventFactory implements IEventFactory {
367367
*/
368368
async generateScreenEvent(
369369
name: string,
370+
category?: string,
370371
properties?: IFormoEventProperties,
371372
context?: IFormoEventContext
372373
): Promise<IFormoEvent> {
373-
const props = { ...(properties ?? {}), name };
374+
const props = { ...(properties ?? {}), name, ...(category && { category }) };
374375

375376
const screenEvent: Partial<IFormoEvent> = {
376377
properties: props,
@@ -476,7 +477,7 @@ class EventFactory implements IEventFactory {
476477

477478
async generateSignatureEvent(
478479
status: SignatureStatus,
479-
chainId: ChainID,
480+
chainId: ChainID | undefined,
480481
address: Address,
481482
message: string,
482483
signatureHash?: string,
@@ -486,7 +487,7 @@ class EventFactory implements IEventFactory {
486487
const signatureEvent: Partial<IFormoEvent> = {
487488
properties: {
488489
status,
489-
chainId,
490+
...(chainId !== undefined && chainId !== null && { chainId }),
490491
message,
491492
...(signatureHash && { signatureHash }),
492493
...properties,
@@ -502,10 +503,12 @@ class EventFactory implements IEventFactory {
502503
status: TransactionStatus,
503504
chainId: ChainID,
504505
address: Address,
505-
data: string,
506-
to: string,
507-
value: string,
506+
data?: string,
507+
to?: string,
508+
value?: string,
508509
transactionHash?: string,
510+
function_name?: string,
511+
function_args?: Record<string, unknown>,
509512
properties?: IFormoEventProperties,
510513
context?: IFormoEventContext
511514
): Promise<IFormoEvent> {
@@ -517,6 +520,8 @@ class EventFactory implements IEventFactory {
517520
to,
518521
value,
519522
...(transactionHash && { transactionHash }),
523+
...(function_name && { function_name }),
524+
...(function_args && { function_args }),
520525
...properties,
521526
},
522527
address,
@@ -569,6 +574,7 @@ class EventFactory implements IEventFactory {
569574
case "screen":
570575
formoEvent = await this.generateScreenEvent(
571576
event.name,
577+
event.category,
572578
event.properties,
573579
event.context
574580
);
@@ -635,6 +641,8 @@ class EventFactory implements IEventFactory {
635641
event.to,
636642
event.value,
637643
event.transactionHash,
644+
event.function_name,
645+
event.function_args,
638646
event.properties,
639647
event.context
640648
);

src/lib/event/types.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export interface IEventFactory {
1919

2020
generateScreenEvent(
2121
name: string,
22+
category?: string,
2223
properties?: IFormoEventProperties,
2324
context?: IFormoEventContext
2425
): Promise<IFormoEvent>;
@@ -62,7 +63,7 @@ export interface IEventFactory {
6263

6364
generateSignatureEvent(
6465
status: SignatureStatus,
65-
chainId: ChainID,
66+
chainId: ChainID | undefined,
6667
address: Address,
6768
message: string,
6869
signatureHash?: string,
@@ -74,10 +75,12 @@ export interface IEventFactory {
7475
status: TransactionStatus,
7576
chainId: ChainID,
7677
address: Address,
77-
data: string,
78-
to: string,
79-
value: string,
78+
data?: string,
79+
to?: string,
80+
value?: string,
8081
transactionHash?: string,
82+
function_name?: string,
83+
function_args?: Record<string, unknown>,
8184
properties?: IFormoEventProperties,
8285
context?: IFormoEventContext
8386
): Promise<IFormoEvent>;

src/types/base.ts

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export type ValidInputTypes = Uint8Array | bigint | string | number | boolean;
1919
export interface IFormoAnalytics {
2020
screen(
2121
name: string,
22+
category?: string,
2223
properties?: IFormoEventProperties,
2324
context?: IFormoEventContext,
2425
callback?: (...args: unknown[]) => void
@@ -52,7 +53,7 @@ export interface IFormoAnalytics {
5253
signature(
5354
params: {
5455
status: SignatureStatus;
55-
chainId: ChainID;
56+
chainId?: ChainID;
5657
address: Address;
5758
message: string;
5859
signatureHash?: string;
@@ -70,6 +71,8 @@ export interface IFormoAnalytics {
7071
to?: string;
7172
value?: string;
7273
transactionHash?: string;
74+
function_name?: string;
75+
function_args?: Record<string, unknown>;
7376
},
7477
properties?: IFormoEventProperties,
7578
context?: IFormoEventContext,
@@ -193,6 +196,21 @@ export interface AppInfo {
193196
bundleId?: string;
194197
}
195198

199+
/**
200+
* Configuration options for custom referral query parameter parsing
201+
*/
202+
export interface ReferralOptions {
203+
/**
204+
* Custom query parameter names to check for referral codes
205+
* These are checked in addition to the defaults: ref, referral, refcode, referrer_code
206+
*/
207+
queryParams?: string[];
208+
/**
209+
* Path pattern for extracting referral codes from URL paths
210+
*/
211+
pathPattern?: string;
212+
}
213+
196214
export interface Options {
197215
tracking?: boolean | TrackingOptions;
198216
/**
@@ -226,12 +244,35 @@ export interface Options {
226244
* App information for context enrichment
227245
*/
228246
app?: AppInfo;
247+
/**
248+
* Custom referral query parameter configuration
249+
*/
250+
referral?: ReferralOptions;
251+
/**
252+
* Global error handler for SDK errors
253+
*/
254+
errorHandler?: (err: Error) => void;
229255
ready?: (formo: IFormoAnalytics) => void;
230256
}
231257

232258
export interface FormoAnalyticsProviderProps {
233259
writeKey: string;
234260
options?: Options;
235261
disabled?: boolean;
262+
/**
263+
* AsyncStorage instance from @react-native-async-storage/async-storage
264+
* Required for persistent storage
265+
*/
266+
asyncStorage?: import("../lib/storage").AsyncStorageInterface;
267+
/**
268+
* Callback when SDK is ready
269+
* Note: Use useCallback to avoid re-initialization on every render
270+
*/
271+
onReady?: (sdk: IFormoAnalytics) => void;
272+
/**
273+
* Callback when SDK initialization fails
274+
* Note: Use useCallback to avoid re-initialization on every render
275+
*/
276+
onError?: (error: Error) => void;
236277
children: ReactNode;
237278
}

0 commit comments

Comments
 (0)