@lapwy/react-native 0.0.0-stage → 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 +19 -2
- package/dist/index.cjs +254 -0
- package/dist/index.d.cts +60 -0
- package/dist/index.d.ts +60 -0
- package/dist/index.js +222 -0
- package/package.json +34 -3
package/README.md
CHANGED
|
@@ -1,3 +1,20 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @lapwy/react-native
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
lapwy for React Native and Expo.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install @lapwy/react-native @react-native-async-storage/async-storage
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
```js
|
|
10
|
+
import AsyncStorage from "@react-native-async-storage/async-storage";
|
|
11
|
+
import { init, screen, track } from "@lapwy/react-native";
|
|
12
|
+
|
|
13
|
+
init({ site: "lw_…", app: "fernshop", storage: AsyncStorage }); // once, at start-up
|
|
14
|
+
screen("Home"); // on every screen
|
|
15
|
+
track("add_to_cart", { product: "monstera" });
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Screens can be followed for you: `navigationTracker` for React Navigation, `useScreen` for Expo Router or any screen component.
|
|
19
|
+
|
|
20
|
+
A user is told apart by one random id per install, kept in the storage you pass. Events wait in a queue that survives being offline and the app being closed. Time on a screen counts only while the app is in the foreground.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var src_exports = {};
|
|
22
|
+
__export(src_exports, {
|
|
23
|
+
flush: () => flush,
|
|
24
|
+
init: () => init,
|
|
25
|
+
navigationTracker: () => navigationTracker,
|
|
26
|
+
optOut: () => optOut,
|
|
27
|
+
reset: () => reset,
|
|
28
|
+
screen: () => screen,
|
|
29
|
+
track: () => track,
|
|
30
|
+
useScreen: () => useScreen
|
|
31
|
+
});
|
|
32
|
+
module.exports = __toCommonJS(src_exports);
|
|
33
|
+
var import_react = require("react");
|
|
34
|
+
var import_react_native = require("react-native");
|
|
35
|
+
var DEFAULT_ENDPOINT = "https://in.lapwy.com/e";
|
|
36
|
+
var ID_KEY = "lapwy_device_id";
|
|
37
|
+
var QUEUE_KEY = "lapwy_queue";
|
|
38
|
+
var MAX_QUEUE = 500;
|
|
39
|
+
var state = null;
|
|
40
|
+
var early = [];
|
|
41
|
+
function hex(bytes) {
|
|
42
|
+
const a = new Uint8Array(bytes);
|
|
43
|
+
const c = globalThis.crypto;
|
|
44
|
+
if (c && c.getRandomValues) c.getRandomValues(a);
|
|
45
|
+
else for (let i = 0; i < bytes; i++) a[i] = Math.floor(Math.random() * 256);
|
|
46
|
+
let s = "";
|
|
47
|
+
for (let i = 0; i < bytes; i++) s += (a[i] + 256).toString(16).slice(1);
|
|
48
|
+
return s;
|
|
49
|
+
}
|
|
50
|
+
function uuid() {
|
|
51
|
+
const h = hex(16);
|
|
52
|
+
return h.slice(0, 8) + "-" + h.slice(8, 12) + "-4" + h.slice(13, 16) + "-a" + h.slice(17, 20) + "-" + h.slice(20);
|
|
53
|
+
}
|
|
54
|
+
function screenUrl(app, name) {
|
|
55
|
+
const path = name.replace(/^\/+/, "").split("/").map(encodeURIComponent).join("/");
|
|
56
|
+
return `app://${app}/${path}`;
|
|
57
|
+
}
|
|
58
|
+
function save(s) {
|
|
59
|
+
var _a;
|
|
60
|
+
(_a = s.options.storage) == null ? void 0 : _a.setItem(QUEUE_KEY, JSON.stringify(s.queue)).catch(() => void 0);
|
|
61
|
+
}
|
|
62
|
+
function add(s, kind, extra) {
|
|
63
|
+
if (s.out) return;
|
|
64
|
+
const e = { id: uuid(), t: Date.now(), k: kind, u: s.screenUrl, p: s.screenId };
|
|
65
|
+
for (const k in extra) if (extra[k] !== void 0) e[k] = extra[k];
|
|
66
|
+
s.queue.push(e);
|
|
67
|
+
if (s.queue.length > MAX_QUEUE) s.queue.splice(0, s.queue.length - MAX_QUEUE);
|
|
68
|
+
save(s);
|
|
69
|
+
if (!s.timer) s.timer = setTimeout(() => void flush(), 2e3);
|
|
70
|
+
}
|
|
71
|
+
function osVersion() {
|
|
72
|
+
var _a;
|
|
73
|
+
if (import_react_native.Platform.OS !== "android") return String(import_react_native.Platform.Version);
|
|
74
|
+
const release = (_a = import_react_native.Platform.constants) == null ? void 0 : _a.Release;
|
|
75
|
+
return release == null ? "" : String(release).split(".").slice(0, 2).join(".");
|
|
76
|
+
}
|
|
77
|
+
function client(s) {
|
|
78
|
+
const { width, height } = import_react_native.Dimensions.get("window");
|
|
79
|
+
let language = "";
|
|
80
|
+
try {
|
|
81
|
+
language = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
sw: Math.round(width),
|
|
86
|
+
sh: Math.round(height),
|
|
87
|
+
l: language,
|
|
88
|
+
rel: s.options.version || "",
|
|
89
|
+
pf: import_react_native.Platform.OS,
|
|
90
|
+
osv: osVersion(),
|
|
91
|
+
// The usual cut between a phone and a tablet: 600 points on the short side.
|
|
92
|
+
dt: import_react_native.Platform.isPad || Math.min(width, height) >= 600 ? "tablet" : "mobile"
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
async function flush() {
|
|
96
|
+
const s = state;
|
|
97
|
+
if (!s) return;
|
|
98
|
+
if (s.timer) clearTimeout(s.timer);
|
|
99
|
+
s.timer = 0;
|
|
100
|
+
await s.ready;
|
|
101
|
+
if (s.sending || !s.queue.length) return;
|
|
102
|
+
s.sending = true;
|
|
103
|
+
const events = s.queue.slice(0, 50);
|
|
104
|
+
try {
|
|
105
|
+
const res = await fetch(s.endpoint, {
|
|
106
|
+
method: "POST",
|
|
107
|
+
headers: { "Content-Type": "text/plain" },
|
|
108
|
+
body: JSON.stringify({ k: s.options.site, v: s.id || void 0, sdk: "react-native-0.1", now: Date.now(), c: client(s), e: events })
|
|
109
|
+
});
|
|
110
|
+
if (res.status === 429 || res.status >= 500) throw new Error("retry");
|
|
111
|
+
s.queue = s.queue.filter((e) => !events.includes(e));
|
|
112
|
+
s.backoff = 0;
|
|
113
|
+
save(s);
|
|
114
|
+
s.sending = false;
|
|
115
|
+
if (s.queue.length) s.timer = setTimeout(() => void flush(), 200);
|
|
116
|
+
} catch {
|
|
117
|
+
s.sending = false;
|
|
118
|
+
s.timer = setTimeout(() => void flush(), Math.min(5e3 * Math.pow(2, s.backoff++), 3e5));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function settle(s) {
|
|
122
|
+
if (s.shownAt) {
|
|
123
|
+
s.engaged += Date.now() - s.shownAt;
|
|
124
|
+
s.shownAt = 0;
|
|
125
|
+
}
|
|
126
|
+
if (s.screenId && s.engaged >= 1e3) add(s, "engagement", { v: s.engaged });
|
|
127
|
+
s.engaged = 0;
|
|
128
|
+
}
|
|
129
|
+
function later(run) {
|
|
130
|
+
if (state) run();
|
|
131
|
+
else if (early.length < 100) early.push(run);
|
|
132
|
+
}
|
|
133
|
+
function init(options) {
|
|
134
|
+
if (state || !options.site) return;
|
|
135
|
+
const s = {
|
|
136
|
+
options,
|
|
137
|
+
endpoint: options.endpoint || DEFAULT_ENDPOINT,
|
|
138
|
+
app: (options.app || "app").toLowerCase().replace(/[^a-z0-9.-]+/g, "-") || "app",
|
|
139
|
+
id: "",
|
|
140
|
+
ready: Promise.resolve(),
|
|
141
|
+
queue: [],
|
|
142
|
+
screenUrl: "",
|
|
143
|
+
screenId: "",
|
|
144
|
+
shownAt: 0,
|
|
145
|
+
engaged: 0,
|
|
146
|
+
timer: 0,
|
|
147
|
+
backoff: 0,
|
|
148
|
+
sending: false,
|
|
149
|
+
out: false
|
|
150
|
+
};
|
|
151
|
+
s.screenUrl = screenUrl(s.app, "");
|
|
152
|
+
state = s;
|
|
153
|
+
const storage = options.storage;
|
|
154
|
+
s.ready = (async () => {
|
|
155
|
+
if (!storage) return;
|
|
156
|
+
try {
|
|
157
|
+
const [id, queued] = await Promise.all([storage.getItem(ID_KEY), storage.getItem(QUEUE_KEY)]);
|
|
158
|
+
s.id = id && /^[0-9a-f]{16,64}$/.test(id) ? id : hex(16);
|
|
159
|
+
if (s.id !== id) await storage.setItem(ID_KEY, s.id);
|
|
160
|
+
const old = queued ? JSON.parse(queued) : [];
|
|
161
|
+
if (Array.isArray(old) && old.length) s.queue = old.concat(s.queue).slice(-MAX_QUEUE);
|
|
162
|
+
} catch {
|
|
163
|
+
}
|
|
164
|
+
})();
|
|
165
|
+
import_react_native.AppState.addEventListener("change", (next) => {
|
|
166
|
+
if (next === "active") {
|
|
167
|
+
if (!s.shownAt) s.shownAt = Date.now();
|
|
168
|
+
void flush();
|
|
169
|
+
} else {
|
|
170
|
+
settle(s);
|
|
171
|
+
void flush();
|
|
172
|
+
}
|
|
173
|
+
});
|
|
174
|
+
if (options.errors !== false) {
|
|
175
|
+
const utils = globalThis.ErrorUtils;
|
|
176
|
+
if (utils) {
|
|
177
|
+
const before = utils.getGlobalHandler();
|
|
178
|
+
let reported = 0;
|
|
179
|
+
utils.setGlobalHandler((error, fatal) => {
|
|
180
|
+
try {
|
|
181
|
+
const e = error;
|
|
182
|
+
const msg = String(e && e.message || error || "").slice(0, 300);
|
|
183
|
+
if (msg && reported++ < 10) {
|
|
184
|
+
add(s, "error", { n: String(e && e.name || "Error").slice(0, 60), props: { msg, fatal: Boolean(fatal) } });
|
|
185
|
+
void flush();
|
|
186
|
+
}
|
|
187
|
+
} catch {
|
|
188
|
+
}
|
|
189
|
+
before(error, fatal);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const waiting = early;
|
|
194
|
+
early = [];
|
|
195
|
+
waiting.forEach((run) => run());
|
|
196
|
+
}
|
|
197
|
+
function screen(name, props) {
|
|
198
|
+
later(() => {
|
|
199
|
+
const s = state;
|
|
200
|
+
const url = screenUrl(s.app, name);
|
|
201
|
+
if (url === s.screenUrl && s.screenId && !props) return;
|
|
202
|
+
settle(s);
|
|
203
|
+
const from = s.screenId ? s.screenUrl : "";
|
|
204
|
+
s.screenUrl = url;
|
|
205
|
+
s.screenId = hex(8);
|
|
206
|
+
s.shownAt = import_react_native.AppState.currentState === "active" || import_react_native.AppState.currentState == null ? Date.now() : 0;
|
|
207
|
+
add(s, "pageview", { r: from, ti: name, props });
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
function track(name, props) {
|
|
211
|
+
later(() => {
|
|
212
|
+
if (typeof name === "string" && name) add(state, "custom", { n: name.slice(0, 100), props });
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
function optOut(stop) {
|
|
216
|
+
later(() => {
|
|
217
|
+
var _a, _b;
|
|
218
|
+
const s = state;
|
|
219
|
+
s.out = stop;
|
|
220
|
+
if (!stop) return;
|
|
221
|
+
s.queue = [];
|
|
222
|
+
s.id = "";
|
|
223
|
+
(_a = s.options.storage) == null ? void 0 : _a.removeItem(QUEUE_KEY).catch(() => void 0);
|
|
224
|
+
(_b = s.options.storage) == null ? void 0 : _b.removeItem(ID_KEY).catch(() => void 0);
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
function navigationTracker(ref) {
|
|
228
|
+
const report = () => {
|
|
229
|
+
const route = ref.getCurrentRoute();
|
|
230
|
+
if (route) screen(route.name);
|
|
231
|
+
};
|
|
232
|
+
return { onReady: report, onStateChange: report };
|
|
233
|
+
}
|
|
234
|
+
function useScreen(name) {
|
|
235
|
+
(0, import_react.useEffect)(() => {
|
|
236
|
+
if (name) screen(name);
|
|
237
|
+
}, [name]);
|
|
238
|
+
}
|
|
239
|
+
function reset() {
|
|
240
|
+
if (state == null ? void 0 : state.timer) clearTimeout(state.timer);
|
|
241
|
+
state = null;
|
|
242
|
+
early = [];
|
|
243
|
+
}
|
|
244
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
245
|
+
0 && (module.exports = {
|
|
246
|
+
flush,
|
|
247
|
+
init,
|
|
248
|
+
navigationTracker,
|
|
249
|
+
optOut,
|
|
250
|
+
reset,
|
|
251
|
+
screen,
|
|
252
|
+
track,
|
|
253
|
+
useScreen
|
|
254
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type Props = Record<string, string | number | boolean>;
|
|
2
|
+
/** Where the id and the waiting events are kept. AsyncStorage fits; so does anything with these three methods. */
|
|
3
|
+
export type Storage = {
|
|
4
|
+
getItem(key: string): Promise<string | null>;
|
|
5
|
+
setItem(key: string, value: string): Promise<unknown>;
|
|
6
|
+
removeItem(key: string): Promise<unknown>;
|
|
7
|
+
};
|
|
8
|
+
export type Options = {
|
|
9
|
+
/** The app's public key, from its Install screen in lapwy. */
|
|
10
|
+
site: string;
|
|
11
|
+
/** A short name for the app, such as its bundle name. It stands where a website's domain would. */
|
|
12
|
+
app?: string;
|
|
13
|
+
/** Where events go. Defaults to lapwy's own ingest. */
|
|
14
|
+
endpoint?: string;
|
|
15
|
+
/** Keeps the install id and the queue between launches. Without it every launch is a new user and nothing survives a restart. */
|
|
16
|
+
storage?: Storage;
|
|
17
|
+
/** Your app's version, to see what a release changed. */
|
|
18
|
+
version?: string;
|
|
19
|
+
/** Report uncaught JavaScript errors. On by default. */
|
|
20
|
+
errors?: boolean;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Sends what is waiting. Called on its own a moment after anything is
|
|
24
|
+
* tracked and when the app goes to the background; call it yourself only
|
|
25
|
+
* when you need the events out right now.
|
|
26
|
+
*/
|
|
27
|
+
export declare function flush(): Promise<void>;
|
|
28
|
+
/** Starts tracking. Call it once, when the app starts. A second call does nothing. */
|
|
29
|
+
export declare function init(options: Options): void;
|
|
30
|
+
/** The user is looking at a screen. Call it when a screen comes up, with its name or path. */
|
|
31
|
+
export declare function screen(name: string, props?: Props): void;
|
|
32
|
+
/** Something a user did: track("add_to_cart", { product: "monstera" }). Revenue goes in as revenue and currency. */
|
|
33
|
+
export declare function track(name: string, props?: Props): void;
|
|
34
|
+
/**
|
|
35
|
+
* Stop or resume tracking for this user, for an in-app "do not track" switch.
|
|
36
|
+
* Stopping also throws away what is waiting and forgets the install id.
|
|
37
|
+
*/
|
|
38
|
+
export declare function optOut(stop: boolean): void;
|
|
39
|
+
/**
|
|
40
|
+
* For React Navigation: reports every screen the user moves to.
|
|
41
|
+
*
|
|
42
|
+
* const navigation = useNavigationContainerRef();
|
|
43
|
+
* <NavigationContainer ref={navigation} {...navigationTracker(navigation)}>
|
|
44
|
+
*/
|
|
45
|
+
export declare function navigationTracker(ref: {
|
|
46
|
+
getCurrentRoute(): {
|
|
47
|
+
name: string;
|
|
48
|
+
} | undefined;
|
|
49
|
+
}): {
|
|
50
|
+
onReady: () => void;
|
|
51
|
+
onStateChange: () => void;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Reports a screen from inside a component. With Expo Router, once in the root layout:
|
|
55
|
+
*
|
|
56
|
+
* useScreen(usePathname());
|
|
57
|
+
*/
|
|
58
|
+
export declare function useScreen(name: string | null | undefined): void;
|
|
59
|
+
/** For tests: forget everything, as if the app had just been installed. */
|
|
60
|
+
export declare function reset(): void;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export type Props = Record<string, string | number | boolean>;
|
|
2
|
+
/** Where the id and the waiting events are kept. AsyncStorage fits; so does anything with these three methods. */
|
|
3
|
+
export type Storage = {
|
|
4
|
+
getItem(key: string): Promise<string | null>;
|
|
5
|
+
setItem(key: string, value: string): Promise<unknown>;
|
|
6
|
+
removeItem(key: string): Promise<unknown>;
|
|
7
|
+
};
|
|
8
|
+
export type Options = {
|
|
9
|
+
/** The app's public key, from its Install screen in lapwy. */
|
|
10
|
+
site: string;
|
|
11
|
+
/** A short name for the app, such as its bundle name. It stands where a website's domain would. */
|
|
12
|
+
app?: string;
|
|
13
|
+
/** Where events go. Defaults to lapwy's own ingest. */
|
|
14
|
+
endpoint?: string;
|
|
15
|
+
/** Keeps the install id and the queue between launches. Without it every launch is a new user and nothing survives a restart. */
|
|
16
|
+
storage?: Storage;
|
|
17
|
+
/** Your app's version, to see what a release changed. */
|
|
18
|
+
version?: string;
|
|
19
|
+
/** Report uncaught JavaScript errors. On by default. */
|
|
20
|
+
errors?: boolean;
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Sends what is waiting. Called on its own a moment after anything is
|
|
24
|
+
* tracked and when the app goes to the background; call it yourself only
|
|
25
|
+
* when you need the events out right now.
|
|
26
|
+
*/
|
|
27
|
+
export declare function flush(): Promise<void>;
|
|
28
|
+
/** Starts tracking. Call it once, when the app starts. A second call does nothing. */
|
|
29
|
+
export declare function init(options: Options): void;
|
|
30
|
+
/** The user is looking at a screen. Call it when a screen comes up, with its name or path. */
|
|
31
|
+
export declare function screen(name: string, props?: Props): void;
|
|
32
|
+
/** Something a user did: track("add_to_cart", { product: "monstera" }). Revenue goes in as revenue and currency. */
|
|
33
|
+
export declare function track(name: string, props?: Props): void;
|
|
34
|
+
/**
|
|
35
|
+
* Stop or resume tracking for this user, for an in-app "do not track" switch.
|
|
36
|
+
* Stopping also throws away what is waiting and forgets the install id.
|
|
37
|
+
*/
|
|
38
|
+
export declare function optOut(stop: boolean): void;
|
|
39
|
+
/**
|
|
40
|
+
* For React Navigation: reports every screen the user moves to.
|
|
41
|
+
*
|
|
42
|
+
* const navigation = useNavigationContainerRef();
|
|
43
|
+
* <NavigationContainer ref={navigation} {...navigationTracker(navigation)}>
|
|
44
|
+
*/
|
|
45
|
+
export declare function navigationTracker(ref: {
|
|
46
|
+
getCurrentRoute(): {
|
|
47
|
+
name: string;
|
|
48
|
+
} | undefined;
|
|
49
|
+
}): {
|
|
50
|
+
onReady: () => void;
|
|
51
|
+
onStateChange: () => void;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Reports a screen from inside a component. With Expo Router, once in the root layout:
|
|
55
|
+
*
|
|
56
|
+
* useScreen(usePathname());
|
|
57
|
+
*/
|
|
58
|
+
export declare function useScreen(name: string | null | undefined): void;
|
|
59
|
+
/** For tests: forget everything, as if the app had just been installed. */
|
|
60
|
+
export declare function reset(): void;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { useEffect } from "react";
|
|
3
|
+
import { AppState, Dimensions, Platform } from "react-native";
|
|
4
|
+
var DEFAULT_ENDPOINT = "https://in.lapwy.com/e";
|
|
5
|
+
var ID_KEY = "lapwy_device_id";
|
|
6
|
+
var QUEUE_KEY = "lapwy_queue";
|
|
7
|
+
var MAX_QUEUE = 500;
|
|
8
|
+
var state = null;
|
|
9
|
+
var early = [];
|
|
10
|
+
function hex(bytes) {
|
|
11
|
+
const a = new Uint8Array(bytes);
|
|
12
|
+
const c = globalThis.crypto;
|
|
13
|
+
if (c && c.getRandomValues) c.getRandomValues(a);
|
|
14
|
+
else for (let i = 0; i < bytes; i++) a[i] = Math.floor(Math.random() * 256);
|
|
15
|
+
let s = "";
|
|
16
|
+
for (let i = 0; i < bytes; i++) s += (a[i] + 256).toString(16).slice(1);
|
|
17
|
+
return s;
|
|
18
|
+
}
|
|
19
|
+
function uuid() {
|
|
20
|
+
const h = hex(16);
|
|
21
|
+
return h.slice(0, 8) + "-" + h.slice(8, 12) + "-4" + h.slice(13, 16) + "-a" + h.slice(17, 20) + "-" + h.slice(20);
|
|
22
|
+
}
|
|
23
|
+
function screenUrl(app, name) {
|
|
24
|
+
const path = name.replace(/^\/+/, "").split("/").map(encodeURIComponent).join("/");
|
|
25
|
+
return `app://${app}/${path}`;
|
|
26
|
+
}
|
|
27
|
+
function save(s) {
|
|
28
|
+
var _a;
|
|
29
|
+
(_a = s.options.storage) == null ? void 0 : _a.setItem(QUEUE_KEY, JSON.stringify(s.queue)).catch(() => void 0);
|
|
30
|
+
}
|
|
31
|
+
function add(s, kind, extra) {
|
|
32
|
+
if (s.out) return;
|
|
33
|
+
const e = { id: uuid(), t: Date.now(), k: kind, u: s.screenUrl, p: s.screenId };
|
|
34
|
+
for (const k in extra) if (extra[k] !== void 0) e[k] = extra[k];
|
|
35
|
+
s.queue.push(e);
|
|
36
|
+
if (s.queue.length > MAX_QUEUE) s.queue.splice(0, s.queue.length - MAX_QUEUE);
|
|
37
|
+
save(s);
|
|
38
|
+
if (!s.timer) s.timer = setTimeout(() => void flush(), 2e3);
|
|
39
|
+
}
|
|
40
|
+
function osVersion() {
|
|
41
|
+
var _a;
|
|
42
|
+
if (Platform.OS !== "android") return String(Platform.Version);
|
|
43
|
+
const release = (_a = Platform.constants) == null ? void 0 : _a.Release;
|
|
44
|
+
return release == null ? "" : String(release).split(".").slice(0, 2).join(".");
|
|
45
|
+
}
|
|
46
|
+
function client(s) {
|
|
47
|
+
const { width, height } = Dimensions.get("window");
|
|
48
|
+
let language = "";
|
|
49
|
+
try {
|
|
50
|
+
language = Intl.DateTimeFormat().resolvedOptions().locale;
|
|
51
|
+
} catch {
|
|
52
|
+
}
|
|
53
|
+
return {
|
|
54
|
+
sw: Math.round(width),
|
|
55
|
+
sh: Math.round(height),
|
|
56
|
+
l: language,
|
|
57
|
+
rel: s.options.version || "",
|
|
58
|
+
pf: Platform.OS,
|
|
59
|
+
osv: osVersion(),
|
|
60
|
+
// The usual cut between a phone and a tablet: 600 points on the short side.
|
|
61
|
+
dt: Platform.isPad || Math.min(width, height) >= 600 ? "tablet" : "mobile"
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
async function flush() {
|
|
65
|
+
const s = state;
|
|
66
|
+
if (!s) return;
|
|
67
|
+
if (s.timer) clearTimeout(s.timer);
|
|
68
|
+
s.timer = 0;
|
|
69
|
+
await s.ready;
|
|
70
|
+
if (s.sending || !s.queue.length) return;
|
|
71
|
+
s.sending = true;
|
|
72
|
+
const events = s.queue.slice(0, 50);
|
|
73
|
+
try {
|
|
74
|
+
const res = await fetch(s.endpoint, {
|
|
75
|
+
method: "POST",
|
|
76
|
+
headers: { "Content-Type": "text/plain" },
|
|
77
|
+
body: JSON.stringify({ k: s.options.site, v: s.id || void 0, sdk: "react-native-0.1", now: Date.now(), c: client(s), e: events })
|
|
78
|
+
});
|
|
79
|
+
if (res.status === 429 || res.status >= 500) throw new Error("retry");
|
|
80
|
+
s.queue = s.queue.filter((e) => !events.includes(e));
|
|
81
|
+
s.backoff = 0;
|
|
82
|
+
save(s);
|
|
83
|
+
s.sending = false;
|
|
84
|
+
if (s.queue.length) s.timer = setTimeout(() => void flush(), 200);
|
|
85
|
+
} catch {
|
|
86
|
+
s.sending = false;
|
|
87
|
+
s.timer = setTimeout(() => void flush(), Math.min(5e3 * Math.pow(2, s.backoff++), 3e5));
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function settle(s) {
|
|
91
|
+
if (s.shownAt) {
|
|
92
|
+
s.engaged += Date.now() - s.shownAt;
|
|
93
|
+
s.shownAt = 0;
|
|
94
|
+
}
|
|
95
|
+
if (s.screenId && s.engaged >= 1e3) add(s, "engagement", { v: s.engaged });
|
|
96
|
+
s.engaged = 0;
|
|
97
|
+
}
|
|
98
|
+
function later(run) {
|
|
99
|
+
if (state) run();
|
|
100
|
+
else if (early.length < 100) early.push(run);
|
|
101
|
+
}
|
|
102
|
+
function init(options) {
|
|
103
|
+
if (state || !options.site) return;
|
|
104
|
+
const s = {
|
|
105
|
+
options,
|
|
106
|
+
endpoint: options.endpoint || DEFAULT_ENDPOINT,
|
|
107
|
+
app: (options.app || "app").toLowerCase().replace(/[^a-z0-9.-]+/g, "-") || "app",
|
|
108
|
+
id: "",
|
|
109
|
+
ready: Promise.resolve(),
|
|
110
|
+
queue: [],
|
|
111
|
+
screenUrl: "",
|
|
112
|
+
screenId: "",
|
|
113
|
+
shownAt: 0,
|
|
114
|
+
engaged: 0,
|
|
115
|
+
timer: 0,
|
|
116
|
+
backoff: 0,
|
|
117
|
+
sending: false,
|
|
118
|
+
out: false
|
|
119
|
+
};
|
|
120
|
+
s.screenUrl = screenUrl(s.app, "");
|
|
121
|
+
state = s;
|
|
122
|
+
const storage = options.storage;
|
|
123
|
+
s.ready = (async () => {
|
|
124
|
+
if (!storage) return;
|
|
125
|
+
try {
|
|
126
|
+
const [id, queued] = await Promise.all([storage.getItem(ID_KEY), storage.getItem(QUEUE_KEY)]);
|
|
127
|
+
s.id = id && /^[0-9a-f]{16,64}$/.test(id) ? id : hex(16);
|
|
128
|
+
if (s.id !== id) await storage.setItem(ID_KEY, s.id);
|
|
129
|
+
const old = queued ? JSON.parse(queued) : [];
|
|
130
|
+
if (Array.isArray(old) && old.length) s.queue = old.concat(s.queue).slice(-MAX_QUEUE);
|
|
131
|
+
} catch {
|
|
132
|
+
}
|
|
133
|
+
})();
|
|
134
|
+
AppState.addEventListener("change", (next) => {
|
|
135
|
+
if (next === "active") {
|
|
136
|
+
if (!s.shownAt) s.shownAt = Date.now();
|
|
137
|
+
void flush();
|
|
138
|
+
} else {
|
|
139
|
+
settle(s);
|
|
140
|
+
void flush();
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
if (options.errors !== false) {
|
|
144
|
+
const utils = globalThis.ErrorUtils;
|
|
145
|
+
if (utils) {
|
|
146
|
+
const before = utils.getGlobalHandler();
|
|
147
|
+
let reported = 0;
|
|
148
|
+
utils.setGlobalHandler((error, fatal) => {
|
|
149
|
+
try {
|
|
150
|
+
const e = error;
|
|
151
|
+
const msg = String(e && e.message || error || "").slice(0, 300);
|
|
152
|
+
if (msg && reported++ < 10) {
|
|
153
|
+
add(s, "error", { n: String(e && e.name || "Error").slice(0, 60), props: { msg, fatal: Boolean(fatal) } });
|
|
154
|
+
void flush();
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
}
|
|
158
|
+
before(error, fatal);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const waiting = early;
|
|
163
|
+
early = [];
|
|
164
|
+
waiting.forEach((run) => run());
|
|
165
|
+
}
|
|
166
|
+
function screen(name, props) {
|
|
167
|
+
later(() => {
|
|
168
|
+
const s = state;
|
|
169
|
+
const url = screenUrl(s.app, name);
|
|
170
|
+
if (url === s.screenUrl && s.screenId && !props) return;
|
|
171
|
+
settle(s);
|
|
172
|
+
const from = s.screenId ? s.screenUrl : "";
|
|
173
|
+
s.screenUrl = url;
|
|
174
|
+
s.screenId = hex(8);
|
|
175
|
+
s.shownAt = AppState.currentState === "active" || AppState.currentState == null ? Date.now() : 0;
|
|
176
|
+
add(s, "pageview", { r: from, ti: name, props });
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
function track(name, props) {
|
|
180
|
+
later(() => {
|
|
181
|
+
if (typeof name === "string" && name) add(state, "custom", { n: name.slice(0, 100), props });
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
function optOut(stop) {
|
|
185
|
+
later(() => {
|
|
186
|
+
var _a, _b;
|
|
187
|
+
const s = state;
|
|
188
|
+
s.out = stop;
|
|
189
|
+
if (!stop) return;
|
|
190
|
+
s.queue = [];
|
|
191
|
+
s.id = "";
|
|
192
|
+
(_a = s.options.storage) == null ? void 0 : _a.removeItem(QUEUE_KEY).catch(() => void 0);
|
|
193
|
+
(_b = s.options.storage) == null ? void 0 : _b.removeItem(ID_KEY).catch(() => void 0);
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
function navigationTracker(ref) {
|
|
197
|
+
const report = () => {
|
|
198
|
+
const route = ref.getCurrentRoute();
|
|
199
|
+
if (route) screen(route.name);
|
|
200
|
+
};
|
|
201
|
+
return { onReady: report, onStateChange: report };
|
|
202
|
+
}
|
|
203
|
+
function useScreen(name) {
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
if (name) screen(name);
|
|
206
|
+
}, [name]);
|
|
207
|
+
}
|
|
208
|
+
function reset() {
|
|
209
|
+
if (state == null ? void 0 : state.timer) clearTimeout(state.timer);
|
|
210
|
+
state = null;
|
|
211
|
+
early = [];
|
|
212
|
+
}
|
|
213
|
+
export {
|
|
214
|
+
flush,
|
|
215
|
+
init,
|
|
216
|
+
navigationTracker,
|
|
217
|
+
optOut,
|
|
218
|
+
reset,
|
|
219
|
+
screen,
|
|
220
|
+
track,
|
|
221
|
+
useScreen
|
|
222
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lapwy/react-native",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "lapwy for React Native and Expo: init, screen, track, with an offline queue.",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"main": "./dist/index.cjs",
|
|
12
|
+
"module": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"import": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"require": {
|
|
21
|
+
"types": "./dist/index.d.cts",
|
|
22
|
+
"default": "./dist/index.cjs"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"react": ">=18",
|
|
29
|
+
"react-native": ">=0.72"
|
|
30
|
+
},
|
|
31
|
+
"publishConfig": {
|
|
32
|
+
"access": "public"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup && tsc -p tsconfig.build.json && node ../../scripts/cts-types.mjs"
|
|
36
|
+
}
|
|
6
37
|
}
|