pulse-updates 1.0.21 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +34 -0
- package/lib/commonjs/config.js +3 -0
- package/lib/commonjs/config.js.map +1 -1
- package/lib/commonjs/index.js +24 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/init.js +150 -0
- package/lib/commonjs/init.js.map +1 -0
- package/lib/commonjs/track.js +149 -0
- package/lib/commonjs/track.js.map +1 -0
- package/lib/module/config.js +3 -0
- package/lib/module/config.js.map +1 -1
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/init.js +144 -0
- package/lib/module/init.js.map +1 -0
- package/lib/module/track.js +139 -0
- package/lib/module/track.js.map +1 -0
- package/lib/typescript/config.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/init.d.ts +73 -0
- package/lib/typescript/init.d.ts.map +1 -0
- package/lib/typescript/track.d.ts +66 -0
- package/lib/typescript/track.d.ts.map +1 -0
- package/package.json +1 -1
- package/scripts/publish.mjs +193 -0
- package/src/config.ts +3 -0
- package/src/index.ts +2 -0
- package/src/init.ts +210 -0
- package/src/track.ts +184 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One call to wire an app to Pulse.
|
|
3
|
+
*
|
|
4
|
+
* The three halves — updates, config, events — each have their own setup, and each
|
|
5
|
+
* one is a place to make a mistake that shows up as an empty chart weeks later: a
|
|
6
|
+
* config url that points at one app and a track url at another, an install with no
|
|
7
|
+
* stable id (so every launch looks like a new device and every arm is re-dealt), a
|
|
8
|
+
* context that forgets the app version (so an experiment cannot be aimed at a build).
|
|
9
|
+
* They are the same three facts every time, so they are asked for once.
|
|
10
|
+
*
|
|
11
|
+
* Everything it derives, it derives from the platform and only when the caller has
|
|
12
|
+
* not said otherwise:
|
|
13
|
+
*
|
|
14
|
+
* - the urls, from the base url and the slug, which is what stops the two halves
|
|
15
|
+
* from ever addressing different apps;
|
|
16
|
+
* - platform and OS version, from React Native;
|
|
17
|
+
* - the device id, minted and persisted on first launch — an app that has a stable
|
|
18
|
+
* identifier of its own (the vendor id, an installation id) should pass it, and an
|
|
19
|
+
* app that has none should not have to invent one to be measurable.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately not automatic: the app version. Nothing here can read the store build
|
|
22
|
+
* reliably on both platforms, and a version guessed wrong aims a rollout at the wrong
|
|
23
|
+
* installs — a targeting bug that looks like a rollout that "did not work".
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { configureConfig, fetchConfig, startConfigAutoRefresh } from './config';
|
|
27
|
+
import { configureTracking } from './track';
|
|
28
|
+
const DEVICE_ID_KEY = 'pulse.device-id';
|
|
29
|
+
|
|
30
|
+
/** Wires config and events, and returns the context the two share. */
|
|
31
|
+
export function initPulse(opts) {
|
|
32
|
+
const base = opts.apiUrl.replace(/\/+$/, '');
|
|
33
|
+
const slug = opts.appSlug.trim();
|
|
34
|
+
const configUrl = `${base}/pulse/config/${slug}`;
|
|
35
|
+
const trackUrl = `${base}/pulse/track/${slug}`;
|
|
36
|
+
const deviceId = opts.deviceId?.trim() || resolveDeviceId(opts.storage);
|
|
37
|
+
const platform = detectPlatform();
|
|
38
|
+
const osVersion = detectOsVersion();
|
|
39
|
+
const getContext = () => ({
|
|
40
|
+
platform,
|
|
41
|
+
osVersion,
|
|
42
|
+
appVersion: opts.appVersion,
|
|
43
|
+
deviceId,
|
|
44
|
+
userId: opts.getUserId?.(),
|
|
45
|
+
userAttributes: opts.getUserAttributes?.()
|
|
46
|
+
});
|
|
47
|
+
configureConfig({
|
|
48
|
+
url: configUrl,
|
|
49
|
+
defaults: opts.defaults,
|
|
50
|
+
storage: opts.storage,
|
|
51
|
+
getContext,
|
|
52
|
+
pollIntervalMs: opts.pollIntervalMs,
|
|
53
|
+
onError: opts.onError
|
|
54
|
+
});
|
|
55
|
+
configureTracking({
|
|
56
|
+
url: trackUrl,
|
|
57
|
+
getContext,
|
|
58
|
+
flushIntervalMs: opts.flushIntervalMs,
|
|
59
|
+
onIgnored: opts.onIgnoredEvents,
|
|
60
|
+
onError: opts.onError
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// The two fields nothing can derive, said out loud when they are missing. Both
|
|
64
|
+
// fail silently and late: without a version, every rule and slice that names one
|
|
65
|
+
// simply does not match this install — which reads as "the rollout did nothing".
|
|
66
|
+
// Without storage the id is new on every launch, so the arm is re-dealt each time
|
|
67
|
+
// and no experiment can mean anything.
|
|
68
|
+
if (!opts.appVersion) {
|
|
69
|
+
opts.onError?.(new Error('Pulse: no appVersion given — version targeting and version slices will not match this install'));
|
|
70
|
+
}
|
|
71
|
+
if (!opts.storage && !opts.deviceId) {
|
|
72
|
+
opts.onError?.(new Error('Pulse: no storage and no deviceId — this install gets a new id every launch, which re-deals its arm'));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Fetch now rather than on the first read: a launch that reads the config before
|
|
76
|
+
// the first response is the launch that runs on defaults, and defaults are the
|
|
77
|
+
// off position of every flag.
|
|
78
|
+
void fetchConfig();
|
|
79
|
+
startConfigAutoRefresh(opts.appState);
|
|
80
|
+
return {
|
|
81
|
+
deviceId,
|
|
82
|
+
configUrl,
|
|
83
|
+
trackUrl
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The install's id: the one it already had, or a new one kept from now on.
|
|
89
|
+
*
|
|
90
|
+
* Without storage this is a new id every launch, which is worse than it looks — the
|
|
91
|
+
* arm is a hash of it, so every launch would be a fresh coin toss and no experiment
|
|
92
|
+
* could mean anything. Said out loud through onError rather than hidden.
|
|
93
|
+
*/
|
|
94
|
+
function resolveDeviceId(storage) {
|
|
95
|
+
if (!storage) return randomId();
|
|
96
|
+
try {
|
|
97
|
+
const existing = storage.getString(DEVICE_ID_KEY);
|
|
98
|
+
if (existing) return existing;
|
|
99
|
+
const minted = randomId();
|
|
100
|
+
storage.set(DEVICE_ID_KEY, minted);
|
|
101
|
+
return minted;
|
|
102
|
+
} catch {
|
|
103
|
+
return randomId();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function randomId() {
|
|
107
|
+
const bytes = new Uint8Array(16);
|
|
108
|
+
const crypto = globalThis.crypto;
|
|
109
|
+
if (crypto?.getRandomValues) {
|
|
110
|
+
crypto.getRandomValues(bytes);
|
|
111
|
+
} else {
|
|
112
|
+
// React Native without a crypto polyfill. Weaker, and only ever used to tell one
|
|
113
|
+
// install from another — never as a secret.
|
|
114
|
+
for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
|
|
115
|
+
}
|
|
116
|
+
bytes[6] = (bytes[6] ?? 0) & 0x0f | 0x40;
|
|
117
|
+
bytes[8] = (bytes[8] ?? 0) & 0x3f | 0x80;
|
|
118
|
+
const hex = Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('');
|
|
119
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** React Native when it is there, and nothing invented when it is not. */
|
|
123
|
+
function detectPlatform() {
|
|
124
|
+
try {
|
|
125
|
+
// Required lazily so the module stays importable from a plain Node script — the
|
|
126
|
+
// CLI and the tests both do that.
|
|
127
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
128
|
+
const rn = require('react-native');
|
|
129
|
+
return rn?.Platform?.OS;
|
|
130
|
+
} catch {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function detectOsVersion() {
|
|
135
|
+
try {
|
|
136
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
137
|
+
const rn = require('react-native');
|
|
138
|
+
const version = rn?.Platform?.Version;
|
|
139
|
+
return version === undefined || version === null ? undefined : String(version);
|
|
140
|
+
} catch {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=init.js.map
|
|
@@ -0,0 +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":[]}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Track — the app's own events, sent to Pulse.
|
|
3
|
+
*
|
|
4
|
+
* The point of this half: an experiment is only worth running if something can be
|
|
5
|
+
* measured on it, and until now measuring meant owning a warehouse — a ClickHouse,
|
|
6
|
+
* a stream, and somebody to keep both alive. An app that has just adopted Pulse has
|
|
7
|
+
* none of that, so its experiments could report retention and nothing else.
|
|
8
|
+
*
|
|
9
|
+
* The event has to be declared in the registry first. That is deliberate and it is
|
|
10
|
+
* the same rule the server enforces: an event nobody declared could never be named by
|
|
11
|
+
* a metric, so accepting it would be a write into a place nothing reads. The response
|
|
12
|
+
* says which keys were ignored, and `onIgnored` surfaces them — a typo in an event
|
|
13
|
+
* name is otherwise invisible until someone wonders why a chart is empty.
|
|
14
|
+
*
|
|
15
|
+
* Batched on purpose. The endpoint takes up to a hundred events per request and is
|
|
16
|
+
* rate-limited per address, so one request per event would spend the whole allowance
|
|
17
|
+
* of a shared network on a single install. Events are queued, flushed on a timer and
|
|
18
|
+
* whenever the batch fills, and dropped oldest-first if the queue overflows: an event
|
|
19
|
+
* lost is a row missing from an average, and a request that blocks the app is a bug
|
|
20
|
+
* everybody sees.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const DEFAULT_FLUSH_MS = 10_000;
|
|
24
|
+
const DEFAULT_MAX_BATCH = 100;
|
|
25
|
+
const DEFAULT_MAX_QUEUE = 500;
|
|
26
|
+
let options = null;
|
|
27
|
+
let queue = [];
|
|
28
|
+
let timer = null;
|
|
29
|
+
let sending = false;
|
|
30
|
+
|
|
31
|
+
/** Turns a config url into the track url; leaves an explicit track url alone. */
|
|
32
|
+
function trackUrl(url) {
|
|
33
|
+
const trimmed = url.replace(/\/+$/, '');
|
|
34
|
+
return trimmed.includes('/pulse/config/') ? trimmed.replace('/pulse/config/', '/pulse/track/') : trimmed;
|
|
35
|
+
}
|
|
36
|
+
export function configureTracking(opts) {
|
|
37
|
+
options = opts;
|
|
38
|
+
startTimer();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Queue an event. Never throws, never awaits: a call site in a tap handler must not
|
|
43
|
+
* be able to make the tap slow, and an analytics call that can throw is one every
|
|
44
|
+
* caller has to wrap.
|
|
45
|
+
*/
|
|
46
|
+
export function track(event, props, time) {
|
|
47
|
+
if (!options || !event) return;
|
|
48
|
+
const flat = {};
|
|
49
|
+
for (const [key, value] of Object.entries(props ?? {})) {
|
|
50
|
+
if (value === null || value === undefined) continue;
|
|
51
|
+
flat[key] = typeof value === 'string' ? value : String(value);
|
|
52
|
+
}
|
|
53
|
+
queue.push({
|
|
54
|
+
event,
|
|
55
|
+
time: (time ?? new Date()).toISOString(),
|
|
56
|
+
props: flat
|
|
57
|
+
});
|
|
58
|
+
const maxQueue = options.maxQueue ?? DEFAULT_MAX_QUEUE;
|
|
59
|
+
if (queue.length > maxQueue) queue = queue.slice(queue.length - maxQueue);
|
|
60
|
+
if (queue.length >= (options.maxBatch ?? DEFAULT_MAX_BATCH)) void flushEvents();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Send what is queued. Returns how many events the server accepted.
|
|
65
|
+
*
|
|
66
|
+
* Failures put the batch back at the front of the queue rather than dropping it: the
|
|
67
|
+
* usual cause is a network that is about to come back, and the usual moment is right
|
|
68
|
+
* after launch, when the first events of a session are the ones a funnel needs most.
|
|
69
|
+
*/
|
|
70
|
+
export async function flushEvents() {
|
|
71
|
+
if (!options || sending || queue.length === 0) return 0;
|
|
72
|
+
const opts = options;
|
|
73
|
+
const ctx = opts.getContext?.() ?? {};
|
|
74
|
+
|
|
75
|
+
// Without a device id the server cannot put the event in an arm, and an empty id
|
|
76
|
+
// would pool every such install into one. Held rather than dropped: the id usually
|
|
77
|
+
// arrives a moment after boot.
|
|
78
|
+
if (!ctx.deviceId) return 0;
|
|
79
|
+
const batch = queue.slice(0, opts.maxBatch ?? DEFAULT_MAX_BATCH);
|
|
80
|
+
queue = queue.slice(batch.length);
|
|
81
|
+
sending = true;
|
|
82
|
+
try {
|
|
83
|
+
const response = await fetch(trackUrl(opts.url), {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: {
|
|
86
|
+
'Content-Type': 'application/json'
|
|
87
|
+
},
|
|
88
|
+
body: JSON.stringify({
|
|
89
|
+
deviceId: ctx.deviceId,
|
|
90
|
+
userId: ctx.userId,
|
|
91
|
+
platform: ctx.platform,
|
|
92
|
+
appVersion: ctx.appVersion,
|
|
93
|
+
events: batch.map(e => ({
|
|
94
|
+
event: e.event,
|
|
95
|
+
time: e.time,
|
|
96
|
+
props: e.props
|
|
97
|
+
}))
|
|
98
|
+
})
|
|
99
|
+
});
|
|
100
|
+
if (!response.ok) {
|
|
101
|
+
// Rate limited or briefly down: keep the events, try on the next tick.
|
|
102
|
+
queue = [...batch, ...queue];
|
|
103
|
+
if (response.status !== 429) opts.onError?.(new Error(`Pulse track failed: ${response.status}`));
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
const body = await response.json();
|
|
107
|
+
if (body.ignored && body.ignored.length > 0) opts.onIgnored?.(body.ignored);
|
|
108
|
+
return body.accepted ?? 0;
|
|
109
|
+
} catch (error) {
|
|
110
|
+
queue = [...batch, ...queue];
|
|
111
|
+
opts.onError?.(error);
|
|
112
|
+
return 0;
|
|
113
|
+
} finally {
|
|
114
|
+
sending = false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** How many events are waiting — for a caller that wants to flush before backgrounding. */
|
|
119
|
+
export function pendingEventCount() {
|
|
120
|
+
return queue.length;
|
|
121
|
+
}
|
|
122
|
+
export function stopTracking() {
|
|
123
|
+
if (timer) {
|
|
124
|
+
clearInterval(timer);
|
|
125
|
+
timer = null;
|
|
126
|
+
}
|
|
127
|
+
options = null;
|
|
128
|
+
queue = [];
|
|
129
|
+
sending = false;
|
|
130
|
+
}
|
|
131
|
+
function startTimer() {
|
|
132
|
+
if (timer) clearInterval(timer);
|
|
133
|
+
const every = options?.flushIntervalMs ?? DEFAULT_FLUSH_MS;
|
|
134
|
+
timer = setInterval(() => void flushEvents(), every);
|
|
135
|
+
// Node keeps the process alive for a pending interval; React Native does not care,
|
|
136
|
+
// and a CLI importing this should still be able to exit.
|
|
137
|
+
timer.unref?.();
|
|
138
|
+
}
|
|
139
|
+
//# sourceMappingURL=track.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["DEFAULT_FLUSH_MS","DEFAULT_MAX_BATCH","DEFAULT_MAX_QUEUE","options","queue","timer","sending","trackUrl","url","trimmed","replace","includes","configureTracking","opts","startTimer","track","event","props","time","flat","key","value","Object","entries","undefined","String","push","Date","toISOString","maxQueue","length","slice","maxBatch","flushEvents","ctx","getContext","deviceId","batch","response","fetch","method","headers","body","JSON","stringify","userId","platform","appVersion","events","map","e","ok","status","onError","Error","json","ignored","onIgnored","accepted","error","pendingEventCount","stopTracking","clearInterval","every","flushIntervalMs","setInterval","unref"],"sourceRoot":"../../src","sources":["track.ts"],"mappings":"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","ignoreList":[]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,MAAM,WAAW,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,MAAM,CAAC;AAEpE;;;;;;;GAOG;AACH,MAAM,WAAW,gBAAgB;IAC/B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,GAAG,SAAS,CAAC;IAClD,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACvC;AAED;;;;GAIG;AACH,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,wFAAwF;IACxF,cAAc,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,aAAa;IAC5B,kFAAkF;IAClF,GAAG,EAAE,MAAM,CAAC;IACZ,8EAA8E;IAC9E,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IACvC,kFAAkF;IAClF,OAAO,CAAC,EAAE,aAAa,CAAC;IACxB,oFAAoF;IACpF,UAAU,CAAC,EAAE,MAAM,aAAa,CAAC;IACjC,kFAAkF;IAClF,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;OAIG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC;;;;;;OAMG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,mCAAmC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC;AAqDD;;;;GAIG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,aAAa,GAAG,IAAI,CAYzD;AAED,uFAAuF;AACvF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,GAAG,IAAI,CAEzE;AAED;;;;;;GAMG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC,CAmEpD;AAED;;;GAGG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAKxC;AAED,gEAAgE;AAChE,wBAAgB,gBAAgB,IAAI,OAAO,CAE1C;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,CAAC,EAAE;IAChD,gBAAgB,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,KAAK;QAAE,MAAM,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC;CAChG,GAAG,MAAM,IAAI,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/config.ts"],"names":[],"mappings":"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"}
|
|
@@ -3,4 +3,6 @@ export * from './types';
|
|
|
3
3
|
export { usePulseUpdates } from './usePulseUpdates';
|
|
4
4
|
export { initializeAssetResolver, updateLocalAssets } from './assetResolver';
|
|
5
5
|
export * from './config';
|
|
6
|
+
export * from './track';
|
|
7
|
+
export * from './init';
|
|
6
8
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC7E,cAAc,UAAU,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,SAAS,CAAC;AACxB,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,EAAE,uBAAuB,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAC7E,cAAc,UAAU,CAAC;AACzB,cAAc,SAAS,CAAC;AACxB,cAAc,QAAQ,CAAC"}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One call to wire an app to Pulse.
|
|
3
|
+
*
|
|
4
|
+
* The three halves — updates, config, events — each have their own setup, and each
|
|
5
|
+
* one is a place to make a mistake that shows up as an empty chart weeks later: a
|
|
6
|
+
* config url that points at one app and a track url at another, an install with no
|
|
7
|
+
* stable id (so every launch looks like a new device and every arm is re-dealt), a
|
|
8
|
+
* context that forgets the app version (so an experiment cannot be aimed at a build).
|
|
9
|
+
* They are the same three facts every time, so they are asked for once.
|
|
10
|
+
*
|
|
11
|
+
* Everything it derives, it derives from the platform and only when the caller has
|
|
12
|
+
* not said otherwise:
|
|
13
|
+
*
|
|
14
|
+
* - the urls, from the base url and the slug, which is what stops the two halves
|
|
15
|
+
* from ever addressing different apps;
|
|
16
|
+
* - platform and OS version, from React Native;
|
|
17
|
+
* - the device id, minted and persisted on first launch — an app that has a stable
|
|
18
|
+
* identifier of its own (the vendor id, an installation id) should pass it, and an
|
|
19
|
+
* app that has none should not have to invent one to be measurable.
|
|
20
|
+
*
|
|
21
|
+
* Deliberately not automatic: the app version. Nothing here can read the store build
|
|
22
|
+
* reliably on both platforms, and a version guessed wrong aims a rollout at the wrong
|
|
23
|
+
* installs — a targeting bug that looks like a rollout that "did not work".
|
|
24
|
+
*/
|
|
25
|
+
import { type ConfigStorage, type ConfigValue } from './config';
|
|
26
|
+
export interface InitPulseOptions {
|
|
27
|
+
/** Where Pulse lives, e.g. https://pulse.example.com — no path. */
|
|
28
|
+
apiUrl: string;
|
|
29
|
+
/** The app's slug in Pulse. Both urls are built from it. */
|
|
30
|
+
appSlug: string;
|
|
31
|
+
/** Values in force before the server has ever been reached. */
|
|
32
|
+
defaults?: Record<string, ConfigValue>;
|
|
33
|
+
/**
|
|
34
|
+
* Somewhere to keep the last good config and the device id. Without it both are
|
|
35
|
+
* memory-only: the config falls back to defaults after a restart, and — worse —
|
|
36
|
+
* the install gets a new id every launch, which re-deals its arm every launch.
|
|
37
|
+
*/
|
|
38
|
+
storage?: ConfigStorage;
|
|
39
|
+
/** The store build, e.g. "5.2.0". Needed to target a rollout at a version. */
|
|
40
|
+
appVersion?: string;
|
|
41
|
+
/**
|
|
42
|
+
* A stable id for this install. Left out, one is minted and persisted.
|
|
43
|
+
*
|
|
44
|
+
* Worth passing when the app already has one the rest of its analytics uses: the
|
|
45
|
+
* arm is a hash of this value, and a metric joined on a different id is not
|
|
46
|
+
* slightly wrong, it is randomised.
|
|
47
|
+
*/
|
|
48
|
+
deviceId?: string;
|
|
49
|
+
/** The signed-in account, read fresh on every request. Only user-level experiments use it. */
|
|
50
|
+
getUserId?: () => string | undefined;
|
|
51
|
+
/** Attributes a rule can target — a plan, a storefront. Keep them few and stable. */
|
|
52
|
+
getUserAttributes?: () => Record<string, string> | undefined;
|
|
53
|
+
/** Foreground poll interval for the config. 0 disables it; launch and resume still fetch. */
|
|
54
|
+
pollIntervalMs?: number;
|
|
55
|
+
/** How often queued events are sent. Default 10s. */
|
|
56
|
+
flushIntervalMs?: number;
|
|
57
|
+
/** App-state hook, so the config refreshes on return from background. */
|
|
58
|
+
appState?: {
|
|
59
|
+
addEventListener: (type: 'change', handler: (state: string) => void) => {
|
|
60
|
+
remove: () => void;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
/** Event keys the server did not recognise — a typo, or an event nobody declared. */
|
|
64
|
+
onIgnoredEvents?: (events: string[]) => void;
|
|
65
|
+
onError?: (error: unknown) => void;
|
|
66
|
+
}
|
|
67
|
+
/** Wires config and events, and returns the context the two share. */
|
|
68
|
+
export declare function initPulse(opts: InitPulseOptions): {
|
|
69
|
+
deviceId: string;
|
|
70
|
+
configUrl: string;
|
|
71
|
+
trackUrl: string;
|
|
72
|
+
};
|
|
73
|
+
//# sourceMappingURL=init.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../../src/init.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAKL,KAAK,aAAa,EAClB,KAAK,WAAW,EACjB,MAAM,UAAU,CAAC;AAGlB,MAAM,WAAW,gBAAgB;IAC/B,mEAAmE;IACnE,MAAM,EAAE,MAAM,CAAC;IAEf,4DAA4D;IAC5D,OAAO,EAAE,MAAM,CAAC;IAEhB,+DAA+D;IAC/D,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAEvC;;;;OAIG;IACH,OAAO,CAAC,EAAE,aAAa,CAAC;IAExB,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,8FAA8F;IAC9F,SAAS,CAAC,EAAE,MAAM,MAAM,GAAG,SAAS,CAAC;IAErC,qFAAqF;IACrF,iBAAiB,CAAC,EAAE,MAAM,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,SAAS,CAAC;IAE7D,6FAA6F;IAC7F,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB,qDAAqD;IACrD,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,yEAAyE;IACzE,QAAQ,CAAC,EAAE;QAAE,gBAAgB,EAAE,CAAC,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,KAAK;YAAE,MAAM,EAAE,MAAM,IAAI,CAAA;SAAE,CAAA;KAAE,CAAC;IAE9G,qFAAqF;IACrF,eAAe,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IAE7C,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC;AAID,sEAAsE;AACtE,wBAAgB,SAAS,CAAC,IAAI,EAAE,gBAAgB,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAA;CAAE,CAyD3G"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pulse Track — the app's own events, sent to Pulse.
|
|
3
|
+
*
|
|
4
|
+
* The point of this half: an experiment is only worth running if something can be
|
|
5
|
+
* measured on it, and until now measuring meant owning a warehouse — a ClickHouse,
|
|
6
|
+
* a stream, and somebody to keep both alive. An app that has just adopted Pulse has
|
|
7
|
+
* none of that, so its experiments could report retention and nothing else.
|
|
8
|
+
*
|
|
9
|
+
* The event has to be declared in the registry first. That is deliberate and it is
|
|
10
|
+
* the same rule the server enforces: an event nobody declared could never be named by
|
|
11
|
+
* a metric, so accepting it would be a write into a place nothing reads. The response
|
|
12
|
+
* says which keys were ignored, and `onIgnored` surfaces them — a typo in an event
|
|
13
|
+
* name is otherwise invisible until someone wonders why a chart is empty.
|
|
14
|
+
*
|
|
15
|
+
* Batched on purpose. The endpoint takes up to a hundred events per request and is
|
|
16
|
+
* rate-limited per address, so one request per event would spend the whole allowance
|
|
17
|
+
* of a shared network on a single install. Events are queued, flushed on a timer and
|
|
18
|
+
* whenever the batch fills, and dropped oldest-first if the queue overflows: an event
|
|
19
|
+
* lost is a row missing from an average, and a request that blocks the app is a bug
|
|
20
|
+
* everybody sees.
|
|
21
|
+
*/
|
|
22
|
+
import type { ConfigContext } from './config';
|
|
23
|
+
export interface TrackOptions {
|
|
24
|
+
/**
|
|
25
|
+
* The config url this app already uses (…/pulse/config/{slug}) or the track url
|
|
26
|
+
* itself. Accepting the first is not a convenience: it means the two halves cannot
|
|
27
|
+
* end up pointing at different apps, which would put an app's events in another
|
|
28
|
+
* app's registry and read as an empty metric on both sides.
|
|
29
|
+
*/
|
|
30
|
+
url: string;
|
|
31
|
+
/** The same context the config uses — identity travels with the batch, once. */
|
|
32
|
+
getContext?: () => ConfigContext;
|
|
33
|
+
/** How often a non-empty queue is sent. Default 10s. */
|
|
34
|
+
flushIntervalMs?: number;
|
|
35
|
+
/** Events per request. The server refuses more than 100. */
|
|
36
|
+
maxBatch?: number;
|
|
37
|
+
/** Events held before the oldest are dropped. Default 500. */
|
|
38
|
+
maxQueue?: number;
|
|
39
|
+
/** Called with the event keys the server did not recognise. */
|
|
40
|
+
onIgnored?: (events: string[]) => void;
|
|
41
|
+
onError?: (error: unknown) => void;
|
|
42
|
+
}
|
|
43
|
+
export interface TrackedEventInput {
|
|
44
|
+
event: string;
|
|
45
|
+
props?: Record<string, string | number | boolean | null | undefined>;
|
|
46
|
+
time?: Date;
|
|
47
|
+
}
|
|
48
|
+
export declare function configureTracking(opts: TrackOptions): void;
|
|
49
|
+
/**
|
|
50
|
+
* Queue an event. Never throws, never awaits: a call site in a tap handler must not
|
|
51
|
+
* be able to make the tap slow, and an analytics call that can throw is one every
|
|
52
|
+
* caller has to wrap.
|
|
53
|
+
*/
|
|
54
|
+
export declare function track(event: string, props?: TrackedEventInput['props'], time?: Date): void;
|
|
55
|
+
/**
|
|
56
|
+
* Send what is queued. Returns how many events the server accepted.
|
|
57
|
+
*
|
|
58
|
+
* Failures put the batch back at the front of the queue rather than dropping it: the
|
|
59
|
+
* usual cause is a network that is about to come back, and the usual moment is right
|
|
60
|
+
* after launch, when the first events of a session are the ones a funnel needs most.
|
|
61
|
+
*/
|
|
62
|
+
export declare function flushEvents(): Promise<number>;
|
|
63
|
+
/** How many events are waiting — for a caller that wants to flush before backgrounding. */
|
|
64
|
+
export declare function pendingEventCount(): number;
|
|
65
|
+
export declare function stopTracking(): void;
|
|
66
|
+
//# sourceMappingURL=track.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"track.d.ts","sourceRoot":"","sources":["../../src/track.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAE9C,MAAM,WAAW,YAAY;IAC3B;;;;;OAKG;IACH,GAAG,EAAE,MAAM,CAAC;IAEZ,gFAAgF;IAChF,UAAU,CAAC,EAAE,MAAM,aAAa,CAAC;IAEjC,wDAAwD;IACxD,eAAe,CAAC,EAAE,MAAM,CAAC;IAEzB,4DAA4D;IAC5D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAElB,+DAA+D;IAC/D,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;IAEvC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;IACrE,IAAI,CAAC,EAAE,IAAI,CAAC;CACb;AAyBD,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI,CAG1D;AAED;;;;GAIG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,iBAAiB,CAAC,OAAO,CAAC,EAAE,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,CAe1F;AAED;;;;;;GAMG;AACH,wBAAsB,WAAW,IAAI,OAAO,CAAC,MAAM,CAAC,CA6CnD;AAED,2FAA2F;AAC3F,wBAAgB,iBAAiB,IAAI,MAAM,CAE1C;AAED,wBAAgB,YAAY,IAAI,IAAI,CAQnC"}
|