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.
- package/README.md +50 -10
- package/SECURITY.md +42 -0
- package/android/src/main/java/app/pulse/updates/PulseController.kt +6 -2
- package/android/src/main/java/app/pulse/updates/PulseUpdatesModule.kt +25 -0
- package/app.plugin.js +47 -0
- package/ios/PulseUpdates/PulseController.swift +7 -2
- package/ios/PulseUpdates/PulseUpdates.m +7 -0
- package/ios/PulseUpdates/PulseUpdates.swift +22 -0
- package/lib/commonjs/NativePulseUpdates.js.map +1 -1
- package/lib/commonjs/PulseUpdates.js +36 -0
- package/lib/commonjs/PulseUpdates.js.map +1 -1
- package/lib/commonjs/config.js +175 -21
- package/lib/commonjs/config.js.map +1 -1
- package/lib/commonjs/decisions.js +128 -0
- package/lib/commonjs/decisions.js.map +1 -0
- package/lib/commonjs/index.js +12 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/init.js +153 -13
- package/lib/commonjs/init.js.map +1 -1
- package/lib/commonjs/track.js +154 -12
- package/lib/commonjs/track.js.map +1 -1
- package/lib/commonjs/usePulseUpdates.js +21 -16
- package/lib/commonjs/usePulseUpdates.js.map +1 -1
- package/lib/module/NativePulseUpdates.js.map +1 -1
- package/lib/module/PulseUpdates.js +33 -0
- package/lib/module/PulseUpdates.js.map +1 -1
- package/lib/module/config.js +174 -21
- package/lib/module/config.js.map +1 -1
- package/lib/module/decisions.js +122 -0
- package/lib/module/decisions.js.map +1 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/init.js +154 -15
- package/lib/module/init.js.map +1 -1
- package/lib/module/track.js +152 -12
- package/lib/module/track.js.map +1 -1
- package/lib/module/usePulseUpdates.js +21 -16
- package/lib/module/usePulseUpdates.js.map +1 -1
- package/lib/typescript/NativePulseUpdates.d.ts +1 -0
- package/lib/typescript/NativePulseUpdates.d.ts.map +1 -1
- package/lib/typescript/PulseUpdates.d.ts +20 -0
- package/lib/typescript/PulseUpdates.d.ts.map +1 -1
- package/lib/typescript/config.d.ts +22 -0
- package/lib/typescript/config.d.ts.map +1 -1
- package/lib/typescript/decisions.d.ts +55 -0
- package/lib/typescript/decisions.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +1 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/init.d.ts +51 -4
- package/lib/typescript/init.d.ts.map +1 -1
- package/lib/typescript/track.d.ts +29 -1
- package/lib/typescript/track.d.ts.map +1 -1
- package/lib/typescript/usePulseUpdates.d.ts.map +1 -1
- package/logo.png +0 -0
- package/package.json +14 -3
- package/scripts/publish.mjs +68 -3
- package/src/NativePulseUpdates.ts +1 -0
- package/src/PulseUpdates.ts +54 -0
- package/src/config.ts +234 -21
- package/src/decisions.ts +179 -0
- package/src/index.ts +1 -0
- package/src/init.ts +227 -12
- package/src/track.ts +191 -13
- 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":[]}
|
package/lib/module/index.js
CHANGED
package/lib/module/index.js.map
CHANGED
|
@@ -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":[]}
|
package/lib/module/init.js
CHANGED
|
@@ -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
|
|
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
|
|
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 (!
|
|
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
|
-
|
|
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(
|
|
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
|
package/lib/module/init.js.map
CHANGED
|
@@ -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":[]}
|
package/lib/module/track.js
CHANGED
|
@@ -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
|
-
|
|
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)
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|