@brekkymetrics/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.
@@ -0,0 +1,9 @@
1
+ import type { BrekkyInitOptions, BrekkyProperties } from "./types";
2
+ export declare const SDK_VERSION = "0.1.0";
3
+ export declare const DEFAULT_INGEST_URL = "https://ndxxbmnaoqnnepfmyoax.supabase.co/functions/v1/ingest";
4
+ export declare function flush(): Promise<void>;
5
+ export declare function init(key: string, options?: BrekkyInitOptions): Promise<void>;
6
+ export declare function track(name: string, properties?: BrekkyProperties): Promise<void>;
7
+ export declare function screen(name: string, properties?: BrekkyProperties): Promise<void>;
8
+ export declare function resetForTests(): void;
9
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAe,MAAM,SAAS,CAAC;AAEhF,eAAO,MAAM,WAAW,UAAU,CAAC;AACnC,eAAO,MAAM,kBAAkB,iEAAiE,CAAC;AAyEjG,wBAAsB,KAAK,kBA8B1B;AAYD,wBAAsB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,iBAgBtE;AAED,wBAAsB,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,gBAAgB,iBAItE;AAED,wBAAsB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,gBAAgB,iBAIvE;AAED,wBAAgB,aAAa,SAW5B"}
package/dist/client.js ADDED
@@ -0,0 +1,149 @@
1
+ import { AppState, Platform } from "react-native";
2
+ import { getSessionId, getVisitorId } from "./storage";
3
+ export const SDK_VERSION = "0.1.0";
4
+ export const DEFAULT_INGEST_URL = "https://ndxxbmnaoqnnepfmyoax.supabase.co/functions/v1/ingest";
5
+ const FLUSH_MS = 1000;
6
+ const MAX_BATCH = 10;
7
+ const SESSION_MS = 30 * 60 * 1000;
8
+ const EVENT_NAME_RE = /^[a-z][a-z0-9_]{0,63}$/;
9
+ let writeKey = null;
10
+ let ingestUrl = DEFAULT_INGEST_URL;
11
+ let queue = [];
12
+ let flushTimer = null;
13
+ let appStateSubscription = null;
14
+ let initialized = false;
15
+ function sanitizeEventName(name) {
16
+ const clean = name.trim().toLowerCase().replace(/[^a-z0-9_]/g, "_").slice(0, 64);
17
+ return EVENT_NAME_RE.test(clean) ? clean : null;
18
+ }
19
+ function sanitizeProperties(properties) {
20
+ if (!properties)
21
+ return {};
22
+ const out = {};
23
+ for (const [key, value] of Object.entries(properties)) {
24
+ if (!key || key.length > 64)
25
+ continue;
26
+ if (typeof value === "string")
27
+ out[key] = value.slice(0, 500);
28
+ else if (typeof value === "number" && Number.isFinite(value))
29
+ out[key] = value;
30
+ else if (typeof value === "boolean" || value === null)
31
+ out[key] = value;
32
+ }
33
+ return out;
34
+ }
35
+ async function buildContext() {
36
+ return {
37
+ platform: Platform.OS,
38
+ locale: Intl.DateTimeFormat().resolvedOptions().locale,
39
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
40
+ sdk: { name: "@brekkymetrics/react-native", version: SDK_VERSION },
41
+ };
42
+ }
43
+ async function enqueue(name, properties) {
44
+ if (!writeKey)
45
+ return;
46
+ const [visitorId, sessionId] = await Promise.all([
47
+ getVisitorId(),
48
+ getSessionId(SESSION_MS),
49
+ ]);
50
+ queue.push({
51
+ name,
52
+ timestamp: new Date().toISOString(),
53
+ visitor_id: visitorId,
54
+ session_id: sessionId,
55
+ properties: sanitizeProperties(properties),
56
+ context: await buildContext(),
57
+ });
58
+ if (queue.length >= MAX_BATCH) {
59
+ void flush();
60
+ }
61
+ else {
62
+ scheduleFlush();
63
+ }
64
+ }
65
+ function scheduleFlush() {
66
+ if (flushTimer)
67
+ return;
68
+ flushTimer = setTimeout(() => {
69
+ void flush();
70
+ }, FLUSH_MS);
71
+ }
72
+ export async function flush() {
73
+ if (flushTimer) {
74
+ clearTimeout(flushTimer);
75
+ flushTimer = null;
76
+ }
77
+ if (!writeKey || queue.length === 0)
78
+ return;
79
+ const events = queue.splice(0, MAX_BATCH);
80
+ const body = JSON.stringify({
81
+ write_key: writeKey,
82
+ sent_at: new Date().toISOString(),
83
+ events,
84
+ });
85
+ try {
86
+ await fetch(ingestUrl, {
87
+ method: "POST",
88
+ headers: { "Content-Type": "text/plain" },
89
+ body,
90
+ });
91
+ }
92
+ catch {
93
+ queue.unshift(...events);
94
+ scheduleFlush();
95
+ return;
96
+ }
97
+ if (queue.length > 0) {
98
+ scheduleFlush();
99
+ }
100
+ }
101
+ function bindLifecycle() {
102
+ if (appStateSubscription)
103
+ return;
104
+ appStateSubscription = AppState.addEventListener("change", (state) => {
105
+ if (state === "background" || state === "inactive") {
106
+ void flush();
107
+ }
108
+ });
109
+ }
110
+ export async function init(key, options = {}) {
111
+ const trimmed = key.trim();
112
+ if (!trimmed) {
113
+ throw new Error("Brekky.init requires a write key.");
114
+ }
115
+ writeKey = trimmed;
116
+ ingestUrl = options.ingestUrl?.trim() || DEFAULT_INGEST_URL;
117
+ bindLifecycle();
118
+ if (!initialized) {
119
+ initialized = true;
120
+ if (options.trackAppOpen !== false) {
121
+ await track("app_open");
122
+ }
123
+ }
124
+ }
125
+ export async function track(name, properties) {
126
+ const clean = sanitizeEventName(name);
127
+ if (!clean)
128
+ return;
129
+ await enqueue(clean, properties);
130
+ }
131
+ export async function screen(name, properties) {
132
+ const cleanScreen = name.trim().slice(0, 200);
133
+ if (!cleanScreen)
134
+ return;
135
+ await track("screen_view", { screen: cleanScreen, ...sanitizeProperties(properties) });
136
+ }
137
+ export function resetForTests() {
138
+ writeKey = null;
139
+ ingestUrl = DEFAULT_INGEST_URL;
140
+ queue = [];
141
+ if (flushTimer) {
142
+ clearTimeout(flushTimer);
143
+ flushTimer = null;
144
+ }
145
+ appStateSubscription?.remove();
146
+ appStateSubscription = null;
147
+ initialized = false;
148
+ }
149
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,14 @@
1
+ import * as client from "./client";
2
+ import { trackNavigationState } from "./navigation";
3
+ export type { BrekkyInitOptions, BrekkyProperties } from "./types";
4
+ export { DEFAULT_INGEST_URL, SDK_VERSION } from "./client";
5
+ export { trackNavigationState } from "./navigation";
6
+ export declare const Brekky: {
7
+ init: typeof client.init;
8
+ track: typeof client.track;
9
+ screen: typeof client.screen;
10
+ flush: typeof client.flush;
11
+ trackNavigationState: typeof trackNavigationState;
12
+ };
13
+ export default Brekky;
14
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,UAAU,CAAC;AACnC,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEpD,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,SAAS,CAAC;AACnE,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEpD,eAAO,MAAM,MAAM;;;;;;CAMlB,CAAC;AAEF,eAAe,MAAM,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,13 @@
1
+ import * as client from "./client";
2
+ import { trackNavigationState } from "./navigation";
3
+ export { DEFAULT_INGEST_URL, SDK_VERSION } from "./client";
4
+ export { trackNavigationState } from "./navigation";
5
+ export const Brekky = {
6
+ init: client.init,
7
+ track: client.track,
8
+ screen: client.screen,
9
+ flush: client.flush,
10
+ trackNavigationState,
11
+ };
12
+ export default Brekky;
13
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,MAAM,UAAU,CAAC;AACnC,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAGpD,OAAO,EAAE,kBAAkB,EAAE,WAAW,EAAE,MAAM,UAAU,CAAC;AAC3D,OAAO,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEpD,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,IAAI,EAAE,MAAM,CAAC,IAAI;IACjB,KAAK,EAAE,MAAM,CAAC,KAAK;IACnB,MAAM,EAAE,MAAM,CAAC,MAAM;IACrB,KAAK,EAAE,MAAM,CAAC,KAAK;IACnB,oBAAoB;CACrB,CAAC;AAEF,eAAe,MAAM,CAAC"}
@@ -0,0 +1,10 @@
1
+ type RouteLike = {
2
+ name?: string;
3
+ state?: RouteLike;
4
+ routes?: RouteLike[];
5
+ index?: number;
6
+ };
7
+ export declare function trackNavigationState(state: RouteLike | undefined): Promise<void>;
8
+ export declare function resetNavigationTracker(): void;
9
+ export {};
10
+ //# sourceMappingURL=navigation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../src/navigation.ts"],"names":[],"mappings":"AAEA,KAAK,SAAS,GAAG;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAkBF,wBAAsB,oBAAoB,CAAC,KAAK,EAAE,SAAS,GAAG,SAAS,iBAMtE;AAED,wBAAgB,sBAAsB,SAErC"}
@@ -0,0 +1,24 @@
1
+ import { screen } from "./client";
2
+ function activeRouteName(route) {
3
+ if (!route)
4
+ return null;
5
+ if (route.state) {
6
+ return activeRouteName(route.state);
7
+ }
8
+ if (route.routes && typeof route.index === "number") {
9
+ return activeRouteName(route.routes[route.index]);
10
+ }
11
+ return route.name?.trim() || null;
12
+ }
13
+ let lastScreen = null;
14
+ export async function trackNavigationState(state) {
15
+ const next = activeRouteName(state);
16
+ if (!next || next === lastScreen)
17
+ return;
18
+ lastScreen = next;
19
+ await screen(next);
20
+ }
21
+ export function resetNavigationTracker() {
22
+ lastScreen = null;
23
+ }
24
+ //# sourceMappingURL=navigation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"navigation.js","sourceRoot":"","sources":["../src/navigation.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AASlC,SAAS,eAAe,CAAC,KAA4B;IACnD,IAAI,CAAC,KAAK;QAAE,OAAO,IAAI,CAAC;IAExB,IAAI,KAAK,CAAC,KAAK,EAAE,CAAC;QAChB,OAAO,eAAe,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACtC,CAAC;IAED,IAAI,KAAK,CAAC,MAAM,IAAI,OAAO,KAAK,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;QACpD,OAAO,eAAe,CAAC,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;IACpD,CAAC;IAED,OAAO,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,IAAI,CAAC;AACpC,CAAC;AAED,IAAI,UAAU,GAAkB,IAAI,CAAC;AAErC,MAAM,CAAC,KAAK,UAAU,oBAAoB,CAAC,KAA4B;IACrE,MAAM,IAAI,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;IACpC,IAAI,CAAC,IAAI,IAAI,IAAI,KAAK,UAAU;QAAE,OAAO;IAEzC,UAAU,GAAG,IAAI,CAAC;IAClB,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC;AACrB,CAAC;AAED,MAAM,UAAU,sBAAsB;IACpC,UAAU,GAAG,IAAI,CAAC;AACpB,CAAC"}
@@ -0,0 +1,4 @@
1
+ export declare function createUuid(): string;
2
+ export declare function getVisitorId(): Promise<string>;
3
+ export declare function getSessionId(sessionMs: number): Promise<string>;
4
+ //# sourceMappingURL=storage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAMA,wBAAgB,UAAU,WAMzB;AAkBD,wBAAsB,YAAY,oBAOjC;AAED,wBAAsB,YAAY,CAAC,SAAS,EAAE,MAAM,mBAcnD"}
@@ -0,0 +1,49 @@
1
+ import AsyncStorage from "@react-native-async-storage/async-storage";
2
+ const VID_KEY = "bm_vid";
3
+ const SID_KEY = "bm_sid";
4
+ const SID_AT_KEY = "bm_sid_at";
5
+ export function createUuid() {
6
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => {
7
+ const random = (Math.random() * 16) | 0;
8
+ const value = char === "x" ? random : (random & 0x3) | 0x8;
9
+ return value.toString(16);
10
+ });
11
+ }
12
+ async function read(key) {
13
+ try {
14
+ return await AsyncStorage.getItem(key);
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
20
+ async function write(key, value) {
21
+ try {
22
+ await AsyncStorage.setItem(key, value);
23
+ }
24
+ catch {
25
+ // Storage can fail in private mode or restricted environments.
26
+ }
27
+ }
28
+ export async function getVisitorId() {
29
+ const existing = await read(VID_KEY);
30
+ if (existing)
31
+ return existing;
32
+ const id = createUuid();
33
+ await write(VID_KEY, id);
34
+ return id;
35
+ }
36
+ export async function getSessionId(sessionMs) {
37
+ const now = Date.now();
38
+ const existing = await read(SID_KEY);
39
+ const at = Number.parseInt((await read(SID_AT_KEY)) ?? "0", 10);
40
+ if (existing && now - at < sessionMs) {
41
+ await write(SID_AT_KEY, String(now));
42
+ return existing;
43
+ }
44
+ const id = createUuid();
45
+ await write(SID_KEY, id);
46
+ await write(SID_AT_KEY, String(now));
47
+ return id;
48
+ }
49
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.js","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA,OAAO,YAAY,MAAM,2CAA2C,CAAC;AAErE,MAAM,OAAO,GAAG,QAAQ,CAAC;AACzB,MAAM,OAAO,GAAG,QAAQ,CAAC;AACzB,MAAM,UAAU,GAAG,WAAW,CAAC;AAE/B,MAAM,UAAU,UAAU;IACxB,OAAO,sCAAsC,CAAC,OAAO,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,EAAE;QACtE,MAAM,MAAM,GAAG,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC;QACxC,MAAM,KAAK,GAAG,IAAI,KAAK,GAAG,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC;QAC3D,OAAO,KAAK,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC;IAC5B,CAAC,CAAC,CAAC;AACL,CAAC;AAED,KAAK,UAAU,IAAI,CAAC,GAAW;IAC7B,IAAI,CAAC;QACH,OAAO,MAAM,YAAY,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,KAAK,UAAU,KAAK,CAAC,GAAW,EAAE,KAAa;IAC7C,IAAI,CAAC;QACH,MAAM,YAAY,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACzC,CAAC;IAAC,MAAM,CAAC;QACP,+DAA+D;IACjE,CAAC;AACH,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY;IAChC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,CAAC;IACrC,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAE9B,MAAM,EAAE,GAAG,UAAU,EAAE,CAAC;IACxB,MAAM,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IACzB,OAAO,EAAE,CAAC;AACZ,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,SAAiB;IAClD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IACvB,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,CAAC;IACrC,MAAM,EAAE,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,IAAI,CAAC,UAAU,CAAC,CAAC,IAAI,GAAG,EAAE,EAAE,CAAC,CAAC;IAEhE,IAAI,QAAQ,IAAI,GAAG,GAAG,EAAE,GAAG,SAAS,EAAE,CAAC;QACrC,MAAM,KAAK,CAAC,UAAU,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;QACrC,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,MAAM,EAAE,GAAG,UAAU,EAAE,CAAC;IACxB,MAAM,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;IACzB,MAAM,KAAK,CAAC,UAAU,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC;IACrC,OAAO,EAAE,CAAC;AACZ,CAAC"}
@@ -0,0 +1,14 @@
1
+ export type BrekkyProperties = Record<string, string | number | boolean | null>;
2
+ export type BrekkyInitOptions = {
3
+ ingestUrl?: string;
4
+ trackAppOpen?: boolean;
5
+ };
6
+ export type QueuedEvent = {
7
+ name: string;
8
+ timestamp: string;
9
+ visitor_id: string;
10
+ session_id: string;
11
+ properties: BrekkyProperties;
12
+ context: Record<string, unknown>;
13
+ };
14
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC,CAAC;AAEhF,MAAM,MAAM,iBAAiB,GAAG;IAC9B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,gBAAgB,CAAC;IAC7B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAClC,CAAC"}
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@brekkymetrics/react-native",
3
+ "version": "0.1.0",
4
+ "description": "Brekky analytics for React Native",
5
+ "license": "MIT",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "react-native": "src/index.ts",
10
+ "files": [
11
+ "dist",
12
+ "src"
13
+ ],
14
+ "scripts": {
15
+ "build": "tsc -p tsconfig.json",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "peerDependencies": {
19
+ "@react-native-async-storage/async-storage": ">=1.17.0",
20
+ "react": ">=18.0.0",
21
+ "react-native": ">=0.71.0"
22
+ },
23
+ "devDependencies": {
24
+ "@react-native-async-storage/async-storage": "^2.2.0",
25
+ "@types/react": "^19",
26
+ "react": "^19.2.8",
27
+ "react-native": "^0.81.0",
28
+ "typescript": "^5"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/brekkymetrics/brekkymetrics"
33
+ },
34
+ "keywords": [
35
+ "analytics",
36
+ "brekky",
37
+ "react-native",
38
+ "tracking"
39
+ ]
40
+ }
package/src/client.ts ADDED
@@ -0,0 +1,162 @@
1
+ import { AppState, Platform } from "react-native";
2
+ import { getSessionId, getVisitorId } from "./storage";
3
+ import type { BrekkyInitOptions, BrekkyProperties, QueuedEvent } from "./types";
4
+
5
+ export const SDK_VERSION = "0.1.0";
6
+ export const DEFAULT_INGEST_URL = "https://ndxxbmnaoqnnepfmyoax.supabase.co/functions/v1/ingest";
7
+
8
+ const FLUSH_MS = 1000;
9
+ const MAX_BATCH = 10;
10
+ const SESSION_MS = 30 * 60 * 1000;
11
+ const EVENT_NAME_RE = /^[a-z][a-z0-9_]{0,63}$/;
12
+
13
+ let writeKey: string | null = null;
14
+ let ingestUrl = DEFAULT_INGEST_URL;
15
+ let queue: QueuedEvent[] = [];
16
+ let flushTimer: ReturnType<typeof setTimeout> | null = null;
17
+ let appStateSubscription: { remove: () => void } | null = null;
18
+ let initialized = false;
19
+
20
+ function sanitizeEventName(name: string) {
21
+ const clean = name.trim().toLowerCase().replace(/[^a-z0-9_]/g, "_").slice(0, 64);
22
+ return EVENT_NAME_RE.test(clean) ? clean : null;
23
+ }
24
+
25
+ function sanitizeProperties(properties?: BrekkyProperties): BrekkyProperties {
26
+ if (!properties) return {};
27
+
28
+ const out: BrekkyProperties = {};
29
+ for (const [key, value] of Object.entries(properties)) {
30
+ if (!key || key.length > 64) continue;
31
+ if (typeof value === "string") out[key] = value.slice(0, 500);
32
+ else if (typeof value === "number" && Number.isFinite(value)) out[key] = value;
33
+ else if (typeof value === "boolean" || value === null) out[key] = value;
34
+ }
35
+
36
+ return out;
37
+ }
38
+
39
+ async function buildContext() {
40
+ return {
41
+ platform: Platform.OS,
42
+ locale: Intl.DateTimeFormat().resolvedOptions().locale,
43
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
44
+ sdk: { name: "@brekkymetrics/react-native", version: SDK_VERSION },
45
+ };
46
+ }
47
+
48
+ async function enqueue(name: string, properties?: BrekkyProperties) {
49
+ if (!writeKey) return;
50
+
51
+ const [visitorId, sessionId] = await Promise.all([
52
+ getVisitorId(),
53
+ getSessionId(SESSION_MS),
54
+ ]);
55
+
56
+ queue.push({
57
+ name,
58
+ timestamp: new Date().toISOString(),
59
+ visitor_id: visitorId,
60
+ session_id: sessionId,
61
+ properties: sanitizeProperties(properties),
62
+ context: await buildContext(),
63
+ });
64
+
65
+ if (queue.length >= MAX_BATCH) {
66
+ void flush();
67
+ } else {
68
+ scheduleFlush();
69
+ }
70
+ }
71
+
72
+ function scheduleFlush() {
73
+ if (flushTimer) return;
74
+ flushTimer = setTimeout(() => {
75
+ void flush();
76
+ }, FLUSH_MS);
77
+ }
78
+
79
+ export async function flush() {
80
+ if (flushTimer) {
81
+ clearTimeout(flushTimer);
82
+ flushTimer = null;
83
+ }
84
+
85
+ if (!writeKey || queue.length === 0) return;
86
+
87
+ const events = queue.splice(0, MAX_BATCH);
88
+ const body = JSON.stringify({
89
+ write_key: writeKey,
90
+ sent_at: new Date().toISOString(),
91
+ events,
92
+ });
93
+
94
+ try {
95
+ await fetch(ingestUrl, {
96
+ method: "POST",
97
+ headers: { "Content-Type": "text/plain" },
98
+ body,
99
+ });
100
+ } catch {
101
+ queue.unshift(...events);
102
+ scheduleFlush();
103
+ return;
104
+ }
105
+
106
+ if (queue.length > 0) {
107
+ scheduleFlush();
108
+ }
109
+ }
110
+
111
+ function bindLifecycle() {
112
+ if (appStateSubscription) return;
113
+
114
+ appStateSubscription = AppState.addEventListener("change", (state) => {
115
+ if (state === "background" || state === "inactive") {
116
+ void flush();
117
+ }
118
+ });
119
+ }
120
+
121
+ export async function init(key: string, options: BrekkyInitOptions = {}) {
122
+ const trimmed = key.trim();
123
+ if (!trimmed) {
124
+ throw new Error("Brekky.init requires a write key.");
125
+ }
126
+
127
+ writeKey = trimmed;
128
+ ingestUrl = options.ingestUrl?.trim() || DEFAULT_INGEST_URL;
129
+ bindLifecycle();
130
+
131
+ if (!initialized) {
132
+ initialized = true;
133
+ if (options.trackAppOpen !== false) {
134
+ await track("app_open");
135
+ }
136
+ }
137
+ }
138
+
139
+ export async function track(name: string, properties?: BrekkyProperties) {
140
+ const clean = sanitizeEventName(name);
141
+ if (!clean) return;
142
+ await enqueue(clean, properties);
143
+ }
144
+
145
+ export async function screen(name: string, properties?: BrekkyProperties) {
146
+ const cleanScreen = name.trim().slice(0, 200);
147
+ if (!cleanScreen) return;
148
+ await track("screen_view", { screen: cleanScreen, ...sanitizeProperties(properties) });
149
+ }
150
+
151
+ export function resetForTests() {
152
+ writeKey = null;
153
+ ingestUrl = DEFAULT_INGEST_URL;
154
+ queue = [];
155
+ if (flushTimer) {
156
+ clearTimeout(flushTimer);
157
+ flushTimer = null;
158
+ }
159
+ appStateSubscription?.remove();
160
+ appStateSubscription = null;
161
+ initialized = false;
162
+ }
package/src/index.ts ADDED
@@ -0,0 +1,16 @@
1
+ import * as client from "./client";
2
+ import { trackNavigationState } from "./navigation";
3
+
4
+ export type { BrekkyInitOptions, BrekkyProperties } from "./types";
5
+ export { DEFAULT_INGEST_URL, SDK_VERSION } from "./client";
6
+ export { trackNavigationState } from "./navigation";
7
+
8
+ export const Brekky = {
9
+ init: client.init,
10
+ track: client.track,
11
+ screen: client.screen,
12
+ flush: client.flush,
13
+ trackNavigationState,
14
+ };
15
+
16
+ export default Brekky;
@@ -0,0 +1,36 @@
1
+ import { screen } from "./client";
2
+
3
+ type RouteLike = {
4
+ name?: string;
5
+ state?: RouteLike;
6
+ routes?: RouteLike[];
7
+ index?: number;
8
+ };
9
+
10
+ function activeRouteName(route: RouteLike | undefined): string | null {
11
+ if (!route) return null;
12
+
13
+ if (route.state) {
14
+ return activeRouteName(route.state);
15
+ }
16
+
17
+ if (route.routes && typeof route.index === "number") {
18
+ return activeRouteName(route.routes[route.index]);
19
+ }
20
+
21
+ return route.name?.trim() || null;
22
+ }
23
+
24
+ let lastScreen: string | null = null;
25
+
26
+ export async function trackNavigationState(state: RouteLike | undefined) {
27
+ const next = activeRouteName(state);
28
+ if (!next || next === lastScreen) return;
29
+
30
+ lastScreen = next;
31
+ await screen(next);
32
+ }
33
+
34
+ export function resetNavigationTracker() {
35
+ lastScreen = null;
36
+ }
package/src/storage.ts ADDED
@@ -0,0 +1,54 @@
1
+ import AsyncStorage from "@react-native-async-storage/async-storage";
2
+
3
+ const VID_KEY = "bm_vid";
4
+ const SID_KEY = "bm_sid";
5
+ const SID_AT_KEY = "bm_sid_at";
6
+
7
+ export function createUuid() {
8
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (char) => {
9
+ const random = (Math.random() * 16) | 0;
10
+ const value = char === "x" ? random : (random & 0x3) | 0x8;
11
+ return value.toString(16);
12
+ });
13
+ }
14
+
15
+ async function read(key: string) {
16
+ try {
17
+ return await AsyncStorage.getItem(key);
18
+ } catch {
19
+ return null;
20
+ }
21
+ }
22
+
23
+ async function write(key: string, value: string) {
24
+ try {
25
+ await AsyncStorage.setItem(key, value);
26
+ } catch {
27
+ // Storage can fail in private mode or restricted environments.
28
+ }
29
+ }
30
+
31
+ export async function getVisitorId() {
32
+ const existing = await read(VID_KEY);
33
+ if (existing) return existing;
34
+
35
+ const id = createUuid();
36
+ await write(VID_KEY, id);
37
+ return id;
38
+ }
39
+
40
+ export async function getSessionId(sessionMs: number) {
41
+ const now = Date.now();
42
+ const existing = await read(SID_KEY);
43
+ const at = Number.parseInt((await read(SID_AT_KEY)) ?? "0", 10);
44
+
45
+ if (existing && now - at < sessionMs) {
46
+ await write(SID_AT_KEY, String(now));
47
+ return existing;
48
+ }
49
+
50
+ const id = createUuid();
51
+ await write(SID_KEY, id);
52
+ await write(SID_AT_KEY, String(now));
53
+ return id;
54
+ }
package/src/types.ts ADDED
@@ -0,0 +1,15 @@
1
+ export type BrekkyProperties = Record<string, string | number | boolean | null>;
2
+
3
+ export type BrekkyInitOptions = {
4
+ ingestUrl?: string;
5
+ trackAppOpen?: boolean;
6
+ };
7
+
8
+ export type QueuedEvent = {
9
+ name: string;
10
+ timestamp: string;
11
+ visitor_id: string;
12
+ session_id: string;
13
+ properties: BrekkyProperties;
14
+ context: Record<string, unknown>;
15
+ };