@entmesh/sdk-miniapp 0.2.0 → 0.2.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 +115 -7
- package/dist/core.d.ts +5 -1
- package/dist/core.js +72 -61
- package/dist/lifecycle.d.ts +29 -0
- package/dist/lifecycle.js +147 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,12 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
One privacy-first SDK for WeChat, Alipay, and Douyin mini programs. Import the runtime-specific entry, initialize before `App` and `Page` registration, grant analytics consent, and register the Collector domain in the platform console.
|
|
4
4
|
|
|
5
|
+
The first public npm release is [@entmesh/sdk-miniapp@0.2.0](https://www.npmjs.com/package/@entmesh/sdk-miniapp/v/0.2.0), published on 2026-10-31. Its source is tagged [v0.2.0](https://cnb.cool/mliev/entmesh/sdk-miniapp/-/tree/v0.2.0).
|
|
6
|
+
|
|
7
|
+
## Installation and platform entries
|
|
8
|
+
|
|
5
9
|
Install from the public npm registry:
|
|
6
10
|
|
|
7
11
|
```sh
|
|
8
12
|
npm install @entmesh/sdk-miniapp --registry=https://registry.npmjs.org/
|
|
9
13
|
```
|
|
10
14
|
|
|
15
|
+
To reproduce the first release, install `@entmesh/sdk-miniapp@0.2.0` explicitly.
|
|
16
|
+
Use the entry for your platform:
|
|
17
|
+
|
|
18
|
+
| Platform | Import |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| WeChat | `@entmesh/sdk-miniapp/wechat` |
|
|
21
|
+
| Alipay | `@entmesh/sdk-miniapp/alipay` |
|
|
22
|
+
| Douyin | `@entmesh/sdk-miniapp/douyin` |
|
|
23
|
+
|
|
24
|
+
Each entry exports `createClient` and the platform-independent option types.
|
|
25
|
+
Import a platform subpath; the package root is not an exported entry.
|
|
26
|
+
|
|
27
|
+
## Quick start
|
|
28
|
+
|
|
11
29
|
```ts
|
|
12
30
|
import { createClient } from '@entmesh/sdk-miniapp/wechat'
|
|
13
31
|
|
|
@@ -25,15 +43,42 @@ await entmesh.track('order_created', { order_id: 'order-1' })
|
|
|
25
43
|
|
|
26
44
|
The SDK uses platform secure-random, storage, and request APIs. It never falls back to `Math.random`, never reads host-device advertising identifiers, and never sends plaintext batches.
|
|
27
45
|
|
|
28
|
-
|
|
46
|
+
## Page context and visible time (0.2.1)
|
|
47
|
+
|
|
48
|
+
With `autocapture` enabled, the SDK tracks application startup/foreground/background
|
|
49
|
+
and each page's show/hide/unload lifecycle. Initialize before `App()` and `Page()`
|
|
50
|
+
registration. Page paths are recorded in `context.page.path`; manual `track()` and
|
|
51
|
+
`identify()` events inherit the page that was current when the call occurred.
|
|
52
|
+
`page(route, { title: 'Order details' })` also supplies `context.page.title`.
|
|
53
|
+
|
|
54
|
+
Page visibility events carry `properties.visible_ms`. Each visible interval is
|
|
55
|
+
counted once, even when page hiding, application backgrounding and unloading all
|
|
56
|
+
occur. Background time is excluded. A page exit following an already-recorded hide
|
|
57
|
+
does not repeat the duration. Startup is recorded once per SDK instance, and the
|
|
58
|
+
last visibility/background events enter the persistent queue before flushing.
|
|
59
|
+
|
|
60
|
+
If the user grants consent while a page is already visible, the SDK records that
|
|
61
|
+
page once and begins timing at consent. Activity before consent is not replayed.
|
|
62
|
+
Repeated grants and duplicate show callbacks do not create additional views.
|
|
63
|
+
Clicks, page-internal tab changes, scrolling and element impressions still require
|
|
64
|
+
manual events; this release does not automatically collect them.
|
|
65
|
+
|
|
66
|
+
Upgrade the business mini program to `@entmesh/sdk-miniapp@0.2.1` to collect visible
|
|
67
|
+
time. Earlier events retain their existing route, but no measured duration; reports
|
|
68
|
+
display **未采集** rather than estimating time from neighboring timestamps.
|
|
69
|
+
|
|
70
|
+
Examples written before the public release used `@entmesh/miniapp-sdk`. Update
|
|
71
|
+
both the dependency name and imports to `@entmesh/sdk-miniapp`; the platform
|
|
72
|
+
suffixes and client API remain the same. The public package is distributed through
|
|
73
|
+
npmjs.org, so a CNB npm registry configuration is not required.
|
|
29
74
|
|
|
30
75
|
## User properties (Proto 4.30)
|
|
31
76
|
|
|
32
77
|
```ts
|
|
33
|
-
await
|
|
34
|
-
await
|
|
35
|
-
await
|
|
36
|
-
await
|
|
78
|
+
await entmesh.identify('user-123', { plan: 'trial' })
|
|
79
|
+
await entmesh.setUserProperties({ plan: 'pro' })
|
|
80
|
+
await entmesh.unsetUserProperties(['legacy_tier'])
|
|
81
|
+
await entmesh.flush()
|
|
37
82
|
```
|
|
38
83
|
|
|
39
84
|
Enable the profile platform and organization entitlement before using these APIs. Attribute mutations use a dedicated transport and do not create behavior events or sessions. Set merges specified top-level fields; objects and arrays replace the previous whole value. Unset removes a key; explicit null remains an empty value. Existing identify properties also populate the profile; ordinary event properties do not.
|
|
@@ -56,8 +101,9 @@ npm run check:package
|
|
|
56
101
|
`package.json` is the version source. Both `npm version` and `npm run build`
|
|
57
102
|
synchronize `VERSION` and the generated runtime constant in `src/version.ts`.
|
|
58
103
|
|
|
59
|
-
|
|
60
|
-
tag runs the release pipeline, sets the package version
|
|
104
|
+
Pushes and pull requests targeting `main` or `develop` only validate the SDK.
|
|
105
|
+
Pushing a stable `vX.Y.Z` tag runs the release pipeline, sets the package version
|
|
106
|
+
before testing and building,
|
|
61
107
|
and publishes `@entmesh/sdk-miniapp` publicly to npmjs.org. The first release is
|
|
62
108
|
`v0.2.0`; preparing an already-matching package version is supported. Publishing a
|
|
63
109
|
version that already exists on npm fails instead of silently skipping it.
|
|
@@ -77,3 +123,65 @@ Only the publish step imports it. `npm run publish:npm` checks the package, uses
|
|
|
77
123
|
temporary npm configuration containing an environment-variable reference, and
|
|
78
124
|
removes that configuration on exit. The token is never written to the SDK source
|
|
79
125
|
or printed by the publishing script.
|
|
126
|
+
|
|
127
|
+
### Publish a new version
|
|
128
|
+
|
|
129
|
+
Run from a clean `main` checkout. The example uses `0.2.2`; replace it with the
|
|
130
|
+
next unused stable version.
|
|
131
|
+
|
|
132
|
+
```sh
|
|
133
|
+
release_version=0.2.2
|
|
134
|
+
npm ci
|
|
135
|
+
npm version "$release_version" --no-git-tag-version --allow-same-version
|
|
136
|
+
npm test
|
|
137
|
+
npm run lint
|
|
138
|
+
npm run build
|
|
139
|
+
npm run check:package
|
|
140
|
+
git add package.json package-lock.json VERSION src/version.ts
|
|
141
|
+
git commit -m "chore: prepare sdk-miniapp $release_version"
|
|
142
|
+
git push origin main
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
After the `main` quality pipeline succeeds, publish its version tag:
|
|
146
|
+
|
|
147
|
+
```sh
|
|
148
|
+
git tag -a "v$release_version" -m "Release @entmesh/sdk-miniapp $release_version"
|
|
149
|
+
git push origin "refs/tags/v$release_version"
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
The tag pipeline validates the tag, installs dependencies, synchronizes versions,
|
|
153
|
+
runs tests and type checking, builds, checks package contents, and publishes with
|
|
154
|
+
public access. Package checks cover the three platform entries, declarations,
|
|
155
|
+
runtime version, and the files included in the npm package.
|
|
156
|
+
|
|
157
|
+
### Verify publication and troubleshoot
|
|
158
|
+
|
|
159
|
+
Check the release pipeline and then verify the public registry:
|
|
160
|
+
|
|
161
|
+
```sh
|
|
162
|
+
npm view "@entmesh/sdk-miniapp@$release_version" version dist.integrity --registry=https://registry.npmjs.org/ --prefer-online
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
In a separate consumer project, install the published version and check its imports:
|
|
166
|
+
|
|
167
|
+
```sh
|
|
168
|
+
npm install "@entmesh/sdk-miniapp@$release_version" --registry=https://registry.npmjs.org/
|
|
169
|
+
node --input-type=module <<'JS'
|
|
170
|
+
for (const platform of ['wechat', 'alipay', 'douyin']) {
|
|
171
|
+
const sdk = await import('@entmesh/sdk-miniapp/' + platform)
|
|
172
|
+
if (typeof sdk.createClient !== 'function') throw new Error(platform)
|
|
173
|
+
console.log(platform + ': import OK')
|
|
174
|
+
}
|
|
175
|
+
JS
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
- If npm reports that the package is being processed, publication has been accepted
|
|
179
|
+
but registry queries or installs may temporarily return 404. Wait and repeat the
|
|
180
|
+
registry and consumer checks before declaring the version available.
|
|
181
|
+
- A missing `NPM_TOKEN` stops publication. Check the secret file URL, its
|
|
182
|
+
`allow_slugs`, and the token's scope permissions and expiration.
|
|
183
|
+
- An already-published version cannot be overwritten. Use a new version and tag;
|
|
184
|
+
do not move the existing release tag.
|
|
185
|
+
- HPKE tests must read the committed `test/fixtures/hpke-browser-v1.json`. Do not
|
|
186
|
+
restore a dependency on a sibling `proto` checkout. The cross-repository E2E
|
|
187
|
+
contract test checks this copy against the protocol source.
|
package/dist/core.d.ts
CHANGED
|
@@ -10,7 +10,10 @@ export declare class MiniProgramClient {
|
|
|
10
10
|
private timer?;
|
|
11
11
|
private sending?;
|
|
12
12
|
private key?;
|
|
13
|
-
private
|
|
13
|
+
private readonly ready;
|
|
14
|
+
private recording;
|
|
15
|
+
private consentGeneration;
|
|
16
|
+
private readonly lifecycle;
|
|
14
17
|
constructor(adapter: RuntimeAdapter, options: MiniProgramOptions);
|
|
15
18
|
private initialize;
|
|
16
19
|
track(eventName: string, properties?: Properties): Promise<void>;
|
|
@@ -25,6 +28,7 @@ export declare class MiniProgramClient {
|
|
|
25
28
|
refreshDeviceIdentifiers(): Promise<void>;
|
|
26
29
|
flush(): Promise<void>;
|
|
27
30
|
shutdown(): Promise<void>;
|
|
31
|
+
private record;
|
|
28
32
|
private capture;
|
|
29
33
|
private deliver;
|
|
30
34
|
private discovery;
|
package/dist/core.js
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
1
|
import { decodeBase64url, encryptedEnvelope, sealHPKE, utf8 } from './hpke.js';
|
|
2
2
|
import { SDK_VERSION } from './version.js';
|
|
3
|
+
import { LifecycleTracker } from './lifecycle.js';
|
|
3
4
|
const ANONYMOUS_KEY = 'entmesh_anonymous_id';
|
|
4
5
|
const SESSION_KEY = 'entmesh_session';
|
|
5
6
|
const QUEUE_KEY = 'entmesh_event_queue';
|
|
6
7
|
const SESSION_TIMEOUT = 30 * 60 * 1000;
|
|
7
8
|
const QUEUE_LIMIT = 500;
|
|
9
|
+
const lifecycleTrackers = new WeakMap();
|
|
8
10
|
export class MiniProgramClient {
|
|
9
11
|
constructor(adapter, options) {
|
|
10
12
|
this.adapter = adapter;
|
|
11
13
|
this.options = options;
|
|
12
14
|
this.anonymous = '';
|
|
13
15
|
this.session = { id: '', lastActivity: 0 };
|
|
14
|
-
this.
|
|
16
|
+
this.recording = Promise.resolve();
|
|
17
|
+
this.consentGeneration = 0;
|
|
15
18
|
if (!options.clientKey.startsWith('ck_'))
|
|
16
19
|
throw new Error('clientKey must be a ck_ key');
|
|
17
20
|
if (!/^https:\/\//.test(options.endpoint) && !/^http:\/\/localhost(?::\d+)?$/.test(options.endpoint))
|
|
18
21
|
throw new Error('endpoint must use HTTPS');
|
|
19
22
|
this.consentState = options.consent ?? 'pending';
|
|
20
23
|
this.queue = this.readQueue();
|
|
21
|
-
|
|
24
|
+
this.lifecycle = new LifecycleTracker(this.consentState === 'grant', (type, name, properties, page) => this.capture(type, name, properties, page), () => this.flush());
|
|
25
|
+
lifecycleTrackers.set(this, this.lifecycle);
|
|
26
|
+
this.ready = this.initialize();
|
|
27
|
+
void this.ready.catch(() => undefined);
|
|
22
28
|
}
|
|
23
29
|
async initialize() {
|
|
24
30
|
this.anonymous = String(this.adapter.getStorage(ANONYMOUS_KEY) || '') || await this.id();
|
|
@@ -30,7 +36,10 @@ export class MiniProgramClient {
|
|
|
30
36
|
this.timer = setInterval(() => { void this.flush(); }, this.options.flushIntervalMs ?? 5000);
|
|
31
37
|
}
|
|
32
38
|
async track(eventName, properties = {}) { await this.capture('track', eventName, properties); }
|
|
33
|
-
async page(route, properties = {}) {
|
|
39
|
+
async page(route, properties = {}) {
|
|
40
|
+
const page = this.lifecycle.setPage(route, properties);
|
|
41
|
+
await this.capture('page', '$page_view', { ...properties, route }, page);
|
|
42
|
+
}
|
|
34
43
|
async screen(route, properties = {}) { await this.page(route, properties); }
|
|
35
44
|
async identify(userID, properties = {}) {
|
|
36
45
|
const value = userID.trim();
|
|
@@ -45,24 +54,40 @@ export class MiniProgramClient {
|
|
|
45
54
|
if (this.consentState !== 'grant')
|
|
46
55
|
return;
|
|
47
56
|
const user = this.user;
|
|
57
|
+
const generation = this.consentGeneration;
|
|
48
58
|
if (!user)
|
|
49
59
|
throw new Error('identify a user before updating properties');
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
this.
|
|
60
|
+
const occurredAt = new Date().toISOString();
|
|
61
|
+
const values = properties ? JSON.parse(JSON.stringify(properties)) : undefined;
|
|
62
|
+
const keys = unset ? [...unset] : undefined;
|
|
63
|
+
await this.record(async () => {
|
|
64
|
+
if (generation !== this.consentGeneration || this.consentState !== 'grant')
|
|
65
|
+
return;
|
|
66
|
+
if (this.queue.length >= QUEUE_LIMIT)
|
|
67
|
+
throw new Error('profile queue is full');
|
|
68
|
+
const mutationID = await this.id();
|
|
69
|
+
if (generation !== this.consentGeneration || this.consentState !== 'grant')
|
|
70
|
+
return;
|
|
71
|
+
this.queue.push({ mutation_id: mutationID, occurred_at: occurredAt, subject: { kind: 'user', id: user }, operation, ...(values ? { properties: values } : {}), ...(keys ? { unset: keys } : {}) });
|
|
72
|
+
this.persistQueue();
|
|
73
|
+
});
|
|
54
74
|
if (this.queue.length >= (this.options.batchSize ?? 20))
|
|
55
75
|
await this.flush();
|
|
56
76
|
}
|
|
57
77
|
async reset() {
|
|
58
78
|
this.user = undefined;
|
|
79
|
+
await this.ready;
|
|
80
|
+
await this.recording;
|
|
59
81
|
this.anonymous = await this.id();
|
|
60
82
|
this.session = { id: await this.id(), lastActivity: Date.now() };
|
|
61
83
|
this.adapter.setStorage(ANONYMOUS_KEY, this.anonymous);
|
|
62
84
|
this.adapter.setStorage(SESSION_KEY, this.session);
|
|
63
85
|
}
|
|
64
86
|
async consent(state) {
|
|
87
|
+
if (this.consentState !== state)
|
|
88
|
+
this.consentGeneration++;
|
|
65
89
|
this.consentState = state;
|
|
90
|
+
const lifecycle = this.lifecycle.setConsent(state === 'grant');
|
|
66
91
|
if (state === 'deny' || state === 'revoke') {
|
|
67
92
|
this.queue = [];
|
|
68
93
|
this.persistQueue();
|
|
@@ -74,19 +99,18 @@ export class MiniProgramClient {
|
|
|
74
99
|
return;
|
|
75
100
|
}
|
|
76
101
|
if (state === 'grant') {
|
|
77
|
-
|
|
78
|
-
this.appStartSent = true;
|
|
79
|
-
await this.capture('track', '$app_start', {});
|
|
80
|
-
}
|
|
102
|
+
await lifecycle;
|
|
81
103
|
await this.flush();
|
|
82
104
|
}
|
|
83
105
|
}
|
|
84
106
|
async refreshDeviceIdentifiers() { }
|
|
85
107
|
async flush() {
|
|
108
|
+
await this.ready;
|
|
109
|
+
await this.recording;
|
|
110
|
+
while (this.sending)
|
|
111
|
+
await this.sending;
|
|
86
112
|
if (this.consentState !== 'grant' || this.queue.length === 0)
|
|
87
113
|
return;
|
|
88
|
-
if (this.sending)
|
|
89
|
-
return this.sending;
|
|
90
114
|
this.sending = (async () => { let remaining = this.queue.length; while (remaining > 0 && this.consentState === 'grant') {
|
|
91
115
|
const before = this.queue.length;
|
|
92
116
|
await this.deliver();
|
|
@@ -97,29 +121,45 @@ export class MiniProgramClient {
|
|
|
97
121
|
} })().finally(() => { this.sending = undefined; });
|
|
98
122
|
return this.sending;
|
|
99
123
|
}
|
|
100
|
-
async shutdown() { if (this.timer)
|
|
124
|
+
async shutdown() { await this.ready; if (this.timer)
|
|
101
125
|
clearInterval(this.timer); await this.flush(); }
|
|
102
|
-
|
|
126
|
+
record(action) {
|
|
127
|
+
const pending = this.recording.then(() => this.ready).then(action);
|
|
128
|
+
this.recording = pending.catch(() => undefined);
|
|
129
|
+
return pending;
|
|
130
|
+
}
|
|
131
|
+
async capture(type, eventName, properties, page = this.lifecycle.pageContext()) {
|
|
103
132
|
if (this.consentState !== 'grant')
|
|
104
133
|
return;
|
|
105
134
|
if (!/^[$A-Za-z][A-Za-z0-9_.-]{0,127}$/.test(eventName))
|
|
106
135
|
throw new Error('event name is invalid');
|
|
107
136
|
const now = Date.now();
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
137
|
+
const generation = this.consentGeneration;
|
|
138
|
+
const user = this.user;
|
|
139
|
+
const snapshot = { ...page };
|
|
140
|
+
const endsVisibleInterval = eventName === '$visibility' || eventName === '$page_exit';
|
|
141
|
+
const values = JSON.parse(JSON.stringify(properties));
|
|
142
|
+
await this.record(async () => {
|
|
143
|
+
if (generation !== this.consentGeneration || this.consentState !== 'grant')
|
|
144
|
+
return;
|
|
145
|
+
if (!this.session.id || (!endsVisibleInterval && now - this.session.lastActivity >= SESSION_TIMEOUT))
|
|
146
|
+
this.session = { id: await this.id(), lastActivity: now };
|
|
147
|
+
this.session.lastActivity = now;
|
|
148
|
+
this.adapter.setStorage(SESSION_KEY, this.session);
|
|
149
|
+
const event = {
|
|
150
|
+
message_id: await this.id(), type, event_name: eventName, occurred_at: new Date(now).toISOString(),
|
|
151
|
+
anonymous_id: this.anonymous, session_id: this.session.id, properties: values,
|
|
152
|
+
context: { sdk: { name: 'entmesh-miniapp', version: SDK_VERSION }, device: this.adapter.systemContext(), ...(snapshot.path ? { page: snapshot } : {}) },
|
|
153
|
+
};
|
|
154
|
+
if (user)
|
|
155
|
+
event.user_id = user;
|
|
156
|
+
if (generation !== this.consentGeneration || this.consentState !== 'grant')
|
|
157
|
+
return;
|
|
158
|
+
if (this.queue.length >= QUEUE_LIMIT)
|
|
159
|
+
this.queue.shift();
|
|
160
|
+
this.queue.push(event);
|
|
161
|
+
this.persistQueue();
|
|
162
|
+
});
|
|
123
163
|
if (this.queue.length >= (this.options.batchSize ?? 20))
|
|
124
164
|
await this.flush();
|
|
125
165
|
}
|
|
@@ -143,7 +183,7 @@ export class MiniProgramClient {
|
|
|
143
183
|
}
|
|
144
184
|
if (response.statusCode < 200 || response.statusCode >= 300)
|
|
145
185
|
return;
|
|
146
|
-
this.queue.
|
|
186
|
+
this.queue = this.queue.filter(event => !events.includes(event));
|
|
147
187
|
this.persistQueue();
|
|
148
188
|
}
|
|
149
189
|
catch { /* Retain the persistent queue for the next bounded flush attempt. */ }
|
|
@@ -165,34 +205,5 @@ export class MiniProgramClient {
|
|
|
165
205
|
async id() { return Array.from(await this.adapter.randomBytes(16), (value) => value.toString(16).padStart(2, '0')).join(''); }
|
|
166
206
|
}
|
|
167
207
|
export function installLifecycle(client) {
|
|
168
|
-
|
|
169
|
-
const wrap = (name) => {
|
|
170
|
-
const original = root[name];
|
|
171
|
-
if (typeof original !== 'function' || original.__entmesh)
|
|
172
|
-
return;
|
|
173
|
-
const wrapped = (options) => {
|
|
174
|
-
const next = { ...options };
|
|
175
|
-
const handlers = name === 'App'
|
|
176
|
-
? { onLaunch: '$app_start', onShow: '$app_foreground', onHide: '$app_background' }
|
|
177
|
-
: { onShow: '$page_view', onHide: '$visibility', onUnload: '$page_exit' };
|
|
178
|
-
for (const [handler, eventName] of Object.entries(handlers)) {
|
|
179
|
-
const existing = next[handler];
|
|
180
|
-
next[handler] = function (...args) {
|
|
181
|
-
const route = String(this.route || this.__route__ || '');
|
|
182
|
-
if (eventName === '$page_view')
|
|
183
|
-
void client.page(route);
|
|
184
|
-
else
|
|
185
|
-
void client.track(eventName, route ? { route } : {});
|
|
186
|
-
if (eventName === '$app_background')
|
|
187
|
-
void client.flush();
|
|
188
|
-
return typeof existing === 'function' ? existing.apply(this, args) : undefined;
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
return original(next);
|
|
192
|
-
};
|
|
193
|
-
wrapped.__entmesh = true;
|
|
194
|
-
root[name] = wrapped;
|
|
195
|
-
};
|
|
196
|
-
wrap('App');
|
|
197
|
-
wrap('Page');
|
|
208
|
+
lifecycleTrackers.get(client)?.install();
|
|
198
209
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Properties } from './types.js';
|
|
2
|
+
export interface PageContext {
|
|
3
|
+
path: string;
|
|
4
|
+
title?: string;
|
|
5
|
+
}
|
|
6
|
+
type Emit = (type: 'page' | 'track', name: string, properties: Properties, page: PageContext) => Promise<void>;
|
|
7
|
+
/** Owns visible intervals; App and Page callbacks can close the same interval safely. */
|
|
8
|
+
export declare class LifecycleTracker {
|
|
9
|
+
private granted;
|
|
10
|
+
private readonly emit;
|
|
11
|
+
private readonly flush;
|
|
12
|
+
private readonly pages;
|
|
13
|
+
private active?;
|
|
14
|
+
private context;
|
|
15
|
+
private foreground?;
|
|
16
|
+
private started;
|
|
17
|
+
constructor(granted: boolean, emit: Emit, flush: () => Promise<void>);
|
|
18
|
+
pageContext(): PageContext;
|
|
19
|
+
setPage(path: string, properties?: Properties): PageContext;
|
|
20
|
+
setConsent(granted: boolean): Promise<void>;
|
|
21
|
+
private start;
|
|
22
|
+
private begin;
|
|
23
|
+
private finish;
|
|
24
|
+
private app;
|
|
25
|
+
private page;
|
|
26
|
+
private route;
|
|
27
|
+
install(): void;
|
|
28
|
+
}
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/** Owns visible intervals; App and Page callbacks can close the same interval safely. */
|
|
2
|
+
export class LifecycleTracker {
|
|
3
|
+
constructor(granted, emit, flush) {
|
|
4
|
+
this.granted = granted;
|
|
5
|
+
this.emit = emit;
|
|
6
|
+
this.flush = flush;
|
|
7
|
+
this.pages = new WeakMap();
|
|
8
|
+
this.context = { path: '' };
|
|
9
|
+
this.started = false;
|
|
10
|
+
}
|
|
11
|
+
pageContext() { return { ...this.context }; }
|
|
12
|
+
setPage(path, properties = {}) {
|
|
13
|
+
const title = typeof properties.title === 'string' ? properties.title : undefined;
|
|
14
|
+
this.context = { path, ...(title ? { title } : {}) };
|
|
15
|
+
if (this.active?.page.path === path)
|
|
16
|
+
this.active.page = this.pageContext();
|
|
17
|
+
return this.pageContext();
|
|
18
|
+
}
|
|
19
|
+
setConsent(granted) {
|
|
20
|
+
const changed = this.granted !== granted;
|
|
21
|
+
this.granted = granted;
|
|
22
|
+
if (!granted) {
|
|
23
|
+
if (this.active)
|
|
24
|
+
this.active.since = undefined;
|
|
25
|
+
return Promise.resolve();
|
|
26
|
+
}
|
|
27
|
+
const start = this.start();
|
|
28
|
+
const page = changed && this.active ? this.begin(this.active) : Promise.resolve();
|
|
29
|
+
return Promise.all([start, page]).then(() => undefined);
|
|
30
|
+
}
|
|
31
|
+
start() {
|
|
32
|
+
if (!this.granted || this.started)
|
|
33
|
+
return Promise.resolve();
|
|
34
|
+
this.started = true;
|
|
35
|
+
return this.emit('track', '$app_start', {}, this.pageContext());
|
|
36
|
+
}
|
|
37
|
+
begin(state) {
|
|
38
|
+
if (!this.granted || this.foreground === false || !state.shown || state.since !== undefined)
|
|
39
|
+
return Promise.resolve();
|
|
40
|
+
state.since = Date.now();
|
|
41
|
+
return this.emit('page', '$page_view', { route: state.page.path }, { ...state.page });
|
|
42
|
+
}
|
|
43
|
+
finish(state, name) {
|
|
44
|
+
if (!state)
|
|
45
|
+
return Promise.resolve();
|
|
46
|
+
const since = state.since;
|
|
47
|
+
state.since = undefined;
|
|
48
|
+
if (!this.granted || (since === undefined && name === '$visibility'))
|
|
49
|
+
return Promise.resolve();
|
|
50
|
+
const properties = { route: state.page.path };
|
|
51
|
+
if (since !== undefined)
|
|
52
|
+
properties.visible_ms = Math.max(0, Date.now() - since);
|
|
53
|
+
return this.emit('track', name, properties, { ...state.page });
|
|
54
|
+
}
|
|
55
|
+
app(handler, options) {
|
|
56
|
+
const path = options && typeof options === 'object' && 'path' in options && typeof options.path === 'string' ? options.path : '';
|
|
57
|
+
if (path && !this.active)
|
|
58
|
+
this.setPage(path);
|
|
59
|
+
if (handler === 'onLaunch')
|
|
60
|
+
return this.start();
|
|
61
|
+
if (handler === 'onShow') {
|
|
62
|
+
if (this.foreground === true)
|
|
63
|
+
return Promise.resolve();
|
|
64
|
+
this.foreground = true;
|
|
65
|
+
const foreground = this.emit('track', '$app_foreground', {}, this.pageContext());
|
|
66
|
+
const page = this.active && (!path || path === this.active.page.path) ? this.begin(this.active) : Promise.resolve();
|
|
67
|
+
return Promise.all([foreground, page]).then(() => undefined);
|
|
68
|
+
}
|
|
69
|
+
if (this.foreground === false)
|
|
70
|
+
return Promise.resolve();
|
|
71
|
+
this.foreground = false;
|
|
72
|
+
const visible = this.finish(this.active, '$visibility');
|
|
73
|
+
const background = this.emit('track', '$app_background', {}, this.pageContext());
|
|
74
|
+
return Promise.all([visible, background]).then(() => this.flush());
|
|
75
|
+
}
|
|
76
|
+
page(handler, owner) {
|
|
77
|
+
let state = this.pages.get(owner);
|
|
78
|
+
if (handler === 'onShow') {
|
|
79
|
+
const path = this.route(owner);
|
|
80
|
+
if (!state) {
|
|
81
|
+
state = { page: { path }, shown: false };
|
|
82
|
+
this.pages.set(owner, state);
|
|
83
|
+
}
|
|
84
|
+
const previous = this.active !== state ? this.finish(this.active, '$visibility') : Promise.resolve();
|
|
85
|
+
if (this.active && this.active !== state)
|
|
86
|
+
this.active.shown = false;
|
|
87
|
+
state.page = { ...state.page, path };
|
|
88
|
+
state.shown = true;
|
|
89
|
+
this.active = state;
|
|
90
|
+
this.context = { ...state.page };
|
|
91
|
+
return Promise.all([previous, this.begin(state)]).then(() => undefined);
|
|
92
|
+
}
|
|
93
|
+
if (!state)
|
|
94
|
+
return Promise.resolve();
|
|
95
|
+
state.shown = false;
|
|
96
|
+
const result = this.finish(state, handler === 'onUnload' ? '$page_exit' : '$visibility');
|
|
97
|
+
if (handler === 'onUnload') {
|
|
98
|
+
this.pages.delete(owner);
|
|
99
|
+
if (this.active === state)
|
|
100
|
+
this.active = undefined;
|
|
101
|
+
}
|
|
102
|
+
return result;
|
|
103
|
+
}
|
|
104
|
+
route(owner) {
|
|
105
|
+
const route = owner.route || owner.__route__;
|
|
106
|
+
if (typeof route === 'string')
|
|
107
|
+
return route;
|
|
108
|
+
const getPages = globalThis.getCurrentPages;
|
|
109
|
+
if (typeof getPages === 'function') {
|
|
110
|
+
try {
|
|
111
|
+
const pages = getPages();
|
|
112
|
+
const current = pages[pages.length - 1];
|
|
113
|
+
return String(current?.route || current?.__route__ || '');
|
|
114
|
+
}
|
|
115
|
+
catch { /* Some runtimes expose no page stack during registration. */ }
|
|
116
|
+
}
|
|
117
|
+
return '';
|
|
118
|
+
}
|
|
119
|
+
install() {
|
|
120
|
+
const root = globalThis;
|
|
121
|
+
for (const name of ['App', 'Page']) {
|
|
122
|
+
const original = root[name];
|
|
123
|
+
if (typeof original !== 'function' || original.__entmesh)
|
|
124
|
+
continue;
|
|
125
|
+
const tracker = this;
|
|
126
|
+
const wrapped = (options) => {
|
|
127
|
+
const next = { ...options };
|
|
128
|
+
const handlers = name === 'App' ? ['onLaunch', 'onShow', 'onHide'] : ['onShow', 'onHide', 'onUnload'];
|
|
129
|
+
for (const handler of handlers) {
|
|
130
|
+
const existing = next[handler];
|
|
131
|
+
next[handler] = function (...args) {
|
|
132
|
+
// Analytics errors must not replace a business callback's return value or exception.
|
|
133
|
+
try {
|
|
134
|
+
const pending = name === 'App' ? tracker.app(handler, args[0]) : tracker.page(handler, this);
|
|
135
|
+
void pending.catch(() => undefined);
|
|
136
|
+
}
|
|
137
|
+
catch { /* The application's lifecycle still runs if collection fails. */ }
|
|
138
|
+
return typeof existing === 'function' ? existing.apply(this, args) : undefined;
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
return original(next);
|
|
142
|
+
};
|
|
143
|
+
wrapped.__entmesh = true;
|
|
144
|
+
root[name] = wrapped;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const SDK_VERSION = "0.2.
|
|
1
|
+
export declare const SDK_VERSION = "0.2.1";
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// Generated from package.json by scripts/sync-version.mjs.
|
|
2
|
-
export const SDK_VERSION = '0.2.
|
|
2
|
+
export const SDK_VERSION = '0.2.1';
|