@jitsu/js 1.7.2 → 1.9.0-canary.548.20240108170646

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.
@@ -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
- const val = storage.getItem("__user_traits");
66
+ let val = storage.getItem("__user_traits");
66
67
  if (typeof val === "string") {
67
- return safeCall(() => JSON.parse(val), {});
68
+ val = safeCall(() => JSON.parse(val), {});
68
69
  }
69
- return val;
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;
@@ -284,12 +298,33 @@ export function isInBrowser() {
284
298
  return typeof document !== "undefined" && typeof window !== "undefined";
285
299
  }
286
300
 
287
- function adjustPayload(payload: any, config: JitsuOptions, storage: PersistentStorage): AnalyticsClientEvent {
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 {
288
318
  const runtime: RuntimeFacade = config.runtime || (isInBrowser() ? windowRuntime(config) : emptyRuntime(config));
289
319
  const url = runtime.pageUrl();
290
320
  const parsedUrl = safeCall(() => new URL(url), undefined);
291
321
  const query = parsedUrl ? parseQuery(parsedUrl.search) : {};
292
322
  const properties = payload.properties || {};
323
+
324
+ if (properties.path) {
325
+ properties.path = fixPath(properties.path);
326
+ }
327
+
293
328
  const customContext = payload.properties?.context || {};
294
329
  delete payload.properties?.context;
295
330
  const referrer = runtime.referrer();
@@ -297,6 +332,7 @@ function adjustPayload(payload: any, config: JitsuOptions, storage: PersistentSt
297
332
  library: {
298
333
  name: jitsuLibraryName,
299
334
  version: jitsuVersion,
335
+ env: s2s ? "node" : "browser",
300
336
  },
301
337
  userAgent: runtime.userAgent(),
302
338
  locale: runtime.language(),
@@ -325,6 +361,7 @@ function adjustPayload(payload: any, config: JitsuOptions, storage: PersistentSt
325
361
  sentAt: new Date().toISOString(),
326
362
  messageId: randomId(properties.path || (parsedUrl && parsedUrl.pathname)),
327
363
  writeKey: maskWriteKey(config.writeKey),
364
+ groupId: storage.getItem("__group_id"),
328
365
  context: deepMerge(context, customContext),
329
366
  };
330
367
  delete withContext.meta;
@@ -471,19 +508,19 @@ function maskWriteKey(writeKey?: string): string | undefined {
471
508
  return writeKey;
472
509
  }
473
510
 
474
- function send(
511
+ async function send(
475
512
  method,
476
513
  payload,
477
514
  jitsuConfig: Required<JitsuOptions>,
478
515
  instance: AnalyticsInstance,
479
516
  store: PersistentStorage
480
- ): Promise<void> {
517
+ ): Promise<any> {
481
518
  if (jitsuConfig.echoEvents) {
482
519
  console.log(`[JITSU DEBUG] sending '${method}' event:`, payload);
483
520
  return;
484
521
  }
485
-
486
- 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}`;
487
524
  const fetch = jitsuConfig.fetch || globalThis.fetch;
488
525
  if (!fetch) {
489
526
  throw new Error(
@@ -495,85 +532,72 @@ function send(
495
532
  // if (jitsuConfig.debug) {
496
533
  // console.log(`[JITSU] Sending event to ${url}: `, JSON.stringify(payload, null, 2));
497
534
  // }
498
- const adjustedPayload = adjustPayload(payload, jitsuConfig, store);
535
+ const adjustedPayload = adjustPayload(payload, jitsuConfig, store, s2s);
499
536
 
500
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",
501
544
 
502
- return fetch(url, {
503
- method: "POST",
504
- headers: {
505
- "Content-Type": "application/json",
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
+ }
506
573
 
507
- ...authHeader,
508
- ...debugHeader,
509
- },
510
- body: JSON.stringify(adjustedPayload),
511
- })
512
- .then(res => {
513
- if (jitsuConfig.debug) {
514
- console.log(
515
- `[JITSU] ${url} replied ${res.status}. Original payload: `,
516
- JSON.stringify(adjustedPayload, null, 2)
517
- );
518
- }
519
- if (res.ok) {
520
- return res.text();
521
- } else {
522
- return Promise.reject(res.text());
523
- }
524
- })
525
- .then(responseText => {
526
- let response: any;
527
- try {
528
- response = JSON.parse(responseText);
529
- } catch (e) {
530
- return Promise.reject(`Can't parse JSON: ${responseText}: ${e?.message}`);
531
- }
532
- if (response.destinations) {
533
- if (jitsuConfig.debug) {
534
- console.log(`[JITSU] Processing device destinations: `, JSON.stringify(response.destinations, null, 2));
535
- }
536
- return processDestinations(response.destinations, method, adjustedPayload, !!jitsuConfig.debug, instance);
537
- }
538
- })
539
- .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 {
540
588
  if (jitsuConfig.debug) {
541
- console.error(`Jitsu ${url} failed: `, err);
589
+ console.log(`[JITSU] Processing device destinations: `, JSON.stringify(responseJson.destinations, null, 2));
542
590
  }
543
- });
591
+ return processDestinations(responseJson.destinations, method, adjustedPayload, !!jitsuConfig.debug, instance);
592
+ }
593
+ }
594
+ return adjustedPayload;
544
595
  }
