pulse-updates 1.1.1 → 1.2.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.
Files changed (64) hide show
  1. package/README.md +50 -10
  2. package/SECURITY.md +42 -0
  3. package/android/src/main/java/app/pulse/updates/PulseController.kt +6 -2
  4. package/android/src/main/java/app/pulse/updates/PulseUpdatesModule.kt +25 -0
  5. package/app.plugin.js +47 -0
  6. package/ios/PulseUpdates/PulseController.swift +7 -2
  7. package/ios/PulseUpdates/PulseUpdates.m +7 -0
  8. package/ios/PulseUpdates/PulseUpdates.swift +22 -0
  9. package/lib/commonjs/NativePulseUpdates.js.map +1 -1
  10. package/lib/commonjs/PulseUpdates.js +36 -0
  11. package/lib/commonjs/PulseUpdates.js.map +1 -1
  12. package/lib/commonjs/config.js +175 -21
  13. package/lib/commonjs/config.js.map +1 -1
  14. package/lib/commonjs/decisions.js +128 -0
  15. package/lib/commonjs/decisions.js.map +1 -0
  16. package/lib/commonjs/index.js +12 -0
  17. package/lib/commonjs/index.js.map +1 -1
  18. package/lib/commonjs/init.js +153 -13
  19. package/lib/commonjs/init.js.map +1 -1
  20. package/lib/commonjs/track.js +154 -12
  21. package/lib/commonjs/track.js.map +1 -1
  22. package/lib/commonjs/usePulseUpdates.js +21 -16
  23. package/lib/commonjs/usePulseUpdates.js.map +1 -1
  24. package/lib/module/NativePulseUpdates.js.map +1 -1
  25. package/lib/module/PulseUpdates.js +33 -0
  26. package/lib/module/PulseUpdates.js.map +1 -1
  27. package/lib/module/config.js +174 -21
  28. package/lib/module/config.js.map +1 -1
  29. package/lib/module/decisions.js +122 -0
  30. package/lib/module/decisions.js.map +1 -0
  31. package/lib/module/index.js +1 -0
  32. package/lib/module/index.js.map +1 -1
  33. package/lib/module/init.js +154 -15
  34. package/lib/module/init.js.map +1 -1
  35. package/lib/module/track.js +152 -12
  36. package/lib/module/track.js.map +1 -1
  37. package/lib/module/usePulseUpdates.js +21 -16
  38. package/lib/module/usePulseUpdates.js.map +1 -1
  39. package/lib/typescript/NativePulseUpdates.d.ts +1 -0
  40. package/lib/typescript/NativePulseUpdates.d.ts.map +1 -1
  41. package/lib/typescript/PulseUpdates.d.ts +20 -0
  42. package/lib/typescript/PulseUpdates.d.ts.map +1 -1
  43. package/lib/typescript/config.d.ts +22 -0
  44. package/lib/typescript/config.d.ts.map +1 -1
  45. package/lib/typescript/decisions.d.ts +55 -0
  46. package/lib/typescript/decisions.d.ts.map +1 -0
  47. package/lib/typescript/index.d.ts +1 -0
  48. package/lib/typescript/index.d.ts.map +1 -1
  49. package/lib/typescript/init.d.ts +51 -4
  50. package/lib/typescript/init.d.ts.map +1 -1
  51. package/lib/typescript/track.d.ts +29 -1
  52. package/lib/typescript/track.d.ts.map +1 -1
  53. package/lib/typescript/usePulseUpdates.d.ts.map +1 -1
  54. package/logo.png +0 -0
  55. package/package.json +14 -3
  56. package/scripts/publish.mjs +68 -3
  57. package/src/NativePulseUpdates.ts +1 -0
  58. package/src/PulseUpdates.ts +54 -0
  59. package/src/config.ts +234 -21
  60. package/src/decisions.ts +179 -0
  61. package/src/index.ts +1 -0
  62. package/src/init.ts +227 -12
  63. package/src/track.ts +191 -13
  64. package/src/usePulseUpdates.ts +21 -16
