@jitsu/js 1.0.0-canary-20230219230449 → 1.1.0-canary.14.20230602211353

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.
@@ -1,13 +1,14 @@
1
1
  /* global analytics */
2
2
 
3
3
  import { JitsuOptions, PersistentStorage, RuntimeFacade } from "./jitsu";
4
- import { AnalyticsClientEvent } from "@jitsu/protocols/analytics";
4
+ import { AnalyticsClientEvent, Callback, ID, JSONObject, Options } from "@jitsu/protocols/analytics";
5
5
  import parse from "./index";
6
6
  import { AnalyticsInstance, AnalyticsPlugin } from "analytics";
7
- import { internalDestinationPlugins } from "./destination-plugins";
8
7
  import { loadScript } from "./script-loader";
8
+ import { internalDestinationPlugins } from "./destination-plugins";
9
+ import { jitsuLibraryName, jitsuVersion } from "./version";
9
10
 
10
- const config: JitsuOptions = {
11
+ const defaultConfig: Required<JitsuOptions> = {
11
12
  /* Your segment writeKey */
12
13
  writeKey: null,
13
14
  /* Disable anonymous MTU */
@@ -15,6 +16,8 @@ const config: JitsuOptions = {
15
16
  debug: false,
16
17
  fetch: null,
17
18
  echoEvents: false,
19
+ cookieDomain: undefined,
20
+ runtime: undefined,
18
21
  };
19
22
 
20
23
  export const parseQuery = (qs?: string): Record<string, string> => {
@@ -255,13 +258,13 @@ function adjustPayload(payload: any, config: JitsuOptions, storage: PersistentSt
255
258
  const referrer = runtime.referrer();
256
259
  const context = {
257
260
  library: {
258
- name: "jitsu-js",
259
- version: "1.0.0",
261
+ name: jitsuLibraryName,
262
+ version: jitsuVersion,
260
263
  },
261
264
  userAgent: runtime.userAgent(),
262
265
  locale: runtime.language(),
263
266
  screen: runtime.screen(),
264
- traits: payload.type != "identify" ? { ...(restoreTraits(storage) || {}) } : undefined,
267
+ traits: payload.type != "identify" && payload.type != "group" ? { ...(restoreTraits(storage) || {}) } : undefined,
265
268
  page: {
266
269
  path: properties.path || (parsedUrl && parsedUrl.pathname),
267
270
  referrer: referrer,
@@ -270,7 +273,7 @@ function adjustPayload(payload: any, config: JitsuOptions, storage: PersistentSt
270
273
  search: properties.search || (parsedUrl && parsedUrl.search),
271
274
  title: properties.title || runtime.pageTitle(),
272
275
  url: properties.url || url,
273
- enconding: properties.enconding || runtime.documentEncoding(),
276
+ encoding: properties.encoding || runtime.documentEncoding(),
274
277
  },
275
278
  campaign: parseUtms(query),
276
279
  };
@@ -279,7 +282,7 @@ function adjustPayload(payload: any, config: JitsuOptions, storage: PersistentSt
279
282
  timestamp: new Date().toISOString(),
280
283
  sentAt: new Date().toISOString(),
281
284
  messageId: randomId(properties.path || (parsedUrl && parsedUrl.pathname)),
282
- writeKey: config.writeKey,
285
+ writeKey: validateWriteKey(config.writeKey),
283
286
  context: deepMerge(context, customContext),
284
287
  };
285
288
  delete withContext.meta;
@@ -291,6 +294,7 @@ export type DestinationDescriptor = {
291
294
  id: string;
292
295
  destinationType: string;
293
296
  credentials: any;
297
+ options: any;
294
298
  deviceOptions: DeviceOptions;
295
299
  };
296
300
  export type AnalyticsPluginDescriptor = {
@@ -315,11 +319,13 @@ async function processDestinations(
315
319
  ) {
316
320
  const promises: Promise<any>[] = [];
317
321
  for (const destination of destinations) {
322
+ const credentials = { ...destination.credentials, ...destination.options };
323
+
318
324
  if (destination.deviceOptions.type === "internal-plugin") {
319
325
  const plugin = internalDestinationPlugins[destination.deviceOptions.name];
320
326
  if (plugin) {
321
327
  try {
322
- promises.push(plugin.handle(destination.credentials, event));
328
+ promises.push(plugin.handle(credentials, event));
323
329
  } catch (e) {
324
330
  console.warn(
325
331
  `[JITSU] Error processing event with internal plugin '${destination.deviceOptions.name}': ${e?.message}`,
@@ -341,7 +347,7 @@ async function processDestinations(
341
347
  } else {
342
348
  let pluginInstance: any;
343
349
  try {
344
- pluginInstance = (typeof plugin === "function" ? plugin : plugin.init)(destination.credentials);
350
+ pluginInstance = (typeof plugin === "function" ? plugin : plugin.init)(credentials);
345
351
  } catch (e) {
346
352
  console.warn(
347
353
  `[JITSU] Error creating plugin '${destination.deviceOptions.moduleVarName}@${destination.deviceOptions.packageCdn}' for destination '${destination.id}': ${e?.message}`,
@@ -381,6 +387,21 @@ async function processDestinations(
381
387
  }
382
388
  }
383
389
 
390
+ function looksLikeCuid(id: string) {
391
+ return id.length === 25 && id.charAt(0) === "c";
392
+ }
393
+
394
+ function validateWriteKey(writeKey?: string): string | undefined {
395
+ if (writeKey) {
396
+ const [, secret] = writeKey.split(":", 2);
397
+ if (!secret && !looksLikeCuid(writeKey)) {
398
+ throw new Error(
399
+ `Legacy write key detected - ${writeKey}! This format doesn't work anymore, it should be 'key:secret'. Please download a new key from Jitsu UI`
400
+ );
401
+ }
402
+ }
403
+ return writeKey;
404
+ }
384
405
  function send(
385
406
  method,
386
407
  payload,
@@ -400,13 +421,15 @@ function send(
400
421
  "Please specify fetch function in jitsu plugin initialization, fetch isn't available in global scope"
401
422
  );
402
423
  }
403
- const authHeader = {};
404
424
  const debugHeader = jitsuConfig.debug ? { "X-Enable-Debug": "true" } : {};
405
425
 
406
426
  // if (jitsuConfig.debug) {
407
427
  // console.log(`[JITSU] Sending event to ${url}: `, JSON.stringify(payload, null, 2));
408
428
  // }
409
429
  const adjustedPayload = adjustPayload(payload, jitsuConfig, store);
430
+
431
+ const authHeader = jitsuConfig.writeKey ? { "X-Write-Key": validateWriteKey(jitsuConfig.writeKey) } : {};
432
+
410
433
  return fetch(url, {
411
434
  method: "POST",
412
435
  headers: {
@@ -439,7 +462,7 @@ function send(
439
462
  }
440
463
  if (response.destinations) {
441
464
  if (jitsuConfig.debug) {
442
- console.log(`[JITSU] Processing device destianations: `, JSON.stringify(response.destinations, null, 2));
465
+ console.log(`[JITSU] Processing device destinations: `, JSON.stringify(response.destinations, null, 2));
443
466
  }
444
467
  return processDestinations(response.destinations, method, adjustedPayload, !!jitsuConfig.debug, instance);
445
468
  }
@@ -469,12 +492,13 @@ const jitsuAnalyticsPlugin = (pluginConfig: JitsuOptions = {}): AnalyticsPlugin
469
492
  persistentStorage.removeItem(key);
470
493
  },
471
494
  });
495
+ const instanceConfig = {
496
+ ...defaultConfig,
497
+ ...pluginConfig,
498
+ };
472
499
  return {
473
500
  name: "jitsu",
474
- config: {
475
- ...config,
476
- ...pluginConfig,
477
- },
501
+ config: instanceConfig,
478
502
  initialize: args => {
479
503
  const { config } = args;
480
504
  if (config.debug) {
@@ -502,6 +526,22 @@ const jitsuAnalyticsPlugin = (pluginConfig: JitsuOptions = {}): AnalyticsPlugin
502
526
  //clear storage cache
503
527
  Object.keys(storageCache).forEach(key => delete storageCache[key]);
504
528
  },
529
+ methods: {
530
+ //analytics doesn't support group as a base method, so we need to add it manually
531
+ group(groupId?: ID, traits?: JSONObject | null, options?: Options, callback?: Callback) {
532
+ const analyticsInstance = this.instance;
533
+ const user = analyticsInstance.user();
534
+ const userId = options?.userId || user?.userId;
535
+ const anonymousId = options?.anonymousId || user?.anonymousId;
536
+ return send(
537
+ "group",
538
+ { type: "group", groupId, traits, ...(anonymousId ? { anonymousId } : {}), ...(userId ? { userId } : {}) },
539
+ instanceConfig,
540
+ analyticsInstance,
541
+ cachingStorageWrapper(analyticsInstance.storage)
542
+ );
543
+ },
544
+ },
505
545
  };
506
546
  };
507
547
 
@@ -0,0 +1,112 @@
1
+ import { loadScript } from "../script-loader";
2
+ import { AnalyticsClientEvent } from "@jitsu/protocols/analytics";
3
+ import { applyFilters, CommonDestinationCredentials, InternalPlugin } from "./index";
4
+
5
+ const defaultScriptSrc = "https://www.googletagmanager.com/gtag/js";
6
+
7
+ export type GtmDestinationCredentials = {
8
+ debug: boolean;
9
+ containerId: string;
10
+ dataLayerName: string;
11
+ preview: string;
12
+ auth: string;
13
+ customScriptSrc: string;
14
+ } & CommonDestinationCredentials;
15
+
16
+ export const gtmPlugin: InternalPlugin<GtmDestinationCredentials> = {
17
+ id: "gtm",
18
+ async handle(config, payload: AnalyticsClientEvent) {
19
+ if (!applyFilters(payload, config)) {
20
+ return;
21
+ }
22
+ await initGtmIfNeeded(config);
23
+
24
+ const dataLayer = window[config.dataLayerName || "dataLayer"];
25
+
26
+ switch (payload.type) {
27
+ case "page":
28
+ const { properties: pageProperties, context } = payload;
29
+ const pageEvent = {
30
+ event: "page_view",
31
+ url: pageProperties.url,
32
+ title: pageProperties.title,
33
+ referer: context?.page?.referrer ?? "",
34
+ };
35
+ if (config.debug) {
36
+ console.log("gtag push", pageEvent);
37
+ }
38
+ dataLayer.push(pageEvent);
39
+ break;
40
+ case "track":
41
+ const { properties: trackProperties } = payload;
42
+ const trackEvent: any = { event: payload.event, ...trackProperties };
43
+ if (payload.userId) {
44
+ trackEvent.userId = payload.userId;
45
+ }
46
+ if (payload.anonymousId) {
47
+ trackEvent.anonymousId = payload.anonymousId;
48
+ }
49
+ if (config.debug) {
50
+ console.log("gtag push", trackEvent);
51
+ }
52
+ dataLayer.push(trackEvent);
53
+ break;
54
+ case "identify":
55
+ const { traits } = payload;
56
+ const identifyEvent: any = { event: "identify", ...traits };
57
+ if (payload.userId) {
58
+ identifyEvent.userId = payload.userId;
59
+ }
60
+ if (payload.anonymousId) {
61
+ identifyEvent.anonymousId = payload.anonymousId;
62
+ }
63
+ if (config.debug) {
64
+ console.log("gtag push", identifyEvent);
65
+ }
66
+ dataLayer.push(identifyEvent);
67
+ break;
68
+ }
69
+ },
70
+ };
71
+
72
+ type GtmState = "fresh" | "loading" | "loaded" | "failed";
73
+
74
+ function getGtmState(): GtmState {
75
+ return window["__jitsuGtmState"] || "fresh";
76
+ }
77
+
78
+ function setGtmState(s: GtmState) {
79
+ window["__jitsuGtmState"] = s;
80
+ }
81
+
82
+ async function initGtmIfNeeded(config: GtmDestinationCredentials) {
83
+ if (getGtmState() !== "fresh") {
84
+ return;
85
+ }
86
+ setGtmState("loading");
87
+
88
+ const dlName = config.dataLayerName || "dataLayer";
89
+ const dlParam = dlName !== "dataLayer" ? "&l=" + dlName : "";
90
+ const previewParams = config.preview
91
+ ? `&gtm_preview=${config.preview}&gtm_auth=${config.auth}&gtm_cookies_win=x`
92
+ : "";
93
+ const scriptSrc = `${config.customScriptSrc || defaultScriptSrc}?id=${config.containerId}${dlParam}${previewParams}`;
94
+
95
+ window[dlName] = window[dlName] || [];
96
+ const gtag = function () {
97
+ window[dlName].push(arguments);
98
+ };
99
+ // @ts-ignore
100
+ gtag("js", new Date());
101
+ // @ts-ignore
102
+ gtag("config", config.containerId);
103
+
104
+ loadScript(scriptSrc)
105
+ .then(() => {
106
+ setGtmState("loaded");
107
+ })
108
+ .catch(e => {
109
+ console.warn(`GTM (containerId=${config.containerId}) init failed: ${e.message}`, e);
110
+ setGtmState("failed");
111
+ });
112
+ }
@@ -0,0 +1,47 @@
1
+ import { AnalyticsClientEvent } from "@jitsu/protocols/analytics";
2
+ import { tagPlugin } from "./tag";
3
+ import { logrocketPlugin } from "./logrocket";
4
+ import { gtmPlugin } from "./gtm";
5
+
6
+ export type InternalPlugin<T> = {
7
+ id: string;
8
+ handle(config: T, payload: AnalyticsClientEvent): Promise<void>;
9
+ };
10
+
11
+ export type CommonDestinationCredentials = {
12
+ hosts?: string;
13
+ events?: string;
14
+ };
15
+
16
+ export function satisfyFilter(filter: string, subject: string | undefined): boolean {
17
+ return filter === "*" || filter.toLowerCase().trim() === (subject || "").trim().toLowerCase();
18
+ }
19
+
20
+ export function satisfyDomainFilter(filter: string, subject: string | undefined): boolean {
21
+ if (filter === "*") {
22
+ return true;
23
+ }
24
+ subject = subject || "";
25
+
26
+ if (filter.startsWith("*.")) {
27
+ return subject.endsWith(filter.substring(1));
28
+ } else {
29
+ return filter === subject;
30
+ }
31
+ }
32
+
33
+ export function applyFilters(event: AnalyticsClientEvent, creds: CommonDestinationCredentials): boolean {
34
+ const { hosts = "*", events = "*" } = creds;
35
+ const eventsArray = events.split("\n");
36
+ return (
37
+ !!hosts.split("\n").find(hostFilter => satisfyDomainFilter(hostFilter, event.context?.host)) &&
38
+ (!!eventsArray.find(eventFilter => satisfyFilter(eventFilter, event.type)) ||
39
+ !!eventsArray.find(eventFilter => satisfyFilter(eventFilter, event.event)))
40
+ );
41
+ }
42
+
43
+ export const internalDestinationPlugins: Record<string, InternalPlugin<any>> = {
44
+ [tagPlugin.id]: tagPlugin,
45
+ [gtmPlugin.id]: gtmPlugin,
46
+ [logrocketPlugin.id]: logrocketPlugin,
47
+ };
@@ -0,0 +1,83 @@
1
+ import { loadScript } from "../script-loader";
2
+ import { AnalyticsClientEvent } from "@jitsu/protocols/analytics";
3
+ import { applyFilters, CommonDestinationCredentials, InternalPlugin } from "./index";
4
+
5
+ export type LogRocketDestinationCredentials = {
6
+ appId: string;
7
+ } & CommonDestinationCredentials;
8
+
9
+ export const logrocketPlugin: InternalPlugin<LogRocketDestinationCredentials> = {
10
+ id: "logrocket",
11
+ async handle(config, payload: AnalyticsClientEvent) {
12
+ if (!applyFilters(payload, config)) {
13
+ return;
14
+ }
15
+ initLogrocketIfNeeded(config.appId);
16
+
17
+ const action = logRocket => {
18
+ if (payload.type === "identify" && payload.userId) {
19
+ logRocket.identify(payload.userId, payload.traits || {});
20
+ }
21
+ };
22
+ getLogRocketQueue().push(action);
23
+ if (getLogRocketState() === "loaded") {
24
+ flushLogRocketQueue(window["LogRocket"]);
25
+ }
26
+ },
27
+ };
28
+
29
+ type LogRocketState = "fresh" | "loading" | "loaded" | "failed";
30
+
31
+ function getLogRocketState(): LogRocketState {
32
+ return window["__jitsuLrState"] || "fresh";
33
+ }
34
+
35
+ function setLogRocketState(s: LogRocketState) {
36
+ window["__jitsuLrState"] = s;
37
+ }
38
+
39
+ function getLogRocketQueue(): ((lr: LogRocket) => void | Promise<void>)[] {
40
+ return window["__jitsuLrQueue"] || (window["__jitsuLrQueue"] = []);
41
+ }
42
+
43
+ export type LogRocket = any;
44
+
45
+ function flushLogRocketQueue(lr: LogRocket) {
46
+ const queue = getLogRocketQueue();
47
+
48
+ while (queue.length > 0) {
49
+ const method = queue.shift();
50
+ try {
51
+ const res = method(lr);
52
+ if (res) {
53
+ res.catch(e => console.warn(`Async LogRocket method failed: ${e.message}`, e));
54
+ }
55
+ } catch (e) {
56
+ console.warn(`LogRocket method failed: ${e.message}`, e);
57
+ }
58
+ }
59
+ }
60
+
61
+ async function initLogrocketIfNeeded(appId: string) {
62
+ if (getLogRocketState() !== "fresh") {
63
+ return;
64
+ }
65
+ setLogRocketState("loading");
66
+ loadScript(`https://cdn.lr-ingest.io/LogRocket.min.js`, { crossOrigin: "anonymous" })
67
+ .then(() => {
68
+ if (window["LogRocket"]) {
69
+ try {
70
+ window["LogRocket"].init(appId);
71
+ } catch (e) {
72
+ console.warn(`LogRocket (id=${appId}) init failed: ${e.message}`, e);
73
+ setLogRocketState("failed");
74
+ }
75
+ setLogRocketState("loaded");
76
+ flushLogRocketQueue(window["LogRocket"]);
77
+ }
78
+ })
79
+ .catch(e => {
80
+ console.warn(`LogRocket (id=${appId}) init failed: ${e.message}`, e);
81
+ setLogRocketState("failed");
82
+ });
83
+ }
@@ -1,33 +1,12 @@
1
1
  import { AnalyticsClientEvent } from "@jitsu/protocols/analytics";
2
- import { isInBrowser, randomId } from "./analytics-plugin";
3
-
4
- export type InternalPlugin<T> = {
5
- id: string;
6
- handle(config: T, payload: AnalyticsClientEvent): Promise<void>;
7
- };
8
-
9
- export type CommonDestinationCredentials = {
10
- hosts?: string[];
11
- events?: string[];
12
- };
2
+ import { applyFilters, CommonDestinationCredentials, InternalPlugin } from "./index";
3
+ import { isInBrowser, randomId } from "../analytics-plugin";
13
4
 
14
5
  export type TagDestinationCredentials = {
15
6
  code: string;
16
7
  } & CommonDestinationCredentials;
17
8
 
18
- function satisfyFilter(filter: string, subject: string | undefined): boolean {
19
- return filter === "*" || filter.toLowerCase().trim() === (subject || "").trim().toLowerCase();
20
- }
21
-
22
- function applyFilters(event: AnalyticsClientEvent, creds: CommonDestinationCredentials): boolean {
23
- const { hosts = ["*"], events = ["*"] } = creds;
24
- return (
25
- !!hosts.find(hostFilter => satisfyFilter(hostFilter, event.context?.host)) &&
26
- !!events.find(eventFilter => satisfyFilter(eventFilter, event.type))
27
- );
28
- }
29
-
30
- const tagPlugin: InternalPlugin<TagDestinationCredentials> = {
9
+ export const tagPlugin: InternalPlugin<TagDestinationCredentials> = {
31
10
  id: "tag",
32
11
  async handle(config, payload: AnalyticsClientEvent) {
33
12
  if (!applyFilters(payload, config)) {
@@ -97,7 +76,3 @@ function execJs(code: string, event: any) {
97
76
  function replaceMacro(code, event) {
98
77
  return code.replace(/{{\s*event\s*}}/g, JSON.stringify(event));
99
78
  }
100
-
101
- export const internalDestinationPlugins: Record<string, InternalPlugin<any>> = {
102
- [tagPlugin.id]: tagPlugin,
103
- };
package/src/index.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  import Analytics from "analytics";
2
2
  import { AnalyticsInterface, JitsuOptions, RuntimeFacade } from "./jitsu";
3
3
  import jitsuAnalyticsPlugin, { emptyRuntime, isInBrowser, windowRuntime } from "./analytics-plugin";
4
- import { delayMethodExec } from "./method-queue";
5
- import e from "express";
4
+ import { Callback, DispatchedEvent, ID, JSONObject, Options } from "@jitsu/protocols/analytics";
6
5
 
7
6
  export default function parse(input) {
8
7
  let value = input;
@@ -24,6 +23,15 @@ export default function parse(input) {
24
23
  return value;
25
24
  }
26
25
 
26
+ export const emptyAnalytics = {
27
+ track: () => Promise.resolve(),
28
+ page: () => Promise.resolve(),
29
+ user: () => ({}),
30
+ identify: () => Promise.resolve({}),
31
+ group: () => Promise.resolve({}),
32
+ reset: () => Promise.resolve({}),
33
+ };
34
+
27
35
  function createUnderlyingAnalyticsInstance(
28
36
  opts: JitsuOptions,
29
37
  rt: RuntimeFacade,
@@ -46,7 +54,17 @@ function createUnderlyingAnalyticsInstance(
46
54
  return originalPage(...args);
47
55
  }
48
56
  };
49
- return analytics as AnalyticsInterface;
57
+ return {
58
+ ...analytics,
59
+ group(groupId?: ID, traits?: JSONObject | null, options?: Options, callback?: Callback): Promise<DispatchedEvent> {
60
+ for (const plugin of Object.values(analytics.plugins)) {
61
+ if (plugin["group"]) {
62
+ plugin["group"](groupId, traits, options, callback);
63
+ }
64
+ }
65
+ return Promise.resolve({});
66
+ },
67
+ } as AnalyticsInterface;
50
68
  }
51
69
 
52
70
  export function jitsuAnalytics(opts: JitsuOptions): AnalyticsInterface {
package/src/jitsu.ts CHANGED
@@ -16,7 +16,7 @@ type JitsuOptions = {
16
16
  */
17
17
  debug?: boolean;
18
18
  /**
19
- * Explicitely specify cookie domain. If not set, cookie domain will be set to top level
19
+ * Explicitly specify cookie domain. If not set, cookie domain will be set to top level
20
20
  * of the current domain. Example: if JS lives on "app.example.com", cookie domain will be
21
21
  * set to ".example.com". If it lives on "example.com", cookie domain will be set to ".example.com" too
22
22
  */
package/src/version.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { name as jitsuLibraryName, version as jitsuVersion } from "../package.json";
2
+
3
+ export { jitsuVersion, jitsuLibraryName };
package/tsconfig.json CHANGED
@@ -5,6 +5,7 @@
5
5
  "declaration": true,
6
6
  "esModuleInterop": true,
7
7
  "moduleResolution": "Node",
8
+ "resolveJsonModule": true,
8
9
  "target": "ES2015",
9
10
  "lib": ["es2017", "dom"],
10
11
  //this makes typescript igore @types/node during compilation
@@ -1,13 +0,0 @@
1
- import { AnalyticsClientEvent } from "@jitsu/protocols/analytics";
2
- export type InternalPlugin<T> = {
3
- id: string;
4
- handle(config: T, payload: AnalyticsClientEvent): Promise<void>;
5
- };
6
- export type CommonDestinationCredentials = {
7
- hosts?: string[];
8
- events?: string[];
9
- };
10
- export type TagDestinationCredentials = {
11
- code: string;
12
- } & CommonDestinationCredentials;
13
- export declare const internalDestinationPlugins: Record<string, InternalPlugin<any>>;