@joryio/web-sdk 0.0.0-stage → 1.3.0
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/CHANGELOG.md +5 -0
- package/LICENSE +54 -0
- package/README.md +497 -2
- package/dist/core/config.d.ts +101 -0
- package/dist/core/config.d.ts.map +1 -0
- package/dist/core/device.d.ts +14 -0
- package/dist/core/device.d.ts.map +1 -0
- package/dist/core/ecommerce.d.ts +67 -0
- package/dist/core/ecommerce.d.ts.map +1 -0
- package/dist/core/identity.d.ts +36 -0
- package/dist/core/identity.d.ts.map +1 -0
- package/dist/core/inapp.d.ts +169 -0
- package/dist/core/inapp.d.ts.map +1 -0
- package/dist/core/network.d.ts +158 -0
- package/dist/core/network.d.ts.map +1 -0
- package/dist/core/outbox.d.ts +79 -0
- package/dist/core/outbox.d.ts.map +1 -0
- package/dist/core/pending-attributes.d.ts +43 -0
- package/dist/core/pending-attributes.d.ts.map +1 -0
- package/dist/core/push.d.ts +59 -0
- package/dist/core/push.d.ts.map +1 -0
- package/dist/core/queue.d.ts +107 -0
- package/dist/core/queue.d.ts.map +1 -0
- package/dist/core/sdk.d.ts +169 -0
- package/dist/core/sdk.d.ts.map +1 -0
- package/dist/core/session.d.ts +36 -0
- package/dist/core/session.d.ts.map +1 -0
- package/dist/core/storage.d.ts +17 -0
- package/dist/core/storage.d.ts.map +1 -0
- package/dist/core/user.d.ts +28 -0
- package/dist/core/user.d.ts.map +1 -0
- package/dist/core/utm.d.ts +39 -0
- package/dist/core/utm.d.ts.map +1 -0
- package/dist/core/values.d.ts +3 -0
- package/dist/core/values.d.ts.map +1 -0
- package/dist/core/write-retry.d.ts +66 -0
- package/dist/core/write-retry.d.ts.map +1 -0
- package/dist/index.d.mts +711 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/joryio-sw.js +6 -0
- package/dist/joryio.cjs.js +7265 -0
- package/dist/joryio.min.js +7 -0
- package/dist/joryio.min.js.map +1 -0
- package/dist/joryio.mjs +7254 -0
- package/dist/joryio.umd.js +7271 -0
- package/dist/utils/bucket-hash.d.ts +2 -0
- package/dist/utils/bucket-hash.d.ts.map +1 -0
- package/dist/utils/logger.d.ts +14 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/uuid.d.ts +3 -0
- package/dist/utils/uuid.d.ts.map +1 -0
- package/package.json +90 -4
- package/src/core/config.ts +362 -0
- package/src/core/device.ts +50 -0
- package/src/core/ecommerce.ts +482 -0
- package/src/core/identity.ts +251 -0
- package/src/core/inapp.ts +2419 -0
- package/src/core/network.ts +1261 -0
- package/src/core/outbox.ts +707 -0
- package/src/core/pending-attributes.ts +350 -0
- package/src/core/push.ts +667 -0
- package/src/core/queue.ts +894 -0
- package/src/core/sdk.ts +3108 -0
- package/src/core/session.ts +311 -0
- package/src/core/storage.ts +157 -0
- package/src/core/user.ts +203 -0
- package/src/core/utm.ts +304 -0
- package/src/core/values.ts +85 -0
- package/src/core/write-retry.ts +136 -0
- package/src/index.ts +305 -0
- package/src/snippet/loader.js +134 -0
- package/src/utils/bucket-hash.ts +44 -0
- package/src/utils/logger.ts +58 -0
- package/src/utils/uuid.ts +29 -0
- package/src/worker/joryio-sw.js +218 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Joryio Web SDK
|
|
3
|
+
* Main entry point
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { JoryioSDK, PRE_INIT_BASELINE, runWithDataCallsDropped, DataGateReason } from './core/sdk';
|
|
7
|
+
import type { TrackingConsent, PreInitDecision } from './core/sdk';
|
|
8
|
+
import type { InitConfig, Plugin, InAppOptions, InAppDisplayOptions } from './core/config';
|
|
9
|
+
import type { UTMParameters, UTMData } from './core/utm';
|
|
10
|
+
import type { InAppCampaign, InAppMessage, InAppTrigger } from './core/inapp';
|
|
11
|
+
import type {
|
|
12
|
+
SdkAuthFailureInfo,
|
|
13
|
+
SdkAuthFailureReason,
|
|
14
|
+
SdkAuthFailureCallback,
|
|
15
|
+
} from './core/network';
|
|
16
|
+
import { User, SubscriptionStatus } from './core/user';
|
|
17
|
+
import type { SubscriptionChannel, SubscriptionPreferences } from './core/user';
|
|
18
|
+
import { PushManager } from './core/push';
|
|
19
|
+
import type { PushSubscribeOptions, PushPermissionState } from './core/push';
|
|
20
|
+
import { EcommerceTracker, createEcommerceTracker } from './core/ecommerce';
|
|
21
|
+
import type {
|
|
22
|
+
EcommerceProduct,
|
|
23
|
+
EcommerceCartItem,
|
|
24
|
+
EcommerceOrder,
|
|
25
|
+
EcommerceConfig,
|
|
26
|
+
} from './core/ecommerce';
|
|
27
|
+
|
|
28
|
+
// Export main SDK class
|
|
29
|
+
export { JoryioSDK };
|
|
30
|
+
|
|
31
|
+
// Export User class and subscription types
|
|
32
|
+
export { User, SubscriptionStatus };
|
|
33
|
+
|
|
34
|
+
// Export Push manager
|
|
35
|
+
export { PushManager };
|
|
36
|
+
|
|
37
|
+
// Export E-commerce tracker
|
|
38
|
+
export { EcommerceTracker, createEcommerceTracker };
|
|
39
|
+
|
|
40
|
+
// Export types
|
|
41
|
+
export type {
|
|
42
|
+
InitConfig,
|
|
43
|
+
Plugin,
|
|
44
|
+
InAppOptions,
|
|
45
|
+
InAppDisplayOptions,
|
|
46
|
+
InAppCampaign,
|
|
47
|
+
InAppMessage,
|
|
48
|
+
InAppTrigger,
|
|
49
|
+
UTMParameters,
|
|
50
|
+
UTMData,
|
|
51
|
+
SubscriptionChannel,
|
|
52
|
+
SubscriptionPreferences,
|
|
53
|
+
PushSubscribeOptions,
|
|
54
|
+
PushPermissionState,
|
|
55
|
+
TrackingConsent,
|
|
56
|
+
EcommerceProduct,
|
|
57
|
+
EcommerceCartItem,
|
|
58
|
+
EcommerceOrder,
|
|
59
|
+
EcommerceConfig,
|
|
60
|
+
SdkAuthFailureInfo,
|
|
61
|
+
SdkAuthFailureReason,
|
|
62
|
+
SdkAuthFailureCallback,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
// Export SDK version
|
|
66
|
+
export { SDK_VERSION } from './core/config';
|
|
67
|
+
|
|
68
|
+
// Default export for convenience
|
|
69
|
+
export default JoryioSDK;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Wire the async snippet's command queue to the SDK.
|
|
73
|
+
*
|
|
74
|
+
* The snippet makes `window.joryio` an array whose stub methods push
|
|
75
|
+
* `[method, ...args]` onto it. When this bundle loads:
|
|
76
|
+
* - if an `init` is already queued, the SDK is created, `window.joryio`
|
|
77
|
+
* becomes the SDK instance, and every other queued call is replayed in
|
|
78
|
+
* order;
|
|
79
|
+
* - if not (a site that calls `joryio.init()` later - after its consent
|
|
80
|
+
* banner, say), the queue keeps collecting and the SDK starts the moment
|
|
81
|
+
* `init` is pushed. That case used to be ignored: the late init sat in the
|
|
82
|
+
* array forever and nothing was ever tracked.
|
|
83
|
+
*
|
|
84
|
+
* This used to also assign `window.joryio.push = ...` on the SDK instance,
|
|
85
|
+
* where `push` is the read-only web-push getter. In the (strict-mode) bundle
|
|
86
|
+
* that assignment THREW - "Cannot set property push of #<L> which has only a
|
|
87
|
+
* getter" - on every page using the snippet, aborting the UMD factory before
|
|
88
|
+
* it exported anything. Late calls need no override: the stubs push onto the
|
|
89
|
+
* original array (handled below), and calls on `window.joryio` after load go
|
|
90
|
+
* straight to the SDK's own methods.
|
|
91
|
+
*
|
|
92
|
+
* Exported for tests. Never throws into the host page.
|
|
93
|
+
*/
|
|
94
|
+
/** Calls held before `init` (snippet queue). The same cap on every Joryio SDK. */
|
|
95
|
+
const MAX_PRE_INIT_CALLS = 1000;
|
|
96
|
+
/** Never dropped by that cap: `init` and the visitor's privacy decisions (rule 51). */
|
|
97
|
+
const KEPT_PRE_INIT_CALLS = ['init', 'optOut', 'optIn', 'updateConsent', 'wipeData'];
|
|
98
|
+
/**
|
|
99
|
+
* Queued calls that COLLECT data (rule 73): one made while the visitor was
|
|
100
|
+
* opted out or consent was denied - by the stored state, the init config or
|
|
101
|
+
* a decision queued before it - is dropped, not replayed after a later
|
|
102
|
+
* queued optIn() or grant. identify() is not one (a user kept for
|
|
103
|
+
* withdrawals, D-b), nor are withdrawals and control calls.
|
|
104
|
+
*/
|
|
105
|
+
const DATA_CALLS = [
|
|
106
|
+
'track',
|
|
107
|
+
'setAttributes',
|
|
108
|
+
'setAttribute',
|
|
109
|
+
'incrementAttribute',
|
|
110
|
+
'unsetAttribute',
|
|
111
|
+
'addToArray',
|
|
112
|
+
'removeFromArray',
|
|
113
|
+
'addAlias',
|
|
114
|
+
'trackInAppConversion',
|
|
115
|
+
];
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Which queued data calls (DATA_CALLS) and ready() callbacks were made while
|
|
119
|
+
* tracking was off - or before a queued optOut(), updateConsent('denied') or
|
|
120
|
+
* pre-init wipeData() (rule 83, the pre-init purge: the person withdrew what
|
|
121
|
+
* was queued before it, and a later queued optIn() or grant does not bring it
|
|
122
|
+
* back). The data calls are dropped; such a ready() callback still runs, with
|
|
123
|
+
* the data calls it makes dropped (rule 83) - and each callback carries WHY
|
|
124
|
+
* (rule 92), so only the matching decision inside it lifts the gate. A
|
|
125
|
+
* pre-init wipeData() gates no ready() callback by itself: the callback runs
|
|
126
|
+
* after it, for the fresh visitor.
|
|
127
|
+
*/
|
|
128
|
+
function callsMadeWhileOff(
|
|
129
|
+
commands: any[],
|
|
130
|
+
baseline: { optedOut: boolean; consent: string } | null | undefined,
|
|
131
|
+
initIndex: number = commands.length,
|
|
132
|
+
reasons: Map<any, Set<DataGateReason>> = new Map(),
|
|
133
|
+
): Set<any> {
|
|
134
|
+
const off = new Set<any>();
|
|
135
|
+
// `reasons` (filled for the caller): why each one is off (rule 92).
|
|
136
|
+
const gate = (command: any, reason: DataGateReason) => {
|
|
137
|
+
off.add(command);
|
|
138
|
+
const why = reasons.get(command) || new Set<DataGateReason>();
|
|
139
|
+
why.add(reason);
|
|
140
|
+
reasons.set(command, why);
|
|
141
|
+
};
|
|
142
|
+
let optedOut = !!baseline?.optedOut;
|
|
143
|
+
let consent = baseline?.consent || 'granted';
|
|
144
|
+
const collecting: any[] = [];
|
|
145
|
+
const purge = (byWipe = false) => {
|
|
146
|
+
const kept = collecting.filter((command) => byWipe && command[0] === 'ready');
|
|
147
|
+
collecting.forEach((command) => {
|
|
148
|
+
if (!kept.includes(command)) gate(command, 'purged');
|
|
149
|
+
});
|
|
150
|
+
collecting.length = 0;
|
|
151
|
+
collecting.push(...kept);
|
|
152
|
+
};
|
|
153
|
+
commands.forEach((command, index) => {
|
|
154
|
+
if (!Array.isArray(command)) return;
|
|
155
|
+
const [method, arg] = command;
|
|
156
|
+
if (method === 'optOut') {
|
|
157
|
+
optedOut = true;
|
|
158
|
+
purge();
|
|
159
|
+
} else if (method === 'optIn') optedOut = false;
|
|
160
|
+
else if (method === 'updateConsent') {
|
|
161
|
+
if (arg === 'granted' || arg === 'denied') consent = arg;
|
|
162
|
+
// 'pending' never reopens a denial.
|
|
163
|
+
else if (arg === 'pending' && consent !== 'denied') consent = 'pending';
|
|
164
|
+
if (arg === 'denied') purge();
|
|
165
|
+
} else if (method === 'wipeData' && index < initIndex) {
|
|
166
|
+
// Applied inside init, before the replay: what was queued before it
|
|
167
|
+
// would otherwise be collected for the fresh visitor the wipe makes.
|
|
168
|
+
purge(true);
|
|
169
|
+
} else if (DATA_CALLS.includes(method) || method === 'ready') {
|
|
170
|
+
if (optedOut) gate(command, 'optedOut');
|
|
171
|
+
if (consent === 'denied') gate(command, 'denied');
|
|
172
|
+
if (!optedOut && consent !== 'denied') collecting.push(command);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
return off;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function installSnippetQueue(win: any): void {
|
|
179
|
+
try {
|
|
180
|
+
const queue = win && win.joryio;
|
|
181
|
+
if (!Array.isArray(queue)) return;
|
|
182
|
+
|
|
183
|
+
const initCommand = queue.find((cmd: any) => Array.isArray(cmd) && cmd[0] === 'init');
|
|
184
|
+
if (!initCommand) {
|
|
185
|
+
if ((queue as any).__joryioWaitingForInit) return;
|
|
186
|
+
(queue as any).__joryioWaitingForInit = true;
|
|
187
|
+
// An own property on the array instance - assignable, unlike the SDK's getter.
|
|
188
|
+
(queue as any).push = function (...commands: any[]) {
|
|
189
|
+
// Same pre-init cap as the snippet (and every Joryio SDK): 1000
|
|
190
|
+
// calls, oldest dropped first - never `init` nor a privacy decision.
|
|
191
|
+
for (let overflow = queue.length + commands.length - MAX_PRE_INIT_CALLS; overflow > 0; overflow--) {
|
|
192
|
+
const oldest = queue.findIndex((cmd: any) => !(Array.isArray(cmd) && KEPT_PRE_INIT_CALLS.includes(cmd[0])));
|
|
193
|
+
if (oldest < 0) break;
|
|
194
|
+
queue.splice(oldest, 1);
|
|
195
|
+
}
|
|
196
|
+
const length = Array.prototype.push.apply(queue, commands);
|
|
197
|
+
if (commands.some((cmd) => Array.isArray(cmd) && cmd[0] === 'init')) {
|
|
198
|
+
installSnippetQueue(win);
|
|
199
|
+
}
|
|
200
|
+
return length;
|
|
201
|
+
};
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Privacy decisions queued before the bundle loaded take effect INSIDE
|
|
206
|
+
// init, before any of its side effects (contract item 25). Replayed
|
|
207
|
+
// afterwards, as they used to be, they came too late: `optOut()` or
|
|
208
|
+
// `updateConsent('denied')` queued ahead of `init` still let init send
|
|
209
|
+
// Session Start, sync in-app messages and re-register push first.
|
|
210
|
+
//
|
|
211
|
+
// optOut / optIn / updateConsent are folded from the WHOLE queue (all of
|
|
212
|
+
// it predates the SDK, and replaying, say, an identify under a consent
|
|
213
|
+
// that a later queued call grants would drop it). wipeData only when it
|
|
214
|
+
// came before init: after init it is ordered against the calls around it.
|
|
215
|
+
const commands = queue.slice();
|
|
216
|
+
const initIndex = commands.indexOf(initCommand);
|
|
217
|
+
const preInit: PreInitDecision[] = [];
|
|
218
|
+
const folded = new Set<any>();
|
|
219
|
+
commands.forEach((command: any, index: number) => {
|
|
220
|
+
if (!Array.isArray(command)) return;
|
|
221
|
+
const [method, arg] = command;
|
|
222
|
+
if (method === 'optOut' || method === 'optIn') {
|
|
223
|
+
preInit.push({ method });
|
|
224
|
+
} else if (method === 'updateConsent') {
|
|
225
|
+
preInit.push({ method, consent: arg });
|
|
226
|
+
} else if (method === 'wipeData' && index < initIndex) {
|
|
227
|
+
preInit.push({ method });
|
|
228
|
+
} else {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
folded.add(command);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
let sdk: JoryioSDK | null = null;
|
|
235
|
+
try {
|
|
236
|
+
sdk = new JoryioSDK(initCommand[1], preInit);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
// eslint-disable-next-line no-console -- no SDK instance (and so no logger) exists when init itself fails
|
|
239
|
+
console.error('[Joryio] init failed:', error);
|
|
240
|
+
}
|
|
241
|
+
if (!sdk || JoryioSDK.getInstance() !== sdk) {
|
|
242
|
+
// The init failed (a missing or malformed key builds an inert
|
|
243
|
+
// instance). Nothing queued is replayed into it - an unsubscribe or an
|
|
244
|
+
// optOut() would be swallowed - and `window.joryio` stays the queue:
|
|
245
|
+
// every call, privacy decisions and withdrawals included, is KEPT in
|
|
246
|
+
// order, and a corrected `joryio.init(...)` starts the SDK with them
|
|
247
|
+
// (decisions first, rule 65). The failed init is dropped so it is not
|
|
248
|
+
// tried again.
|
|
249
|
+
const at = queue.indexOf(initCommand);
|
|
250
|
+
if (at >= 0) queue.splice(at, 1);
|
|
251
|
+
(queue as any).__joryioWaitingForInit = false;
|
|
252
|
+
installSnippetQueue(win);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Data calls made while tracking was off are dropped, whatever a later
|
|
257
|
+
// queued decision says (rule 73).
|
|
258
|
+
const gateReasons = new Map<any, Set<DataGateReason>>();
|
|
259
|
+
const madeWhileOff = callsMadeWhileOff(commands, PRE_INIT_BASELINE.get(sdk), initIndex, gateReasons);
|
|
260
|
+
// Swap first, so a replayed ready(cb) - or anything it calls - sees the SDK.
|
|
261
|
+
win.joryio = sdk;
|
|
262
|
+
for (const command of commands) {
|
|
263
|
+
if (!Array.isArray(command) || folded.has(command)) continue;
|
|
264
|
+
const [method, ...args] = command;
|
|
265
|
+
if (method === 'init') continue;
|
|
266
|
+
const gated = madeWhileOff.has(command);
|
|
267
|
+
if (gated && method !== 'ready') {
|
|
268
|
+
(sdk as any).logger?.debug?.(`${String(method)} queued before load dropped: tracking was off when it was called`);
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
const fn = (sdk as any)[method];
|
|
272
|
+
if (typeof fn !== 'function') {
|
|
273
|
+
// eslint-disable-next-line no-console -- surfaced regardless of log level: a typo in a queued call is otherwise invisible
|
|
274
|
+
console.warn(`[Joryio] Unknown method queued before load: ${String(method)}`);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
let result: any;
|
|
279
|
+
// A ready() callback queued while tracking was off runs at the state
|
|
280
|
+
// of its queue position: the data calls it makes are dropped (rule 83).
|
|
281
|
+
if (gated) runWithDataCallsDropped(sdk, gateReasons.get(command) || [], () => (result = fn.apply(sdk, args)));
|
|
282
|
+
else result = fn.apply(sdk, args);
|
|
283
|
+
if (result && typeof result.catch === 'function') {
|
|
284
|
+
result.catch((error: unknown) => {
|
|
285
|
+
// eslint-disable-next-line no-console -- see above
|
|
286
|
+
console.error(`[Joryio] Error executing ${String(method)}:`, error);
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
} catch (error) {
|
|
290
|
+
// eslint-disable-next-line no-console -- see above
|
|
291
|
+
console.error(`[Joryio] Error executing ${String(method)}:`, error);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
} catch (error) {
|
|
295
|
+
// eslint-disable-next-line no-console -- see above
|
|
296
|
+
console.error('[Joryio] Could not process the snippet queue:', error);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Browser global initialization
|
|
301
|
+
if (typeof window !== 'undefined') {
|
|
302
|
+
// Make SDK available on window object as 'Joryio'
|
|
303
|
+
(window as any).Joryio = JoryioSDK;
|
|
304
|
+
installSnippetQueue(window);
|
|
305
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Joryio Web SDK - Async Loader Snippet
|
|
3
|
+
*
|
|
4
|
+
* This snippet should be embedded in the <head> of your website.
|
|
5
|
+
* It creates a command queue pattern that allows you to call the SDK
|
|
6
|
+
* before the full SDK is loaded.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* <script>
|
|
10
|
+
* !function(){var e=window.joryio=window.joryio||[];if(!e.initialize)if(e.invoked)window.console&&console.error&&console.error("Joryio snippet included twice.");else{e.invoked=!0,e.methods=["init","track","identify","addAlias","setAttributes","setAttribute","incrementAttribute","unsetAttribute","addToArray","removeFromArray","reset","flush","optOut","optIn","updateConsent","getTrackingConsent","isUserOptedOut","setSdkAuthenticationToken","setSdkAuthenticationErrorCallback","getAnonymousId","getUserId","ready","getIdentity","getSessionInfo","getUTMData","updateUTM","syncInAppCampaigns","getInAppCampaigns","displayInAppCampaign","dismissInAppMessage","trackInAppConversion","ecommerce","wipeData","destroy"],e.keep=["init","optOut","optIn","updateConsent","wipeData"],e.factory=function(t){return function(){var n=Array.prototype.slice.call(arguments);if(n.unshift(t),e.length>=1e3)for(var i=0;i<e.length;i++)if(e.keep.indexOf(e[i][0])<0){e.splice(i,1);break}return e.push(n),/^(get|is)[A-Z]/.test(t)?void 0:e}};for(var t=0;t<e.methods.length;t++){var n=e.methods[t];e[n]=e.factory(n)}e.load=function(e){var t=document.createElement("script");t.type="text/javascript",t.async=!0,t.src="https://sdk.joryio.com/web/1/joryio.min.js";var n=document.getElementsByTagName("script")[0];n.parentNode.insertBefore(t,n)},e.load()}}();
|
|
11
|
+
* joryio.init({ sdkKey: 'YOUR_SDK_KEY' });
|
|
12
|
+
* </script>
|
|
13
|
+
*
|
|
14
|
+
* `init` may also come later (for example from a consent banner's callback):
|
|
15
|
+
* calls made before it are kept and run, in order, once it arrives.
|
|
16
|
+
*
|
|
17
|
+
* Reading a value: the queued stubs above cannot return anything before the
|
|
18
|
+
* SDK finishes loading - a getter (getUserId(), isUserOptedOut(), ...) returns
|
|
19
|
+
* `undefined` until then, never a truthy placeholder - so read values in
|
|
20
|
+
* ready(), or on the resolved instance after load. The same goes for `user`,
|
|
21
|
+
* `push` and the tracker `ecommerce()` returns:
|
|
22
|
+
*
|
|
23
|
+
* joryio.ready(function (sdk) {
|
|
24
|
+
* var anonId = sdk.getAnonymousId();
|
|
25
|
+
* var userId = sdk.getUserId(); // null until identify()
|
|
26
|
+
* sdk.user.setSubscription('email', 'optedIn');
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* The readable version is below for reference. Keep the two in step: a test
|
|
30
|
+
* (src/snippet/loader.spec.ts) checks that both list every public SDK method
|
|
31
|
+
* and that the README carries the minified line unchanged.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
(function() {
|
|
35
|
+
// Create the joryio queue
|
|
36
|
+
var joryio = window.joryio = window.joryio || [];
|
|
37
|
+
|
|
38
|
+
// If already initialized, skip
|
|
39
|
+
if (!joryio.initialize) {
|
|
40
|
+
if (joryio.invoked) {
|
|
41
|
+
window.console && console.error && console.error('Joryio snippet included twice.');
|
|
42
|
+
} else {
|
|
43
|
+
joryio.invoked = true;
|
|
44
|
+
|
|
45
|
+
// Every public method of the SDK instance (src/core/sdk.ts), so no call
|
|
46
|
+
// made before load throws "is not a function". Methods that RETURN a
|
|
47
|
+
// value can only do so after load - see ready() above.
|
|
48
|
+
joryio.methods = [
|
|
49
|
+
'init',
|
|
50
|
+
'track',
|
|
51
|
+
'identify',
|
|
52
|
+
'addAlias',
|
|
53
|
+
'setAttributes',
|
|
54
|
+
'setAttribute',
|
|
55
|
+
'incrementAttribute',
|
|
56
|
+
'unsetAttribute',
|
|
57
|
+
'addToArray',
|
|
58
|
+
'removeFromArray',
|
|
59
|
+
'reset',
|
|
60
|
+
'flush',
|
|
61
|
+
'optOut',
|
|
62
|
+
'optIn',
|
|
63
|
+
'updateConsent',
|
|
64
|
+
'getTrackingConsent',
|
|
65
|
+
'isUserOptedOut',
|
|
66
|
+
'setSdkAuthenticationToken',
|
|
67
|
+
'setSdkAuthenticationErrorCallback',
|
|
68
|
+
'getAnonymousId',
|
|
69
|
+
'getUserId',
|
|
70
|
+
'ready',
|
|
71
|
+
'getIdentity',
|
|
72
|
+
'getSessionInfo',
|
|
73
|
+
'getUTMData',
|
|
74
|
+
'updateUTM',
|
|
75
|
+
'syncInAppCampaigns',
|
|
76
|
+
'getInAppCampaigns',
|
|
77
|
+
'displayInAppCampaign',
|
|
78
|
+
'dismissInAppMessage',
|
|
79
|
+
'trackInAppConversion',
|
|
80
|
+
'ecommerce',
|
|
81
|
+
'wipeData',
|
|
82
|
+
'destroy'
|
|
83
|
+
];
|
|
84
|
+
|
|
85
|
+
// Calls the cap below never drops: `init`, and the visitor's privacy
|
|
86
|
+
// decisions (rule 51) - an optOut() lost to a burst of track() calls
|
|
87
|
+
// would let the SDK start tracking someone who said no.
|
|
88
|
+
joryio.keep = ['init', 'optOut', 'optIn', 'updateConsent', 'wipeData'];
|
|
89
|
+
|
|
90
|
+
// Factory for creating queue methods. The queue holds at most 1000
|
|
91
|
+
// calls - the pre-init cap every Joryio SDK uses - dropping the OLDEST
|
|
92
|
+
// first, but never one in `keep`: a page that never loads the bundle
|
|
93
|
+
// (blocked script) must not grow it without bound.
|
|
94
|
+
joryio.factory = function(method) {
|
|
95
|
+
return function() {
|
|
96
|
+
var args = Array.prototype.slice.call(arguments);
|
|
97
|
+
args.unshift(method);
|
|
98
|
+
if (joryio.length >= 1000) {
|
|
99
|
+
for (var j = 0; j < joryio.length; j++) {
|
|
100
|
+
if (joryio.keep.indexOf(joryio[j][0]) < 0) {
|
|
101
|
+
joryio.splice(j, 1);
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
joryio.push(args);
|
|
107
|
+
// A getter has no answer before load: undefined, not the queue (an
|
|
108
|
+
// array is truthy, so `if (joryio.isUserOptedOut())` was wrong).
|
|
109
|
+
return /^(get|is)[A-Z]/.test(method) ? undefined : joryio;
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
// Create queue methods
|
|
114
|
+
for (var i = 0; i < joryio.methods.length; i++) {
|
|
115
|
+
var key = joryio.methods[i];
|
|
116
|
+
joryio[key] = joryio.factory(key);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Load the SDK asynchronously
|
|
120
|
+
joryio.load = function() {
|
|
121
|
+
var script = document.createElement('script');
|
|
122
|
+
script.type = 'text/javascript';
|
|
123
|
+
script.async = true;
|
|
124
|
+
script.src = 'https://sdk.joryio.com/web/1/joryio.min.js';
|
|
125
|
+
|
|
126
|
+
var first = document.getElementsByTagName('script')[0];
|
|
127
|
+
first.parentNode.insertBefore(script, first);
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
// Auto-load
|
|
131
|
+
joryio.load();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
})();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The same 32-bit hash the backend uses, kept byte-for-byte identical.
|
|
3
|
+
*
|
|
4
|
+
* MIRRORS packages/backend/src/common/utils/bucket-hash.util.ts. A backend
|
|
5
|
+
* tripwire (sdk-web-hash-parity.spec.ts) fails if the two drift, because a hash
|
|
6
|
+
* that is "nearly" the same is worse than an obviously different one - it
|
|
7
|
+
* agrees on most inputs and disagrees on the ones nobody tested.
|
|
8
|
+
*
|
|
9
|
+
* FNV-1a XORs each byte in before multiplying, so every input bit reaches the
|
|
10
|
+
* whole word. Its last operation is a multiply though, and multiplication only
|
|
11
|
+
* ever carries LEFT - nothing propagates down into the low bits. Anything
|
|
12
|
+
* reading those (a `% n`) sees a biased digest, so MurmurHash3's fmix32
|
|
13
|
+
* finalizer folds the high bits back down.
|
|
14
|
+
*
|
|
15
|
+
* NOT for anything security-bearing. It is a fast non-cryptographic hash.
|
|
16
|
+
*/
|
|
17
|
+
const FNV_OFFSET_BASIS = 2166136261;
|
|
18
|
+
const FNV_PRIME = 16777619;
|
|
19
|
+
|
|
20
|
+
/** MurmurHash3's 32-bit finalizer - see the note above about low bits. */
|
|
21
|
+
function fmix32(h: number): number {
|
|
22
|
+
h ^= h >>> 16;
|
|
23
|
+
h = Math.imul(h, 0x85ebca6b);
|
|
24
|
+
h ^= h >>> 13;
|
|
25
|
+
h = Math.imul(h, 0xc2b2ae35);
|
|
26
|
+
h ^= h >>> 16;
|
|
27
|
+
return h >>> 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 32-bit FNV-1a over the UTF-16 code units, finalized, returned unsigned. */
|
|
31
|
+
export function bucketHash(input: string): number {
|
|
32
|
+
let hash = FNV_OFFSET_BASIS;
|
|
33
|
+
for (let i = 0; i < input.length; i++) {
|
|
34
|
+
const code = input.charCodeAt(i);
|
|
35
|
+
// Both bytes of the code unit, so 'Ā' and 'A' cannot collide by truncation.
|
|
36
|
+
hash ^= code & 0xff;
|
|
37
|
+
hash = Math.imul(hash, FNV_PRIME);
|
|
38
|
+
hash ^= (code >>> 8) & 0xff;
|
|
39
|
+
hash = Math.imul(hash, FNV_PRIME);
|
|
40
|
+
}
|
|
41
|
+
// >>> 0 rather than Math.abs: Math.abs(-2147483648) is not a 32-bit int, and
|
|
42
|
+
// negating loses the top bit's contribution.
|
|
43
|
+
return fmix32(hash >>> 0);
|
|
44
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple logger utility for SDK debugging
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type LogLevel = 'error' | 'warn' | 'info' | 'debug';
|
|
6
|
+
|
|
7
|
+
const LOG_LEVELS: Record<LogLevel, number> = {
|
|
8
|
+
error: 0,
|
|
9
|
+
warn: 1,
|
|
10
|
+
info: 2,
|
|
11
|
+
debug: 3,
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export class Logger {
|
|
15
|
+
private enabled: boolean;
|
|
16
|
+
private level: LogLevel;
|
|
17
|
+
|
|
18
|
+
constructor(enabled: boolean, level: LogLevel = 'error') {
|
|
19
|
+
this.enabled = enabled;
|
|
20
|
+
this.level = level;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private shouldLog(level: LogLevel): boolean {
|
|
24
|
+
return this.enabled && LOG_LEVELS[level] <= LOG_LEVELS[this.level];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
error(...args: any[]): void {
|
|
28
|
+
if (this.shouldLog('error')) {
|
|
29
|
+
console.error('[Joryio SDK]', ...args);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
warn(...args: any[]): void {
|
|
34
|
+
if (this.shouldLog('warn')) {
|
|
35
|
+
console.warn('[Joryio SDK]', ...args);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
info(...args: any[]): void {
|
|
40
|
+
if (this.shouldLog('info')) {
|
|
41
|
+
console.info('[Joryio SDK]', ...args);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
debug(...args: any[]): void {
|
|
46
|
+
if (this.shouldLog('debug')) {
|
|
47
|
+
console.debug('[Joryio SDK]', ...args);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
setEnabled(enabled: boolean): void {
|
|
52
|
+
this.enabled = enabled;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
setLevel(level: LogLevel): void {
|
|
56
|
+
this.level = level;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate UUID v4
|
|
3
|
+
* Lightweight implementation without dependencies
|
|
4
|
+
*/
|
|
5
|
+
export function generateUUID(): string {
|
|
6
|
+
// Use crypto.randomUUID if available (modern browsers)
|
|
7
|
+
if (typeof crypto !== 'undefined' && crypto.randomUUID) {
|
|
8
|
+
return crypto.randomUUID();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Fallback to Math.random based UUID v4
|
|
12
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
13
|
+
const r = (Math.random() * 16) | 0;
|
|
14
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
15
|
+
return v.toString(16);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Generate a short unique ID (for session IDs, etc.)
|
|
21
|
+
*/
|
|
22
|
+
export function generateShortId(): string {
|
|
23
|
+
if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
|
|
24
|
+
const arr = new Uint8Array(6);
|
|
25
|
+
crypto.getRandomValues(arr);
|
|
26
|
+
return Date.now().toString(36) + Array.from(arr, x => x.toString(16).padStart(2, '0')).join('');
|
|
27
|
+
}
|
|
28
|
+
return Date.now().toString(36) + Math.random().toString(36).substr(2, 9);
|
|
29
|
+
}
|