@@ -0,0 +1,122 @@
1
+ /** Generic decision protocol. Product-specific action handlers remain in each app. */
2
+ export function createPulseDecisionClient(options) {
3
+ const fetcher = options.fetch ?? globalThis.fetch;
4
+ const base = options.apiUrl.replace(/\/+$/, '');
5
+ const slug = encodeURIComponent(options.appSlug);
6
+ const timeoutMs = Math.max(250, options.timeoutMs ?? 3_000);
7
+ const executionMode = options.executionMode ?? 'shadow-only';
8
+ const reportOutcome = async (decisionId, outcome, metadata) => {
9
+ try {
10
+ const response = await timedFetch(fetcher, `${base}/pulse/decisions/${slug}/${encodeURIComponent(decisionId)}/outcome`, {
11
+ method: 'POST',
12
+ headers: {
13
+ 'Content-Type': 'application/json'
14
+ },
15
+ body: JSON.stringify({
16
+ outcome,
17
+ ...(metadata ? {
18
+ metadata
19
+ } : {})
20
+ })
21
+ }, timeoutMs);
22
+ return response.ok;
23
+ } catch (error) {
24
+ options.onError?.(error);
25
+ return false;
26
+ }
27
+ };
28
+ const decide = async (trigger, request = {}) => {
29
+ try {
30
+ const context = await options.getContext();
31
+ const response = await timedFetch(fetcher, `${base}/pulse/decisions/${slug}`, {
32
+ method: 'POST',
33
+ headers: {
34
+ 'Content-Type': 'application/json'
35
+ },
36
+ body: JSON.stringify({
37
+ ...context,
38
+ attributes: {
39
+ ...(context.attributes ?? {}),
40
+ ...(request.attributes ?? {})
41
+ },
42
+ trigger,
43
+ channel: request.channel ?? 'in_app',
44
+ idempotencyKey: request.idempotencyKey ?? randomKey()
45
+ })
46
+ }, timeoutMs);
47
+ if (!response.ok) throw new Error(`Pulse decision HTTP ${response.status}`);
48
+ const decision = parseDecision(await response.json());
49
+ if (!decision.execute) return decision;
50
+ if (executionMode !== 'allow-live') {
51
+ void reportOutcome(decision.decisionId, 'failed', {
52
+ reason: 'client_executor_not_armed'
53
+ });
54
+ return decision;
55
+ }
56
+ const action = decision.action;
57
+ const handler = action?.kind ? options.handlers?.[action.kind] : undefined;
58
+ if (!action || !handler) {
59
+ void reportOutcome(decision.decisionId, 'failed', {
60
+ reason: 'unknown_action_kind',
61
+ kind: action?.kind ?? null
62
+ });
63
+ return decision;
64
+ }
65
+ try {
66
+ const result = await handler(action, decision);
67
+ void reportOutcome(decision.decisionId, result?.outcome ?? 'accepted', result?.metadata);
68
+ } catch (error) {
69
+ options.onError?.(error);
70
+ void reportOutcome(decision.decisionId, 'failed', {
71
+ reason: 'handler_failed'
72
+ });
73
+ }
74
+ return decision;
75
+ } catch (error) {
76
+ options.onError?.(error);
77
+ return null;
78
+ }
79
+ };
80
+ return {
81
+ decide,
82
+ reportOutcome
83
+ };
84
+ }
85
+ function parseDecision(raw) {
86
+ if (!raw || typeof raw !== 'object') throw new Error('Malformed Pulse decision');
87
+ const value = raw;
88
+ if (typeof value.decisionId !== 'string' || typeof value.execute !== 'boolean' || typeof value.mode !== 'string' || typeof value.verdict !== 'string' || typeof value.reasonCode !== 'string') throw new Error('Malformed Pulse decision');
89
+ const action = value.action;
90
+ if (action != null && (typeof action !== 'object' || typeof action.kind !== 'string')) {
91
+ throw new Error('Malformed Pulse action');
92
+ }
93
+ return {
94
+ decisionId: value.decisionId,
95
+ replayed: value.replayed === true,
96
+ execute: value.execute,
97
+ mode: value.mode,
98
+ verdict: value.verdict,
99
+ reasonCode: value.reasonCode,
100
+ policyKey: typeof value.policyKey === 'string' ? value.policyKey : null,
101
+ policyVersion: typeof value.policyVersion === 'number' ? value.policyVersion : null,
102
+ action: action ?? null
103
+ };
104
+ }
105
+ async function timedFetch(fetcher, url, init, timeoutMs) {
106
+ const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
107
+ const timer = controller ? setTimeout(() => controller.abort(), timeoutMs) : null;
108
+ try {
109
+ return await fetcher(url, {
110
+ ...init,
111
+ signal: controller?.signal
112
+ });
113
+ } finally {
114
+ if (timer) clearTimeout(timer);
115
+ }
116
+ }
117
+ function randomKey() {
118
+ const cryptoLike = globalThis.crypto;
119
+ if (typeof cryptoLike?.randomUUID === 'function') return cryptoLike.randomUUID();
120
+ return `moment-${Date.now()}-${Math.random().toString(36).slice(2, 14)}`;
121
+ }
122
+ //# sourceMappingURL=decisions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["createPulseDecisionClient","options","fetcher","fetch","globalThis","base","apiUrl","replace","slug","encodeURIComponent","appSlug","timeoutMs","Math","max","executionMode","reportOutcome","decisionId","outcome","metadata","response","timedFetch","method","headers","body","JSON","stringify","ok","error","onError","decide","trigger","request","context","getContext","attributes","channel","idempotencyKey","randomKey","Error","status","decision","parseDecision","json","execute","reason","action","handler","kind","handlers","undefined","result","raw","value","mode","verdict","reasonCode","replayed","policyKey","policyVersion","url","init","controller","AbortController","timer","setTimeout","abort","signal","clearTimeout","cryptoLike","crypto","randomUUID","Date","now","random","toString","slice"],"sourceRoot":"../../src","sources":["decisions.ts"],"mappings":"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","ignoreList":[]}
@@ -5,4 +5,5 @@ export { initializeAssetResolver, updateLocalAssets } from './assetResolver';
5
5
  export * from './config';
