@tracelog/capture-web 1.3.1 → 2.0.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/CHANGELOG.md CHANGED
@@ -18,6 +18,23 @@ patch, and neither requires reading your code.
18
18
  Entries after 1.0.0 are drafted from the commits that touched this package and
19
19
  edited before release.
20
20
 
21
+ ## [2.0.0](https://github.com/nacorga/tracelog-sdk/compare/capture-web@1.3.1...capture-web@2.0.0) (2026-10-01)
22
+
23
+
24
+ ### ⚠ BREAKING CHANGES
25
+
26
+ * **capture-core:** call TraceLog.consent.grant() on every load while the visitor's consent stands, not only when they accept.
27
+
28
+ ### Added
29
+
30
+ * **capture-web:** calls before init are held ([b8d4fe5](https://github.com/nacorga/tracelog-sdk/commit/b8d4fe5e4bb104c2343aeceeb1b17f70d8cc697a))
31
+
32
+
33
+ ### Fixed
34
+
35
+ * **capture-core:** a grant is not remembered across loads ([75d4cac](https://github.com/nacorga/tracelog-sdk/commit/75d4cac28055007553e36b97d4eef0dbabf27103))
36
+ * **capture-web:** the verification mark follows its tab ([9653074](https://github.com/nacorga/tracelog-sdk/commit/9653074fb57ef5523a43a7fa6bc681f8b8a8814f))
37
+
21
38
  ## [1.3.1](https://github.com/nacorga/tracelog-sdk/compare/capture-web@1.3.0...capture-web@1.3.1) (2026-09-30)
22
39
 
23
40
 
package/README.md CHANGED
@@ -22,13 +22,13 @@ as they are served.
22
22
  <!-- x-release-please-start-version -->
23
23
 
24
24
  ```bash
25
- npm install --save-exact @tracelog/capture-web@1.3.1
25
+ npm install --save-exact @tracelog/capture-web@2.0.0
26
26
  ```
27
27
 
28
28
  Without a build step, the same runtime as a script tag:
29
29
 
30
30
  ```html
31
- <script src="https://cdn.tracelog.io/v/1.3.1/tracelog.js"></script>
31
+ <script src="https://cdn.tracelog.io/v/2.0.0/tracelog.js"></script>
32
32
  ```
33
33
 
34
34
  <!-- x-release-please-end -->
@@ -42,7 +42,7 @@ the floor before a release.
42
42
  Before consent is granted the runtime creates no identifiers, writes no storage,
43
43
  and sends no network traffic — none, not a reduced set. Consent is a state
44
44
  machine you drive; until it reaches `granted`, capture is inert. Denying keeps it
45
- inert without breaking the page.
45
+ inert without breaking the page, and is remembered; a grant is not.
46
46
 
47
47
  ```js
48
48
  import TraceLog from "@tracelog/capture-web";
@@ -52,10 +52,16 @@ TraceLog.init({
52
52
  endpoint: "https://api.tracelog.io/v1/events",
53
53
  });
54
54
 
55
- // Only once your consent surface says yes:
55
+ // When your consent surface says yes, and on every later load while
56
+ // that consent stands:
56
57
  TraceLog.consent.grant();
57
58
  ```
58
59
 
60
+ A grant lasts the page it was given on. Your consent surface keeps the
61
+ visitor's answer and its expiry, so call `grant()` on each load where that
62
+ answer is still yes; a grant that outlived it would capture a visitor whose
63
+ consent has lapsed.
64
+
59
65
  A site that never calls `consent.grant()` captures nothing and costs its
60
66
  visitors nothing.
61
67
 
@@ -74,11 +80,17 @@ That is all of it, and it is what the major version protects.
74
80
 
75
81
  `init` also accepts `mode: "verification"`, which a distributed platform artifact
76
82
  declares for its platform's own test order. A site's own snippet never sets it —
77
- the runtime derives verification mode from the window that opened the page.
83
+ the runtime derives verification mode from the window that opened the page, and
84
+ once consent is granted there the tab keeps it (`sessionStorage`, `__tl.v`) for
85
+ the site's next pages and the return from a payment taken elsewhere, to the same
86
+ origin. A denial forgets it.
78
87
 
79
88
  Call `init` once per page load. The runtime is built on the first call and
80
89
  kept; a later call re-reads the key and the endpoint and rebuilds nothing else,
81
- so a mode or an acquisition the first call decided stands for the page.
90
+ so a mode or an acquisition the first call decided stands for the page. A
91
+ call made before it — a grant, a step, a conversion — is held, up to 100, and
92
+ made once it has run. The runtime can hold only what reaches it: a page that
93
+ imports it lazily holds what it calls before the import resolves.
82
94
 
83
95
  The application generates the exact calls your declared plan needs, so you never
84
96
  type a name TraceLog already knows.
@@ -550,22 +550,26 @@ function createCaptureEngine(ports, acquisition) {
550
550
  endpoint: options.endpoint ?? "/v1/events",
551
551
  };
552
552
  initialized = true;
553
+ /**
554
+ * Only a denial is remembered; a grant is the integrator's on every load
555
+ * while consent stands, so it never outlives the banner that gave it
556
+ * ([spec/capture.md] § Consent first). A grant an earlier runtime
557
+ * stored is deleted. A second init keeps the decision this page made.
558
+ */
559
+ if (consentState !== "unknown")
560
+ return;
553
561
  const storedConsent = safeGet(ports.storage, CONSENT_KEY);
554
- consentState =
555
- storedConsent === "granted" || storedConsent === "denied"
556
- ? storedConsent
557
- : "unknown";
558
- if (consentState === "granted") {
559
- ports.sightings?.start();
560
- scheduleFlush(FLUSH_INTERVAL_MS);
561
- }
562
+ if (storedConsent === "denied")
563
+ consentState = "denied";
564
+ else if (storedConsent !== null)
565
+ safeRemove(ports.storage, CONSENT_KEY);
562
566
  },
563
567
  consent: {
564
568
  grant() {
565
569
  if (!initialized)
566
570
  return;
567
571
  consentState = "granted";
568
- safeSet(ports.storage, CONSENT_KEY, "granted");
572
+ safeRemove(ports.storage, CONSENT_KEY);
569
573
  ports.sightings?.start();
570
574
  const buffered = pending;
571
575
  pending = [];
@@ -918,6 +922,8 @@ function createTagSightingPort() {
918
922
  };
919
923
  }
920
924
  const WEB_PUBLIC_KEY_PATTERN = /^tl_pk_[a-z2-7]{26}$/;
925
+ /** The tab's verification nonce, in `sessionStorage`, written at the grant. */
926
+ const VERIFICATION_KEY = "__tl.v";
921
927
  class MemoryStorage {
922
928
  constructor() {
923
929
  this.values = new Map();
@@ -938,6 +944,20 @@ let engine;
938
944
  let verification;
939
945
  let configValid = false;
940
946
  let listenersInstalled = false;
947
+ /**
948
+ * Calls made before `init`, made in order once it has run, up to the same 100
949
+ * the engine holds before consent; past that they are dropped
950
+ * ([spec/capture.md] § Consent first).
951
+ */
952
+ let early = [];
953
+ /** True when there is no engine yet, and the call was held, or dropped. */
954
+ function heldForInit(call) {
955
+ if (engine !== undefined)
956
+ return false;
957
+ if (early.length < PRE_CONSENT_CAP)
958
+ early.push(call);
959
+ return true;
960
+ }
941
961
  function browserStorage() {
942
962
  try {
943
963
  return window.localStorage;
@@ -946,11 +966,39 @@ function browserStorage() {
946
966
  return new MemoryStorage();
947
967
  }
948
968
  }
969
+ /**
970
+ * A page opened from a verification session carries the marker and has an
971
+ * opener. Once consent is granted there, the tab keeps the nonce, so the mark
972
+ * follows it to the site's next pages and back from a payment taken
973
+ * elsewhere, to the same origin ([spec/capture.md] § Verification mode).
974
+ */
949
975
  function verificationMode() {
950
976
  const nonce = new URL(window.location.href).searchParams.get("__tl_verify");
951
- return nonce !== null && nonce.length > 0 && window.opener !== null
952
- ? { nonce, opener: window.opener }
953
- : undefined;
977
+ if (nonce !== null && nonce.length > 0 && window.opener !== null) {
978
+ return { nonce, opener: window.opener };
979
+ }
980
+ let kept = null;
981
+ try {
982
+ kept = window.sessionStorage.getItem(VERIFICATION_KEY);
983
+ }
984
+ catch {
985
+ // An unreadable tab storage keeps no mark.
986
+ }
987
+ return kept === null || kept.length === 0
988
+ ? undefined
989
+ : { nonce: kept, opener: window.opener };
990
+ }
991
+ function keepVerification(keep) {
992
+ try {
993
+ if (!keep)
994
+ window.sessionStorage.removeItem(VERIFICATION_KEY);
995
+ else if (verification !== undefined) {
996
+ window.sessionStorage.setItem(VERIFICATION_KEY, verification.nonce);
997
+ }
998
+ }
999
+ catch {
1000
+ // A tab that cannot keep it marks this page only.
1001
+ }
954
1002
  }
955
1003
  function landingPage() {
956
1004
  const url = new URL(window.location.href);
@@ -1005,7 +1053,7 @@ function isEndpointValid(endpoint) {
1005
1053
  }
1006
1054
  }
1007
1055
  function reportDiagnostic() {
1008
- if (verification === undefined)
1056
+ if (verification === undefined || verification.opener === null)
1009
1057
  return;
1010
1058
  verification.opener.postMessage({
1011
1059
  type: "tracelog:diag",
@@ -1102,17 +1150,28 @@ function init(options) {
1102
1150
  ...(options.endpoint === undefined ? {} : { endpoint }),
1103
1151
  });
1104
1152
  installListeners();
1153
+ const held = early;
1154
+ early = [];
1155
+ for (const call of held)
1156
+ call();
1105
1157
  reportDiagnostic();
1106
1158
  }
1107
1159
  const TraceLog = {
1108
1160
  init,
1109
1161
  consent: {
1110
1162
  grant() {
1163
+ if (heldForInit(() => TraceLog.consent.grant()))
1164
+ return;
1111
1165
  engine?.consent.grant();
1166
+ if (engine?.consent.state() === "granted")
1167
+ keepVerification(true);
1112
1168
  reportDiagnostic();
1113
1169
  },
1114
1170
  deny() {
1171
+ if (heldForInit(() => TraceLog.consent.deny()))
1172
+ return;
1115
1173
  engine?.consent.deny();
1174
+ keepVerification(false);
1116
1175
  reportDiagnostic();
1117
1176
  },
1118
1177
  state() {
@@ -1120,10 +1179,14 @@ const TraceLog = {
1120
1179
  },
1121
1180
  },
1122
1181
  step(name, context) {
1182
+ if (heldForInit(() => TraceLog.step(name, context)))
1183
+ return;
1123
1184
  engine?.step(name, context);
1124
1185
  reportDiagnostic();
1125
1186
  },
1126
1187
  conversion(name, options) {
1188
+ if (heldForInit(() => TraceLog.conversion(name, options)))
1189
+ return;
1127
1190
  engine?.conversion(name, options);
1128
1191
  reportDiagnostic();
1129
1192
  },
@@ -551,22 +551,26 @@ function createCaptureEngine(ports, acquisition) {
551
551
  endpoint: options.endpoint ?? "/v1/events",
552
552
  };
553
553
  initialized = true;
554
+ /**
555
+ * Only a denial is remembered; a grant is the integrator's on every load
556
+ * while consent stands, so it never outlives the banner that gave it
557
+ * ([spec/capture.md] § Consent first). A grant an earlier runtime
558
+ * stored is deleted. A second init keeps the decision this page made.
559
+ */
560
+ if (consentState !== "unknown")
561
+ return;
554
562
  const storedConsent = safeGet(ports.storage, CONSENT_KEY);
555
- consentState =
556
- storedConsent === "granted" || storedConsent === "denied"
557
- ? storedConsent
558
- : "unknown";
559
- if (consentState === "granted") {
560
- ports.sightings?.start();
561
- scheduleFlush(FLUSH_INTERVAL_MS);
562
- }
563
+ if (storedConsent === "denied")
564
+ consentState = "denied";
565
+ else if (storedConsent !== null)
566
+ safeRemove(ports.storage, CONSENT_KEY);
563
567
  },
564
568
  consent: {
565
569
  grant() {
566
570
  if (!initialized)
567
571
  return;
568
572
  consentState = "granted";
569
- safeSet(ports.storage, CONSENT_KEY, "granted");
573
+ safeRemove(ports.storage, CONSENT_KEY);
570
574
  ports.sightings?.start();
571
575
  const buffered = pending;
572
576
  pending = [];
@@ -919,6 +923,8 @@ function createTagSightingPort() {
919
923
  };
920
924
  }
921
925
  const WEB_PUBLIC_KEY_PATTERN = /^tl_pk_[a-z2-7]{26}$/;
926
+ /** The tab's verification nonce, in `sessionStorage`, written at the grant. */
927
+ const VERIFICATION_KEY = "__tl.v";
922
928
  class MemoryStorage {
923
929
  constructor() {
924
930
  this.values = new Map();
@@ -939,6 +945,20 @@ let engine;
939
945
  let verification;
940
946
  let configValid = false;
941
947
  let listenersInstalled = false;
948
+ /**
949
+ * Calls made before `init`, made in order once it has run, up to the same 100
950
+ * the engine holds before consent; past that they are dropped
951
+ * ([spec/capture.md] § Consent first).
952
+ */
953
+ let early = [];
954
+ /** True when there is no engine yet, and the call was held, or dropped. */
955
+ function heldForInit(call) {
956
+ if (engine !== undefined)
957
+ return false;
958
+ if (early.length < PRE_CONSENT_CAP)
959
+ early.push(call);
960
+ return true;
961
+ }
942
962
  function browserStorage() {
943
963
  try {
944
964
  return window.localStorage;
@@ -947,11 +967,39 @@ function browserStorage() {
947
967
  return new MemoryStorage();
948
968
  }
949
969
  }
970
+ /**
971
+ * A page opened from a verification session carries the marker and has an
972
+ * opener. Once consent is granted there, the tab keeps the nonce, so the mark
973
+ * follows it to the site's next pages and back from a payment taken
974
+ * elsewhere, to the same origin ([spec/capture.md] § Verification mode).
975
+ */
950
976
  function verificationMode() {
951
977
  const nonce = new URL(window.location.href).searchParams.get("__tl_verify");
952
- return nonce !== null && nonce.length > 0 && window.opener !== null
953
- ? { nonce, opener: window.opener }
954
- : undefined;
978
+ if (nonce !== null && nonce.length > 0 && window.opener !== null) {
979
+ return { nonce, opener: window.opener };
980
+ }
981
+ let kept = null;
982
+ try {
983
+ kept = window.sessionStorage.getItem(VERIFICATION_KEY);
984
+ }
985
+ catch {
986
+ // An unreadable tab storage keeps no mark.
987
+ }
988
+ return kept === null || kept.length === 0
989
+ ? undefined
990
+ : { nonce: kept, opener: window.opener };
991
+ }
992
+ function keepVerification(keep) {
993
+ try {
994
+ if (!keep)
995
+ window.sessionStorage.removeItem(VERIFICATION_KEY);
996
+ else if (verification !== undefined) {
997
+ window.sessionStorage.setItem(VERIFICATION_KEY, verification.nonce);
998
+ }
999
+ }
1000
+ catch {
1001
+ // A tab that cannot keep it marks this page only.
1002
+ }
955
1003
  }
956
1004
  function landingPage() {
957
1005
  const url = new URL(window.location.href);
@@ -1006,7 +1054,7 @@ function isEndpointValid(endpoint) {
1006
1054
  }
1007
1055
  }
1008
1056
  function reportDiagnostic() {
1009
- if (verification === undefined)
1057
+ if (verification === undefined || verification.opener === null)
1010
1058
  return;
1011
1059
  verification.opener.postMessage({
1012
1060
  type: "tracelog:diag",
@@ -1103,17 +1151,28 @@ function init(options) {
1103
1151
  ...(options.endpoint === undefined ? {} : { endpoint }),
1104
1152
  });
1105
1153
  installListeners();
1154
+ const held = early;
1155
+ early = [];
1156
+ for (const call of held)
1157
+ call();
1106
1158
  reportDiagnostic();
1107
1159
  }
1108
1160
  const TraceLog = {
1109
1161
  init,
1110
1162
  consent: {
1111
1163
  grant() {
1164
+ if (heldForInit(() => TraceLog.consent.grant()))
1165
+ return;
1112
1166
  engine?.consent.grant();
1167
+ if (engine?.consent.state() === "granted")
1168
+ keepVerification(true);
1113
1169
  reportDiagnostic();
1114
1170
  },
1115
1171
  deny() {
1172
+ if (heldForInit(() => TraceLog.consent.deny()))
1173
+ return;
1116
1174
  engine?.consent.deny();
1175
+ keepVerification(false);
1117
1176
  reportDiagnostic();
1118
1177
  },
1119
1178
  state() {
@@ -1121,10 +1180,14 @@ const TraceLog = {
1121
1180
  },
1122
1181
  },
1123
1182
  step(name, context) {
1183
+ if (heldForInit(() => TraceLog.step(name, context)))
1184
+ return;
1124
1185
  engine?.step(name, context);
1125
1186
  reportDiagnostic();
1126
1187
  },
1127
1188
  conversion(name, options) {
1189
+ if (heldForInit(() => TraceLog.conversion(name, options)))
1190
+ return;
1128
1191
  engine?.conversion(name, options);
1129
1192
  reportDiagnostic();
1130
1193
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tracelog/capture-web",
3
- "version": "1.3.1",
3
+ "version": "2.0.0",
4
4
  "description": "The TraceLog browser capture runtime: consent-first, pinned per version.",
5
5
  "keywords": [
6
6
  "conversion",
@@ -45,8 +45,8 @@
45
45
  "access": "public"
46
46
  },
47
47
  "devDependencies": {
48
- "@tracelog/capture-core": "1.3.1",
49
- "@tracelog/event-contract": "1.3.1"
48
+ "@tracelog/capture-core": "2.0.0",
49
+ "@tracelog/event-contract": "2.0.0"
50
50
  },
51
51
  "scripts": {
52
52
  "build": "tsc -p tsconfig.build.json && node build.mjs",