@jitsu/js 1.9.10 → 1.9.12

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.
@@ -214,6 +214,33 @@ describe("Test Jitsu NodeJS client", () => {
214
214
  expect((p.body.anonymousId ?? "").length).toBeGreaterThan(0);
215
215
  });
216
216
 
217
+ test("test defaultPayloadContext", async () => {
218
+ const config = {
219
+ host: server.baseUrl,
220
+ writeKey: "key:secret",
221
+ debug: true,
222
+ defaultPayloadContext: {
223
+ awesomeIdentifier: "awesome-identifier",
224
+ awesome: {
225
+ nestedKey: "awesome-key",
226
+ },
227
+ },
228
+ };
229
+ const client = jitsuAnalytics(config);
230
+ expect(requestLog.length).toBe(0);
231
+ await client.identify("myUserId", { email: "myUserId@example.com" });
232
+ await client.group("myGroupId", { name: "myGroupId" });
233
+ await client.track("myEvent", { prop1: "value1" });
234
+ await new Promise(resolve => setTimeout(resolve, 1000));
235
+ expect(requestLog.length).toBe(3);
236
+ expect(requestLog[0].body.context.awesomeIdentifier).toBe("awesome-identifier");
237
+ expect(requestLog[0].body.context.awesome.nestedKey).toBe("awesome-key");
238
+ expect(requestLog[1].body.context.awesomeIdentifier).toBe("awesome-identifier");
239
+ expect(requestLog[1].body.context.awesome.nestedKey).toBe("awesome-key");
240
+ expect(requestLog[2].body.context.awesomeIdentifier).toBe("awesome-identifier");
241
+ expect(requestLog[2].body.context.awesome.nestedKey).toBe("awesome-key");
242
+ });
243
+
217
244
  test("node js", async () => {
218
245
  const jitsu: AnalyticsInterface = jitsuAnalytics({
219
246
  writeKey: "key:secret",
@@ -0,0 +1,26 @@
1
+ <!DOCTYPE html>
2
+
3
+ <html lang="en">
4
+ <head>
5
+ <meta charset="utf-8" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
7
+
8
+ <title>Tracking page</title>
9
+ <script>
10
+ window.testOnload = async j => {
11
+ j.track("pageLoaded");
12
+ };
13
+ </script>
14
+ <script
15
+ type="text/javascript"
16
+ src="<%=trackingBase%>/p.js"
17
+ data-onload="testOnload"
18
+ data-debug="true"
19
+ defer
20
+ ></script>
21
+ </head>
22
+
23
+ <body>
24
+ <h1>Test</h1>
25
+ </body>
26
+ </html>
@@ -7,6 +7,11 @@
7
7
 
8
8
  <title>Tracking page</title>
9
9
  <script>
10
+ window.jitsuConfig = {
11
+ cookieCapture: {
12
+ ttp: "_ttp",
13
+ },
14
+ };
10
15
  window.testOnload = async j => {
11
16
  j.identify("john-doe-id-1", { email: "john.doe@gmail.com" });
12
17
  j.track("pageLoaded", { trackParam: "trackValue" });
@@ -10,6 +10,7 @@
10
10
  window.testOnload = async j => {
11
11
  await j.setAnonymousId("john-doe-id-1");
12
12
  await j.identify("john-nondoe", { email: "john@example.com" });
13
+ await j.identify("john-nondoe", { email: "john2@example.com", $doNotSend: true });
13
14
  await j.track("pageLoaded", { trackParam: "trackValue" });
14
15
  await j.reset();
15
16
  await j.track("pageLoaded", { trackParam: "trackValue" });
@@ -273,6 +273,9 @@ test("reset", async ({ browser }) => {
273
273
  const newAnonymousId = cookies[0].value;
274
274
  console.log(`🍪Cookies`, cookies);
275
275
 
276
+ //second identify call should not reach the server, but it should change the traits
277
+ expect(firstTrack.body.context.traits?.email).toEqual("john2@example.com");
278
+
276
279
  expect(secondTrack.body.anonymousId).not.toBeNull();
277
280
  expect(secondTrack.body.anonymousId).toBeDefined();
278
281
  expect(secondTrack.body.anonymousId).toEqual(newAnonymousId);
@@ -447,9 +450,46 @@ test("disable-user-ids-then-consent", async ({ browser }) => {
447
450
  expect((p.body.anonymousId ?? "").length).toBeGreaterThan(0);
448
451
  });
449
452
 
453
+ test("anonymous-id-bug", async ({ browser }) => {
454
+ clearRequestLog();
455
+ const anonymousId = "1724633695283.638279";
456
+ const browserContext = await browser.newContext();
457
+ await browserContext.addCookies([{ name: "__eventn_id", value: anonymousId, url: server.baseUrl }]);
458
+ const { page, uncaughtErrors } = await createLoggingPage(browserContext);
459
+ const [pageResult] = await Promise.all([page.goto(`${server.baseUrl}/anonymous-id-bug.html`)]);
460
+ await page.waitForFunction(() => window["jitsu"] !== undefined, undefined, {
461
+ timeout: 1000,
462
+ polling: 100,
463
+ });
464
+ expect(pageResult.status()).toBe(200);
465
+ const cookies = (await browserContext.cookies()).reduce(
466
+ (res, cookie) => ({
467
+ ...res,
468
+ [cookie.name]: cookie.value,
469
+ }),
470
+ {}
471
+ );
472
+ console.log("🍪 Jitsu Cookies", cookies);
473
+ //wait for some time since the server has an artificial latency of 30ms
474
+ await new Promise(resolve => setTimeout(resolve, 1000));
475
+ expect(uncaughtErrors.length).toEqual(0);
476
+ console.log(
477
+ `📝 Request log size of ${requestLog.length}`,
478
+ requestLog.map(x => describeEvent(x.type, x.body))
479
+ );
480
+ const p = requestLog[0];
481
+ console.log(chalk.bold("📝 Checking page event"), JSON.stringify(p, null, 3));
482
+ expect(p.body.anonymousId).toEqual(anonymousId);
483
+ });
484
+
450
485
  test("basic", async ({ browser }) => {
451
486
  clearRequestLog();
452
487
  const browserContext = await browser.newContext();
488
+ await browserContext.addCookies([
489
+ { name: "_fbc", value: "fbc-id", url: server.baseUrl },
490
+ { name: "_fbp", value: "fbp-id", url: server.baseUrl },
491
+ { name: "_ttp", value: "ttp-id", url: server.baseUrl },
492
+ ]);
453
493
 
454
494
  const { page: firstPage, uncaughtErrors: firstPageErrors } = await createLoggingPage(browserContext);
455
495
  const [pageResult] = await Promise.all([
@@ -495,6 +535,9 @@ test("basic", async ({ browser }) => {
495
535
  console.log(chalk.bold("📝 Checking track event"), JSON.stringify(track, null, 3));
496
536
  expect(track.properties.trackParam).toEqual("trackValue");
497
537
  expect(track.type).toEqual("track");
538
+ expect(track.context.clientIds).toHaveProperty("fbc", "fbc-id");
539
+ expect(track.context.clientIds).toHaveProperty("fbp", "fbp-id");
540
+ expect(track.context.clientIds).toHaveProperty("ttp", "ttp-id");
498
541
  expect(track.context.traits.email).toEqual("john.doe@gmail.com");
499
542
  expect(track.userId).toEqual("john-doe-id-1");
500
543
  expect(track.event).toEqual("pageLoaded");
@@ -506,6 +549,9 @@ test("basic", async ({ browser }) => {
506
549
 
507
550
  console.log(chalk.bold("📝 Checking page event"), JSON.stringify(page, null, 3));
508
551
  expect(page.anonymousId).toEqual(anonymousId);
552
+ expect(page.context.clientIds).toHaveProperty("fbc", "fbc-id");
553
+ expect(page.context.clientIds).toHaveProperty("fbp", "fbp-id");
554
+ expect(page.context.clientIds).toHaveProperty("ttp", "ttp-id");
509
555
  expect(page.context.traits.email).toEqual("john.doe@gmail.com");
510
556
  expect(page.userId).toEqual("john-doe-id-1");
511
557
 
package/dist/jitsu.cjs.js CHANGED
@@ -1061,10 +1061,13 @@ const defaultConfig = {
1061
1061
  fetch: null,
1062
1062
  echoEvents: false,
1063
1063
  cookieDomain: undefined,
1064
+ cookieCapture: {},
1064
1065
  runtime: undefined,
1065
1066
  fetchTimeoutMs: undefined,
1066
1067
  s2s: undefined,
1067
1068
  idEndpoint: undefined,
1069
+ errorPolicy: "log",
1070
+ defaultPayloadContext: {},
1068
1071
  privacy: {
1069
1072
  dontSend: false,
1070
1073
  disableUserIds: false,
@@ -1149,6 +1152,14 @@ function getCookie(name) {
1149
1152
  const parts = value.split(`; ${name}=`);
1150
1153
  return parts.length === 2 ? parts.pop().split(";").shift() : undefined;
1151
1154
  }
1155
+ function getClientIds(runtime, customCookieCapture) {
1156
+ const cookieCapture = Object.assign({ fbc: "_fbc", fbp: "_fbp" }, customCookieCapture);
1157
+ const clientIds = Object.entries(cookieCapture).reduce((acc, [key, cookieName]) => {
1158
+ acc[key] = runtime.getCookie(cookieName);
1159
+ return acc;
1160
+ }, {});
1161
+ return Object.assign(Object.assign({}, clientIds), getGa4Ids(runtime));
1162
+ }
1152
1163
  function getGa4Ids(runtime) {
1153
1164
  var _a;
1154
1165
  const allCookies = runtime.getCookies();
@@ -1226,6 +1237,10 @@ const cookieStorage = (cookieDomain, key2cookie) => {
1226
1237
  getItem(key) {
1227
1238
  const cookieName = key2cookie[key] || key;
1228
1239
  const result = getCookie(cookieName);
1240
+ if (key === "__anon_id") {
1241
+ //anonymous id must always be a string, so we don't parse it to preserve its exact value
1242
+ return result;
1243
+ }
1229
1244
  if (typeof result === "undefined" && key === "__user_id") {
1230
1245
  //backward compatibility with old jitsu cookie. get user id if from traits
1231
1246
  const traits = parse(getCookie("__eventn_id_usr")) || {};
@@ -1401,18 +1416,19 @@ function urlPath(url) {
1401
1416
  return "/" + pathMatch;
1402
1417
  }
1403
1418
  function adjustPayload(payload, config, storage, s2s) {
1404
- var _a, _b, _c, _d, _e;
1419
+ var _a, _b, _c, _d, _e, _f;
1405
1420
  const runtime = config.runtime || (isInBrowser() ? windowRuntime(config) : emptyRuntime(config));
1406
1421
  const url = runtime.pageUrl();
1407
1422
  const parsedUrl = safeCall(() => new URL(url), undefined);
1408
1423
  const query = parsedUrl ? parseQuery(parsedUrl.search) : {};
1409
1424
  const properties = payload.properties || {};
1410
- if (payload.type === "page" && url) {
1411
- properties.url = url.replace(hashRegex, "");
1412
- properties.path = fixPath(urlPath(url));
1425
+ if (payload.type === "page" && (properties.url || url)) {
1426
+ const targetUrl = properties.url || url;
1427
+ properties.url = targetUrl.replace(hashRegex, "");
1428
+ properties.path = fixPath(urlPath(targetUrl));
1413
1429
  }
1414
- const customContext = ((_a = payload.properties) === null || _a === void 0 ? void 0 : _a.context) || {};
1415
- (_b = payload.properties) === null || _b === void 0 ? true : delete _b.context;
1430
+ const customContext = deepMerge(config.defaultPayloadContext, ((_a = payload.properties) === null || _a === void 0 ? void 0 : _a.context) || ((_b = payload.options) === null || _b === void 0 ? void 0 : _b.context) || {});
1431
+ (_c = payload.properties) === null || _c === void 0 ? true : delete _c.context;
1416
1432
  const referrer = runtime.referrer();
1417
1433
  const context = {
1418
1434
  library: {
@@ -1420,7 +1436,7 @@ function adjustPayload(payload, config, storage, s2s) {
1420
1436
  version: jitsuVersion,
1421
1437
  env: isInBrowser() ? "browser" : "node",
1422
1438
  },
1423
- consent: ((_c = config.privacy) === null || _c === void 0 ? void 0 : _c.consentCategories)
1439
+ consent: ((_d = config.privacy) === null || _d === void 0 ? void 0 : _d.consentCategories)
1424
1440
  ? {
1425
1441
  categoryPreferences: config.privacy.consentCategories,
1426
1442
  }
@@ -1439,14 +1455,13 @@ function adjustPayload(payload, config, storage, s2s) {
1439
1455
  url: properties.url || url,
1440
1456
  encoding: properties.encoding || runtime.documentEncoding(),
1441
1457
  },
1442
- clientIds: !((_d = config.privacy) === null || _d === void 0 ? void 0 : _d.disableUserIds)
1443
- ? Object.assign({ fbc: runtime.getCookie("_fbc"), fbp: runtime.getCookie("_fbp") }, getGa4Ids(runtime)) : undefined,
1458
+ clientIds: !((_e = config.privacy) === null || _e === void 0 ? void 0 : _e.disableUserIds) ? getClientIds(runtime, config.cookieCapture) : undefined,
1444
1459
  campaign: parseUtms(query),
1445
1460
  };
1446
1461
  const withContext = Object.assign(Object.assign({}, payload), { timestamp: new Date().toISOString(), sentAt: new Date().toISOString(), messageId: randomId(properties.path || (parsedUrl && parsedUrl.pathname)), writeKey: maskWriteKey(config.writeKey), groupId: storage.getItem("__group_id"), context: deepMerge(context, customContext) });
1447
1462
  delete withContext.meta;
1448
1463
  delete withContext.options;
1449
- if ((_e = config.privacy) === null || _e === void 0 ? void 0 : _e.disableUserIds) {
1464
+ if ((_f = config.privacy) === null || _f === void 0 ? void 0 : _f.disableUserIds) {
1450
1465
  delete withContext.userId;
1451
1466
  delete withContext.anonymousId;
1452
1467
  delete withContext.context.traits;
@@ -1555,6 +1570,23 @@ function maskWriteKey(writeKey) {
1555
1570
  }
1556
1571
  return writeKey;
1557
1572
  }
1573
+ function getErrorHandler(opts) {
1574
+ const configuredHandler = opts.errorPolicy || "log";
1575
+ if (typeof configuredHandler === "function") {
1576
+ return configuredHandler;
1577
+ }
1578
+ else if (configuredHandler === "rethrow") {
1579
+ return (msg, ...args) => {
1580
+ //ignore args, not clear what to do with them
1581
+ throw new Error(msg);
1582
+ };
1583
+ }
1584
+ else {
1585
+ return (msg, ...args) => {
1586
+ console.error(msg, ...args);
1587
+ };
1588
+ }
1589
+ }
1558
1590
  function send(method, payload, jitsuConfig, instance, store) {
1559
1591
  return __awaiter$1(this, void 0, void 0, function* () {
1560
1592
  var _a, _b;
@@ -1566,6 +1598,7 @@ function send(method, payload, jitsuConfig, instance, store) {
1566
1598
  const url = s2s ? `${jitsuConfig.host}/api/s/s2s/${method}` : `${jitsuConfig.host}/api/s/${method}`;
1567
1599
  const fetch = jitsuConfig.fetch || globalThis.fetch;
1568
1600
  if (!fetch) {
1601
+ //don't run it through error handler since error is critical and should be addressed
1569
1602
  throw new Error("Please specify fetch function in jitsu plugin initialization, fetch isn't available in global scope");
1570
1603
  }
1571
1604
  const debugHeader = jitsuConfig.debug ? { "X-Enable-Debug": "true" } : {};
@@ -1596,7 +1629,8 @@ function send(method, payload, jitsuConfig, instance, store) {
1596
1629
  }
1597
1630
  }
1598
1631
  catch (e) {
1599
- throw new Error(`Calling ${url} failed: ${e.message}`);
1632
+ getErrorHandler(jitsuConfig)(`Call to ${url} failed with error ${e.message}`);
1633
+ return Promise.resolve();
1600
1634
  }
1601
1635
  let responseText;
1602
1636
  try {
@@ -1609,14 +1643,16 @@ function send(method, payload, jitsuConfig, instance, store) {
1609
1643
  console.log(`[JITSU DEBUG] ${url} replied ${fetchResult.status}: ${responseText}. Original payload:\n${JSON.stringify(adjustedPayload, null, 2)}`);
1610
1644
  }
1611
1645
  if (!fetchResult.ok) {
1612
- throw new Error(`Jitsu ${url} replied ${fetchResult.status} - ${fetchResult.statusText}: ${responseText}`);
1646
+ getErrorHandler(jitsuConfig)(`Call to ${url} failed with error: ${fetchResult.status} - ${fetchResult.statusText}: ${responseText}`);
1647
+ return Promise.resolve();
1613
1648
  }
1614
1649
  let responseJson;
1615
1650
  try {
1616
1651
  responseJson = JSON.parse(responseText);
1617
1652
  }
1618
1653
  catch (e) {
1619
- return Promise.reject(`Can't parse JSON: ${responseText}: ${e === null || e === void 0 ? void 0 : e.message}`);
1654
+ getErrorHandler(jitsuConfig)(`Can't parse JSON: ${responseText}: ${e === null || e === void 0 ? void 0 : e.message}`);
1655
+ return Promise.resolve();
1620
1656
  }
1621
1657
  if (responseJson.destinations && responseJson.destinations.length > 0) {
1622
1658
  if (jitsuConfig.s2s) {
@@ -1635,6 +1671,22 @@ function send(method, payload, jitsuConfig, instance, store) {
1635
1671
  return adjustedPayload;
1636
1672
  });
1637
1673
  }
1674
+ const controllingTraits = ["$doNotSend"];
1675
+ /**
1676
+ * Remove all members of traits that controls identify/group behavior (see analytics.d.ts), and should not be recorded. Returns
1677
+ * copy of the object with these members removed.
1678
+ *
1679
+ * Do not modify traits object, but creates one
1680
+ * @param traits
1681
+ */
1682
+ function stripControllingTraits(traits) {
1683
+ const res = Object.assign({}, traits);
1684
+ // see Traits definition in analytics.d.ts. We cannot define const here, so here's a little code duplication
1685
+ for (const key of controllingTraits) {
1686
+ delete res[key];
1687
+ }
1688
+ return res;
1689
+ }
1638
1690
  const jitsuAnalyticsPlugin = (jitsuOptions = {}, storage) => {
1639
1691
  // just to make sure that all undefined values are replaced with defaultConfig values
1640
1692
  mergeConfig(jitsuOptions, jitsuOptions);
@@ -1693,16 +1745,21 @@ const jitsuAnalyticsPlugin = (jitsuOptions = {}, storage) => {
1693
1745
  return send("track", payload, config, instance, storage);
1694
1746
  },
1695
1747
  identify: args => {
1696
- var _a, _b;
1748
+ var _a, _b, _c;
1697
1749
  const { payload, config, instance } = args;
1698
1750
  if (((_a = config.privacy) === null || _a === void 0 ? void 0 : _a.dontSend) || ((_b = config.privacy) === null || _b === void 0 ? void 0 : _b.disableUserIds)) {
1699
1751
  return;
1700
1752
  }
1701
1753
  // Store traits in cache to be able to use them in page and track events that run asynchronously with current identify.
1702
1754
  storage.setItem("__user_id", payload.userId);
1755
+ const doNotSend = (_c = payload.traits) === null || _c === void 0 ? void 0 : _c.$doNotSend;
1703
1756
  if (payload.traits && typeof payload.traits === "object") {
1757
+ payload.traits = stripControllingTraits(payload.traits);
1704
1758
  storage.setItem("__user_traits", payload.traits);
1705
1759
  }
1760
+ if (doNotSend) {
1761
+ return Promise.resolve();
1762
+ }
1706
1763
  return send("identify", payload, config, instance, storage);
1707
1764
  },
1708
1765
  reset: args => {
@@ -1747,9 +1804,14 @@ const jitsuAnalyticsPlugin = (jitsuOptions = {}, storage) => {
1747
1804
  const userId = (options === null || options === void 0 ? void 0 : options.userId) || (user === null || user === void 0 ? void 0 : user.userId);
1748
1805
  const anonymousId = (options === null || options === void 0 ? void 0 : options.anonymousId) || (user === null || user === void 0 ? void 0 : user.anonymousId) || storage.getItem("__anon_id");
1749
1806
  storage.setItem("__group_id", groupId);
1807
+ const doNotSend = traits === null || traits === void 0 ? void 0 : traits.$doNotSend;
1750
1808
  if (traits && typeof traits === "object") {
1809
+ traits = stripControllingTraits(traits);
1751
1810
  storage.setItem("__group_traits", traits);
1752
1811
  }
1812
+ if (doNotSend) {
1813
+ return Promise.resolve();
1814
+ }
1753
1815
  return send("group", Object.assign(Object.assign({ type: "group", groupId, traits }, (anonymousId ? { anonymousId } : {})), (userId ? { userId } : {})), jitsuOptions, instance, storage);
1754
1816
  },
1755
1817
  },
package/dist/jitsu.es.js CHANGED
@@ -1059,10 +1059,13 @@ const defaultConfig = {
1059
1059
  fetch: null,
1060
1060
  echoEvents: false,
1061
1061
  cookieDomain: undefined,
1062
+ cookieCapture: {},
1062
1063
  runtime: undefined,
1063
1064
  fetchTimeoutMs: undefined,
1064
1065
  s2s: undefined,
1065
1066
  idEndpoint: undefined,
1067
+ errorPolicy: "log",
1068
+ defaultPayloadContext: {},
1066
1069
  privacy: {
1067
1070
  dontSend: false,
1068
1071
  disableUserIds: false,
@@ -1147,6 +1150,14 @@ function getCookie(name) {
1147
1150
  const parts = value.split(`; ${name}=`);
1148
1151
  return parts.length === 2 ? parts.pop().split(";").shift() : undefined;
1149
1152
  }
1153
+ function getClientIds(runtime, customCookieCapture) {
1154
+ const cookieCapture = Object.assign({ fbc: "_fbc", fbp: "_fbp" }, customCookieCapture);
1155
+ const clientIds = Object.entries(cookieCapture).reduce((acc, [key, cookieName]) => {
1156
+ acc[key] = runtime.getCookie(cookieName);
1157
+ return acc;
1158
+ }, {});
1159
+ return Object.assign(Object.assign({}, clientIds), getGa4Ids(runtime));
1160
+ }
1150
1161
  function getGa4Ids(runtime) {
1151
1162
  var _a;
1152
1163
  const allCookies = runtime.getCookies();
@@ -1224,6 +1235,10 @@ const cookieStorage = (cookieDomain, key2cookie) => {
1224
1235
  getItem(key) {
1225
1236
  const cookieName = key2cookie[key] || key;
1226
1237
  const result = getCookie(cookieName);
1238
+ if (key === "__anon_id") {
1239
+ //anonymous id must always be a string, so we don't parse it to preserve its exact value
1240
+ return result;
1241
+ }
1227
1242
  if (typeof result === "undefined" && key === "__user_id") {
1228
1243
  //backward compatibility with old jitsu cookie. get user id if from traits
1229
1244
  const traits = parse(getCookie("__eventn_id_usr")) || {};
@@ -1399,18 +1414,19 @@ function urlPath(url) {
1399
1414
  return "/" + pathMatch;
1400
1415
  }
1401
1416
  function adjustPayload(payload, config, storage, s2s) {
1402
- var _a, _b, _c, _d, _e;
1417
+ var _a, _b, _c, _d, _e, _f;
1403
1418
  const runtime = config.runtime || (isInBrowser() ? windowRuntime(config) : emptyRuntime(config));
1404
1419
  const url = runtime.pageUrl();
1405
1420
  const parsedUrl = safeCall(() => new URL(url), undefined);
1406
1421
  const query = parsedUrl ? parseQuery(parsedUrl.search) : {};
1407
1422
  const properties = payload.properties || {};
1408
- if (payload.type === "page" && url) {
1409
- properties.url = url.replace(hashRegex, "");
1410
- properties.path = fixPath(urlPath(url));
1423
+ if (payload.type === "page" && (properties.url || url)) {
1424
+ const targetUrl = properties.url || url;
1425
+ properties.url = targetUrl.replace(hashRegex, "");
1426
+ properties.path = fixPath(urlPath(targetUrl));
1411
1427
  }
1412
- const customContext = ((_a = payload.properties) === null || _a === void 0 ? void 0 : _a.context) || {};
1413
- (_b = payload.properties) === null || _b === void 0 ? true : delete _b.context;
1428
+ const customContext = deepMerge(config.defaultPayloadContext, ((_a = payload.properties) === null || _a === void 0 ? void 0 : _a.context) || ((_b = payload.options) === null || _b === void 0 ? void 0 : _b.context) || {});
1429
+ (_c = payload.properties) === null || _c === void 0 ? true : delete _c.context;
1414
1430
  const referrer = runtime.referrer();
1415
1431
  const context = {
1416
1432
  library: {
@@ -1418,7 +1434,7 @@ function adjustPayload(payload, config, storage, s2s) {
1418
1434
  version: jitsuVersion,
1419
1435
  env: isInBrowser() ? "browser" : "node",
1420
1436
  },
1421
- consent: ((_c = config.privacy) === null || _c === void 0 ? void 0 : _c.consentCategories)
1437
+ consent: ((_d = config.privacy) === null || _d === void 0 ? void 0 : _d.consentCategories)
1422
1438
  ? {
1423
1439
  categoryPreferences: config.privacy.consentCategories,
1424
1440
  }
@@ -1437,14 +1453,13 @@ function adjustPayload(payload, config, storage, s2s) {
1437
1453
  url: properties.url || url,
1438
1454
  encoding: properties.encoding || runtime.documentEncoding(),
1439
1455
  },
1440
- clientIds: !((_d = config.privacy) === null || _d === void 0 ? void 0 : _d.disableUserIds)
1441
- ? Object.assign({ fbc: runtime.getCookie("_fbc"), fbp: runtime.getCookie("_fbp") }, getGa4Ids(runtime)) : undefined,
1456
+ clientIds: !((_e = config.privacy) === null || _e === void 0 ? void 0 : _e.disableUserIds) ? getClientIds(runtime, config.cookieCapture) : undefined,
1442
1457
  campaign: parseUtms(query),
1443
1458
  };
1444
1459
  const withContext = Object.assign(Object.assign({}, payload), { timestamp: new Date().toISOString(), sentAt: new Date().toISOString(), messageId: randomId(properties.path || (parsedUrl && parsedUrl.pathname)), writeKey: maskWriteKey(config.writeKey), groupId: storage.getItem("__group_id"), context: deepMerge(context, customContext) });
1445
1460
  delete withContext.meta;
1446
1461
  delete withContext.options;
1447
- if ((_e = config.privacy) === null || _e === void 0 ? void 0 : _e.disableUserIds) {
1462
+ if ((_f = config.privacy) === null || _f === void 0 ? void 0 : _f.disableUserIds) {
1448
1463
  delete withContext.userId;
1449
1464
  delete withContext.anonymousId;
1450
1465
  delete withContext.context.traits;
@@ -1553,6 +1568,23 @@ function maskWriteKey(writeKey) {
1553
1568
  }
1554
1569
  return writeKey;
1555
1570
  }
1571
+ function getErrorHandler(opts) {
1572
+ const configuredHandler = opts.errorPolicy || "log";
1573
+ if (typeof configuredHandler === "function") {
1574
+ return configuredHandler;
1575
+ }
1576
+ else if (configuredHandler === "rethrow") {
1577
+ return (msg, ...args) => {
1578
+ //ignore args, not clear what to do with them
1579
+ throw new Error(msg);
1580
+ };
1581
+ }
1582
+ else {
1583
+ return (msg, ...args) => {
1584
+ console.error(msg, ...args);
1585
+ };
1586
+ }
1587
+ }
1556
1588
  function send(method, payload, jitsuConfig, instance, store) {
1557
1589
  return __awaiter$1(this, void 0, void 0, function* () {
1558
1590
  var _a, _b;
@@ -1564,6 +1596,7 @@ function send(method, payload, jitsuConfig, instance, store) {
1564
1596
  const url = s2s ? `${jitsuConfig.host}/api/s/s2s/${method}` : `${jitsuConfig.host}/api/s/${method}`;
1565
1597
  const fetch = jitsuConfig.fetch || globalThis.fetch;
1566
1598
  if (!fetch) {
1599
+ //don't run it through error handler since error is critical and should be addressed
1567
1600
  throw new Error("Please specify fetch function in jitsu plugin initialization, fetch isn't available in global scope");
1568
1601
  }
1569
1602
  const debugHeader = jitsuConfig.debug ? { "X-Enable-Debug": "true" } : {};
@@ -1594,7 +1627,8 @@ function send(method, payload, jitsuConfig, instance, store) {
1594
1627
  }
1595
1628
  }
1596
1629
  catch (e) {
1597
- throw new Error(`Calling ${url} failed: ${e.message}`);
1630
+ getErrorHandler(jitsuConfig)(`Call to ${url} failed with error ${e.message}`);
1631
+ return Promise.resolve();
1598
1632
  }
1599
1633
  let responseText;
1600
1634
  try {
@@ -1607,14 +1641,16 @@ function send(method, payload, jitsuConfig, instance, store) {
1607
1641
  console.log(`[JITSU DEBUG] ${url} replied ${fetchResult.status}: ${responseText}. Original payload:\n${JSON.stringify(adjustedPayload, null, 2)}`);
1608
1642
  }
1609
1643
  if (!fetchResult.ok) {
1610
- throw new Error(`Jitsu ${url} replied ${fetchResult.status} - ${fetchResult.statusText}: ${responseText}`);
1644
+ getErrorHandler(jitsuConfig)(`Call to ${url} failed with error: ${fetchResult.status} - ${fetchResult.statusText}: ${responseText}`);
1645
+ return Promise.resolve();
1611
1646
  }
1612
1647
  let responseJson;
1613
1648
  try {
1614
1649
  responseJson = JSON.parse(responseText);
1615
1650
  }
1616
1651
  catch (e) {
1617
- return Promise.reject(`Can't parse JSON: ${responseText}: ${e === null || e === void 0 ? void 0 : e.message}`);
1652
+ getErrorHandler(jitsuConfig)(`Can't parse JSON: ${responseText}: ${e === null || e === void 0 ? void 0 : e.message}`);
1653
+ return Promise.resolve();
1618
1654
  }
1619
1655
  if (responseJson.destinations && responseJson.destinations.length > 0) {
1620
1656
  if (jitsuConfig.s2s) {
@@ -1633,6 +1669,22 @@ function send(method, payload, jitsuConfig, instance, store) {
1633
1669
  return adjustedPayload;
1634
1670
  });
1635
1671
  }
1672
+ const controllingTraits = ["$doNotSend"];
1673
+ /**
1674
+ * Remove all members of traits that controls identify/group behavior (see analytics.d.ts), and should not be recorded. Returns
1675
+ * copy of the object with these members removed.
1676
+ *
1677
+ * Do not modify traits object, but creates one
1678
+ * @param traits
1679
+ */
1680
+ function stripControllingTraits(traits) {
1681
+ const res = Object.assign({}, traits);
1682
+ // see Traits definition in analytics.d.ts. We cannot define const here, so here's a little code duplication
1683
+ for (const key of controllingTraits) {
1684
+ delete res[key];
1685
+ }
1686
+ return res;
1687
+ }
1636
1688
  const jitsuAnalyticsPlugin = (jitsuOptions = {}, storage) => {
1637
1689
  // just to make sure that all undefined values are replaced with defaultConfig values
1638
1690
  mergeConfig(jitsuOptions, jitsuOptions);
@@ -1691,16 +1743,21 @@ const jitsuAnalyticsPlugin = (jitsuOptions = {}, storage) => {
1691
1743
  return send("track", payload, config, instance, storage);
1692
1744
  },
1693
1745
  identify: args => {
1694
- var _a, _b;
1746
+ var _a, _b, _c;
1695
1747
  const { payload, config, instance } = args;
1696
1748
  if (((_a = config.privacy) === null || _a === void 0 ? void 0 : _a.dontSend) || ((_b = config.privacy) === null || _b === void 0 ? void 0 : _b.disableUserIds)) {
1697
1749
  return;
1698
1750
  }
1699
1751
  // Store traits in cache to be able to use them in page and track events that run asynchronously with current identify.
1700
1752
  storage.setItem("__user_id", payload.userId);
1753
+ const doNotSend = (_c = payload.traits) === null || _c === void 0 ? void 0 : _c.$doNotSend;
1701
1754
  if (payload.traits && typeof payload.traits === "object") {
1755
+ payload.traits = stripControllingTraits(payload.traits);
1702
1756
  storage.setItem("__user_traits", payload.traits);
1703
1757
  }
1758
+ if (doNotSend) {
1759
+ return Promise.resolve();
1760
+ }
1704
1761
  return send("identify", payload, config, instance, storage);
1705
1762
  },
1706
1763
  reset: args => {
@@ -1745,9 +1802,14 @@ const jitsuAnalyticsPlugin = (jitsuOptions = {}, storage) => {
1745
1802
  const userId = (options === null || options === void 0 ? void 0 : options.userId) || (user === null || user === void 0 ? void 0 : user.userId);
1746
1803
  const anonymousId = (options === null || options === void 0 ? void 0 : options.anonymousId) || (user === null || user === void 0 ? void 0 : user.anonymousId) || storage.getItem("__anon_id");
1747
1804
  storage.setItem("__group_id", groupId);
1805
+ const doNotSend = traits === null || traits === void 0 ? void 0 : traits.$doNotSend;
1748
1806
  if (traits && typeof traits === "object") {
1807
+ traits = stripControllingTraits(traits);
1749
1808
  storage.setItem("__group_traits", traits);
1750
1809
  }
1810
+ if (doNotSend) {
1811
+ return Promise.resolve();
1812
+ }
1751
1813
  return send("group", Object.assign(Object.assign({ type: "group", groupId, traits }, (anonymousId ? { anonymousId } : {})), (userId ? { userId } : {})), jitsuOptions, instance, storage);
1752
1814
  },
1753
1815
  },