6
6
  export * from './track';
7
7
  export * from './init';
8
+ export * from './decisions';
8
9
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["usePulseUpdates","initializeAssetResolver","updateLocalAssets"],"sourceRoot":"../../src","sources":["index.ts"],"mappings":"AAAA,cAAc,gBAAgB;AAC9B,cAAc,SAAS;AACvB,SAASA,eAAe,QAAQ,mBAAmB;AACnD,SAASC,uBAAuB,EAAEC,iBAAiB,QAAQ,iBAAiB;AAC5E,cAAc,UAAU;AACxB,cAAc,SAAS;AACvB,cAAc,QAAQ","ignoreList":[]}
1
+ {"version":3,"names":["usePulseUpdates","initializeAssetResolver","updateLocalAssets"],"sourceRoot":"../../src","sources":["index.ts"],"mappings":"AAAA,cAAc,gBAAgB;AAC9B,cAAc,SAAS;AACvB,SAASA,eAAe,QAAQ,mBAAmB;AACnD,SAASC,uBAAuB,EAAEC,iBAAiB,QAAQ,iBAAiB;AAC5E,cAAc,UAAU;AACxB,cAAc,SAAS;AACvB,cAAc,QAAQ;AACtB,cAAc,aAAa","ignoreList":[]}
@@ -23,39 +23,68 @@
23
23
  * installs — a targeting bug that looks like a rollout that "did not work".
24
24
  */
25
25
 
26
- import { configureConfig, fetchConfig, startConfigAutoRefresh } from './config';
27
- import { configureTracking } from './track';
26
+ import { configureConfig, fetchConfig, startConfigAutoRefresh, getConfigInfo, getConfigBoolean, getConfigNumber, getConfigString, getConfigJson } from './config';
27
+ import { configureTracking, disposeTracking, flushEvents, getTrackingInfo, track } from './track';
28
28
  const DEVICE_ID_KEY = 'pulse.device-id';
29
+ let activeClient = null;
30
+ let stopActiveRefresh = null;
29
31
 
30
32
  /** Wires config and events, and returns the context the two share. */
