@jitsu/js 1.9.18-canary.1269.20250403142744 → 1.9.18-canary.1288.20250415192732
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/.turbo/turbo-build.log +67 -0
- package/.turbo/turbo-clean.log +5 -0
- package/.turbo/turbo-test.log +2884 -0
- package/__tests__/node/method-queue.test.ts +66 -0
- package/__tests__/node/nodejs.test.ts +306 -0
- package/__tests__/playwright/cases/anonymous-id-bug.html +26 -0
- package/__tests__/playwright/cases/basic.html +32 -0
- package/__tests__/playwright/cases/callbacks.html +21 -0
- package/__tests__/playwright/cases/cookie-names.html +22 -0
- package/__tests__/playwright/cases/disable-user-ids.html +23 -0
- package/__tests__/playwright/cases/dont-send.html +22 -0
- package/__tests__/playwright/cases/ip-policy.html +22 -0
- package/__tests__/playwright/cases/reset.html +32 -0
- package/__tests__/playwright/cases/segment-reference.html +64 -0
- package/__tests__/playwright/cases/url-bug.html +20 -0
- package/__tests__/playwright/integration.test.ts +640 -0
- package/__tests__/simple-syrup.ts +136 -0
- package/dist/jitsu.cjs.js +4 -8
- package/dist/jitsu.es.js +4 -8
- package/dist/web/p.js.txt +4 -8
- package/jest.config.js +13 -0
- package/package/README.md +9 -0
- package/package/dist/analytics-plugin.d.ts +28 -0
- package/package/dist/browser.d.ts +10 -0
- package/package/dist/index.d.ts +28 -0
- package/package/dist/jitsu.cjs.js +2100 -0
- package/package/dist/jitsu.d.ts +78 -0
- package/package/dist/jitsu.es.js +2090 -0
- package/package/dist/method-queue.d.ts +19 -0
- package/package/dist/script-loader.d.ts +8 -0
- package/package/dist/tlds.d.ts +3 -0
- package/package/dist/version.d.ts +3 -0
- package/package/dist/web/p.js.txt +2219 -0
- package/package/package.json +56 -0
- package/package.json +3 -7
- package/playwrite.config.ts +91 -0
- package/rollup.config.js +32 -0
- package/src/analytics-plugin.ts +989 -0
- package/src/browser.ts +163 -0
- package/src/destination-plugins/ga4.ts +138 -0
- package/src/destination-plugins/gtm.ts +142 -0
- package/src/destination-plugins/index.ts +61 -0
- package/src/destination-plugins/logrocket.ts +85 -0
- package/src/destination-plugins/tag.ts +85 -0
- package/src/index.ts +255 -0
- package/src/method-queue.ts +70 -0
- package/src/script-loader.ts +76 -0
- package/src/tlds.ts +27 -0
- package/src/version.ts +6 -0
- package/tsconfig.json +23 -0
- package/tsconfig.test.json +15 -0
|
@@ -0,0 +1,989 @@
|
|
|
1
|
+
/* global analytics */
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
AnalyticsClientEvent,
|
|
5
|
+
Callback,
|
|
6
|
+
DynamicJitsuOptions,
|
|
7
|
+
ErrorHandler,
|
|
8
|
+
ID,
|
|
9
|
+
JitsuOptions,
|
|
10
|
+
JSONObject,
|
|
11
|
+
Options,
|
|
12
|
+
PersistentStorage,
|
|
13
|
+
RuntimeFacade,
|
|
14
|
+
Traits,
|
|
15
|
+
} from "@jitsu/protocols/analytics";
|
|
16
|
+
import parse from "./index";
|
|
17
|
+
|
|
18
|
+
import { AnalyticsInstance, AnalyticsPlugin } from "analytics";
|
|
19
|
+
import { loadScript } from "./script-loader";
|
|
20
|
+
import { internalDestinationPlugins } from "./destination-plugins";
|
|
21
|
+
import { jitsuLibraryName, jitsuVersion } from "./version";
|
|
22
|
+
import { getTopLevelDomain } from "./tlds";
|
|
23
|
+
import * as jsondiffpatch from "jsondiffpatch";
|
|
24
|
+
|
|
25
|
+
const diff = jsondiffpatch.create();
|
|
26
|
+
|
|
27
|
+
const defaultConfig: Required<JitsuOptions> = {
|
|
28
|
+
/* Your segment writeKey */
|
|
29
|
+
writeKey: null,
|
|
30
|
+
/* Disable anonymous MTU */
|
|
31
|
+
host: null,
|
|
32
|
+
debug: false,
|
|
33
|
+
fetch: null,
|
|
34
|
+
echoEvents: false,
|
|
35
|
+
cookieDomain: undefined,
|
|
36
|
+
cookieNames: {},
|
|
37
|
+
cookieCapture: {},
|
|
38
|
+
runtime: undefined,
|
|
39
|
+
fetchTimeoutMs: undefined,
|
|
40
|
+
s2s: undefined,
|
|
41
|
+
idEndpoint: undefined,
|
|
42
|
+
errorPolicy: "log",
|
|
43
|
+
defaultPayloadContext: {},
|
|
44
|
+
privacy: {
|
|
45
|
+
dontSend: false,
|
|
46
|
+
disableUserIds: false,
|
|
47
|
+
ipPolicy: "keep",
|
|
48
|
+
consentCategories: undefined,
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// mergeConfig merges newConfig into currentConfig also making sure that all undefined values are replaced with defaultConfig values
|
|
53
|
+
const mergeConfig = (current: JitsuOptions, newConfig: JitsuOptions): void => {
|
|
54
|
+
for (const key of Object.keys(defaultConfig)) {
|
|
55
|
+
const value = newConfig[key];
|
|
56
|
+
if (key === "privacy") {
|
|
57
|
+
if (typeof value === "object") {
|
|
58
|
+
current.privacy = {
|
|
59
|
+
...defaultConfig.privacy,
|
|
60
|
+
...current.privacy,
|
|
61
|
+
...value,
|
|
62
|
+
};
|
|
63
|
+
} else if (newConfig.hasOwnProperty("privacy") && typeof value === "undefined") {
|
|
64
|
+
// explicitly set to undefined - reset to default
|
|
65
|
+
current.privacy = { ...defaultConfig.privacy };
|
|
66
|
+
}
|
|
67
|
+
} else if (typeof value === "undefined") {
|
|
68
|
+
if (newConfig.hasOwnProperty(key) || !current.hasOwnProperty(key)) {
|
|
69
|
+
// explicitly set to undefined - reset to default
|
|
70
|
+
// or was not set at all - set to default
|
|
71
|
+
current[key] = defaultConfig[key];
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
current[key] = value;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export const parseQuery = (qs?: string): Record<string, string> => {
|
|
80
|
+
if (!qs) {
|
|
81
|
+
return {};
|
|
82
|
+
}
|
|
83
|
+
let queryString = qs.length > 0 && qs.charAt(0) === "?" ? qs.substring(1) : qs;
|
|
84
|
+
let query: Record<string, string> = {};
|
|
85
|
+
let pairs = (queryString[0] === "?" ? queryString.substr(1) : queryString).split("&");
|
|
86
|
+
for (let i = 0; i < pairs.length; i++) {
|
|
87
|
+
let pair = pairs[i].split("=");
|
|
88
|
+
query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || "");
|
|
89
|
+
}
|
|
90
|
+
return query;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
function utmToKey(key) {
|
|
94
|
+
const name = key.substring("utm_".length);
|
|
95
|
+
return name === "campaign" ? "name" : name;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function parseUtms(query: Record<string, string>) {
|
|
99
|
+
return Object.entries(query)
|
|
100
|
+
.filter(([key]) => key.indexOf("utm_") === 0)
|
|
101
|
+
.reduce(
|
|
102
|
+
(acc, [key, value]) => ({
|
|
103
|
+
...acc,
|
|
104
|
+
[utmToKey(key)]: value,
|
|
105
|
+
}),
|
|
106
|
+
{}
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function safeCall<T>(f: () => T, defaultVal?: T): T | undefined {
|
|
111
|
+
try {
|
|
112
|
+
return f();
|
|
113
|
+
} catch (e) {
|
|
114
|
+
return defaultVal;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function restoreTraits(storage: PersistentStorage) {
|
|
119
|
+
let val = storage.getItem("__user_traits");
|
|
120
|
+
if (typeof val === "string") {
|
|
121
|
+
val = safeCall(() => JSON.parse(val), {});
|
|
122
|
+
}
|
|
123
|
+
if (typeof val !== "object" || val === null || Array.isArray(val)) {
|
|
124
|
+
val = {};
|
|
125
|
+
}
|
|
126
|
+
let groupVal = storage.getItem("__group_traits");
|
|
127
|
+
if (typeof groupVal === "string") {
|
|
128
|
+
groupVal = safeCall(() => JSON.parse(groupVal), {});
|
|
129
|
+
}
|
|
130
|
+
if (typeof groupVal !== "object" || groupVal === null || Array.isArray(groupVal)) {
|
|
131
|
+
groupVal = {};
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
...(groupVal || {}),
|
|
135
|
+
...(val || {}), //user traits override group traits
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export type StorageFactory = (cookieDomain: string, key2Cookie: (key: string) => string) => PersistentStorage;
|
|
140
|
+
|
|
141
|
+
function getCookie(name: string) {
|
|
142
|
+
const value = `; ${document.cookie}`;
|
|
143
|
+
const parts = value.split(`; ${name}=`);
|
|
144
|
+
return parts.length === 2 ? parts.pop().split(";").shift() : undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function getClientIds(runtime: RuntimeFacade, customCookieCapture: Record<string, string>) {
|
|
148
|
+
const cookieCapture = {
|
|
149
|
+
fbc: "_fbc",
|
|
150
|
+
fbp: "_fbp",
|
|
151
|
+
...customCookieCapture,
|
|
152
|
+
};
|
|
153
|
+
const clientIds = Object.entries(cookieCapture).reduce((acc, [key, cookieName]) => {
|
|
154
|
+
acc[key] = runtime.getCookie(cookieName);
|
|
155
|
+
return acc;
|
|
156
|
+
}, {});
|
|
157
|
+
return {
|
|
158
|
+
...clientIds,
|
|
159
|
+
...getGa4Ids(runtime),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function getGa4Ids(runtime: RuntimeFacade) {
|
|
164
|
+
const allCookies = runtime.getCookies();
|
|
165
|
+
const clientId = allCookies["_ga"]?.split(".").slice(-2).join(".");
|
|
166
|
+
const gaSessionCookies = Object.entries(allCookies).filter(([key]) => key.startsWith("_ga_"));
|
|
167
|
+
const sessionIds =
|
|
168
|
+
gaSessionCookies.length > 0
|
|
169
|
+
? Object.fromEntries(
|
|
170
|
+
gaSessionCookies
|
|
171
|
+
.map(([key, value]) => {
|
|
172
|
+
if (typeof value !== "string") {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
const parts = value.split(".");
|
|
176
|
+
if (parts.length < 3) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
return [key.substring("_ga_".length), parts[2]];
|
|
180
|
+
})
|
|
181
|
+
.filter(v => v !== null)
|
|
182
|
+
)
|
|
183
|
+
: undefined;
|
|
184
|
+
if (clientId || sessionIds) {
|
|
185
|
+
return { ga4: { clientId, sessionIds } };
|
|
186
|
+
} else {
|
|
187
|
+
return undefined;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function removeCookie(name: string, { domain, secure }: { domain: string; secure: boolean }) {
|
|
192
|
+
document.cookie =
|
|
193
|
+
name +
|
|
194
|
+
"=;domain=" +
|
|
195
|
+
domain +
|
|
196
|
+
";path=/" +
|
|
197
|
+
";expires=Thu, 01 Jan 1970 00:00:01 GMT;SameSite=" +
|
|
198
|
+
(secure ? "None" : "Lax") +
|
|
199
|
+
(secure ? ";secure" : "");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function setCookie(name: string, val: string, { domain, secure }: { domain: string; secure: boolean }) {
|
|
203
|
+
document.cookie =
|
|
204
|
+
name +
|
|
205
|
+
"=" +
|
|
206
|
+
val +
|
|
207
|
+
";domain=" +
|
|
208
|
+
domain +
|
|
209
|
+
";path=/" +
|
|
210
|
+
";expires=" +
|
|
211
|
+
new Date(new Date().getTime() + 1000 * 60 * 60 * 24 * 365 * 5).toUTCString() +
|
|
212
|
+
";SameSite=" +
|
|
213
|
+
(secure ? "None" : "Lax") +
|
|
214
|
+
(secure ? ";secure" : "");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const defaultCookie2Key = {
|
|
218
|
+
__anon_id: "__eventn_id",
|
|
219
|
+
__user_id: "__eventn_uid",
|
|
220
|
+
__user_traits: "__eventn_id_usr",
|
|
221
|
+
__group_id: "__group_id",
|
|
222
|
+
__group_traits: "__group_traits",
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const cookieStorage: StorageFactory = (cookieDomain, key2cookie) => {
|
|
226
|
+
return {
|
|
227
|
+
setItem(key: string, val: any) {
|
|
228
|
+
const cookieName = key2cookie(key) || key;
|
|
229
|
+
if (typeof val === "undefined") {
|
|
230
|
+
removeCookie(cookieName, {
|
|
231
|
+
domain: cookieDomain,
|
|
232
|
+
secure: window.location.protocol === "https:",
|
|
233
|
+
});
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
const strVal = typeof val === "object" && val !== null ? encodeURIComponent(JSON.stringify(val)) : val;
|
|
237
|
+
setCookie(cookieName, strVal, {
|
|
238
|
+
domain: cookieDomain,
|
|
239
|
+
secure: window.location.protocol === "https:",
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
getItem(key: string) {
|
|
243
|
+
const cookieName = key2cookie(key) || key;
|
|
244
|
+
const result = getCookie(cookieName);
|
|
245
|
+
if (key === "__anon_id") {
|
|
246
|
+
//anonymous id must always be a string, so we don't parse it to preserve its exact value
|
|
247
|
+
return result;
|
|
248
|
+
}
|
|
249
|
+
if (typeof result === "undefined" && key === "__user_id") {
|
|
250
|
+
//backward compatibility with old jitsu cookie. get user id if from traits
|
|
251
|
+
const traits = parse(getCookie(key2cookie("__user_traits") || "__eventn_id_usr")) || {};
|
|
252
|
+
return traits.internal_id || traits.user_id || traits.id || traits.userId;
|
|
253
|
+
}
|
|
254
|
+
return parse(result);
|
|
255
|
+
},
|
|
256
|
+
removeItem(key: string) {
|
|
257
|
+
removeCookie(key2cookie(key) || key, {
|
|
258
|
+
domain: cookieDomain,
|
|
259
|
+
secure: window.location.protocol === "https:",
|
|
260
|
+
});
|
|
261
|
+
},
|
|
262
|
+
reset() {
|
|
263
|
+
for (const key of Object.keys(defaultCookie2Key)) {
|
|
264
|
+
removeCookie(key2cookie(key) || key, {
|
|
265
|
+
domain: cookieDomain,
|
|
266
|
+
secure: window.location.protocol === "https:",
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
export function windowRuntime(opts: JitsuOptions): RuntimeFacade {
|
|
274
|
+
const key2Cookie = (key: string) => {
|
|
275
|
+
switch (key) {
|
|
276
|
+
case "__anon_id":
|
|
277
|
+
return opts.cookieNames?.anonymousId || defaultCookie2Key.__anon_id;
|
|
278
|
+
case "__user_id":
|
|
279
|
+
return opts.cookieNames?.userId || defaultCookie2Key.__user_id;
|
|
280
|
+
case "__user_traits":
|
|
281
|
+
return opts.cookieNames?.userTraits || defaultCookie2Key.__user_traits;
|
|
282
|
+
case "__group_id":
|
|
283
|
+
return opts.cookieNames?.groupId || defaultCookie2Key.__group_id;
|
|
284
|
+
case "__group_traits":
|
|
285
|
+
return opts.cookieNames?.groupTraits || defaultCookie2Key.__group_traits;
|
|
286
|
+
default:
|
|
287
|
+
return key;
|
|
288
|
+
}
|
|
289
|
+
};
|
|
290
|
+
return {
|
|
291
|
+
getCookie(name: string): string | undefined {
|
|
292
|
+
const value = `; ${document.cookie}`;
|
|
293
|
+
const parts = value.split(`; ${name}=`);
|
|
294
|
+
return parts.length === 2 ? parts.pop().split(";").shift() : undefined;
|
|
295
|
+
},
|
|
296
|
+
getCookies(): Record<string, string> {
|
|
297
|
+
const value = `; ${document.cookie}`;
|
|
298
|
+
const cookies: Record<string, string> = {};
|
|
299
|
+
const matches = value.matchAll(/(\w+)=([^;]+)/g);
|
|
300
|
+
for (const match of matches) {
|
|
301
|
+
cookies[match[1]] = match[2];
|
|
302
|
+
}
|
|
303
|
+
return cookies;
|
|
304
|
+
},
|
|
305
|
+
documentEncoding(): string | undefined {
|
|
306
|
+
return window.document.characterSet;
|
|
307
|
+
},
|
|
308
|
+
timezoneOffset(): number | undefined {
|
|
309
|
+
return new Date().getTimezoneOffset();
|
|
310
|
+
},
|
|
311
|
+
store(): PersistentStorage {
|
|
312
|
+
return cookieStorage(opts.cookieDomain || getTopLevelDomain(window.location.hostname), key2Cookie);
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
language(): string {
|
|
316
|
+
return window.navigator.language;
|
|
317
|
+
},
|
|
318
|
+
pageTitle(): string {
|
|
319
|
+
return window.document.title;
|
|
320
|
+
},
|
|
321
|
+
pageUrl(): string {
|
|
322
|
+
return window.location.href;
|
|
323
|
+
},
|
|
324
|
+
referrer(): string {
|
|
325
|
+
return window.document.referrer;
|
|
326
|
+
},
|
|
327
|
+
screen(): { width: number; height: number; innerWidth: number; innerHeight: number; density: number } {
|
|
328
|
+
return {
|
|
329
|
+
width: window.screen.width,
|
|
330
|
+
height: window.screen.height,
|
|
331
|
+
innerWidth: window.innerWidth,
|
|
332
|
+
innerHeight: window.innerHeight,
|
|
333
|
+
density: Math.floor(window.devicePixelRatio),
|
|
334
|
+
};
|
|
335
|
+
},
|
|
336
|
+
userAgent(): string {
|
|
337
|
+
return window.navigator.userAgent;
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export const emptyRuntime = (config: JitsuOptions): RuntimeFacade => ({
|
|
343
|
+
documentEncoding(): string | undefined {
|
|
344
|
+
return undefined;
|
|
345
|
+
},
|
|
346
|
+
timezoneOffset(): number | undefined {
|
|
347
|
+
return undefined;
|
|
348
|
+
},
|
|
349
|
+
getCookie(name: string): string | undefined {
|
|
350
|
+
return undefined;
|
|
351
|
+
},
|
|
352
|
+
getCookies(): Record<string, string> {
|
|
353
|
+
return {};
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
store(): PersistentStorage {
|
|
357
|
+
const storage = {};
|
|
358
|
+
return {
|
|
359
|
+
reset(): void {
|
|
360
|
+
Object.keys(storage).forEach(key => delete storage[key]);
|
|
361
|
+
},
|
|
362
|
+
setItem(key: string, val: any) {
|
|
363
|
+
if (config.debug) {
|
|
364
|
+
console.log(`[JITSU EMPTY RUNTIME] Set storage item ${key}=${JSON.stringify(val)}`);
|
|
365
|
+
}
|
|
366
|
+
if (typeof val === "undefined") {
|
|
367
|
+
delete storage[key];
|
|
368
|
+
} else {
|
|
369
|
+
storage[key] = val;
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
getItem(key: string) {
|
|
373
|
+
const val = storage[key];
|
|
374
|
+
if (config.debug) {
|
|
375
|
+
console.log(`[JITSU EMPTY RUNTIME] Get storage item ${key}=${JSON.stringify(val)}`);
|
|
376
|
+
}
|
|
377
|
+
return val;
|
|
378
|
+
},
|
|
379
|
+
removeItem(key: string) {
|
|
380
|
+
if (config.debug) {
|
|
381
|
+
console.log(`[JITSU EMPTY RUNTIME] Get storage item ${key}=${storage[key]}`);
|
|
382
|
+
}
|
|
383
|
+
delete storage[key];
|
|
384
|
+
},
|
|
385
|
+
};
|
|
386
|
+
},
|
|
387
|
+
language() {
|
|
388
|
+
return undefined;
|
|
389
|
+
},
|
|
390
|
+
pageTitle() {
|
|
391
|
+
return undefined;
|
|
392
|
+
},
|
|
393
|
+
pageUrl() {
|
|
394
|
+
return undefined;
|
|
395
|
+
},
|
|
396
|
+
referrer() {
|
|
397
|
+
return undefined;
|
|
398
|
+
},
|
|
399
|
+
screen() {
|
|
400
|
+
return undefined;
|
|
401
|
+
},
|
|
402
|
+
userAgent() {
|
|
403
|
+
return undefined;
|
|
404
|
+
},
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
function deepMerge(target: any, source: any) {
|
|
408
|
+
if (typeof source !== "object" || source === null) {
|
|
409
|
+
return source;
|
|
410
|
+
}
|
|
411
|
+
if (typeof target !== "object" || target === null) {
|
|
412
|
+
return source;
|
|
413
|
+
}
|
|
414
|
+
return Object.entries(source).reduce((acc, [key, value]) => {
|
|
415
|
+
acc[key] = deepMerge(target[key], value);
|
|
416
|
+
return acc;
|
|
417
|
+
}, target);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export function isInBrowser() {
|
|
421
|
+
return typeof document !== "undefined" && typeof window !== "undefined";
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Fixes a weird bug in analytics URL where path
|
|
426
|
+
* of https://test.com becomes //test.com
|
|
427
|
+
*/
|
|
428
|
+
function fixPath(path: string): string {
|
|
429
|
+
if (path.indexOf("//") === 0 && path.lastIndexOf("/") === 1) {
|
|
430
|
+
return "/";
|
|
431
|
+
}
|
|
432
|
+
return path;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const hashRegex = /#.*$/;
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* for compatibility with path produced by analytics.js
|
|
439
|
+
* @param url
|
|
440
|
+
*/
|
|
441
|
+
function urlPath(url) {
|
|
442
|
+
const regex = /(http[s]?:\/\/)?([^\/\s]+\/)(.*)/g;
|
|
443
|
+
const matches = regex.exec(url);
|
|
444
|
+
const pathMatch = matches && matches[3] ? matches[3].split("?")[0].replace(hashRegex, "") : "";
|
|
445
|
+
return "/" + pathMatch;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function adjustPayload(
|
|
449
|
+
payload: any,
|
|
450
|
+
config: JitsuOptions,
|
|
451
|
+
storage: PersistentStorage,
|
|
452
|
+
s2s: boolean
|
|
453
|
+
): AnalyticsClientEvent {
|
|
454
|
+
const runtime: RuntimeFacade = config.runtime || (isInBrowser() ? windowRuntime(config) : emptyRuntime(config));
|
|
455
|
+
const url = runtime.pageUrl();
|
|
456
|
+
const parsedUrl = safeCall(() => new URL(url), undefined);
|
|
457
|
+
const query = parsedUrl ? parseQuery(parsedUrl.search) : {};
|
|
458
|
+
const properties = payload.properties || {};
|
|
459
|
+
|
|
460
|
+
if (payload.type === "page" && (properties.url || url)) {
|
|
461
|
+
const targetUrl = properties.url || url;
|
|
462
|
+
properties.url = targetUrl.replace(hashRegex, "");
|
|
463
|
+
properties.path = fixPath(urlPath(targetUrl));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const customContext = deepMerge(
|
|
467
|
+
config.defaultPayloadContext,
|
|
468
|
+
payload.properties?.context || payload.options?.context || {}
|
|
469
|
+
);
|
|
470
|
+
delete payload.properties?.context;
|
|
471
|
+
const referrer = runtime.referrer();
|
|
472
|
+
const context: AnalyticsClientEvent["context"] = {
|
|
473
|
+
library: {
|
|
474
|
+
name: jitsuLibraryName,
|
|
475
|
+
version: jitsuVersion,
|
|
476
|
+
env: isInBrowser() ? "browser" : "node",
|
|
477
|
+
},
|
|
478
|
+
consent: config.privacy?.consentCategories
|
|
479
|
+
? {
|
|
480
|
+
categoryPreferences: config.privacy.consentCategories,
|
|
481
|
+
}
|
|
482
|
+
: undefined,
|
|
483
|
+
userAgent: runtime.userAgent(),
|
|
484
|
+
locale: runtime.language(),
|
|
485
|
+
screen: runtime.screen(),
|
|
486
|
+
traits: payload.type != "identify" && payload.type != "group" ? { ...(restoreTraits(storage) || {}) } : undefined,
|
|
487
|
+
page: {
|
|
488
|
+
path: properties.path || (parsedUrl && parsedUrl.pathname),
|
|
489
|
+
referrer: referrer,
|
|
490
|
+
referring_domain: safeCall(() => referrer && new URL(referrer).hostname),
|
|
491
|
+
host: parsedUrl && parsedUrl.host,
|
|
492
|
+
search: properties.search || (parsedUrl && parsedUrl.search),
|
|
493
|
+
title: properties.title || runtime.pageTitle(),
|
|
494
|
+
url: properties.url || url,
|
|
495
|
+
encoding: properties.encoding || runtime.documentEncoding(),
|
|
496
|
+
},
|
|
497
|
+
clientIds: !config.privacy?.disableUserIds ? getClientIds(runtime, config.cookieCapture) : undefined,
|
|
498
|
+
campaign: parseUtms(query),
|
|
499
|
+
};
|
|
500
|
+
const withContext = {
|
|
501
|
+
...payload,
|
|
502
|
+
timestamp: new Date().toISOString(),
|
|
503
|
+
sentAt: new Date().toISOString(),
|
|
504
|
+
messageId: randomId(properties.path || (parsedUrl && parsedUrl.pathname)),
|
|
505
|
+
writeKey: maskWriteKey(config.writeKey),
|
|
506
|
+
groupId: storage.getItem("__group_id"),
|
|
507
|
+
context: deepMerge(context, customContext),
|
|
508
|
+
};
|
|
509
|
+
delete withContext.meta;
|
|
510
|
+
delete withContext.options;
|
|
511
|
+
if (config.privacy?.disableUserIds) {
|
|
512
|
+
delete withContext.userId;
|
|
513
|
+
delete withContext.anonymousId;
|
|
514
|
+
delete withContext.context.traits;
|
|
515
|
+
delete withContext.groupId;
|
|
516
|
+
}
|
|
517
|
+
return withContext;
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
export type DestinationDescriptor = {
|
|
521
|
+
id: string;
|
|
522
|
+
destinationType: string;
|
|
523
|
+
credentials: any;
|
|
524
|
+
options: any;
|
|
525
|
+
newEvents?: any[];
|
|
526
|
+
deviceOptions: DeviceOptions;
|
|
527
|
+
};
|
|
528
|
+
export type AnalyticsPluginDescriptor = {
|
|
529
|
+
type: "analytics-plugin";
|
|
530
|
+
packageCdn: string;
|
|
531
|
+
moduleVarName: string;
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
export type InternalPluginDescriptor = {
|
|
535
|
+
type: "internal-plugin";
|
|
536
|
+
name: string;
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
export type DeviceOptions = AnalyticsPluginDescriptor | InternalPluginDescriptor;
|
|
540
|
+
|
|
541
|
+
function isDiff(obj: any) {
|
|
542
|
+
const keys = Object.keys(obj);
|
|
543
|
+
return keys.length === 1 && keys[0] === "__diff";
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
async function processDestinations(
|
|
547
|
+
destinations: DestinationDescriptor[],
|
|
548
|
+
method: string,
|
|
549
|
+
originalEvent: AnalyticsClientEvent,
|
|
550
|
+
debug: boolean,
|
|
551
|
+
analyticsInstance: AnalyticsInstance
|
|
552
|
+
) {
|
|
553
|
+
const promises: Promise<any>[] = [];
|
|
554
|
+
|
|
555
|
+
for (const destination of destinations) {
|
|
556
|
+
let newEvents = [originalEvent];
|
|
557
|
+
if (destination.newEvents) {
|
|
558
|
+
try {
|
|
559
|
+
newEvents = destination.newEvents.map(e =>
|
|
560
|
+
e === "same" ? originalEvent : isDiff(e) ? diff.patch(originalEvent, e.__diff) : e
|
|
561
|
+
);
|
|
562
|
+
} catch (e) {
|
|
563
|
+
console.error(`[JITSU] Error applying '${destination.id}' changes to event: ${e?.message}`, e);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
const credentials = { ...destination.credentials, ...destination.options };
|
|
567
|
+
|
|
568
|
+
if (destination.deviceOptions.type === "internal-plugin") {
|
|
569
|
+
const plugin = internalDestinationPlugins[destination.deviceOptions.name];
|
|
570
|
+
if (plugin) {
|
|
571
|
+
for (const event of newEvents) {
|
|
572
|
+
try {
|
|
573
|
+
promises.push(plugin.handle({ ...credentials, debug }, event));
|
|
574
|
+
} catch (e) {
|
|
575
|
+
console.warn(
|
|
576
|
+
`[JITSU] Error processing event with internal plugin '${destination.deviceOptions.name}': ${e?.message}`,
|
|
577
|
+
e
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
} else {
|
|
582
|
+
console.warn(
|
|
583
|
+
`[JITSU] Unknown internal plugin '${destination.deviceOptions.name}' for destination '${destination.id}'`
|
|
584
|
+
);
|
|
585
|
+
}
|
|
586
|
+
} else if (destination.deviceOptions.type === "analytics-plugin") {
|
|
587
|
+
await loadScript(destination.deviceOptions.packageCdn);
|
|
588
|
+
const plugin = window[destination.deviceOptions.moduleVarName];
|
|
589
|
+
if (!plugin) {
|
|
590
|
+
console.warn(
|
|
591
|
+
`[JITSU] Broken plugin '${destination.deviceOptions.packageCdn}' for destination '${destination.id}' - it doesn't export '${destination.deviceOptions.moduleVarName}' variable`
|
|
592
|
+
);
|
|
593
|
+
} else {
|
|
594
|
+
let pluginInstance: any;
|
|
595
|
+
try {
|
|
596
|
+
pluginInstance = (typeof plugin === "function" ? plugin : plugin.init)(credentials);
|
|
597
|
+
} catch (e) {
|
|
598
|
+
console.warn(
|
|
599
|
+
`[JITSU] Error creating plugin '${destination.deviceOptions.moduleVarName}@${destination.deviceOptions.packageCdn}' for destination '${destination.id}': ${e?.message}`,
|
|
600
|
+
e
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
try {
|
|
604
|
+
if (debug) {
|
|
605
|
+
console.log(
|
|
606
|
+
`[JITSU] Plugin '${destination.deviceOptions.moduleVarName}@${destination.deviceOptions.packageCdn}' for destination '${destination.id}' initialized with config:`,
|
|
607
|
+
pluginInstance.config
|
|
608
|
+
);
|
|
609
|
+
}
|
|
610
|
+
pluginInstance.initialize({ config: pluginInstance.config, instance: analyticsInstance });
|
|
611
|
+
} catch (e) {
|
|
612
|
+
console.warn(
|
|
613
|
+
`[JITSU] Error initializing plugin '${destination.deviceOptions.moduleVarName}@${
|
|
614
|
+
destination.deviceOptions.packageCdn
|
|
615
|
+
}' for destination '${destination.id}': ${e?.message}. Config: ${JSON.stringify(pluginInstance.config)}`,
|
|
616
|
+
e
|
|
617
|
+
);
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
if (pluginInstance[method]) {
|
|
622
|
+
for (const event of newEvents) {
|
|
623
|
+
try {
|
|
624
|
+
pluginInstance[method]({
|
|
625
|
+
payload: event,
|
|
626
|
+
config: pluginInstance.config,
|
|
627
|
+
instance: analyticsInstance,
|
|
628
|
+
});
|
|
629
|
+
} catch (e) {
|
|
630
|
+
console.warn(
|
|
631
|
+
`[JITSU] Error processing ${method}() with plugin '${destination.deviceOptions.moduleVarName}@${destination.deviceOptions.packageCdn}' for destination '${destination.id}': ${e?.message}`,
|
|
632
|
+
e
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
function looksLikeCuid(id: string) {
|
|
643
|
+
return id.length === 25 && id.charAt(0) === "c";
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
function validateWriteKey(writeKey?: string): string | undefined {
|
|
647
|
+
if (writeKey) {
|
|
648
|
+
const [, secret] = writeKey.split(":", 2);
|
|
649
|
+
if (!secret && !looksLikeCuid(writeKey)) {
|
|
650
|
+
throw new Error(
|
|
651
|
+
`Legacy write key detected - ${writeKey}! This format doesn't work anymore, it should be 'key:secret'. Please download a new key from Jitsu UI`
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
return writeKey;
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
function maskWriteKey(writeKey?: string): string | undefined {
|
|
659
|
+
if (writeKey) {
|
|
660
|
+
const [id, secret] = writeKey.split(":", 2);
|
|
661
|
+
if (secret) {
|
|
662
|
+
return `${id}:***`;
|
|
663
|
+
} else {
|
|
664
|
+
return "***";
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
return writeKey;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function getErrorHandler(opts: JitsuOptions): ErrorHandler {
|
|
671
|
+
const configuredHandler = opts.errorPolicy || "log";
|
|
672
|
+
if (typeof configuredHandler === "function") {
|
|
673
|
+
return configuredHandler;
|
|
674
|
+
} else if (configuredHandler === "rethrow") {
|
|
675
|
+
return (msg, ...args) => {
|
|
676
|
+
//ignore args, not clear what to do with them
|
|
677
|
+
throw new Error(msg);
|
|
678
|
+
};
|
|
679
|
+
} else {
|
|
680
|
+
return (msg, ...args) => {
|
|
681
|
+
console.error(msg, ...args);
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
async function send(
|
|
687
|
+
method,
|
|
688
|
+
payload,
|
|
689
|
+
jitsuConfig: JitsuOptions,
|
|
690
|
+
instance: AnalyticsInstance,
|
|
691
|
+
store: PersistentStorage
|
|
692
|
+
): Promise<any> {
|
|
693
|
+
const s2s = !!jitsuConfig.s2s;
|
|
694
|
+
const debugHeader = jitsuConfig.debug ? { "X-Enable-Debug": "true" } : {};
|
|
695
|
+
const adjustedPayload = adjustPayload(payload, jitsuConfig, store, s2s);
|
|
696
|
+
if (jitsuConfig.echoEvents) {
|
|
697
|
+
console.log(`[JITSU DEBUG] sending '${method}' event:`, adjustedPayload);
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
const url = s2s ? `${jitsuConfig.host}/api/s/s2s/${method}` : `${jitsuConfig.host}/api/s/${method}`;
|
|
701
|
+
const fetch = jitsuConfig.fetch || globalThis.fetch;
|
|
702
|
+
if (!fetch) {
|
|
703
|
+
//don't run it through error handler since error is critical and should be addressed
|
|
704
|
+
throw new Error(
|
|
705
|
+
"Please specify fetch function in jitsu plugin initialization, fetch isn't available in global scope"
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
const abortController = jitsuConfig.fetchTimeoutMs ? new AbortController() : undefined;
|
|
709
|
+
const abortTimeout = jitsuConfig.fetchTimeoutMs
|
|
710
|
+
? setTimeout(() => {
|
|
711
|
+
abortController.abort();
|
|
712
|
+
}, jitsuConfig.fetchTimeoutMs)
|
|
713
|
+
: undefined;
|
|
714
|
+
|
|
715
|
+
const authHeader = jitsuConfig.writeKey ? { "X-Write-Key": jitsuConfig.writeKey } : {};
|
|
716
|
+
const ipHeader =
|
|
717
|
+
typeof jitsuConfig.privacy?.ipPolicy === "undefined" || jitsuConfig.privacy?.ipPolicy === "keep"
|
|
718
|
+
? {}
|
|
719
|
+
: { "X-IP-Policy": jitsuConfig.privacy.ipPolicy };
|
|
720
|
+
let fetchResult;
|
|
721
|
+
try {
|
|
722
|
+
fetchResult = await fetch(url, {
|
|
723
|
+
method: "POST",
|
|
724
|
+
headers: {
|
|
725
|
+
"Content-Type": "application/json",
|
|
726
|
+
...authHeader,
|
|
727
|
+
...debugHeader,
|
|
728
|
+
...ipHeader,
|
|
729
|
+
},
|
|
730
|
+
body: JSON.stringify(adjustedPayload),
|
|
731
|
+
signal: abortController?.signal,
|
|
732
|
+
});
|
|
733
|
+
if (abortTimeout) {
|
|
734
|
+
clearTimeout(abortTimeout);
|
|
735
|
+
}
|
|
736
|
+
} catch (e: any) {
|
|
737
|
+
getErrorHandler(jitsuConfig)(`Call to ${url} failed with error ${e.message}`);
|
|
738
|
+
return Promise.resolve();
|
|
739
|
+
}
|
|
740
|
+
let responseText;
|
|
741
|
+
try {
|
|
742
|
+
responseText = await fetchResult.text();
|
|
743
|
+
} catch (e) {
|
|
744
|
+
console.warn(
|
|
745
|
+
`Can't read response text from ${url} (status - ${fetchResult.status} ${fetchResult.statusText}): ${e?.message}`
|
|
746
|
+
);
|
|
747
|
+
}
|
|
748
|
+
if (jitsuConfig.debug) {
|
|
749
|
+
console.log(
|
|
750
|
+
`[JITSU DEBUG] ${url} replied ${fetchResult.status}: ${responseText}. Original payload:\n${JSON.stringify(
|
|
751
|
+
adjustedPayload,
|
|
752
|
+
null,
|
|
753
|
+
2
|
|
754
|
+
)}`
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
if (!fetchResult.ok) {
|
|
758
|
+
getErrorHandler(jitsuConfig)(
|
|
759
|
+
`Call to ${url} failed with error: ${fetchResult.status} - ${fetchResult.statusText}: ${responseText}`
|
|
760
|
+
);
|
|
761
|
+
return Promise.resolve();
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
let responseJson: any;
|
|
765
|
+
try {
|
|
766
|
+
responseJson = JSON.parse(responseText);
|
|
767
|
+
} catch (e) {
|
|
768
|
+
getErrorHandler(jitsuConfig)(`Can't parse JSON: ${responseText}: ${e?.message}`);
|
|
769
|
+
return Promise.resolve();
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
if (responseJson.destinations && responseJson.destinations.length > 0) {
|
|
773
|
+
if (jitsuConfig.s2s) {
|
|
774
|
+
console.warn(
|
|
775
|
+
`[JITSU] ${payload.type} responded with list of ${responseJson.destinations.length} destinations. However, this code is running in server-to-server mode, so destinations will be ignored`,
|
|
776
|
+
jitsuConfig.debug ? JSON.stringify(responseJson.destinations, null, 2) : undefined
|
|
777
|
+
);
|
|
778
|
+
} else {
|
|
779
|
+
//double protection, ingest should not return destinations in s2s mode
|
|
780
|
+
if (isInBrowser()) {
|
|
781
|
+
if (jitsuConfig.debug) {
|
|
782
|
+
console.log(`[JITSU] Processing device destinations: `, JSON.stringify(responseJson.destinations, null, 2));
|
|
783
|
+
}
|
|
784
|
+
return processDestinations(responseJson.destinations, method, adjustedPayload, !!jitsuConfig.debug, instance);
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
return adjustedPayload;
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
const controllingTraits = ["$doNotSend"] as const;
|
|
792
|
+
/**
|
|
793
|
+
* Remove all members of traits that controls identify/group behavior (see analytics.d.ts), and should not be recorded. Returns
|
|
794
|
+
* copy of the object with these members removed.
|
|
795
|
+
*
|
|
796
|
+
* Do not modify traits object, but creates one
|
|
797
|
+
* @param traits
|
|
798
|
+
*/
|
|
799
|
+
function stripControllingTraits(traits: Traits): Exclude<Traits, (typeof controllingTraits)[number]> {
|
|
800
|
+
const res = { ...traits };
|
|
801
|
+
// see Traits definition in analytics.d.ts. We cannot define const here, so here's a little code duplication
|
|
802
|
+
for (const key of controllingTraits) {
|
|
803
|
+
delete res[key];
|
|
804
|
+
}
|
|
805
|
+
return res;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
export const jitsuAnalyticsPlugin = (jitsuOptions: JitsuOptions = {}, storage: PersistentStorage): AnalyticsPlugin => {
|
|
809
|
+
// just to make sure that all undefined values are replaced with defaultConfig values
|
|
810
|
+
mergeConfig(jitsuOptions, jitsuOptions);
|
|
811
|
+
return {
|
|
812
|
+
name: "jitsu",
|
|
813
|
+
config: jitsuOptions,
|
|
814
|
+
|
|
815
|
+
initialize: async args => {
|
|
816
|
+
const { config } = args;
|
|
817
|
+
if (config.debug) {
|
|
818
|
+
console.debug("[JITSU DEBUG] Initializing Jitsu plugin with config: ", JSON.stringify(config, null, 2));
|
|
819
|
+
}
|
|
820
|
+
if (!config.host && !config.echoEvents) {
|
|
821
|
+
throw new Error("Please specify host variable in jitsu plugin initialization, or set echoEvents to true");
|
|
822
|
+
}
|
|
823
|
+
validateWriteKey(config.writeKey);
|
|
824
|
+
|
|
825
|
+
if (config.idEndpoint) {
|
|
826
|
+
if (!isInBrowser()) {
|
|
827
|
+
console.error(`[JITSU] 'idEndpoint' option can be used only in browser environment`);
|
|
828
|
+
return;
|
|
829
|
+
}
|
|
830
|
+
try {
|
|
831
|
+
const fetch = config.fetch || globalThis.fetch;
|
|
832
|
+
const controller = new AbortController();
|
|
833
|
+
setTimeout(() => controller.abort(), 1000);
|
|
834
|
+
const domain = config.cookieDomain || getTopLevelDomain(window.location.hostname);
|
|
835
|
+
const res = await fetch(config.idEndpoint + "?domain=" + encodeURIComponent(domain), {
|
|
836
|
+
credentials: "include",
|
|
837
|
+
signal: controller.signal,
|
|
838
|
+
});
|
|
839
|
+
if (!res.ok) {
|
|
840
|
+
console.error(`[JITSU] Can't fetch idEndpoint: ${res.status} - ${res.statusText}`);
|
|
841
|
+
} else if (config.debug) {
|
|
842
|
+
console.log(`[JITSU DEBUG] Fetch idEndpoint: ${res.status}`);
|
|
843
|
+
}
|
|
844
|
+
} catch (e) {
|
|
845
|
+
console.error(`[JITSU] Can't fetch idEndpoint: ${e.message}`);
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
},
|
|
849
|
+
page: args => {
|
|
850
|
+
const { payload, config, instance } = args;
|
|
851
|
+
if (config.privacy?.dontSend) {
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
return send("page", payload, config, instance, storage);
|
|
855
|
+
},
|
|
856
|
+
track: args => {
|
|
857
|
+
const { payload, config, instance } = args;
|
|
858
|
+
if (config.privacy?.dontSend) {
|
|
859
|
+
return;
|
|
860
|
+
}
|
|
861
|
+
return send("track", payload, config, instance, storage);
|
|
862
|
+
},
|
|
863
|
+
identify: args => {
|
|
864
|
+
const { payload, config, instance } = args;
|
|
865
|
+
if (config.privacy?.dontSend || config.privacy?.disableUserIds) {
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
// Store traits in cache to be able to use them in page and track events that run asynchronously with current identify.
|
|
869
|
+
storage.setItem("__user_id", payload.userId);
|
|
870
|
+
const doNotSend = payload.traits?.$doNotSend;
|
|
871
|
+
if (payload.traits && typeof payload.traits === "object") {
|
|
872
|
+
payload.traits = stripControllingTraits(payload.traits);
|
|
873
|
+
storage.setItem("__user_traits", payload.traits);
|
|
874
|
+
}
|
|
875
|
+
if (doNotSend) {
|
|
876
|
+
return Promise.resolve();
|
|
877
|
+
}
|
|
878
|
+
return send("identify", payload, config, instance, storage);
|
|
879
|
+
},
|
|
880
|
+
reset: args => {
|
|
881
|
+
const { config, instance } = args;
|
|
882
|
+
storage.reset();
|
|
883
|
+
if (config.debug) {
|
|
884
|
+
console.log("[JITSU DEBUG] Resetting Jitsu plugin storage");
|
|
885
|
+
}
|
|
886
|
+
},
|
|
887
|
+
methods: {
|
|
888
|
+
//analytics doesn't support group as a base method, so we need to add it manually
|
|
889
|
+
configure(newOptions: DynamicJitsuOptions) {
|
|
890
|
+
const idsWasDisabled = jitsuOptions.privacy?.disableUserIds || jitsuOptions.privacy?.dontSend;
|
|
891
|
+
mergeConfig(jitsuOptions, newOptions);
|
|
892
|
+
const idsDisabledNow = jitsuOptions.privacy?.disableUserIds || jitsuOptions.privacy?.dontSend;
|
|
893
|
+
if (!idsDisabledNow && idsWasDisabled) {
|
|
894
|
+
if (jitsuOptions.debug) {
|
|
895
|
+
console.log("[JITSU] Enabling Anonymous ID. Generating new Id.");
|
|
896
|
+
}
|
|
897
|
+
const instance = (this as any).instance;
|
|
898
|
+
const newAnonymousId = uuid();
|
|
899
|
+
const userState = instance.user();
|
|
900
|
+
if (userState) {
|
|
901
|
+
userState.anonymousId = newAnonymousId;
|
|
902
|
+
}
|
|
903
|
+
storage.setItem("__anon_id", newAnonymousId);
|
|
904
|
+
instance.setAnonymousId(newAnonymousId);
|
|
905
|
+
}
|
|
906
|
+
},
|
|
907
|
+
group(groupId?: ID, traits?: JSONObject | null, options?: Options, callback?: Callback) {
|
|
908
|
+
if (jitsuOptions.privacy?.dontSend || jitsuOptions.privacy?.disableUserIds) {
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
if (typeof groupId === "number") {
|
|
912
|
+
//fix potential issues with group id being used incorrectly
|
|
913
|
+
groupId = groupId + "";
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
const instance = (this as any).instance;
|
|
917
|
+
const user = instance.user();
|
|
918
|
+
const userId = options?.userId || user?.userId;
|
|
919
|
+
const anonymousId = options?.anonymousId || user?.anonymousId || storage.getItem("__anon_id");
|
|
920
|
+
storage.setItem("__group_id", groupId);
|
|
921
|
+
const doNotSend = traits?.$doNotSend;
|
|
922
|
+
if (traits && typeof traits === "object") {
|
|
923
|
+
traits = stripControllingTraits(traits);
|
|
924
|
+
storage.setItem("__group_traits", traits);
|
|
925
|
+
}
|
|
926
|
+
if (doNotSend) {
|
|
927
|
+
return Promise.resolve();
|
|
928
|
+
}
|
|
929
|
+
return send(
|
|
930
|
+
"group",
|
|
931
|
+
{ type: "group", groupId, traits, ...(anonymousId ? { anonymousId } : {}), ...(userId ? { userId } : {}) },
|
|
932
|
+
jitsuOptions,
|
|
933
|
+
instance,
|
|
934
|
+
storage
|
|
935
|
+
);
|
|
936
|
+
},
|
|
937
|
+
},
|
|
938
|
+
};
|
|
939
|
+
};
|
|
940
|
+
|
|
941
|
+
let seedCounter = 0;
|
|
942
|
+
function getSeed() {
|
|
943
|
+
seedCounter = (seedCounter + 1) % Number.MAX_SAFE_INTEGER;
|
|
944
|
+
|
|
945
|
+
const defaultSeed = Date.now() % 2147483647;
|
|
946
|
+
const seed = isInBrowser() ? window?.performance?.now() || defaultSeed : defaultSeed;
|
|
947
|
+
|
|
948
|
+
return seed + seedCounter;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
export function randomId(hashString: string | undefined = ""): string {
|
|
952
|
+
const d = Date.now();
|
|
953
|
+
return (
|
|
954
|
+
((Math.random() * d * hash(hashString ?? "", getSeed())) % Number.MAX_SAFE_INTEGER).toString(36) +
|
|
955
|
+
((Math.random() * d * hash(hashString ?? "", getSeed())) % Number.MAX_SAFE_INTEGER).toString(36)
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
export function uuid() {
|
|
960
|
+
var u = "",
|
|
961
|
+
m = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx",
|
|
962
|
+
i = 0,
|
|
963
|
+
rb = (Math.random() * 0xffffffff) | 0;
|
|
964
|
+
|
|
965
|
+
while (i++ < 36) {
|
|
966
|
+
var c = m[i - 1],
|
|
967
|
+
r = rb & 0xf,
|
|
968
|
+
v = c == "x" ? r : (r & 0x3) | 0x8;
|
|
969
|
+
|
|
970
|
+
u += c == "-" || c == "4" ? c : v.toString(16);
|
|
971
|
+
rb = i % 8 == 0 ? (Math.random() * 0xffffffff) | 0 : rb >> 4;
|
|
972
|
+
}
|
|
973
|
+
return u;
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
function hash(str: string, seed: number = 0): number {
|
|
977
|
+
let h1 = 0xdeadbeef ^ seed,
|
|
978
|
+
h2 = 0x41c6ce57 ^ seed;
|
|
979
|
+
for (let i = 0, ch; i < str.length; i++) {
|
|
980
|
+
ch = str.charCodeAt(i);
|
|
981
|
+
h1 = Math.imul(h1 ^ ch, 2654435761);
|
|
982
|
+
h2 = Math.imul(h2 ^ ch, 1597334677);
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
|
|
986
|
+
h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
|
|
987
|
+
|
|
988
|
+
return 4294967296 * (2097151 & h2) + (h1 >>> 0);
|
|
989
|
+
}
|