545
596
 
546
- const jitsuAnalyticsPlugin = (pluginConfig: JitsuOptions = {}): AnalyticsPlugin => {
547
- const storageCache: any = {};
548
-
549
- // AnalyticsInstance's storage is async somewhere inside. So if we make 'page' call right after 'identify' call
550
- // 'page' call will load traits from storage before 'identify' call had a change to save them.
551
- // to avoid that we use in-memory cache for storage
552
- const cachingStorageWrapper = (persistentStorage: PersistentStorage): PersistentStorage => ({
553
- setItem(key: string, val: any) {
554
- if (pluginConfig.debug) {
555
- console.log(`[JITSU DEBUG] Caching storage setItem: ${key}=${val}`);
556
- }
557
- storageCache[key] = val;
558
- persistentStorage.setItem(key, val);
559
- },
560
- getItem(key: string) {
561
- const value = storageCache[key] || persistentStorage.getItem(key);
562
- if (pluginConfig.debug) {
563
- console.log(
564
- `[JITSU DEBUG] Caching storage getItem: ${key}=${value}. Evicted from cache: ${!storageCache[key]}`
565
- );
566
- }
567
- return value;
568
- },
569
- removeItem(key: string) {
570
- if (pluginConfig.debug) {
571
- console.log(`[JITSU DEBUG] Caching storage removeItem: ${key}`);
572
- }
573
- delete storageCache[key];
574
- persistentStorage.removeItem(key);
575
- },
576
- });
597
+ export type JitsuPluginConfig = JitsuOptions & {
598
+ storageWrapper?: (persistentStorage: PersistentStorage) => PersistentStorage & { reset: () => void };
599
+ };
600
+ const jitsuAnalyticsPlugin = (pluginConfig: JitsuPluginConfig = {}): AnalyticsPlugin => {
577
601
  const instanceConfig = {
578
602
  ...defaultConfig,
579
603
  ...pluginConfig,
@@ -594,30 +618,59 @@ const jitsuAnalyticsPlugin = (pluginConfig: JitsuOptions = {}): AnalyticsPlugin
594
618
  },
595
619
  page: args => {
596
620
  const { payload, config, instance } = args;
597
- return send("page", payload, config, instance, cachingStorageWrapper(instance.storage));
621
+ return send(
622
+ "page",
623
+ payload,
624
+ config,
625
+ instance,
626
+ pluginConfig.storageWrapper ? pluginConfig.storageWrapper(instance.storage) : instance.storage
627
+ );
598
628
  },
599
629
  track: args => {
600
630
  const { payload, config, instance } = args;
601
- return send("track", payload, config, instance, cachingStorageWrapper(instance.storage));
631
+ return send(
632
+ "track",
633
+ payload,
634
+ config,
635
+ instance,
636
+ pluginConfig.storageWrapper ? pluginConfig.storageWrapper(instance.storage) : instance.storage
637
+ );
602
638
  },
603
639
  identify: args => {
604
640
  const { payload, config, instance } = args;
605
641
  // Store traits in cache to be able to use them in page and track events that run asynchronously with current identify.
606
- storageCache["__user_traits"] = payload.traits;
607
- return send("identify", payload, config, instance, cachingStorageWrapper(instance.storage));
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);
608
648
  },
609
649
  reset: args => {
610
650
  //clear storage cache
611
- Object.keys(storageCache).forEach(key => delete storageCache[key]);
651
+ if (pluginConfig.storageWrapper) {
652
+ pluginConfig.storageWrapper(args.instance.storage).reset();
653
+ }
612
654
  },
613
655
  methods: {
614
656
  //analytics doesn't support group as a base method, so we need to add it manually
615
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
+
616
663
  const analyticsInstance = this.instance;
617
- const cacheWrap = cachingStorageWrapper(analyticsInstance.storage);
664
+ const cacheWrap = pluginConfig.storageWrapper
665
+ ? pluginConfig.storageWrapper(analyticsInstance.storage)
666
+ : analyticsInstance.storage;
618
667
  const user = analyticsInstance.user();
619
668
  const userId = options?.userId || user?.userId;
620
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
+ }
621
674
  return send(
622
675
  "group",
623
676
  { type: "group", groupId, traits, ...(anonymousId ? { anonymousId } : {}), ...(userId ? { userId } : {}) },
package/src/browser.ts CHANGED
@@ -75,7 +75,8 @@ function getScriptAttributes(scriptElement: HTMLScriptElement) {
75
75
  * New callback based queue, see below
76
76
  */
77
77
  //make a copy of the queue
78
- const callbackQueue = [...(window[JITSU_V2_ID + "Q"] || [])];
78
+ const callbackQueue =
79
+ window[JITSU_V2_ID + "Q"] && window[JITSU_V2_ID + "Q"].length ? [...window[JITSU_V2_ID + "Q"]] : [];
79
80
  //replace push with a function that calls callback immediately
80
81
  window[JITSU_V2_ID + "Q"] = {
81
82
  push: (callback: any) => {
@@ -88,7 +89,7 @@ function getScriptAttributes(scriptElement: HTMLScriptElement) {
88
89
  };
89
90
 
90
91
  if (options.debug) {
91
- console.log(`Jitsu callback queue size: ${callbackQueue.length}`, callbackQueue);
92
+ console.log(`[JITSU DEBUG] Jitsu callback queue size: ${callbackQueue.length}`, callbackQueue);
92
93
  }
93
94
  callbackQueue.forEach((callback: any) => {
94
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
 
@@ -39,25 +39,87 @@ function createUnderlyingAnalyticsInstance(
39
39
  plugins: any[] = []
40
40
  ): AnalyticsInterface {
41
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
+
42
80
  const analytics = Analytics({
43
- app: "test",
44
81
  debug: !!opts.debug,
45
82
  storage,
46
- plugins: [jitsuAnalyticsPlugin(opts), ...plugins],
83
+ plugins: [jitsuAnalyticsPlugin({ ...opts, storageWrapper: cachingStorageWrapper }), ...plugins],
47
84
  } as any);
48
- const originalPage = analytics.page;
49
- analytics.page = (...args) => {
50
- if (args.length === 2 && typeof args[0] === "string" && typeof args[1] === "object") {
51
- return originalPage({
52
- name: args[0],
53
- ...args[1],
54
- });
55
- } else {
56
- return originalPage(...args);
57
- }
58
- };
85
+
59
86
  return {
60
87
  ...analytics,
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
+ },
61
123
  setAnonymousId: (id: string) => {
62
124
  if (opts.debug) {
63
125
  console.log("[JITSU DEBUG] Setting anonymous id to " + id);
@@ -71,13 +133,21 @@ function createUnderlyingAnalyticsInstance(
71
133
  (analytics as any).setAnonymousId(id);
72
134
  }
73
135
  },
74
- group(groupId?: ID, traits?: JSONObject | null, options?: Options, callback?: Callback): Promise<DispatchedEvent> {
136
+ async group(
137
+ groupId?: ID,
138
+ traits?: JSONObject | null,
139
+ options?: Options,
140
+ callback?: Callback
141
+ ): Promise<DispatchedEvent> {
142
+ const results: any[] = [];
75
143
  for (const plugin of Object.values(analytics.plugins)) {
76
144
  if (plugin["group"]) {
77
- plugin["group"](groupId, traits, options, callback);
145
+ results.push(await plugin["group"](groupId, traits, options, callback));
78
146
  }
79
147
  }
80
- return Promise.resolve({});
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];
81
151
  },
82
152
  } as AnalyticsInterface;
83
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
@@ -1,3 +1,6 @@
1
- import { name as jitsuLibraryName, version as jitsuVersion } from "../package.json";
1
+ //import pkg from "../package.json";
2
+
3
+ const jitsuVersion = "0.0.0";
4
+ const jitsuLibraryName = "@jitsu/js";
2
5
 
3
6
  export { jitsuVersion, jitsuLibraryName };