@jitsu/js 1.7.1 → 1.8.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/.turbo/turbo-build.log +85 -105
- package/.turbo/turbo-clean.log +5 -5
- package/.turbo/turbo-test.log +1215 -317
- package/__tests__/node/nodejs.test.ts +50 -8
- package/__tests__/playwright/cases/basic.html +1 -1
- package/__tests__/playwright/cases/url-bug.html +20 -0
- package/__tests__/playwright/integration.test.ts +60 -8
- package/dist/analytics-plugin.d.ts +6 -1
- package/dist/index.d.ts +1 -8
- package/dist/jitsu.cjs.js +250 -132
- package/dist/jitsu.d.ts +6 -0
- package/dist/jitsu.es.js +250 -132
- package/dist/version.d.ts +2 -1
- package/dist/web/p.js.txt +1311 -1
- package/package.json +4 -4
- package/rollup.config.js +8 -1
- package/src/analytics-plugin.ts +168 -96
- package/src/browser.ts +18 -28
- package/src/index.ts +104 -19
- package/src/jitsu.ts +7 -0
- package/src/version.ts +4 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jitsu/js",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "",
|
|
5
5
|
"author": "Jitsu Dev Team <dev@jitsu.com>",
|
|
6
6
|
"main": "dist/jitsu.cjs.js",
|
|
@@ -33,11 +33,11 @@
|
|
|
33
33
|
"raw-loader": "^4.0.2",
|
|
34
34
|
"rollup": "^3.2.5",
|
|
35
35
|
"ts-jest": "29.0.5",
|
|
36
|
-
"typescript": "^
|
|
37
|
-
"@jitsu/protocols": "1.
|
|
36
|
+
"typescript": "^5.3.3",
|
|
37
|
+
"@jitsu/protocols": "1.8.0"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"analytics": "
|
|
40
|
+
"analytics": "0.8.9"
|
|
41
41
|
},
|
|
42
42
|
"scripts": {
|
|
43
43
|
"clean": "rm -rf ./dist",
|
package/rollup.config.js
CHANGED
|
@@ -4,9 +4,16 @@ const commonjs = require("@rollup/plugin-commonjs");
|
|
|
4
4
|
const rollupJson = require("@rollup/plugin-json");
|
|
5
5
|
const terser = require("@rollup/plugin-terser");
|
|
6
6
|
|
|
7
|
+
|
|
7
8
|
module.exports = [
|
|
8
9
|
{
|
|
9
|
-
plugins: [
|
|
10
|
+
plugins: [
|
|
11
|
+
multi(),
|
|
12
|
+
resolve({ preferBuiltins: false }),
|
|
13
|
+
commonjs(),
|
|
14
|
+
rollupJson(),
|
|
15
|
+
(process.JITSU_JS_DEBUG_BUILD = "1" ? undefined : terser()),
|
|
16
|
+
],
|
|
10
17
|
input: "./compiled/src/browser.js",
|
|
11
18
|
output: {
|
|
12
19
|
file: `dist/web/p.js.txt`,
|
package/src/analytics-plugin.ts
CHANGED
|
@@ -20,6 +20,7 @@ const defaultConfig: Required<JitsuOptions> = {
|
|
|
20
20
|
echoEvents: false,
|
|
21
21
|
cookieDomain: undefined,
|
|
22
22
|
runtime: undefined,
|
|
23
|
+
s2s: undefined,
|
|
23
24
|
};
|
|
24
25
|
|
|
25
26
|
export const parseQuery = (qs?: string): Record<string, string> => {
|
|
@@ -62,11 +63,24 @@ function safeCall<T>(f: () => T, defaultVal?: T): T | undefined {
|
|
|
62
63
|
}
|
|
63
64
|
|
|
64
65
|
function restoreTraits(storage: PersistentStorage) {
|
|
65
|
-
|
|
66
|
+
let val = storage.getItem("__user_traits");
|
|
66
67
|
if (typeof val === "string") {
|
|
67
|
-
|
|
68
|
+
val = safeCall(() => JSON.parse(val), {});
|
|
68
69
|
}
|
|
69
|
-
|
|
70
|
+
if (typeof val !== "object" || val === null) {
|
|
71
|
+
val = {};
|
|
72
|
+
}
|
|
73
|
+
let groupVal = storage.getItem("__group_traits");
|
|
74
|
+
if (typeof groupVal === "string") {
|
|
75
|
+
groupVal = safeCall(() => JSON.parse(groupVal), {});
|
|
76
|
+
}
|
|
77
|
+
if (typeof groupVal !== "object" || groupVal === null) {
|
|
78
|
+
groupVal = {};
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
...(groupVal || {}),
|
|
82
|
+
...(val || {}), //user traits override group traits
|
|
83
|
+
};
|
|
70
84
|
}
|
|
71
85
|
|
|
72
86
|
export type StorageFactory = (cookieDomain: string, cookie2key: Record<string, string>) => PersistentStorage;
|
|
@@ -77,25 +91,32 @@ function getCookie(name: string) {
|
|
|
77
91
|
return parts.length === 2 ? parts.pop().split(";").shift() : undefined;
|
|
78
92
|
}
|
|
79
93
|
|
|
80
|
-
function
|
|
81
|
-
const
|
|
82
|
-
|
|
94
|
+
function getGa4Ids(runtime: RuntimeFacade) {
|
|
95
|
+
const allCookies = runtime.getCookies();
|
|
96
|
+
const clientId = allCookies["_ga"]?.split(".").slice(-2).join(".");
|
|
97
|
+
const gaSessionCookies = Object.entries(allCookies).filter(([key]) => key.startsWith("_ga_"));
|
|
98
|
+
const sessionIds =
|
|
99
|
+
gaSessionCookies.length > 0
|
|
100
|
+
? Object.fromEntries(
|
|
101
|
+
gaSessionCookies
|
|
102
|
+
.map(([key, value]) => {
|
|
103
|
+
if (typeof value !== "string") {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
const parts = value.split(".");
|
|
107
|
+
if (parts.length < 3) {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return [key.substring("_ga_".length), parts[2]];
|
|
111
|
+
})
|
|
112
|
+
.filter(v => v !== null)
|
|
113
|
+
)
|
|
114
|
+
: undefined;
|
|
115
|
+
if (clientId || sessionIds) {
|
|
116
|
+
return { ga4: { clientId, sessionIds } };
|
|
117
|
+
} else {
|
|
83
118
|
return undefined;
|
|
84
119
|
}
|
|
85
|
-
return Object.fromEntries(
|
|
86
|
-
gaCookies
|
|
87
|
-
.map(([key, value]) => {
|
|
88
|
-
if (typeof value !== "string") {
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
const parts = value.split(".");
|
|
92
|
-
if (parts.length < 3) {
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
return [key.substring("_ga_".length), parts[2]];
|
|
96
|
-
})
|
|
97
|
-
.filter(v => v !== null)
|
|
98
|
-
);
|
|
99
120
|
}
|
|
100
121
|
|
|
101
122
|
function removeCookie(name: string) {
|
|
@@ -277,12 +298,33 @@ export function isInBrowser() {
|
|
|
277
298
|
return typeof document !== "undefined" && typeof window !== "undefined";
|
|
278
299
|
}
|
|
279
300
|
|
|
280
|
-
|
|
301
|
+
/**
|
|
302
|
+
* Fixes a weird bug in analytics URL where path
|
|
303
|
+
* of https://test.com becomes //test.com
|
|
304
|
+
*/
|
|
305
|
+
function fixPath(path: string): string {
|
|
306
|
+
if (path.indexOf("//") === 0 && path.lastIndexOf("/") === 1) {
|
|
307
|
+
return "/";
|
|
308
|
+
}
|
|
309
|
+
return path;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function adjustPayload(
|
|
313
|
+
payload: any,
|
|
314
|
+
config: JitsuOptions,
|
|
315
|
+
storage: PersistentStorage,
|
|
316
|
+
s2s: boolean
|
|
317
|
+
): AnalyticsClientEvent {
|
|
281
318
|
const runtime: RuntimeFacade = config.runtime || (isInBrowser() ? windowRuntime(config) : emptyRuntime(config));
|
|
282
319
|
const url = runtime.pageUrl();
|
|
283
320
|
const parsedUrl = safeCall(() => new URL(url), undefined);
|
|
284
321
|
const query = parsedUrl ? parseQuery(parsedUrl.search) : {};
|
|
285
322
|
const properties = payload.properties || {};
|
|
323
|
+
|
|
324
|
+
if (properties.path) {
|
|
325
|
+
properties.path = fixPath(properties.path);
|
|
326
|
+
}
|
|
327
|
+
|
|
286
328
|
const customContext = payload.properties?.context || {};
|
|
287
329
|
delete payload.properties?.context;
|
|
288
330
|
const referrer = runtime.referrer();
|
|
@@ -290,6 +332,7 @@ function adjustPayload(payload: any, config: JitsuOptions, storage: PersistentSt
|
|
|
290
332
|
library: {
|
|
291
333
|
name: jitsuLibraryName,
|
|
292
334
|
version: jitsuVersion,
|
|
335
|
+
env: s2s ? "node" : "browser",
|
|
293
336
|
},
|
|
294
337
|
userAgent: runtime.userAgent(),
|
|
295
338
|
locale: runtime.language(),
|
|
@@ -308,10 +351,7 @@ function adjustPayload(payload: any, config: JitsuOptions, storage: PersistentSt
|
|
|
308
351
|
clientIds: {
|
|
309
352
|
fbc: runtime.getCookie("_fbc"),
|
|
310
353
|
fbp: runtime.getCookie("_fbp"),
|
|
311
|
-
|
|
312
|
-
clientId: runtime.getCookie("_ga")?.split(".").slice(-2).join("."), //last 2 parts of GA cookie
|
|
313
|
-
sessions: getGa4Sessions(runtime.getCookies()),
|
|
314
|
-
},
|
|
354
|
+
...getGa4Ids(runtime),
|
|
315
355
|
},
|
|
316
356
|
campaign: parseUtms(query),
|
|
317
357
|
};
|
|
@@ -321,6 +361,7 @@ function adjustPayload(payload: any, config: JitsuOptions, storage: PersistentSt
|
|
|
321
361
|
sentAt: new Date().toISOString(),
|
|
322
362
|
messageId: randomId(properties.path || (parsedUrl && parsedUrl.pathname)),
|
|
323
363
|
writeKey: maskWriteKey(config.writeKey),
|
|
364
|
+
groupId: storage.getItem("__group_id"),
|
|
324
365
|
context: deepMerge(context, customContext),
|
|
325
366
|
};
|
|
326
367
|
delete withContext.meta;
|
|
@@ -467,19 +508,19 @@ function maskWriteKey(writeKey?: string): string | undefined {
|
|
|
467
508
|
return writeKey;
|
|
468
509
|
}
|
|
469
510
|
|
|
470
|
-
function send(
|
|
511
|
+
async function send(
|
|
471
512
|
method,
|
|
472
513
|
payload,
|
|
473
514
|
jitsuConfig: Required<JitsuOptions>,
|
|
474
515
|
instance: AnalyticsInstance,
|
|
475
516
|
store: PersistentStorage
|
|
476
|
-
): Promise<
|
|
517
|
+
): Promise<any> {
|
|
477
518
|
if (jitsuConfig.echoEvents) {
|
|
478
|
-
console.log(`[JITSU] sending '${method}' event:`, payload);
|
|
519
|
+
console.log(`[JITSU DEBUG] sending '${method}' event:`, payload);
|
|
479
520
|
return;
|
|
480
521
|
}
|
|
481
|
-
|
|
482
|
-
const url = `${jitsuConfig.host}/api/s/${method}`;
|
|
522
|
+
const s2s = jitsuConfig.s2s === undefined ? !isInBrowser() : jitsuConfig.s2s;
|
|
523
|
+
const url = s2s ? `${jitsuConfig.host}/api/s/s2s/${method}` : `${jitsuConfig.host}/api/s/${method}`;
|
|
483
524
|
const fetch = jitsuConfig.fetch || globalThis.fetch;
|
|
484
525
|
if (!fetch) {
|
|
485
526
|
throw new Error(
|
|
@@ -491,72 +532,72 @@ function send(
|
|
|
491
532
|
// if (jitsuConfig.debug) {
|
|
492
533
|
// console.log(`[JITSU] Sending event to ${url}: `, JSON.stringify(payload, null, 2));
|
|
493
534
|
// }
|
|
494
|
-
const adjustedPayload = adjustPayload(payload, jitsuConfig, store);
|
|
535
|
+
const adjustedPayload = adjustPayload(payload, jitsuConfig, store, s2s);
|
|
495
536
|
|
|
496
537
|
const authHeader = jitsuConfig.writeKey ? { "X-Write-Key": jitsuConfig.writeKey } : {};
|
|
538
|
+
let fetchResult;
|
|
539
|
+
try {
|
|
540
|
+
fetchResult = await fetch(url, {
|
|
541
|
+
method: "POST",
|
|
542
|
+
headers: {
|
|
543
|
+
"Content-Type": "application/json",
|
|
497
544
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
545
|
+
...authHeader,
|
|
546
|
+
...debugHeader,
|
|
547
|
+
},
|
|
548
|
+
body: JSON.stringify(adjustedPayload),
|
|
549
|
+
});
|
|
550
|
+
} catch (e: any) {
|
|
551
|
+
throw new Error(`Calling ${url} failed: ${e.message}`);
|
|
552
|
+
}
|
|
553
|
+
let responseText;
|
|
554
|
+
try {
|
|
555
|
+
responseText = await fetchResult.text();
|
|
556
|
+
} catch (e) {
|
|
557
|
+
console.warn(
|
|
558
|
+
`Can't read response text from ${url} (status - ${fetchResult.status} ${fetchResult.statusText}): ${e?.message}`
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
if (jitsuConfig.debug) {
|
|
562
|
+
console.log(
|
|
563
|
+
`[JITSU DEBUG] ${url} replied ${fetchResult.status}: ${responseText}. Original payload:\n${JSON.stringify(
|
|
564
|
+
adjustedPayload,
|
|
565
|
+
null,
|
|
566
|
+
2
|
|
567
|
+
)}`
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
if (!fetchResult.ok) {
|
|
571
|
+
throw new Error(`Jitsu ${url} replied ${fetchResult.status} - ${fetchResult.statusText}: ${responseText}`);
|
|
572
|
+
}
|
|
502
573
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
} else {
|
|
518
|
-
return Promise.reject(res.text());
|
|
519
|
-
}
|
|
520
|
-
})
|
|
521
|
-
.then(responseText => {
|
|
522
|
-
let response: any;
|
|
523
|
-
try {
|
|
524
|
-
response = JSON.parse(responseText);
|
|
525
|
-
} catch (e) {
|
|
526
|
-
return Promise.reject(`Can't parse JSON: ${responseText}: ${e?.message}`);
|
|
527
|
-
}
|
|
528
|
-
if (response.destinations) {
|
|
529
|
-
if (jitsuConfig.debug) {
|
|
530
|
-
console.log(`[JITSU] Processing device destinations: `, JSON.stringify(response.destinations, null, 2));
|
|
531
|
-
}
|
|
532
|
-
return processDestinations(response.destinations, method, adjustedPayload, !!jitsuConfig.debug, instance);
|
|
533
|
-
}
|
|
534
|
-
})
|
|
535
|
-
.catch(err => {
|
|
574
|
+
let responseJson: any;
|
|
575
|
+
try {
|
|
576
|
+
responseJson = JSON.parse(responseText);
|
|
577
|
+
} catch (e) {
|
|
578
|
+
return Promise.reject(`Can't parse JSON: ${responseText}: ${e?.message}`);
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (responseJson.destinations && responseJson.destinations.length > 0) {
|
|
582
|
+
if (jitsuConfig.s2s) {
|
|
583
|
+
console.warn(
|
|
584
|
+
`[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`,
|
|
585
|
+
jitsuConfig.debug ? JSON.stringify(responseJson.destinations, null, 2) : undefined
|
|
586
|
+
);
|
|
587
|
+
} else {
|
|
536
588
|
if (jitsuConfig.debug) {
|
|
537
|
-
console.
|
|
589
|
+
console.log(`[JITSU] Processing device destinations: `, JSON.stringify(responseJson.destinations, null, 2));
|
|
538
590
|
}
|
|
539
|
-
|
|
591
|
+
return processDestinations(responseJson.destinations, method, adjustedPayload, !!jitsuConfig.debug, instance);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return adjustedPayload;
|
|
540
595
|
}
|
|
541
596
|
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
// to avoid that we use in-memory cache for storage
|
|
547
|
-
const cachingStorageWrapper = (persistentStorage: PersistentStorage): PersistentStorage => ({
|
|
548
|
-
setItem(key: string, val: any) {
|
|
549
|
-
storageCache[key] = val;
|
|
550
|
-
persistentStorage.setItem(key, val);
|
|
551
|
-
},
|
|
552
|
-
getItem(key: string) {
|
|
553
|
-
return storageCache[key] || persistentStorage.getItem(key);
|
|
554
|
-
},
|
|
555
|
-
removeItem(key: string) {
|
|
556
|
-
delete storageCache[key];
|
|
557
|
-
persistentStorage.removeItem(key);
|
|
558
|
-
},
|
|
559
|
-
});
|
|
597
|
+
export type JitsuPluginConfig = JitsuOptions & {
|
|
598
|
+
storageWrapper?: (persistentStorage: PersistentStorage) => PersistentStorage & { reset: () => void };
|
|
599
|
+
};
|
|
600
|
+
const jitsuAnalyticsPlugin = (pluginConfig: JitsuPluginConfig = {}): AnalyticsPlugin => {
|
|
560
601
|
const instanceConfig = {
|
|
561
602
|
...defaultConfig,
|
|
562
603
|
...pluginConfig,
|
|
@@ -564,10 +605,11 @@ const jitsuAnalyticsPlugin = (pluginConfig: JitsuOptions = {}): AnalyticsPlugin
|
|
|
564
605
|
return {
|
|
565
606
|
name: "jitsu",
|
|
566
607
|
config: instanceConfig,
|
|
608
|
+
|
|
567
609
|
initialize: args => {
|
|
568
610
|
const { config } = args;
|
|
569
611
|
if (config.debug) {
|
|
570
|
-
console.debug("[JITSU] Initializing Jitsu plugin with config: ", JSON.stringify(config));
|
|
612
|
+
console.debug("[JITSU DEBUG] Initializing Jitsu plugin with config: ", JSON.stringify(config, null, 2));
|
|
571
613
|
}
|
|
572
614
|
if (!config.host && !config.echoEvents) {
|
|
573
615
|
throw new Error("Please specify host variable in jitsu plugin initialization, or set echoEvents to true");
|
|
@@ -576,35 +618,65 @@ const jitsuAnalyticsPlugin = (pluginConfig: JitsuOptions = {}): AnalyticsPlugin
|
|
|
576
618
|
},
|
|
577
619
|
page: args => {
|
|
578
620
|
const { payload, config, instance } = args;
|
|
579
|
-
return send(
|
|
621
|
+
return send(
|
|
622
|
+
"page",
|
|
623
|
+
payload,
|
|
624
|
+
config,
|
|
625
|
+
instance,
|
|
626
|
+
pluginConfig.storageWrapper ? pluginConfig.storageWrapper(instance.storage) : instance.storage
|
|
627
|
+
);
|
|
580
628
|
},
|
|
581
629
|
track: args => {
|
|
582
630
|
const { payload, config, instance } = args;
|
|
583
|
-
return send(
|
|
631
|
+
return send(
|
|
632
|
+
"track",
|
|
633
|
+
payload,
|
|
634
|
+
config,
|
|
635
|
+
instance,
|
|
636
|
+
pluginConfig.storageWrapper ? pluginConfig.storageWrapper(instance.storage) : instance.storage
|
|
637
|
+
);
|
|
584
638
|
},
|
|
585
639
|
identify: args => {
|
|
586
640
|
const { payload, config, instance } = args;
|
|
587
641
|
// Store traits in cache to be able to use them in page and track events that run asynchronously with current identify.
|
|
588
|
-
|
|
589
|
-
|
|
642
|
+
const storage = pluginConfig.storageWrapper ? pluginConfig.storageWrapper(instance.storage) : instance.storage;
|
|
643
|
+
storage.setItem("__user_id", payload.userId);
|
|
644
|
+
if (payload.traits && typeof payload.traits === "object") {
|
|
645
|
+
storage.setItem("__user_traits", payload.traits);
|
|
646
|
+
}
|
|
647
|
+
return send("identify", payload, config, instance, storage);
|
|
590
648
|
},
|
|
591
649
|
reset: args => {
|
|
592
650
|
//clear storage cache
|
|
593
|
-
|
|
651
|
+
if (pluginConfig.storageWrapper) {
|
|
652
|
+
pluginConfig.storageWrapper(args.instance.storage).reset();
|
|
653
|
+
}
|
|
594
654
|
},
|
|
595
655
|
methods: {
|
|
596
656
|
//analytics doesn't support group as a base method, so we need to add it manually
|
|
597
657
|
group(groupId?: ID, traits?: JSONObject | null, options?: Options, callback?: Callback) {
|
|
658
|
+
if (typeof groupId === "number") {
|
|
659
|
+
//fix potential issues with group id being used incorrectly
|
|
660
|
+
groupId = groupId + "";
|
|
661
|
+
}
|
|
662
|
+
|
|
598
663
|
const analyticsInstance = this.instance;
|
|
664
|
+
const cacheWrap = pluginConfig.storageWrapper
|
|
665
|
+
? pluginConfig.storageWrapper(analyticsInstance.storage)
|
|
666
|
+
: analyticsInstance.storage;
|
|
599
667
|
const user = analyticsInstance.user();
|
|
600
668
|
const userId = options?.userId || user?.userId;
|
|
601
|
-
const anonymousId = options?.anonymousId || user?.anonymousId;
|
|
669
|
+
const anonymousId = options?.anonymousId || user?.anonymousId || cacheWrap.getItem("__anon_id");
|
|
670
|
+
cacheWrap.setItem("__group_id", groupId);
|
|
671
|
+
if (traits && typeof traits === "object") {
|
|
672
|
+
cacheWrap.setItem("__group_traits", traits);
|
|
673
|
+
}
|
|
602
674
|
return send(
|
|
603
675
|
"group",
|
|
604
676
|
{ type: "group", groupId, traits, ...(anonymousId ? { anonymousId } : {}), ...(userId ? { userId } : {}) },
|
|
605
677
|
instanceConfig,
|
|
606
678
|
analyticsInstance,
|
|
607
|
-
|
|
679
|
+
cacheWrap
|
|
608
680
|
);
|
|
609
681
|
},
|
|
610
682
|
},
|
package/src/browser.ts
CHANGED
|
@@ -53,35 +53,10 @@ function getScriptAttributes(scriptElement: HTMLScriptElement) {
|
|
|
53
53
|
|
|
54
54
|
const options = readJitsuOptions();
|
|
55
55
|
const JITSU_V2_ID: string = options.namespace || "jitsu";
|
|
56
|
-
const queue = [];
|
|
57
|
-
if (window[JITSU_V2_ID]) {
|
|
58
|
-
if (Array.isArray(window[JITSU_V2_ID])) {
|
|
59
|
-
//processing queue of events
|
|
60
|
-
if (options.debug) {
|
|
61
|
-
console.log(
|
|
62
|
-
`Initializing Jitsu with prior events queue size of ${window[JITSU_V2_ID].length}`,
|
|
63
|
-
window[JITSU_V2_ID]
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
queue.push(...window[JITSU_V2_ID]);
|
|
67
|
-
} else {
|
|
68
|
-
console.warn("Attempted to initialize Jitsu twice. Returning the existing instance");
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
56
|
if (options.debug) {
|
|
72
57
|
console.log(`Jitsu options: `, JSON.stringify(options));
|
|
73
58
|
}
|
|
74
59
|
const jitsu = jitsuAnalytics(options);
|
|
75
|
-
for (const [method, args] of queue) {
|
|
76
|
-
if (options.debug) {
|
|
77
|
-
console.log(`Processing event ${method} from Jitsu queue on`, args);
|
|
78
|
-
}
|
|
79
|
-
try {
|
|
80
|
-
jitsu[method](...args);
|
|
81
|
-
} catch (e: any) {
|
|
82
|
-
console.warn(`Error processing event ${method} from Jitsu queue on`, args, e);
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
60
|
|
|
86
61
|
if (options.onload) {
|
|
87
62
|
const onloadFunction = window[options.onload] as any;
|
|
@@ -96,10 +71,25 @@ function getScriptAttributes(scriptElement: HTMLScriptElement) {
|
|
|
96
71
|
}
|
|
97
72
|
window[JITSU_V2_ID] = jitsu;
|
|
98
73
|
|
|
99
|
-
|
|
100
|
-
|
|
74
|
+
/**
|
|
75
|
+
* New callback based queue, see below
|
|
76
|
+
*/
|
|
77
|
+
//make a copy of the queue
|
|
78
|
+
const callbackQueue =
|
|
79
|
+
window[JITSU_V2_ID + "Q"] && window[JITSU_V2_ID + "Q"].length ? [...window[JITSU_V2_ID + "Q"]] : [];
|
|
80
|
+
//replace push with a function that calls callback immediately
|
|
81
|
+
window[JITSU_V2_ID + "Q"] = {
|
|
82
|
+
push: (callback: any) => {
|
|
83
|
+
if (typeof callback === "function") {
|
|
84
|
+
callback(jitsu);
|
|
85
|
+
} else {
|
|
86
|
+
console.warn(`${JITSU_V2_ID}Q.push() accepts only function, ${typeof callback} given`);
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
};
|
|
90
|
+
|
|
101
91
|
if (options.debug) {
|
|
102
|
-
console.log(`Jitsu callback queue size: ${callbackQueue.length}`, callbackQueue);
|
|
92
|
+
console.log(`[JITSU DEBUG] Jitsu callback queue size: ${callbackQueue.length}`, callbackQueue);
|
|
103
93
|
}
|
|
104
94
|
callbackQueue.forEach((callback: any) => {
|
|
105
95
|
try {
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import Analytics from "analytics";
|
|
2
|
-
import { AnalyticsInterface, JitsuOptions, RuntimeFacade } from "./jitsu";
|
|
2
|
+
import { AnalyticsInterface, JitsuOptions, PersistentStorage, RuntimeFacade } from "./jitsu";
|
|
3
3
|
import jitsuAnalyticsPlugin, { emptyRuntime, isInBrowser, windowRuntime } from "./analytics-plugin";
|
|
4
4
|
import { Callback, DispatchedEvent, ID, JSONObject, Options } from "@jitsu/protocols/analytics";
|
|
5
5
|
|
|
@@ -23,7 +23,8 @@ export default function parse(input) {
|
|
|
23
23
|
return value;
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export const emptyAnalytics = {
|
|
26
|
+
export const emptyAnalytics: AnalyticsInterface = {
|
|
27
|
+
setAnonymousId: () => {},
|
|
27
28
|
track: () => Promise.resolve(),
|
|
28
29
|
page: () => Promise.resolve(),
|
|
29
30
|
user: () => ({}),
|
|
@@ -37,32 +38,116 @@ function createUnderlyingAnalyticsInstance(
|
|
|
37
38
|
rt: RuntimeFacade,
|
|
38
39
|
plugins: any[] = []
|
|
39
40
|
): AnalyticsInterface {
|
|
41
|
+
const storage = rt.store();
|
|
42
|
+
|
|
43
|
+
const storageCache: any = {};
|
|
44
|
+
|
|
45
|
+
// AnalyticsInstance's storage is async somewhere inside. So if we make 'page' call right after 'identify' call
|
|
46
|
+
// 'page' call will load traits from storage before 'identify' call had a change to save them.
|
|
47
|
+
// to avoid that we use in-memory cache for storage
|
|
48
|
+
const cachingStorageWrapper = (persistentStorage: PersistentStorage) => ({
|
|
49
|
+
setItem(key: string, val: any) {
|
|
50
|
+
if (opts.debug) {
|
|
51
|
+
console.log(`[JITSU DEBUG] Caching storage setItem: ${key}=${val}`);
|
|
52
|
+
}
|
|
53
|
+
storageCache[key] = val;
|
|
54
|
+
persistentStorage.setItem(key, val);
|
|
55
|
+
},
|
|
56
|
+
getItem(key: string) {
|
|
57
|
+
const value = storageCache[key] || persistentStorage.getItem(key);
|
|
58
|
+
if (opts.debug) {
|
|
59
|
+
console.log(
|
|
60
|
+
`[JITSU DEBUG] Caching storage getItem: ${key}=${value}. Evicted from cache: ${!storageCache[key]}`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return value;
|
|
64
|
+
},
|
|
65
|
+
reset() {
|
|
66
|
+
for (const key of [...Object.keys(storageCache)]) {
|
|
67
|
+
storage.removeItem(key);
|
|
68
|
+
delete storageCache[key];
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
removeItem(key: string) {
|
|
72
|
+
if (opts.debug) {
|
|
73
|
+
console.log(`[JITSU DEBUG] Caching storage removeItem: ${key}`);
|
|
74
|
+
}
|
|
75
|
+
delete storageCache[key];
|
|
76
|
+
persistentStorage.removeItem(key);
|
|
77
|
+
},
|
|
78
|
+
});
|
|
79
|
+
|
|
40
80
|
const analytics = Analytics({
|
|
41
|
-
app: "test",
|
|
42
81
|
debug: !!opts.debug,
|
|
43
|
-
storage
|
|
44
|
-
plugins: [jitsuAnalyticsPlugin(opts), ...plugins],
|
|
82
|
+
storage,
|
|
83
|
+
plugins: [jitsuAnalyticsPlugin({ ...opts, storageWrapper: cachingStorageWrapper }), ...plugins],
|
|
45
84
|
} as any);
|
|
46
|
-
|
|
47
|
-
analytics.page = (...args) => {
|
|
48
|
-
if (args.length === 2 && typeof args[0] === "string" && typeof args[1] === "object") {
|
|
49
|
-
return originalPage({
|
|
50
|
-
name: args[0],
|
|
51
|
-
...args[1],
|
|
52
|
-
});
|
|
53
|
-
} else {
|
|
54
|
-
return originalPage(...args);
|
|
55
|
-
}
|
|
56
|
-
};
|
|
85
|
+
|
|
57
86
|
return {
|
|
58
87
|
...analytics,
|
|
59
|
-
|
|
88
|
+
page: (...args) => {
|
|
89
|
+
if (args.length === 2 && typeof args[0] === "string" && typeof args[1] === "object") {
|
|
90
|
+
return analytics.page({
|
|
91
|
+
name: args[0],
|
|
92
|
+
...args[1],
|
|
93
|
+
});
|
|
94
|
+
} else {
|
|
95
|
+
return (analytics.page as any)(...args);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
identify: (...args) => {
|
|
99
|
+
if (args[0] && typeof args[0] !== "object" && typeof args[0] !== "string") {
|
|
100
|
+
//fix the quirk of analytics.js: if you pass number as first argument, it will be converted to string
|
|
101
|
+
args[0] = args[0] + "";
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
//analytics.js sets userId and traits asynchronously, so if
|
|
105
|
+
//we want them to be available immediately after identify call in subsequent page() calls,
|
|
106
|
+
//we need to put them into storage manually
|
|
107
|
+
const storage = (analytics as any).storage;
|
|
108
|
+
const storageWrapper = cachingStorageWrapper(storage);
|
|
109
|
+
if (typeof args[0] === "string") {
|
|
110
|
+
//first argument is user id
|
|
111
|
+
storageWrapper.setItem("__user_id", args[0]);
|
|
112
|
+
} else if (typeof args[0] === "object") {
|
|
113
|
+
//first argument is traits
|
|
114
|
+
storageWrapper.setItem("__user_traits", args[0]);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (args.length === 2 && typeof args[1] === "object") {
|
|
118
|
+
//first argument is user id, second is traits
|
|
119
|
+
storageWrapper.setItem("__user_traits", args[1]);
|
|
120
|
+
}
|
|
121
|
+
return (analytics.identify as any)(...args);
|
|
122
|
+
},
|
|
123
|
+
setAnonymousId: (id: string) => {
|
|
124
|
+
if (opts.debug) {
|
|
125
|
+
console.log("[JITSU DEBUG] Setting anonymous id to " + id);
|
|
126
|
+
//Workaround for analytics.js bug. Underlying setAnonymousId doesn't work set the id immediately,
|
|
127
|
+
//so we got to it manually here. See https://github.com/jitsucom/jitsu/issues/1060
|
|
128
|
+
storage.setItem("__anon_id", id);
|
|
129
|
+
const userState = analytics.user();
|
|
130
|
+
if (userState) {
|
|
131
|
+
userState.anonymousId = id;
|
|
132
|
+
}
|
|
133
|
+
(analytics as any).setAnonymousId(id);
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
async group(
|
|
137
|
+
groupId?: ID,
|
|
138
|
+
traits?: JSONObject | null,
|
|
139
|
+
options?: Options,
|
|
140
|
+
callback?: Callback
|
|
141
|
+
): Promise<DispatchedEvent> {
|
|
142
|
+
const results: any[] = [];
|
|
60
143
|
for (const plugin of Object.values(analytics.plugins)) {
|
|
61
144
|
if (plugin["group"]) {
|
|
62
|
-
plugin["group"](groupId, traits, options, callback);
|
|
145
|
+
results.push(await plugin["group"](groupId, traits, options, callback));
|
|
63
146
|
}
|
|
64
147
|
}
|
|
65
|
-
|
|
148
|
+
//It's incorrect at many levels. First, it's not a dispatched event. Second, we take a first result
|
|
149
|
+
//However, since returned values are used for debugging purposes only, it's ok
|
|
150
|
+
return results[0];
|
|
66
151
|
},
|
|
67
152
|
} as AnalyticsInterface;
|
|
68
153
|
}
|
package/src/jitsu.ts
CHANGED
|
@@ -38,6 +38,13 @@ type JitsuOptions = {
|
|
|
38
38
|
* writeKey / host. It's useful for debugging development environment
|
|
39
39
|
*/
|
|
40
40
|
echoEvents?: boolean;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* If true, events will go to s2s endpoints like ${host}/api/s/s2s/{type}. Otherwise they'll go to ${host}/api/s/{type}.
|
|
44
|
+
*
|
|
45
|
+
* If not set at all, it will be detected automatically by presence of `window` object
|
|
46
|
+
*/
|
|
47
|
+
s2s?: boolean;
|
|
41
48
|
};
|
|
42
49
|
|
|
43
50
|
type PersistentStorage = {
|
package/src/version.ts
CHANGED