@jitsu/js 1.1.0 → 1.1.1

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 config: 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,12 +319,12 @@ 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
- //to support old versions, where credentials were stored in root
323
- const credentials = destination.credentials || destination;
324
328
  promises.push(plugin.handle(credentials, event));
325
329
  } catch (e) {
326
330
  console.warn(
@@ -343,7 +347,7 @@ async function processDestinations(
343
347
  } else {
344
348
  let pluginInstance: any;
345
349
  try {
346
- pluginInstance = (typeof plugin === "function" ? plugin : plugin.init)(destination.credentials);
350
+ pluginInstance = (typeof plugin === "function" ? plugin : plugin.init)(credentials);
347
351
  } catch (e) {
348
352
  console.warn(
349
353
  `[JITSU] Error creating plugin '${destination.deviceOptions.moduleVarName}@${destination.deviceOptions.packageCdn}' for destination '${destination.id}': ${e?.message}`,
@@ -383,6 +387,18 @@ async function processDestinations(
383
387
  }
384
388
  }
385
389
 
390
+ function validateWriteKey(writeKey?: string): string | undefined {
391
+ if (writeKey) {
392
+ const [, secret] = writeKey.split(":", 2);
393
+ if (!secret) {
394
+ throw new Error(
395
+ `Legacy write key detected - ${writeKey}! This format doesn't work anymore, it should be 'key:secret'. Please download a new key from Jitsu UI`
396
+ );
397
+ }
398
+ }
399
+ return writeKey;
400
+ }
401
+
386
402
  function send(
387
403
  method,
388
404
  payload,
@@ -408,7 +424,7 @@ function send(
408
424
  // console.log(`[JITSU] Sending event to ${url}: `, JSON.stringify(payload, null, 2));
409
425
  // }
410
426
  const adjustedPayload = adjustPayload(payload, jitsuConfig, store);
411
- const authHeader = adjustedPayload.writeKey ? { "X-Write-Key": adjustedPayload.writeKey } : {};
427
+ const authHeader = config.writeKey ? { "X-Write-Key": validateWriteKey(config.writeKey) } : {};
412
428
  return fetch(url, {
413
429
  method: "POST",
414
430
  headers: {
@@ -441,7 +457,7 @@ function send(
441
457
  }
442
458
  if (response.destinations) {
443
459
  if (jitsuConfig.debug) {
444
- console.log(`[JITSU] Processing device destianations: `, JSON.stringify(response.destinations, null, 2));
460
+ console.log(`[JITSU] Processing device destinations: `, JSON.stringify(response.destinations, null, 2));
445
461
  }
446
462
  return processDestinations(response.destinations, method, adjustedPayload, !!jitsuConfig.debug, instance);
447
463
  }
@@ -471,12 +487,13 @@ const jitsuAnalyticsPlugin = (pluginConfig: JitsuOptions = {}): AnalyticsPlugin
471
487
  persistentStorage.removeItem(key);
472
488
  },
473
489
  });
490
+ const instanceConfig = {
491
+ ...config,
492
+ ...pluginConfig,
493
+ };
474
494
  return {
475
495
  name: "jitsu",
476
- config: {
477
- ...config,
478
- ...pluginConfig,
479
- },
496
+ config: instanceConfig,
480
497
  initialize: args => {
481
498
  const { config } = args;
482
499
  if (config.debug) {
@@ -504,6 +521,22 @@ const jitsuAnalyticsPlugin = (pluginConfig: JitsuOptions = {}): AnalyticsPlugin
504
521
  //clear storage cache
505
522
  Object.keys(storageCache).forEach(key => delete storageCache[key]);
506
523
  },
524
+ methods: {
525
+ //analytics doesn't support group as a base method, so we need to add it manually
526
+ group(groupId?: ID, traits?: JSONObject | null, options?: Options, callback?: Callback) {
527
+ const analyticsInstance = this.instance;
528
+ const user = analyticsInstance.user();
529
+ const userId = options?.userId || user?.userId;
530
+ const anonymousId = options?.anonymousId || user?.anonymousId;
531
+ return send(
532
+ "group",
533
+ { type: "group", groupId, traits, ...(anonymousId ? { anonymousId } : {}), ...(userId ? { userId } : {}) },
534
+ instanceConfig,
535
+ analyticsInstance,
536
+ cachingStorageWrapper(analyticsInstance.storage)
537
+ );
538
+ },
539
+ },
507
540
  };
508
541
  };
509
542
 
@@ -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;
@@ -29,6 +28,7 @@ export const emptyAnalytics = {
29
28
  page: () => Promise.resolve(),
30
29
  user: () => ({}),
31
30
  identify: () => Promise.resolve({}),
31
+ group: () => Promise.resolve({}),
32
32
  reset: () => Promise.resolve({}),
33
33
  };
34
34
 
@@ -54,7 +54,17 @@ function createUnderlyingAnalyticsInstance(
54
54
  return originalPage(...args);
55
55
  }
56
56
  };
57
- 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;
58
68
  }
59
69
 
60
70
  export function jitsuAnalytics(opts: JitsuOptions): AnalyticsInterface {
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