@metrictrail/react-native 0.1.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 +3 -0
- package/dist/index.d.ts +37 -0
- package/dist/index.js +72 -0
- package/package.json +10 -0
package/README.md
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# MetricTrail React Native SDK
|
|
2
|
+
|
|
3
|
+
React Native with an ES2022 runtime. Inject the app's encrypted/AsyncStorage-compatible adapter through `storage`; MetricTrail stores only events, never the write key. Call `onAppBackground()` from `AppState` and `shutdown()` during controlled teardown. Platform must be `ios` or `android`. The package requests no location permission.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export interface AsyncStore {
|
|
2
|
+
getItem(key: string): Promise<string | null>;
|
|
3
|
+
setItem(key: string, value: string): Promise<void>;
|
|
4
|
+
}
|
|
5
|
+
export interface Options {
|
|
6
|
+
endpoint: string;
|
|
7
|
+
projectId: string;
|
|
8
|
+
writeKey: string;
|
|
9
|
+
platform: 'ios' | 'android';
|
|
10
|
+
storage?: AsyncStore;
|
|
11
|
+
storageKey?: string;
|
|
12
|
+
anonymousId?: string;
|
|
13
|
+
version?: string;
|
|
14
|
+
maxQueue?: number;
|
|
15
|
+
maxRetries?: number;
|
|
16
|
+
idFactory?: () => string;
|
|
17
|
+
}
|
|
18
|
+
export declare class MetricTrail {
|
|
19
|
+
private options;
|
|
20
|
+
private queue;
|
|
21
|
+
private anonymousId;
|
|
22
|
+
private userId;
|
|
23
|
+
private flushing?;
|
|
24
|
+
private key;
|
|
25
|
+
private id;
|
|
26
|
+
private constructor();
|
|
27
|
+
static create(options: Options): Promise<MetricTrail>;
|
|
28
|
+
identify(userId: string): void;
|
|
29
|
+
reset(): void;
|
|
30
|
+
track(name: string, properties?: Record<string, unknown>): Promise<string>;
|
|
31
|
+
flush(signal?: AbortSignal): Promise<void>;
|
|
32
|
+
onAppBackground(signal?: AbortSignal): Promise<void>;
|
|
33
|
+
shutdown(signal?: AbortSignal): Promise<void>;
|
|
34
|
+
private doFlush;
|
|
35
|
+
private persist;
|
|
36
|
+
get pending(): number;
|
|
37
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const fallbackId = () => globalThis.crypto?.randomUUID?.() || 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, c => { const r = Math.random() * 16 | 0; return (c === 'x' ? r : r & 3 | 8).toString(16); });
|
|
2
|
+
export class MetricTrail {
|
|
3
|
+
options;
|
|
4
|
+
queue = [];
|
|
5
|
+
anonymousId;
|
|
6
|
+
userId = '';
|
|
7
|
+
flushing;
|
|
8
|
+
key;
|
|
9
|
+
id;
|
|
10
|
+
constructor(options) {
|
|
11
|
+
this.options = options;
|
|
12
|
+
if (!/^https?:\/\//.test(options.endpoint) || !options.projectId || !options.writeKey)
|
|
13
|
+
throw Error('Endpoint, project and write key required');
|
|
14
|
+
this.id = options.idFactory || fallbackId;
|
|
15
|
+
this.anonymousId = options.anonymousId || this.id();
|
|
16
|
+
this.key = options.storageKey || `metrictrail:${options.endpoint}:${options.projectId}:queue`;
|
|
17
|
+
}
|
|
18
|
+
static async create(options) { const c = new MetricTrail(options); if (options.storage) {
|
|
19
|
+
try {
|
|
20
|
+
const raw = await options.storage.getItem(c.key);
|
|
21
|
+
const value = raw ? JSON.parse(raw) : [];
|
|
22
|
+
if (Array.isArray(value))
|
|
23
|
+
c.queue = value.slice(0, options.maxQueue ?? 1000);
|
|
24
|
+
}
|
|
25
|
+
catch { /* optional storage falls back to memory */ }
|
|
26
|
+
} return c; }
|
|
27
|
+
identify(userId) { if (!userId || userId.length > 128)
|
|
28
|
+
throw Error('Invalid user ID'); if (this.userId && this.userId !== userId)
|
|
29
|
+
this.anonymousId = this.id(); this.userId = userId; }
|
|
30
|
+
reset() { this.userId = ''; this.anonymousId = this.id(); }
|
|
31
|
+
async track(name, properties = {}) { if (!/^[A-Za-z][A-Za-z0-9_.-]{0,79}$/.test(name))
|
|
32
|
+
throw Error('Invalid event name'); const encoded = JSON.stringify(properties); if (Object.keys(properties).length > 40 || encoded.length > 8192)
|
|
33
|
+
throw Error('Invalid event properties'); if (this.queue.length >= (this.options.maxQueue ?? 1000))
|
|
34
|
+
throw Error('Event queue full'); const eventId = this.id(); this.queue.push({ schema_version: 1, event_id: eventId, name, anonymous_id: this.anonymousId, user_id: this.userId, occurred_at: new Date().toISOString(), platform: this.options.platform, version: this.options.version || '', properties: JSON.parse(encoded) }); await this.persist(); return eventId; }
|
|
35
|
+
flush(signal) { if (!this.flushing)
|
|
36
|
+
this.flushing = this.doFlush(signal).finally(() => this.flushing = undefined); return this.flushing; }
|
|
37
|
+
async onAppBackground(signal) { return this.flush(signal); }
|
|
38
|
+
async shutdown(signal) { return this.flush(signal); }
|
|
39
|
+
async doFlush(signal) { while (this.queue.length) {
|
|
40
|
+
const batch = this.queue.slice(0, 100), body = JSON.stringify({ events: batch });
|
|
41
|
+
let failure;
|
|
42
|
+
const retries = Math.min(5, Math.max(0, this.options.maxRetries ?? 2));
|
|
43
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
44
|
+
try {
|
|
45
|
+
const response = await fetch(`${this.options.endpoint.replace(/\/$/, '')}/v1/projects/${encodeURIComponent(this.options.projectId)}/events`, { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${this.options.writeKey}` }, body, signal });
|
|
46
|
+
if (response.ok) {
|
|
47
|
+
failure = undefined;
|
|
48
|
+
break;
|
|
49
|
+
}
|
|
50
|
+
failure = Error(`Collection failed (${response.status})`);
|
|
51
|
+
if (response.status < 500 && response.status !== 429)
|
|
52
|
+
throw failure;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
failure = error;
|
|
56
|
+
if (signal?.aborted)
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
if (attempt < retries)
|
|
60
|
+
await new Promise(resolve => setTimeout(resolve, 250 * 2 ** attempt));
|
|
61
|
+
}
|
|
62
|
+
if (failure)
|
|
63
|
+
throw failure;
|
|
64
|
+
this.queue.splice(0, batch.length);
|
|
65
|
+
await this.persist();
|
|
66
|
+
} }
|
|
67
|
+
async persist() { try {
|
|
68
|
+
await this.options.storage?.setItem(this.key, JSON.stringify(this.queue));
|
|
69
|
+
}
|
|
70
|
+
catch { /* queue remains in memory */ } }
|
|
71
|
+
get pending() { return this.queue.length; }
|
|
72
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@metrictrail/react-native",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"files": ["dist", "README.md"],
|
|
8
|
+
"scripts": {"build": "tsc -p tsconfig.json", "test": "node --test tests/*.test.mjs"},
|
|
9
|
+
"devDependencies": {"typescript": "^5.7.0"}
|
|
10
|
+
}
|