31
33
  export function initPulse(opts) {
34
+ // Reconfiguration is synchronous. Calling the old async dispose without awaiting
35
+ // it lets its eventual flush tear down the brand-new tracker — exactly the race a
36
+ // fast app switch or test setup triggers. Persist the old outbox, stop its refresh,
37
+ // then configure the new app in one uninterrupted turn.
38
+ stopActiveRefresh?.();
39
+ disposeTracking();
40
+ activeClient = null;
41
+ stopActiveRefresh = null;
32
42
  const base = opts.apiUrl.replace(/\/+$/, '');
33
43
  const slug = opts.appSlug.trim();
34
44
  const configUrl = `${base}/pulse/config/${slug}`;
35
45
  const trackUrl = `${base}/pulse/track/${slug}`;
36
- const deviceId = opts.deviceId?.trim() || resolveDeviceId(opts.storage);
46
+ const deviceStorageKey = `pulse.${slug}.device-id`;
47
+ let deviceId = opts.deviceId?.trim() || resolveDeviceId(opts.storage, deviceStorageKey);
37
48
  const platform = detectPlatform();
38
49
  const osVersion = detectOsVersion();
50
+ const appVersion = opts.appVersion ?? detectAppVersion();
51
+ const language = opts.language ?? detectLanguage();
52
+ let userId;
53
+ let userAttributes;
54
+ let consentOverride = typeof opts.analyticsConsent === 'boolean' ? opts.analyticsConsent : undefined;
55
+ const hasConsent = () => consentOverride ?? (typeof opts.analyticsConsent === 'function' ? opts.analyticsConsent() : true);
39
56
  const getContext = () => ({
40
57
  platform,
41
58
  osVersion,
42
- appVersion: opts.appVersion,
59
+ appVersion,
60
+ language,
43
61
  deviceId,
44
- userId: opts.getUserId?.(),
45
- userAttributes: opts.getUserAttributes?.()
62
+ userId: userId ?? opts.getUserId?.(),
63
+ userAttributes: userAttributes ?? opts.getUserAttributes?.()
46
64
  });
47
65
  configureConfig({
48
66
  url: configUrl,
49
67
  defaults: opts.defaults,
50
68
  storage: opts.storage,
69
+ storageKey: `pulse.${slug}.config.v1`,
51
70
  getContext,
52
71
  pollIntervalMs: opts.pollIntervalMs,
53
- onError: opts.onError
72
+ onError: opts.onError,
73
+ signingPublicKey: opts.signingPublicKey,
74
+ signingKeyId: opts.signingKeyId,
75
+ requireSignature: opts.requireConfigSignature,
76
+ verifySignature: opts.verifyConfigSignature ?? (opts.requireConfigSignature && !opts.signingPublicKey ? verifyWithNativeKey : undefined)
54
77
  });
55
78
  configureTracking({
56
79
  url: trackUrl,
57
80
  getContext,
58
81
  flushIntervalMs: opts.flushIntervalMs,
82
+ storage: opts.storage,
83
+ storageKey: `pulse.${slug}.events.v1`,
84
+ hasConsent,
85
+ propertyAllowlist: opts.eventPropertyAllowlist,
86
+ redactProperties: opts.redactEventProperties,
87
+ appState: opts.appState,
59
88
  onIgnored: opts.onIgnoredEvents,
60
89
  onError: opts.onError
61
90
  });
@@ -65,7 +94,7 @@ export function initPulse(opts) {
65
94
  // simply does not match this install — which reads as "the rollout did nothing".
66
95
  // Without storage the id is new on every launch, so the arm is re-dealt each time
67
96
  // and no experiment can mean anything.
68
- if (!opts.appVersion) {
97
+ if (!appVersion) {
69
98
  opts.onError?.(new Error('Pulse: no appVersion given — version targeting and version slices will not match this install'));
70
99
  }
71
100
  if (!opts.storage && !opts.deviceId) {
@@ -76,12 +105,84 @@ export function initPulse(opts) {
76
105
  // the first response is the launch that runs on defaults, and defaults are the
77
106
  // off position of every flag.
78
107
  void fetchConfig();
79
- startConfigAutoRefresh(opts.appState);
80
- return {
81
- deviceId,
108
+ const stopRefresh = startConfigAutoRefresh(opts.appState);
109
+ const client = {
110
+ get deviceId() {
111
+ return deviceId;
112
+ },
82
113
  configUrl,
83
- trackUrl
114
+ trackUrl,
115
+ track,
116
+ flush: flushEvents,
117
+ setUser: (nextUserId, attributes) => {
118
+ userId = nextUserId?.trim() || undefined;
119
+ userAttributes = sanitizeAttributes(attributes);
120
+ },
121
+ clearUser: () => {
122
+ userId = undefined;
123
+ userAttributes = undefined;
124
+ },
125
+ setAnalyticsConsent: granted => {
126
+ consentOverride = granted;
127
+ },
128
+ resetInstallId: () => {
129
+ deviceId = randomId();
130
+ try {
131
+ opts.storage?.set(deviceStorageKey, deviceId);
132
+ } catch {/* memory-only fallback */}
133
+ return deviceId;
134
+ },
135
+ health: () => ({
136
+ appSlug: slug,
137
+ deviceId,
138
+ config: getConfigInfo(),
139
+ events: getTrackingInfo(),
140
+ analyticsConsent: hasConsent()
141
+ }),
142
+ config: {
143
+ boolean: getConfigBoolean,
144
+ number: getConfigNumber,
145
+ string: getConfigString,
146
+ json: getConfigJson,
147
+ refresh: fetchConfig
148
+ },
149
+ dispose: async () => {
150
+ stopRefresh();
151
+ if (activeClient !== client) return;
152
+ await flushEvents();
153
+ // Another init may have taken ownership while this network flush was pending.
154
+ if (activeClient === client) {
155
+ disposeTracking();
156
+ activeClient = null;
157
+ stopActiveRefresh = null;
158
+ }
159
+ }
160
+ };
161
+ activeClient = client;
162
+ stopActiveRefresh = stopRefresh;
163
+ return client;
164
+ }
165
+
166
+ /**
167
+ * AsyncStorage-friendly one-call setup. It preloads only Pulse's three app-scoped
168
+ * records, then exposes the same synchronous hot-path API as initPulse.
169
+ */
170
+ export async function initPulseAsync(opts) {
171
+ const slug = opts.appSlug.trim();
172
+ const keys = [`pulse.${slug}.device-id`, `pulse.${slug}.config.v1`, `pulse.${slug}.events.v1`, DEVICE_ID_KEY];
173
+ const loaded = await Promise.all(keys.map(async key => [key, await opts.storage.getItem(key)]));
174
+ const memory = new Map(loaded.filter(entry => entry[1] !== null));
175
+ const storage = {
176
+ getString: key => memory.get(key),
177
+ set: (key, value) => {
178
+ memory.set(key, value);
179
+ void opts.storage.setItem(key, value);
180
+ }
84
181
  };
182
+ return initPulse({
183
+ ...opts,
184
+ storage
185
+ });
85
186
  }
86
187
 
87
188
  /**
@@ -91,18 +192,23 @@ export function initPulse(opts) {
91
192
  * arm is a hash of it, so every launch would be a fresh coin toss and no experiment
92
193
  * could mean anything. Said out loud through onError rather than hidden.
93
194
  */
94
- function resolveDeviceId(storage) {
195
+ function resolveDeviceId(storage, key = DEVICE_ID_KEY) {
95
196
  if (!storage) return randomId();
96
197
  try {
97
- const existing = storage.getString(DEVICE_ID_KEY);
198
+ const existing = storage.getString(key) ?? storage.getString(DEVICE_ID_KEY);
98
199
  if (existing) return existing;
99
200
  const minted = randomId();
100
- storage.set(DEVICE_ID_KEY, minted);
201
+ storage.set(key, minted);
101
202
  return minted;
102
203
  } catch {
103
204
  return randomId();
104
205
  }
105
206
  }
207
+ function sanitizeAttributes(attributes) {
208
+ if (!attributes) return undefined;
209
+ const clean = Object.fromEntries(Object.entries(attributes).filter(([key, value]) => key.trim() !== '' && typeof value === 'string').slice(0, 50));
210
+ return Object.keys(clean).length > 0 ? clean : undefined;
211
+ }
106
212
  function randomId() {
107
213
  const bytes = new Uint8Array(16);
108
214
  const crypto = globalThis.crypto;
@@ -141,4 +247,37 @@ function detectOsVersion() {
141
247
  return undefined;
142
248
  }
143
249
  }
250
+ function detectAppVersion() {
251
+ try {
252
+ // Optional by design: most RN apps already carry this package, plain JS hosts do not.
253
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
254
+ const loaded = require('react-native-device-info');
255
+ return loaded.default?.getVersion?.() ?? loaded.getVersion?.();
256
+ } catch {
257
+ return undefined;
258
+ }
259
+ }
260
+ async function verifyWithNativeKey(canonicalPayload, signature) {
261
+ try {
262
+ // Kept lazy so init/config remain usable in Node, web and test hosts without RN.
263
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
264
+ const updates = require('./PulseUpdates');
265
+ return (await updates.verifyConfigSignatureAsync?.(canonicalPayload, signature)) ?? false;
266
+ } catch {
267
+ return false;
268
+ }
269
+ }
270
+ function detectLanguage() {
271
+ try {
272
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
273
+ const localize = require('react-native-localize');
274
+ return localize.getLocales?.()[0]?.languageTag;
275
+ } catch {
276
+ try {
277
+ return Intl.DateTimeFormat().resolvedOptions().locale;
278
+ } catch {
279
+ return undefined;
280
+ }
281
+ }
282
+ }
144
283
  //# sourceMappingURL=init.js.map
@@ -1 +1 @@
1
- {"version":3,"names":["configureConfig","fetchConfig","startConfigAutoRefresh","configureTracking","DEVICE_ID_KEY","initPulse","opts","base","apiUrl","replace","slug","appSlug","trim","configUrl","trackUrl","deviceId","resolveDeviceId","storage","platform","detectPlatform","osVersion","detectOsVersion","getContext","appVersion","userId","getUserId","userAttributes","getUserAttributes","url","defaults","pollIntervalMs","onError","flushIntervalMs","onIgnored","onIgnoredEvents","Error","appState","randomId","existing","getString","minted","set","bytes","Uint8Array","crypto","globalThis","getRandomValues","i","length","Math","floor","random","hex","Array","from","b","toString","padStart","join","slice","rn","require","Platform","OS","undefined","version","Version","String"],"sourceRoot":"../../src","sources":["init.ts"],"mappings":"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","ignoreList":[]}
1
+ {"version":3,"names":["configureConfig","fetchConfig","startConfigAutoRefresh","getConfigInfo","getConfigBoolean","getConfigNumber","getConfigString","getConfigJson","configureTracking","disposeTracking","flushEvents","getTrackingInfo","track","DEVICE_ID_KEY","activeClient","stopActiveRefresh","initPulse","opts","base","apiUrl","replace","slug","appSlug","trim","configUrl","trackUrl","deviceStorageKey","deviceId","resolveDeviceId","storage","platform","detectPlatform","osVersion","detectOsVersion","appVersion","detectAppVersion","language","detectLanguage","userId","userAttributes","consentOverride","analyticsConsent","undefined","hasConsent","getContext","getUserId","getUserAttributes","url","defaults","storageKey","pollIntervalMs","onError","signingPublicKey","signingKeyId","requireSignature","requireConfigSignature","verifySignature","verifyConfigSignature","verifyWithNativeKey","flushIntervalMs","propertyAllowlist","eventPropertyAllowlist","redactProperties","redactEventProperties","appState","onIgnored","onIgnoredEvents","Error","stopRefresh","client","flush","setUser","nextUserId","attributes","sanitizeAttributes","clearUser","setAnalyticsConsent","granted","resetInstallId","randomId","set","health","config","events","boolean","number","string","json","refresh","dispose","initPulseAsync","keys","loaded","Promise","all","map","key","getItem","memory","Map","filter","entry","getString","get","value","setItem","existing","minted","clean","Object","fromEntries","entries","slice","length","bytes","Uint8Array","crypto","globalThis","getRandomValues","i","Math","floor","random","hex","Array","from","b","toString","padStart","join","rn","require","Platform","OS","version","Version","String","default","getVersion","canonicalPayload","signature","updates","verifyConfigSignatureAsync","localize","getLocales","languageTag","Intl","DateTimeFormat","resolvedOptions","locale"],"sourceRoot":"../../src","sources":["init.ts"],"mappings":"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","ignoreList":[]}
@@ -23,10 +23,16 @@
23
23
  const DEFAULT_FLUSH_MS = 10_000;
24
24
  const DEFAULT_MAX_BATCH = 100;
25
25
  const DEFAULT_MAX_QUEUE = 500;
26
+ const DEFAULT_TIMEOUT_MS = 10_000;
27
+ const DEFAULT_MAX_ATTEMPTS = 10;
28
+ const DEFAULT_STORAGE_KEY = 'pulse.events.v1';
26
29
  let options = null;
27
30
  let queue = [];
28
31
  let timer = null;
29
32
  let sending = false;
33
+ let nextAttemptAt = 0;
34
+ let lifecycleSubscription = null;
35
+ let trackingGeneration = 0;
30
36
 
31
37
  /** Turns a config url into the track url; leaves an explicit track url alone. */
32
38
  function trackUrl(url) {
@@ -34,8 +40,14 @@ function trackUrl(url) {
34
40
  return trimmed.includes('/pulse/config/') ? trimmed.replace('/pulse/config/', '/pulse/track/') : trimmed;
35
41
  }
36
42
  export function configureTracking(opts) {
43
+ disposeTracking();
37
44
  options = opts;
45
+ queue = readQueue(opts);
46
+ nextAttemptAt = 0;
38
47
  startTimer();
48
+ lifecycleSubscription = opts.appState?.addEventListener('change', state => {
49
+ if (state !== 'active') void flushEvents();
50
+ }) ?? null;
39
51
  }
40
52
 
41
53
  /**
@@ -45,18 +57,32 @@ export function configureTracking(opts) {
45
57
  */
46
58
  export function track(event, props, time) {
47
59
  if (!options || !event) return;
60
+ if (options.hasConsent?.() === false) {
61
+ options.onDropped?.(1, 'no-consent');
62
+ return;
63
+ }
48
64
  const flat = {};
65
+ const allow = options.propertyAllowlist ? new Set(options.propertyAllowlist) : null;
66
+ const redact = new Set(options.redactProperties ?? []);
49
67
  for (const [key, value] of Object.entries(props ?? {})) {
50
68
  if (value === null || value === undefined) continue;
51
- flat[key] = typeof value === 'string' ? value : String(value);
69
+ if (allow && !allow.has(key)) continue;
70
+ flat[key] = redact.has(key) ? '[REDACTED]' : typeof value === 'string' ? value : String(value);
52
71
  }
53
72
  queue.push({
73
+ id: randomEventId(),
54
74
  event,
55
75
  time: (time ?? new Date()).toISOString(),
56
- props: flat
76
+ props: flat,
77
+ attempts: 0
57
78
  });
58
79
  const maxQueue = options.maxQueue ?? DEFAULT_MAX_QUEUE;
59
- if (queue.length > maxQueue) queue = queue.slice(queue.length - maxQueue);
80
+ if (queue.length > maxQueue) {
81
+ const dropped = queue.length - maxQueue;
82
+ queue = queue.slice(dropped);
83
+ options.onDropped?.(dropped, 'queue-full');
84
+ }
85
+ persistQueue();
60
86
  if (queue.length >= (options.maxBatch ?? DEFAULT_MAX_BATCH)) void flushEvents();
61
87
  }
62
88
 
@@ -68,8 +94,9 @@ export function track(event, props, time) {
68
94
  * after launch, when the first events of a session are the ones a funnel needs most.
69
95
  */
70
96
  export async function flushEvents() {
71
- if (!options || sending || queue.length === 0) return 0;
97
+ if (!options || sending || queue.length === 0 || Date.now() < nextAttemptAt) return 0;
72
98
  const opts = options;
99
+ const requestGeneration = trackingGeneration;
73
100
  const ctx = opts.getContext?.() ?? {};
74
101
 
75
102
  // Without a device id the server cannot put the event in an arm, and an empty id
@@ -78,7 +105,10 @@ export async function flushEvents() {
78
105
  if (!ctx.deviceId) return 0;
79
106
  const batch = queue.slice(0, opts.maxBatch ?? DEFAULT_MAX_BATCH);
80
107
  queue = queue.slice(batch.length);
108
+ persistQueue();
81
109
  sending = true;
110
+ const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
111
+ const timeout = controller ? setTimeout(() => controller.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS) : null;
82
112
  try {
83
113
  const response = await fetch(trackUrl(opts.url), {
84
114
  method: 'POST',
@@ -91,27 +121,37 @@ export async function flushEvents() {
91
121
  platform: ctx.platform,
92
122
  appVersion: ctx.appVersion,
93
123
  events: batch.map(e => ({
124
+ id: e.id,
94
125
  event: e.event,
95
126
  time: e.time,
96
127
  props: e.props
97
128
  }))
98
- })
129
+ }),
130
+ signal: controller?.signal
99
131
  });
100
132
  if (!response.ok) {
101
133
  // Rate limited or briefly down: keep the events, try on the next tick.
102
- queue = [...batch, ...queue];
134
+ if (requestGeneration === trackingGeneration && options === opts) {
135
+ requeueWithBackoff(batch, response.status);
136
+ } else {
137
+ restoreStaleBatch(opts, batch, response.status);
138
+ }
103
139
  if (response.status !== 429) opts.onError?.(new Error(`Pulse track failed: ${response.status}`));
104
140
  return 0;
105
141
  }
106
142
  const body = await response.json();
143
+ if (requestGeneration !== trackingGeneration || options !== opts) return body.accepted ?? 0;
107
144
  if (body.ignored && body.ignored.length > 0) opts.onIgnored?.(body.ignored);
145
+ nextAttemptAt = 0;
146
+ persistQueue();
108
147
  return body.accepted ?? 0;
109
148
  } catch (error) {
110
- queue = [...batch, ...queue];
149
+ if (requestGeneration === trackingGeneration && options === opts) requeueWithBackoff(batch);else restoreStaleBatch(opts, batch);
111
150
  opts.onError?.(error);
112
151
  return 0;
113
152
  } finally {
114
- sending = false;
153
+ if (timeout) clearTimeout(timeout);
154
+ if (requestGeneration === trackingGeneration) sending = false;
115
155
  }
116
156
  }
117
157
 
@@ -119,14 +159,31 @@ export async function flushEvents() {
119
159
  export function pendingEventCount() {
120
160
  return queue.length;
121
161
  }
162
+ export function getTrackingInfo() {
163
+ return {
164
+ queueDepth: queue.length,
165
+ sending,
166
+ nextAttemptAt
167
+ };
168
+ }
169
+
170
+ /** Stop timers and listeners while preserving the durable outbox. */
171
+ export function disposeTracking() {
172
+ trackingGeneration += 1;
173
+ if (timer) clearInterval(timer);
174
+ timer = null;
175
+ lifecycleSubscription?.remove();
176
+ lifecycleSubscription = null;
177
+ persistQueue();
178
+ options = null;
179
+ sending = false;
180
+ }
122
181
  export function stopTracking() {
123
- if (timer) {
124
- clearInterval(timer);
125
- timer = null;
126
- }
182
+ disposeTracking();
127
183
  options = null;
128
184
  queue = [];
129
185
  sending = false;
186
+ nextAttemptAt = 0;
130
187
  }
131
188
  function startTimer() {
132
189
  if (timer) clearInterval(timer);
@@ -136,4 +193,87 @@ function startTimer() {
136
193
  // and a CLI importing this should still be able to exit.
137
194
  timer.unref?.();
138
195
  }
196
+ function requeueWithBackoff(batch, status) {
197
+ if (!options) return;
198
+ const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
199
+ const retryable = [];
200
+ let dropped = 0;
201
+ for (const event of batch) {
202
+ const next = {
203
+ ...event,
204
+ attempts: event.attempts + 1
205
+ };
206
+ if (next.attempts >= maxAttempts && status !== 429) dropped++;else retryable.push(next);
207
+ }
208
+ queue = [...retryable, ...queue];
209
+ if (dropped > 0) options.onDropped?.(dropped, 'max-attempts');
210
+ const attempts = retryable.reduce((max, event) => Math.max(max, event.attempts), 1);
211
+ const base = Math.min(60_000, 1_000 * 2 ** Math.min(attempts - 1, 6));
212
+ nextAttemptAt = Date.now() + base + Math.floor(Math.random() * Math.max(1, base / 4));
213
+ persistQueue();
214
+ }
215
+
216
+ /**
217
+ * An old request may fail after another app has been configured. Its rows belong in
218
+ * the old app's outbox, never in the new app's in-memory queue. When the same app was
219
+ * merely reconfigured, merge them back into the live queue so it need not restart to
220
+ * see them; otherwise persist directly under the captured app-scoped key.
221
+ */
222
+ function restoreStaleBatch(opts, batch, status) {
223
+ const maxAttempts = opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
224
+ const retryable = [];
225
+ let dropped = 0;
226
+ for (const event of batch) {
227
+ const next = {
228
+ ...event,
229
+ attempts: event.attempts + 1
230
+ };
231
+ if (next.attempts >= maxAttempts && status !== 429) dropped++;else retryable.push(next);
232
+ }
233
+ if (dropped > 0) opts.onDropped?.(dropped, 'max-attempts');
234
+ if (retryable.length === 0) return;
235
+ const currentOptions = options;
236
+ const sameOutbox = currentOptions?.storage === opts.storage && currentOptions !== null && storageKey(currentOptions) === storageKey(opts);
237
+ if (sameOutbox) {
238
+ queue = [...retryable, ...queue].slice(-(options?.maxQueue ?? DEFAULT_MAX_QUEUE));
239
+ persistQueue();
240
+ return;
241
+ }
242
+ if (!opts.storage) return;
243
+ try {
244
+ const stored = readQueue(opts);
245
+ const restored = [...retryable, ...stored].slice(-(opts.maxQueue ?? DEFAULT_MAX_QUEUE));
246
+ opts.storage.set(storageKey(opts), JSON.stringify(restored));
247
+ } catch {
248
+ // Best effort only; a telemetry recovery must not affect the new app session.
249
+ }
250
+ }
251
+ function storageKey(opts) {
252
+ return opts.storageKey?.trim() || DEFAULT_STORAGE_KEY;
253
+ }
254
+ function readQueue(opts) {
255
+ if (!opts.storage) return [];
256
+ try {
257
+ const raw = opts.storage.getString(storageKey(opts));
258
+ if (!raw) return [];
259
+ const parsed = JSON.parse(raw);
260
+ if (!Array.isArray(parsed)) return [];
261
+ return parsed.filter(item => Boolean(item && typeof item === 'object' && typeof item.id === 'string' && typeof item.event === 'string' && typeof item.time === 'string' && typeof item.props === 'object' && typeof item.attempts === 'number')).slice(-(opts.maxQueue ?? DEFAULT_MAX_QUEUE));
262
+ } catch {
263
+ return [];
264
+ }
265
+ }
266
+ function persistQueue() {
267
+ if (!options?.storage) return;
268
+ try {
269
+ options.storage.set(storageKey(options), JSON.stringify(queue));
270
+ } catch {
271
+ // Telemetry persistence must never become an app failure.
272
+ }
273
+ }
274
+ function randomEventId() {
275
+ const cryptoLike = globalThis.crypto;
276
+ if (typeof cryptoLike?.randomUUID === 'function') return cryptoLike.randomUUID();
277
+ return `evt-${Date.now()}-${Math.random().toString(36).slice(2, 14)}`;
278
+ }
139
279
  //# sourceMappingURL=track.js.map