@frak-labs/core-sdk 1.0.0-beta.0cd79998 → 1.0.0-beta.10dada3f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/cdn/bundle.js +3 -3
- package/dist/actions-Bsl5ub7_.js +1 -0
- package/dist/actions-C_B0fn1P.cjs +1 -0
- package/dist/actions.cjs +1 -1
- package/dist/actions.d.cts +2 -2
- package/dist/actions.d.ts +2 -2
- package/dist/actions.js +1 -1
- package/dist/bundle.cjs +1 -1
- package/dist/bundle.d.cts +4 -4
- package/dist/bundle.d.ts +4 -4
- package/dist/bundle.js +1 -1
- package/dist/{index-BCwGNRmk.d.cts → index-BSGP3dbi.d.ts} +113 -22
- package/dist/{index-DZuYiI2M.d.ts → index-Bh0TuKYS.d.ts} +1 -1
- package/dist/{index-CVnwk1E_.d.cts → index-DW8xes2o.d.cts} +1 -1
- package/dist/{index-BfmJnxzo.d.ts → index-TRJNS6B5.d.cts} +113 -22
- package/dist/index.cjs +1 -1
- package/dist/index.d.cts +3 -3
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/{openSso-BQ-q-_Y9.d.ts → openSso-Bvhy_urG.d.cts} +24 -6
- package/dist/{openSso-CMBCbhvP.d.cts → openSso-D2kTUv0-.d.ts} +24 -6
- package/dist/sdkConfigStore-B6CkorsU.cjs +1 -0
- package/dist/sdkConfigStore-Dx0oAVEO.js +1 -0
- package/dist/src-CLF8o8WB.cjs +13 -0
- package/dist/src-al3X6r-n.js +13 -0
- package/package.json +2 -2
- package/src/actions/referral/processReferral.test.ts +73 -8
- package/src/actions/referral/processReferral.ts +15 -12
- package/src/clients/createIFrameFrakClient.ts +72 -2
- package/src/index.ts +4 -1
- package/src/types/context.ts +16 -4
- package/src/types/lifecycle/client.ts +7 -0
- package/src/utils/FrakContext.test.ts +41 -7
- package/src/utils/FrakContext.ts +24 -15
- package/src/utils/analytics/events/component.ts +58 -0
- package/src/utils/analytics/events/index.ts +20 -0
- package/src/utils/analytics/events/lifecycle.ts +26 -0
- package/src/utils/analytics/events/referral.ts +11 -0
- package/src/utils/analytics/index.ts +8 -0
- package/src/utils/{trackEvent.test.ts → analytics/trackEvent.test.ts} +22 -30
- package/src/utils/analytics/trackEvent.ts +34 -0
- package/src/utils/index.ts +1 -1
- package/dist/actions-BMTVobuH.js +0 -1
- package/dist/actions-ukNCM0d7.cjs +0 -1
- package/dist/src-Cx0RZEA3.js +0 -13
- package/dist/src-DmYZ4ZLk.cjs +0 -13
- package/dist/trackEvent-B5xo_5K3.cjs +0 -1
- package/dist/trackEvent-DdykyX0U.js +0 -1
- package/src/utils/trackEvent.ts +0 -41
|
@@ -54,15 +54,15 @@ function trackArrivalIfValid(
|
|
|
54
54
|
|
|
55
55
|
if (isV2Context(frakContext)) {
|
|
56
56
|
trackEvent(client, "user_referred_started", {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
},
|
|
57
|
+
referrerClientId: frakContext.c,
|
|
58
|
+
referrerWallet: frakContext.w,
|
|
59
|
+
walletStatus: walletStatus?.key,
|
|
61
60
|
});
|
|
62
61
|
sendInteraction(client, {
|
|
63
62
|
type: "arrival",
|
|
64
63
|
referrerClientId: frakContext.c,
|
|
65
64
|
referrerMerchantId: frakContext.m,
|
|
65
|
+
referrerWallet: frakContext.w,
|
|
66
66
|
referralTimestamp: frakContext.t,
|
|
67
67
|
landingUrl,
|
|
68
68
|
});
|
|
@@ -71,10 +71,8 @@ function trackArrivalIfValid(
|
|
|
71
71
|
|
|
72
72
|
if (isV1Context(frakContext)) {
|
|
73
73
|
trackEvent(client, "user_referred_started", {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
walletStatus: walletStatus?.key,
|
|
77
|
-
},
|
|
74
|
+
referrer: frakContext.r,
|
|
75
|
+
walletStatus: walletStatus?.key,
|
|
78
76
|
});
|
|
79
77
|
sendInteraction(client, {
|
|
80
78
|
type: "arrival",
|
|
@@ -111,7 +109,14 @@ function isSelfReferral(
|
|
|
111
109
|
walletStatus?: WalletStatusReturnType
|
|
112
110
|
): boolean {
|
|
113
111
|
if (isV2Context(frakContext)) {
|
|
114
|
-
|
|
112
|
+
// Wallet match takes precedence — it's the strongest signal we have.
|
|
113
|
+
if (frakContext.w && walletStatus?.wallet) {
|
|
114
|
+
return isAddressEqual(frakContext.w, walletStatus.wallet);
|
|
115
|
+
}
|
|
116
|
+
if (frakContext.c) {
|
|
117
|
+
return getClientId() === frakContext.c;
|
|
118
|
+
}
|
|
119
|
+
return false;
|
|
115
120
|
}
|
|
116
121
|
if (isV1Context(frakContext) && walletStatus?.wallet) {
|
|
117
122
|
return isAddressEqual(frakContext.r, walletStatus.wallet);
|
|
@@ -177,9 +182,7 @@ export function processReferral(
|
|
|
177
182
|
});
|
|
178
183
|
|
|
179
184
|
trackEvent(client, "user_referred_completed", {
|
|
180
|
-
|
|
181
|
-
status: "success",
|
|
182
|
-
},
|
|
185
|
+
status: "success",
|
|
183
186
|
});
|
|
184
187
|
|
|
185
188
|
return "success";
|
|
@@ -80,6 +80,10 @@ export function createIFrameFrakClient({
|
|
|
80
80
|
// Resolved after first resolved-config is sent to iframe (prevents RPC before context exists)
|
|
81
81
|
const contextSent = new Deferred<void>();
|
|
82
82
|
|
|
83
|
+
// Handshake timing: measured from client creation until the iframe
|
|
84
|
+
// lifecycle manager resolves the `isConnected` promise.
|
|
85
|
+
const handshakeStartedAt = Date.now();
|
|
86
|
+
|
|
83
87
|
// Create our debug info gatherer
|
|
84
88
|
const debugInfo = new DebugInfoGatherer(config, iframe);
|
|
85
89
|
|
|
@@ -180,6 +184,38 @@ export function createIFrameFrakClient({
|
|
|
180
184
|
userAnonymousClientId: getClientId(),
|
|
181
185
|
});
|
|
182
186
|
openPanel.init();
|
|
187
|
+
openPanel.track("sdk_initialized", {
|
|
188
|
+
sdkVersion: process.env.SDK_VERSION,
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// Race the connection against the heartbeat timeout so we can
|
|
192
|
+
// distinguish "connected" from "timeout" cleanly without touching
|
|
193
|
+
// the heartbeat plumbing. 30s matches `HEARTBEAT_TIMEOUT`.
|
|
194
|
+
let settled = false;
|
|
195
|
+
const timeoutHandle = setTimeout(() => {
|
|
196
|
+
if (settled) return;
|
|
197
|
+
settled = true;
|
|
198
|
+
openPanel?.track("sdk_iframe_handshake_failed", {
|
|
199
|
+
reason: "timeout",
|
|
200
|
+
});
|
|
201
|
+
}, 30_000);
|
|
202
|
+
lifecycleManager.isConnected
|
|
203
|
+
.then(() => {
|
|
204
|
+
if (settled) return;
|
|
205
|
+
settled = true;
|
|
206
|
+
clearTimeout(timeoutHandle);
|
|
207
|
+
openPanel?.track("sdk_iframe_connected", {
|
|
208
|
+
handshake_duration_ms: Date.now() - handshakeStartedAt,
|
|
209
|
+
});
|
|
210
|
+
})
|
|
211
|
+
.catch(() => {
|
|
212
|
+
if (settled) return;
|
|
213
|
+
settled = true;
|
|
214
|
+
clearTimeout(timeoutHandle);
|
|
215
|
+
openPanel?.track("sdk_iframe_handshake_failed", {
|
|
216
|
+
reason: "unknown",
|
|
217
|
+
});
|
|
218
|
+
});
|
|
183
219
|
}
|
|
184
220
|
|
|
185
221
|
// Perform the post connection setup
|
|
@@ -189,6 +225,7 @@ export function createIFrameFrakClient({
|
|
|
189
225
|
lifecycleManager,
|
|
190
226
|
configPromise,
|
|
191
227
|
contextSent,
|
|
228
|
+
openPanel,
|
|
192
229
|
})
|
|
193
230
|
.then(() => debugInfo.updateSetupStatus(true))
|
|
194
231
|
.catch((err) => {
|
|
@@ -273,12 +310,14 @@ async function postConnectionSetup({
|
|
|
273
310
|
lifecycleManager,
|
|
274
311
|
configPromise,
|
|
275
312
|
contextSent,
|
|
313
|
+
openPanel,
|
|
276
314
|
}: {
|
|
277
315
|
config: FrakWalletSdkConfig;
|
|
278
316
|
rpcClient: SdkRpcClient;
|
|
279
317
|
lifecycleManager: IframeLifecycleManager;
|
|
280
318
|
configPromise: Promise<MerchantConfigResult> | undefined;
|
|
281
319
|
contextSent: Deferred<void>;
|
|
320
|
+
openPanel: OpenPanel | undefined;
|
|
282
321
|
}): Promise<void> {
|
|
283
322
|
await lifecycleManager.isConnected;
|
|
284
323
|
|
|
@@ -342,7 +381,11 @@ async function postConnectionSetup({
|
|
|
342
381
|
);
|
|
343
382
|
};
|
|
344
383
|
|
|
345
|
-
// Send the resolved-config lifecycle event to the iframe
|
|
384
|
+
// Send the resolved-config lifecycle event to the iframe.
|
|
385
|
+
// This is where we also update SDK-side OpenPanel global props with
|
|
386
|
+
// `merchantId` + `domain` (first time they are known) so every
|
|
387
|
+
// subsequent SDK event is merchant-attributed. We pass
|
|
388
|
+
// `sdkAnonymousId` through so the listener can join SDK funnels.
|
|
346
389
|
let mergeTokenConsumed = false;
|
|
347
390
|
const sendLifecycleConfig = (resolved: SdkResolvedConfig) => {
|
|
348
391
|
const token = mergeTokenConsumed ? undefined : pendingMergeToken;
|
|
@@ -365,6 +408,17 @@ async function postConnectionSetup({
|
|
|
365
408
|
? { attribution: resolved.attribution }
|
|
366
409
|
: undefined;
|
|
367
410
|
|
|
411
|
+
const sdkAnonymousId = getClientId();
|
|
412
|
+
|
|
413
|
+
if (openPanel) {
|
|
414
|
+
const current = openPanel.global ?? {};
|
|
415
|
+
openPanel.setGlobalProperties({
|
|
416
|
+
...current,
|
|
417
|
+
merchantId: resolved.merchantId,
|
|
418
|
+
domain: resolved.domain ?? "",
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
368
422
|
rpcClient.sendLifecycle({
|
|
369
423
|
clientLifecycle: "resolved-config",
|
|
370
424
|
data: {
|
|
@@ -372,6 +426,7 @@ async function postConnectionSetup({
|
|
|
372
426
|
domain: resolved.domain ?? "",
|
|
373
427
|
allowedDomains: resolved.allowedDomains ?? [],
|
|
374
428
|
sourceUrl: window.location.href,
|
|
429
|
+
...(sdkAnonymousId && { sdkAnonymousId }),
|
|
375
430
|
...(token && { pendingMergeToken: token }),
|
|
376
431
|
...(sdkConfig && { sdkConfig }),
|
|
377
432
|
},
|
|
@@ -423,5 +478,20 @@ async function postConnectionSetup({
|
|
|
423
478
|
});
|
|
424
479
|
}
|
|
425
480
|
|
|
426
|
-
|
|
481
|
+
// Inspect each setup result — a failed CSS/i18n/backup push leaves the
|
|
482
|
+
// partner UI in a broken-but-connected state (iframe reports
|
|
483
|
+
// `sdk_iframe_connected`, user sees no modal styles / wrong locale).
|
|
484
|
+
// Surface it as a distinct handshake reason so dashboards can
|
|
485
|
+
// distinguish timeout vs. asset-push failures.
|
|
486
|
+
const results = await Promise.allSettled([
|
|
487
|
+
pushCss(),
|
|
488
|
+
pushI18n(),
|
|
489
|
+
pushBackup(),
|
|
490
|
+
]);
|
|
491
|
+
const hasFailedAssetPush = results.some((r) => r.status === "rejected");
|
|
492
|
+
if (hasFailedAssetPush) {
|
|
493
|
+
openPanel?.track("sdk_iframe_handshake_failed", {
|
|
494
|
+
reason: "asset_push",
|
|
495
|
+
});
|
|
496
|
+
}
|
|
427
497
|
}
|
package/src/index.ts
CHANGED
|
@@ -102,7 +102,6 @@ export {
|
|
|
102
102
|
type DeepLinkFallbackOptions,
|
|
103
103
|
decompressJsonFromB64,
|
|
104
104
|
FrakContextManager,
|
|
105
|
-
type FrakEvent,
|
|
106
105
|
type FullSsoParams,
|
|
107
106
|
findIframeInOpener,
|
|
108
107
|
formatAmount,
|
|
@@ -126,4 +125,8 @@ export {
|
|
|
126
125
|
triggerDeepLinkWithFallback,
|
|
127
126
|
withCache,
|
|
128
127
|
} from "./utils";
|
|
128
|
+
export type {
|
|
129
|
+
SdkEventMap,
|
|
130
|
+
SdkHandshakeFailureReason,
|
|
131
|
+
} from "./utils/analytics";
|
|
129
132
|
export { computeLegacyProductId } from "./utils/computeLegacyProductId";
|
package/src/types/context.ts
CHANGED
|
@@ -11,19 +11,31 @@ export type FrakContextV1 = {
|
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* V2 Frak Context — anonymous-first referral context.
|
|
15
|
-
*
|
|
14
|
+
* V2 Frak Context — anonymous-first referral context with optional wallet.
|
|
15
|
+
*
|
|
16
|
+
* Carries merchant context (`m`) and creation timestamp (`t`) unconditionally.
|
|
17
|
+
* Identifies the sharer via either the anonymous clientId (`c`) or, when the
|
|
18
|
+
* sharer is authenticated, the stronger wallet identifier (`w`). A valid V2
|
|
19
|
+
* context MUST contain at least one of `c` or `w`; both may be present when
|
|
20
|
+
* a logged-in user shares a link (best attribution signal).
|
|
21
|
+
*
|
|
22
|
+
* `w` takes precedence as the source of truth because the wallet is bound to
|
|
23
|
+
* the user's WebAuthn credential, survives localStorage clears, and is global
|
|
24
|
+
* across merchants — unlike `c`, which is a per-browser UUID.
|
|
25
|
+
*
|
|
16
26
|
* @ignore
|
|
17
27
|
*/
|
|
18
28
|
export type FrakContextV2 = {
|
|
19
29
|
/** Version discriminator */
|
|
20
30
|
v: 2;
|
|
21
|
-
/** Sharer's anonymous clientId (UUID from localStorage) */
|
|
22
|
-
c: string;
|
|
23
31
|
/** Merchant ID (UUID) */
|
|
24
32
|
m: string;
|
|
25
33
|
/** Link creation timestamp (epoch seconds) */
|
|
26
34
|
t: number;
|
|
35
|
+
/** Sharer's anonymous clientId (UUID from localStorage). Optional when `w` is provided. */
|
|
36
|
+
c?: string;
|
|
37
|
+
/** Sharer's wallet address. Preferred source of truth when the sharer is authenticated. Optional when `c` is provided. */
|
|
38
|
+
w?: Address;
|
|
27
39
|
};
|
|
28
40
|
|
|
29
41
|
/**
|
|
@@ -59,6 +59,13 @@ type ResolvedConfigEvent = {
|
|
|
59
59
|
* When present, listener should execute identity merge in background.
|
|
60
60
|
*/
|
|
61
61
|
pendingMergeToken?: string;
|
|
62
|
+
/**
|
|
63
|
+
* Persistent per-origin anonymous id generated on the partner site
|
|
64
|
+
* (SDK-side localStorage). Propagated here so the listener can
|
|
65
|
+
* set it as an OpenPanel global property and stitch SDK events
|
|
66
|
+
* with listener events in the same funnel.
|
|
67
|
+
*/
|
|
68
|
+
sdkAnonymousId?: string;
|
|
62
69
|
sdkConfig?: ResolvedSdkConfig;
|
|
63
70
|
};
|
|
64
71
|
};
|
|
@@ -42,7 +42,7 @@ describe("FrakContextManager", () => {
|
|
|
42
42
|
expect(result).not.toMatch(/[+/=]/);
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
-
it("should return undefined when v2 context
|
|
45
|
+
it("should return undefined when v2 context has neither clientId nor wallet", () => {
|
|
46
46
|
const partial = { v: 2 as const, m: "m", t: 123 };
|
|
47
47
|
const result = FrakContextManager.compress(
|
|
48
48
|
partial as FrakContextV2
|
|
@@ -50,6 +50,33 @@ describe("FrakContextManager", () => {
|
|
|
50
50
|
expect(result).toBeUndefined();
|
|
51
51
|
});
|
|
52
52
|
|
|
53
|
+
it("should compress v2 context with wallet only (no clientId)", () => {
|
|
54
|
+
const v2WithWalletOnly: FrakContextV2 = {
|
|
55
|
+
v: 2,
|
|
56
|
+
m: "merchant-uuid-1234",
|
|
57
|
+
t: 1709654400,
|
|
58
|
+
w: "0x1234567890123456789012345678901234567890" as Address,
|
|
59
|
+
};
|
|
60
|
+
const result = FrakContextManager.compress(v2WithWalletOnly);
|
|
61
|
+
expect(result).toBeDefined();
|
|
62
|
+
const decompressed = FrakContextManager.decompress(result);
|
|
63
|
+
expect(decompressed).toEqual(v2WithWalletOnly);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("should compress v2 context with both clientId and wallet", () => {
|
|
67
|
+
const v2Hybrid: FrakContextV2 = {
|
|
68
|
+
v: 2,
|
|
69
|
+
c: "test-client-id-uuid",
|
|
70
|
+
m: "merchant-uuid-1234",
|
|
71
|
+
t: 1709654400,
|
|
72
|
+
w: "0x1234567890123456789012345678901234567890" as Address,
|
|
73
|
+
};
|
|
74
|
+
const result = FrakContextManager.compress(v2Hybrid);
|
|
75
|
+
expect(result).toBeDefined();
|
|
76
|
+
const decompressed = FrakContextManager.decompress(result);
|
|
77
|
+
expect(decompressed).toEqual(v2Hybrid);
|
|
78
|
+
});
|
|
79
|
+
|
|
53
80
|
it("should return undefined when v2 context is missing merchantId", () => {
|
|
54
81
|
const partial = { v: 2 as const, c: "c", t: 123 };
|
|
55
82
|
const result = FrakContextManager.compress(
|
|
@@ -125,7 +152,7 @@ describe("FrakContextManager", () => {
|
|
|
125
152
|
describe("update with attribution", () => {
|
|
126
153
|
const url = "https://example.com/product";
|
|
127
154
|
|
|
128
|
-
it("should
|
|
155
|
+
it("should apply default attribution params when attribution is omitted", () => {
|
|
129
156
|
const result = FrakContextManager.update({
|
|
130
157
|
url,
|
|
131
158
|
context: v2Context,
|
|
@@ -133,11 +160,18 @@ describe("FrakContextManager", () => {
|
|
|
133
160
|
|
|
134
161
|
expect(result).toBeDefined();
|
|
135
162
|
expect(result).toContain("fCtx=");
|
|
136
|
-
|
|
137
|
-
expect(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
expect(
|
|
163
|
+
const parsedUrl = new URL(result!);
|
|
164
|
+
expect(parsedUrl.searchParams.get("utm_source")).toBe(
|
|
165
|
+
"frak"
|
|
166
|
+
);
|
|
167
|
+
expect(parsedUrl.searchParams.get("utm_medium")).toBe(
|
|
168
|
+
"referral"
|
|
169
|
+
);
|
|
170
|
+
expect(parsedUrl.searchParams.get("utm_campaign")).toBe(
|
|
171
|
+
v2Context.m
|
|
172
|
+
);
|
|
173
|
+
expect(parsedUrl.searchParams.get("via")).toBe("frak");
|
|
174
|
+
expect(parsedUrl.searchParams.get("ref")).toBe(v2Context.c);
|
|
141
175
|
});
|
|
142
176
|
|
|
143
177
|
it("should apply default attribution params when attribution is an empty object", () => {
|
package/src/utils/FrakContext.ts
CHANGED
|
@@ -28,13 +28,16 @@ function compress(context?: FrakContextV1 | FrakContextV2): string | undefined {
|
|
|
28
28
|
if (!context) return;
|
|
29
29
|
try {
|
|
30
30
|
if (isV2Context(context)) {
|
|
31
|
-
// Runtime validation:
|
|
32
|
-
|
|
31
|
+
// Runtime validation: m + t are always required, and at least one of
|
|
32
|
+
// c (anonymous fingerprint) or w (wallet) must be present.
|
|
33
|
+
if (!context.m || !context.t) return undefined;
|
|
34
|
+
if (!context.c && !context.w) return undefined;
|
|
33
35
|
return compressJsonToB64({
|
|
34
36
|
v: 2,
|
|
35
|
-
c: context.c,
|
|
36
37
|
m: context.m,
|
|
37
38
|
t: context.t,
|
|
39
|
+
...(context.c ? { c: context.c } : {}),
|
|
40
|
+
...(context.w ? { w: context.w } : {}),
|
|
38
41
|
});
|
|
39
42
|
}
|
|
40
43
|
|
|
@@ -61,10 +64,15 @@ function decompress(context?: string): FrakContext | undefined {
|
|
|
61
64
|
// Try V2 JSON first — V2 payloads are longer than V1's 20-byte address
|
|
62
65
|
const json = decompressJsonFromB64<FrakContextV2>(context);
|
|
63
66
|
if (json && typeof json === "object" && json.v === 2) {
|
|
64
|
-
if (json.
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
67
|
+
if (!json.m || !json.t) return undefined;
|
|
68
|
+
if (!json.c && !json.w) return undefined;
|
|
69
|
+
return {
|
|
70
|
+
v: 2,
|
|
71
|
+
m: json.m,
|
|
72
|
+
t: json.t,
|
|
73
|
+
...(json.c ? { c: json.c } : {}),
|
|
74
|
+
...(json.w ? { w: json.w } : {}),
|
|
75
|
+
};
|
|
68
76
|
}
|
|
69
77
|
|
|
70
78
|
// Fall back to V1: raw 20-byte address
|
|
@@ -109,9 +117,10 @@ const DEFAULT_ATTRIBUTION_SOURCE = "frak";
|
|
|
109
117
|
/**
|
|
110
118
|
* Resolve attribution defaults from the provided context.
|
|
111
119
|
*
|
|
112
|
-
* V2 contexts expose the merchantId (`m`) and
|
|
113
|
-
* `utm_campaign` and `ref` respectively.
|
|
114
|
-
*
|
|
120
|
+
* V2 contexts expose the merchantId (`m`) and, when anonymous, the clientId
|
|
121
|
+
* (`c`), which feed `utm_campaign` and `ref` respectively. When V2 only carries
|
|
122
|
+
* a wallet (`w`), `ref` is intentionally left unset — we don't want wallet
|
|
123
|
+
* addresses leaking into UTM params. V1 contexts have no equivalent.
|
|
115
124
|
*/
|
|
116
125
|
function resolveAttributionValues(
|
|
117
126
|
context: FrakContextV1 | FrakContextV2,
|
|
@@ -151,15 +160,15 @@ function applyAttributionParams(
|
|
|
151
160
|
/**
|
|
152
161
|
* Add or replace the `fCtx` query parameter in a URL with the given context.
|
|
153
162
|
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
163
|
+
* Standard affiliation params (`utm_source`, `utm_medium`, `utm_campaign`,
|
|
164
|
+
* `ref`, `via`, ...) are always appended using gap-fill semantics: pre-existing
|
|
165
|
+
* params on the URL are preserved, defaults are derived from the context when
|
|
166
|
+
* applicable, and `attribution` overrides take precedence when provided.
|
|
158
167
|
*
|
|
159
168
|
* @param args
|
|
160
169
|
* @param args.url - The URL to update
|
|
161
170
|
* @param args.context - The context to embed (V1 or V2)
|
|
162
|
-
* @param args.attribution - Optional attribution overrides.
|
|
171
|
+
* @param args.attribution - Optional attribution overrides. Defaults are applied even when omitted.
|
|
163
172
|
* @returns The updated URL string, or null on failure
|
|
164
173
|
*/
|
|
165
174
|
function update({
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
type ButtonBaseProps = {
|
|
2
|
+
placement?: string;
|
|
3
|
+
target_interaction?: string;
|
|
4
|
+
has_reward?: boolean;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
type BannerVariant = "referral" | "inapp";
|
|
8
|
+
type BannerOutcome = "clicked" | "dismissed";
|
|
9
|
+
type PostPurchaseVariant = "referrer" | "referee";
|
|
10
|
+
type ShareClickAction = "share-modal" | "embedded-wallet" | "sharing-page";
|
|
11
|
+
|
|
12
|
+
export type SdkComponentEventMap = {
|
|
13
|
+
// Share button — click carries the resolved action + reward presence so
|
|
14
|
+
// we can compare per-merchant configuration impact on conversion.
|
|
15
|
+
share_button_clicked: ButtonBaseProps & {
|
|
16
|
+
click_action: ShareClickAction;
|
|
17
|
+
};
|
|
18
|
+
share_modal_error: ButtonBaseProps & {
|
|
19
|
+
error?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Wallet button (floating) — NOT actively used in production. No tracking.
|
|
23
|
+
|
|
24
|
+
// Open in app — path lets us compare deep-link destinations once we add more.
|
|
25
|
+
open_in_app_clicked: {
|
|
26
|
+
placement?: string;
|
|
27
|
+
path: string;
|
|
28
|
+
};
|
|
29
|
+
app_not_installed: {
|
|
30
|
+
placement?: string;
|
|
31
|
+
path: string;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Banner — referral vs in-app variants share the funnel shape.
|
|
35
|
+
banner_impression: {
|
|
36
|
+
placement?: string;
|
|
37
|
+
variant: BannerVariant;
|
|
38
|
+
has_reward?: boolean;
|
|
39
|
+
};
|
|
40
|
+
banner_resolved: {
|
|
41
|
+
placement?: string;
|
|
42
|
+
variant: BannerVariant;
|
|
43
|
+
outcome: BannerOutcome;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Post-purchase — the card drives the highest-intent entry into the
|
|
47
|
+
// referral loop; variant tells us whether we upsold a new share or
|
|
48
|
+
// celebrated an existing referee.
|
|
49
|
+
post_purchase_impression: {
|
|
50
|
+
placement?: string;
|
|
51
|
+
variant: PostPurchaseVariant;
|
|
52
|
+
has_reward?: boolean;
|
|
53
|
+
};
|
|
54
|
+
post_purchase_clicked: {
|
|
55
|
+
placement?: string;
|
|
56
|
+
variant: PostPurchaseVariant;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { SdkComponentEventMap } from "./component";
|
|
2
|
+
import type { SdkLifecycleEventMap } from "./lifecycle";
|
|
3
|
+
import type { SdkReferralEventMap } from "./referral";
|
|
4
|
+
|
|
5
|
+
export type { SdkComponentEventMap } from "./component";
|
|
6
|
+
export type {
|
|
7
|
+
SdkHandshakeFailureReason,
|
|
8
|
+
SdkLifecycleEventMap,
|
|
9
|
+
} from "./lifecycle";
|
|
10
|
+
export type { SdkReferralEventMap } from "./referral";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Merged SDK event map. Consumed by the SDK's typed `trackEvent`.
|
|
14
|
+
* Stays isolated from wallet-shared because the SDK ships in partner
|
|
15
|
+
* bundles (different OpenPanel client id, no wallet-shared dependency
|
|
16
|
+
* allowed — see `packages/wallet-shared/AGENTS.md`).
|
|
17
|
+
*/
|
|
18
|
+
export type SdkEventMap = SdkLifecycleEventMap &
|
|
19
|
+
SdkComponentEventMap &
|
|
20
|
+
SdkReferralEventMap;
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type SdkHandshakeFailureReason =
|
|
2
|
+
| "timeout"
|
|
3
|
+
| "origin"
|
|
4
|
+
| "asset_push"
|
|
5
|
+
| "unknown";
|
|
6
|
+
|
|
7
|
+
export type SdkLifecycleEventMap = {
|
|
8
|
+
sdk_initialized: {
|
|
9
|
+
sdkVersion?: string;
|
|
10
|
+
};
|
|
11
|
+
sdk_iframe_connected: {
|
|
12
|
+
handshake_duration_ms: number;
|
|
13
|
+
};
|
|
14
|
+
sdk_iframe_handshake_failed: {
|
|
15
|
+
reason: SdkHandshakeFailureReason;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Emitted by the CDN bootstrap when `initFrakSdk()` throws before a
|
|
19
|
+
* client is available. Uses a transient OpenPanel instance so broken
|
|
20
|
+
* partner integrations are still captured.
|
|
21
|
+
*/
|
|
22
|
+
sdk_init_failed: {
|
|
23
|
+
reason: string;
|
|
24
|
+
config_missing?: boolean;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
@@ -10,8 +10,8 @@ import {
|
|
|
10
10
|
expect,
|
|
11
11
|
it,
|
|
12
12
|
vi,
|
|
13
|
-
} from "
|
|
14
|
-
import type { FrakClient } from "
|
|
13
|
+
} from "../../../tests/vitest-fixtures";
|
|
14
|
+
import type { FrakClient } from "../../types";
|
|
15
15
|
import { trackEvent } from "./trackEvent";
|
|
16
16
|
|
|
17
17
|
describe("trackEvent", () => {
|
|
@@ -42,16 +42,19 @@ describe("trackEvent", () => {
|
|
|
42
42
|
|
|
43
43
|
expect(mockClient.openPanel?.track).toHaveBeenCalledWith(
|
|
44
44
|
"share_button_clicked",
|
|
45
|
-
|
|
45
|
+
undefined
|
|
46
46
|
);
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
it("should track event with props", () => {
|
|
50
|
-
const props = {
|
|
51
|
-
|
|
50
|
+
const props = {
|
|
51
|
+
placement: "footer",
|
|
52
|
+
click_action: "share-modal",
|
|
53
|
+
} as const;
|
|
54
|
+
trackEvent(mockClient, "share_button_clicked", props);
|
|
52
55
|
|
|
53
56
|
expect(mockClient.openPanel?.track).toHaveBeenCalledWith(
|
|
54
|
-
"
|
|
57
|
+
"share_button_clicked",
|
|
55
58
|
props
|
|
56
59
|
);
|
|
57
60
|
});
|
|
@@ -71,25 +74,18 @@ describe("trackEvent", () => {
|
|
|
71
74
|
|
|
72
75
|
expect(mockClient.openPanel?.track).toHaveBeenCalledWith(
|
|
73
76
|
"user_referred_started",
|
|
74
|
-
|
|
77
|
+
undefined
|
|
75
78
|
);
|
|
76
79
|
});
|
|
77
80
|
|
|
78
81
|
it("should track user_referred_completed event", () => {
|
|
79
|
-
trackEvent(mockClient, "user_referred_completed"
|
|
82
|
+
trackEvent(mockClient, "user_referred_completed", {
|
|
83
|
+
status: "success",
|
|
84
|
+
});
|
|
80
85
|
|
|
81
86
|
expect(mockClient.openPanel?.track).toHaveBeenCalledWith(
|
|
82
87
|
"user_referred_completed",
|
|
83
|
-
{}
|
|
84
|
-
);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it("should track user_referred_error event", () => {
|
|
88
|
-
trackEvent(mockClient, "user_referred_error");
|
|
89
|
-
|
|
90
|
-
expect(mockClient.openPanel?.track).toHaveBeenCalledWith(
|
|
91
|
-
"user_referred_error",
|
|
92
|
-
{}
|
|
88
|
+
{ status: "success" }
|
|
93
89
|
);
|
|
94
90
|
});
|
|
95
91
|
});
|
|
@@ -102,7 +98,7 @@ describe("trackEvent", () => {
|
|
|
102
98
|
});
|
|
103
99
|
|
|
104
100
|
it("should log debug message when client is undefined", () => {
|
|
105
|
-
trackEvent(undefined, "
|
|
101
|
+
trackEvent(undefined, "share_button_clicked");
|
|
106
102
|
|
|
107
103
|
expect(consoleDebugSpy).toHaveBeenCalledWith(
|
|
108
104
|
"[Frak] No client provided, skipping event tracking"
|
|
@@ -143,30 +139,26 @@ describe("trackEvent", () => {
|
|
|
143
139
|
const clientWithoutPanel = {} as FrakClient;
|
|
144
140
|
|
|
145
141
|
expect(() => {
|
|
146
|
-
trackEvent(clientWithoutPanel, "
|
|
142
|
+
trackEvent(clientWithoutPanel, "share_button_clicked");
|
|
147
143
|
}).not.toThrow();
|
|
148
144
|
});
|
|
149
145
|
});
|
|
150
146
|
|
|
151
147
|
describe("edge cases", () => {
|
|
152
|
-
it("should handle
|
|
153
|
-
trackEvent(mockClient, "share_button_clicked", {
|
|
148
|
+
it("should handle typed props object", () => {
|
|
149
|
+
trackEvent(mockClient, "share_button_clicked", {
|
|
150
|
+
click_action: "share-modal",
|
|
151
|
+
});
|
|
154
152
|
|
|
155
153
|
expect(mockClient.openPanel?.track).toHaveBeenCalledWith(
|
|
156
154
|
"share_button_clicked",
|
|
157
|
-
{}
|
|
155
|
+
{ click_action: "share-modal" }
|
|
158
156
|
);
|
|
159
157
|
});
|
|
160
158
|
|
|
161
159
|
it("should handle complex props object", () => {
|
|
162
160
|
const complexProps = {
|
|
163
|
-
|
|
164
|
-
metadata: {
|
|
165
|
-
page: "home",
|
|
166
|
-
section: "header",
|
|
167
|
-
},
|
|
168
|
-
tags: ["tag1", "tag2"],
|
|
169
|
-
timestamp: Date.now(),
|
|
161
|
+
status: "success" as const,
|
|
170
162
|
};
|
|
171
163
|
|
|
172
164
|
trackEvent(mockClient, "user_referred_completed", complexProps);
|