@nexly/react-native 0.9.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/LICENSE +21 -0
- package/README.md +75 -0
- package/dist/device-meta.d.ts +11 -0
- package/dist/device-meta.d.ts.map +1 -0
- package/dist/device-meta.js +69 -0
- package/dist/engagement.d.ts +13 -0
- package/dist/engagement.d.ts.map +1 -0
- package/dist/engagement.js +62 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/nexly-provider.d.ts +25 -0
- package/dist/nexly-provider.d.ts.map +1 -0
- package/dist/nexly-provider.js +44 -0
- package/dist/nexly.d.ts +29 -0
- package/dist/nexly.d.ts.map +1 -0
- package/dist/nexly.js +65 -0
- package/dist/session.d.ts +13 -0
- package/dist/session.d.ts.map +1 -0
- package/dist/session.js +63 -0
- package/dist/storage.d.ts +7 -0
- package/dist/storage.d.ts.map +1 -0
- package/dist/storage.js +56 -0
- package/dist/transport.d.ts +7 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +21 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nexly
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# @nexly/react-native
|
|
2
|
+
|
|
3
|
+
React Native SDK for Nexly. Implements [`NexlyBase`](https://www.npmjs.com/package/@nexly/core) with:
|
|
4
|
+
|
|
5
|
+
- `fetch({ keepalive: true })` transport,
|
|
6
|
+
- visitor/session IDs persisted via `@react-native-async-storage/async-storage`,
|
|
7
|
+
- device metadata via `Platform`, `Dimensions`, `PixelRatio`,
|
|
8
|
+
- engagement tracking driven by `AppState` (active seconds, heartbeat, `session_ping`).
|
|
9
|
+
|
|
10
|
+
## Install
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install @nexly/react-native @react-native-async-storage/async-storage
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
`react` and `react-native` must already be installed in your app.
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```tsx
|
|
21
|
+
import { NexlyProvider, useNexlyClient } from '@nexly/react-native'
|
|
22
|
+
|
|
23
|
+
export default function App() {
|
|
24
|
+
return (
|
|
25
|
+
<NexlyProvider appId="your-app-id" ingestKey="your-ingest-key" autoEngagement initialScreen="Home">
|
|
26
|
+
<Root />
|
|
27
|
+
</NexlyProvider>
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function Root() {
|
|
32
|
+
const client = useNexlyClient()
|
|
33
|
+
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
client?.screenview('Home')
|
|
36
|
+
}, [client])
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<Button
|
|
40
|
+
title="Upgrade"
|
|
41
|
+
onPress={() => client?.event({ name: 'upgrade_clicked', type: 'custom', data: { plan: 'pro' } })}
|
|
42
|
+
/>
|
|
43
|
+
)
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Screens / paths
|
|
48
|
+
|
|
49
|
+
React Native has no URL, so Nexly uses a manual **screen name** instead:
|
|
50
|
+
|
|
51
|
+
- `client.setScreen('Settings')` — updates the screen used as `path` on future events.
|
|
52
|
+
- `client.screenview('Settings')` — sets the screen and sends a `pageview`.
|
|
53
|
+
|
|
54
|
+
Hook it up to your router of choice. Example with `@react-navigation/native`:
|
|
55
|
+
|
|
56
|
+
```tsx
|
|
57
|
+
<NavigationContainer
|
|
58
|
+
onStateChange={(state) => {
|
|
59
|
+
const route = state?.routes[state.index ?? 0]
|
|
60
|
+
if (route) client?.screenview(route.name)
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
...
|
|
64
|
+
</NavigationContainer>
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Engagement
|
|
68
|
+
|
|
69
|
+
`autoEngagement` attaches an `AppState` listener that:
|
|
70
|
+
|
|
71
|
+
- Increments `active_seconds` only while the app is foregrounded.
|
|
72
|
+
- Sends a `heartbeat` every 60 s (lifecycle event for realtime dashboards).
|
|
73
|
+
- Sends `session_ping` when the app goes to background, and `session_end` on provider unmount.
|
|
74
|
+
|
|
75
|
+
No scroll/click tracking (those are DOM-only); if you want tap analytics, send custom `engagement` events yourself.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Session-stable device metadata: OS, screen, locale, timezone.
|
|
3
|
+
* Collected once per session and sent as `session_context`.
|
|
4
|
+
*/
|
|
5
|
+
export declare function collectSessionMeta(): Record<string, unknown>;
|
|
6
|
+
/**
|
|
7
|
+
* Per-event volatile metadata: visitor/session IDs (from cache) and current screen.
|
|
8
|
+
* `path` is set by the client from the current screen name.
|
|
9
|
+
*/
|
|
10
|
+
export declare function collectEventMeta(): Record<string, unknown>;
|
|
11
|
+
//# sourceMappingURL=device-meta.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"device-meta.d.ts","sourceRoot":"","sources":["../src/device-meta.ts"],"names":[],"mappings":"AA2BA;;;GAGG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAmC5D;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAO1D"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Dimensions, NativeModules, PixelRatio, Platform } from 'react-native';
|
|
2
|
+
import { getCachedSessionId, getCachedVisitorId } from './session.js';
|
|
3
|
+
function safe(fn) {
|
|
4
|
+
try {
|
|
5
|
+
return fn();
|
|
6
|
+
}
|
|
7
|
+
catch {
|
|
8
|
+
return undefined;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
function filterEmpty(obj) {
|
|
12
|
+
return Object.fromEntries(Object.entries(obj).filter(([, v]) => v !== undefined && v !== '' && !(Array.isArray(v) && v.length === 0)));
|
|
13
|
+
}
|
|
14
|
+
function resolveLocale() {
|
|
15
|
+
const settings = NativeModules?.SettingsManager?.settings;
|
|
16
|
+
const iosLocale = settings?.AppleLocale ||
|
|
17
|
+
(Array.isArray(settings?.AppleLanguages) ? settings.AppleLanguages[0] : undefined);
|
|
18
|
+
const androidLocale = NativeModules?.I18nManager?.localeIdentifier;
|
|
19
|
+
const intlLocale = safe(() => new Intl.Locale(Intl.DateTimeFormat().resolvedOptions().locale).toString());
|
|
20
|
+
return iosLocale || androidLocale || intlLocale;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Session-stable device metadata: OS, screen, locale, timezone.
|
|
24
|
+
* Collected once per session and sent as `session_context`.
|
|
25
|
+
*/
|
|
26
|
+
export function collectSessionMeta() {
|
|
27
|
+
const out = {};
|
|
28
|
+
out.platform = safe(() => Platform.OS);
|
|
29
|
+
out.os_version = safe(() => String(Platform.Version));
|
|
30
|
+
const screen = safe(() => Dimensions.get('screen'));
|
|
31
|
+
if (screen) {
|
|
32
|
+
out.screen_width = Math.round(screen.width);
|
|
33
|
+
out.screen_height = Math.round(screen.height);
|
|
34
|
+
}
|
|
35
|
+
const window_ = safe(() => Dimensions.get('window'));
|
|
36
|
+
if (window_) {
|
|
37
|
+
out.viewport_width = Math.round(window_.width);
|
|
38
|
+
out.viewport_height = Math.round(window_.height);
|
|
39
|
+
}
|
|
40
|
+
out.device_pixel_ratio = safe(() => PixelRatio.get());
|
|
41
|
+
out.font_scale = safe(() => PixelRatio.getFontScale());
|
|
42
|
+
const locale = resolveLocale();
|
|
43
|
+
if (locale) {
|
|
44
|
+
out.language = locale;
|
|
45
|
+
out.locale = locale;
|
|
46
|
+
}
|
|
47
|
+
out.timezone = safe(() => Intl.DateTimeFormat().resolvedOptions().timeZone);
|
|
48
|
+
out.is_tv = safe(() => Platform.isTV);
|
|
49
|
+
out.user_agent = safe(() => {
|
|
50
|
+
const os = Platform.OS;
|
|
51
|
+
const ver = String(Platform.Version);
|
|
52
|
+
return `nexly-react-native/${os} ${ver}`;
|
|
53
|
+
});
|
|
54
|
+
return filterEmpty(out);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Per-event volatile metadata: visitor/session IDs (from cache) and current screen.
|
|
58
|
+
* `path` is set by the client from the current screen name.
|
|
59
|
+
*/
|
|
60
|
+
export function collectEventMeta() {
|
|
61
|
+
const out = {};
|
|
62
|
+
const visitorId = getCachedVisitorId();
|
|
63
|
+
const sessionId = getCachedSessionId();
|
|
64
|
+
if (visitorId)
|
|
65
|
+
out.visitor_id = visitorId;
|
|
66
|
+
if (sessionId)
|
|
67
|
+
out.session_id = sessionId;
|
|
68
|
+
return filterEmpty(out);
|
|
69
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { type IngestCredentials } from '@nexly/core';
|
|
2
|
+
type EngagementCreds = {
|
|
3
|
+
collectUrl: string;
|
|
4
|
+
} & IngestCredentials;
|
|
5
|
+
type StopFn = () => void;
|
|
6
|
+
/**
|
|
7
|
+
* React Native engagement tracking: active-seconds counter driven by `AppState`
|
|
8
|
+
* (ticks only while the app is `active`), plus a 60s heartbeat and
|
|
9
|
+
* a `session_ping` when the app goes to background.
|
|
10
|
+
*/
|
|
11
|
+
export declare function startEngagementTracking(creds: EngagementCreds): StopFn;
|
|
12
|
+
export {};
|
|
13
|
+
//# sourceMappingURL=engagement.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engagement.d.ts","sourceRoot":"","sources":["../src/engagement.ts"],"names":[],"mappings":"AACA,OAAO,EAAuB,KAAK,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAQzE,KAAK,eAAe,GAAG;IAAE,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,iBAAiB,CAAA;AAoBjE,KAAK,MAAM,GAAG,MAAM,IAAI,CAAA;AAExB;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,eAAe,GAAG,MAAM,CAyCtE"}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { AppState } from 'react-native';
|
|
2
|
+
import { buildCollectPayload } from '@nexly/core';
|
|
3
|
+
import { collectEventMeta, collectSessionMeta } from './device-meta.js';
|
|
4
|
+
import { sendPayloadFetch } from './transport.js';
|
|
5
|
+
import { touchSession } from './session.js';
|
|
6
|
+
const ACTIVE_TICK_MS = 1_000;
|
|
7
|
+
const KEEPALIVE_INTERVAL_MS = 60_000;
|
|
8
|
+
function send(creds, eventName, eventType, data, sessionContext) {
|
|
9
|
+
const payload = buildCollectPayload({
|
|
10
|
+
credentials: { appId: creds.appId, apiToken: creds.apiToken },
|
|
11
|
+
eventName,
|
|
12
|
+
eventType,
|
|
13
|
+
context: collectEventMeta(),
|
|
14
|
+
data,
|
|
15
|
+
sessionContext,
|
|
16
|
+
});
|
|
17
|
+
sendPayloadFetch(creds.collectUrl, payload);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* React Native engagement tracking: active-seconds counter driven by `AppState`
|
|
21
|
+
* (ticks only while the app is `active`), plus a 60s heartbeat and
|
|
22
|
+
* a `session_ping` when the app goes to background.
|
|
23
|
+
*/
|
|
24
|
+
export function startEngagementTracking(creds) {
|
|
25
|
+
let activeSeconds = 0;
|
|
26
|
+
let sessionContextSent = false;
|
|
27
|
+
function getSessionContextOnce() {
|
|
28
|
+
if (sessionContextSent)
|
|
29
|
+
return undefined;
|
|
30
|
+
sessionContextSent = true;
|
|
31
|
+
return collectSessionMeta();
|
|
32
|
+
}
|
|
33
|
+
const tickId = setInterval(() => {
|
|
34
|
+
if (AppState.currentState === 'active')
|
|
35
|
+
activeSeconds++;
|
|
36
|
+
}, ACTIVE_TICK_MS);
|
|
37
|
+
function sendKeepalive() {
|
|
38
|
+
if (AppState.currentState !== 'active')
|
|
39
|
+
return;
|
|
40
|
+
send(creds, 'heartbeat', 'lifecycle', { active_seconds: activeSeconds }, getSessionContextOnce());
|
|
41
|
+
}
|
|
42
|
+
sendKeepalive();
|
|
43
|
+
const keepaliveId = setInterval(sendKeepalive, KEEPALIVE_INTERVAL_MS);
|
|
44
|
+
const sub = AppState.addEventListener('change', (next) => {
|
|
45
|
+
if (next === 'background' || next === 'inactive') {
|
|
46
|
+
if (activeSeconds > 0) {
|
|
47
|
+
send(creds, 'session_ping', 'lifecycle', { active_seconds: activeSeconds });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (next === 'active') {
|
|
51
|
+
void touchSession();
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
return () => {
|
|
55
|
+
clearInterval(tickId);
|
|
56
|
+
clearInterval(keepaliveId);
|
|
57
|
+
sub.remove();
|
|
58
|
+
if (activeSeconds > 0) {
|
|
59
|
+
send(creds, 'session_end', 'lifecycle', { active_seconds: activeSeconds });
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Native Nexly SDK (public API).
|
|
3
|
+
*/
|
|
4
|
+
export { Nexly } from './nexly.js';
|
|
5
|
+
export { NexlyProvider, useNexlyClient, type NexlyProviderProps } from './nexly-provider.js';
|
|
6
|
+
export { collectEventMeta, collectSessionMeta } from './device-meta.js';
|
|
7
|
+
export { prewarmIds, touchSession, getCachedSessionId, getCachedVisitorId } from './session.js';
|
|
8
|
+
export type { NexlyEventInput, NexlyEventType, NexlyInit } from '@nexly/core';
|
|
9
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAClC,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAA;AAC5F,OAAO,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAA;AACvE,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAA;AAC/F,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Native Nexly SDK (public API).
|
|
3
|
+
*/
|
|
4
|
+
export { Nexly } from './nexly.js';
|
|
5
|
+
export { NexlyProvider, useNexlyClient } from './nexly-provider.js';
|
|
6
|
+
export { collectEventMeta, collectSessionMeta } from './device-meta.js';
|
|
7
|
+
export { prewarmIds, touchSession, getCachedSessionId, getCachedVisitorId } from './session.js';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type ReactNode } from 'react';
|
|
2
|
+
import { Nexly } from './nexly.js';
|
|
3
|
+
export type NexlyProviderProps = {
|
|
4
|
+
/** Override the ingest endpoint. Defaults to the Nexly production gateway. */
|
|
5
|
+
collectUrl?: string;
|
|
6
|
+
appId: string;
|
|
7
|
+
/** Ingest API token (maps to `key` on {@link Nexly}). Not named `key` — reserved in React. */
|
|
8
|
+
ingestKey: string;
|
|
9
|
+
/** Attach AppState-driven engagement tracking (active seconds, heartbeat, session_ping). */
|
|
10
|
+
autoEngagement?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Initial screen name used as `path` for events sent before any `setScreen()` call.
|
|
13
|
+
* Update it later with `client.setScreen(name)` or `client.screenview(name)`.
|
|
14
|
+
*/
|
|
15
|
+
initialScreen?: string;
|
|
16
|
+
children: ReactNode;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Initializes the Nexly client asynchronously (AsyncStorage prewarm) and exposes it
|
|
20
|
+
* via context. `useNexlyClient()` returns `null` until init completes or if credentials are missing.
|
|
21
|
+
*/
|
|
22
|
+
export declare function NexlyProvider({ collectUrl, appId, ingestKey, autoEngagement, initialScreen, children, }: NexlyProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
23
|
+
/** Returns the configured {@link Nexly} client, or `null` if not yet ready / credentials missing. */
|
|
24
|
+
export declare function useNexlyClient(): Nexly | null;
|
|
25
|
+
//# sourceMappingURL=nexly-provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nexly-provider.d.ts","sourceRoot":"","sources":["../src/nexly-provider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAkD,KAAK,SAAS,EAAE,MAAM,OAAO,CAAA;AACtF,OAAO,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAIlC,MAAM,MAAM,kBAAkB,GAAG;IAC/B,8EAA8E;IAC9E,UAAU,CAAC,EAAE,MAAM,CAAA;IACnB,KAAK,EAAE,MAAM,CAAA;IACb,8FAA8F;IAC9F,SAAS,EAAE,MAAM,CAAA;IACjB,4FAA4F;IAC5F,cAAc,CAAC,EAAE,OAAO,CAAA;IACxB;;;OAGG;IACH,aAAa,CAAC,EAAE,MAAM,CAAA;IACtB,QAAQ,EAAE,SAAS,CAAA;CACpB,CAAA;AAED;;;GAGG;AACH,wBAAgB,aAAa,CAAC,EAC5B,UAAU,EACV,KAAK,EACL,SAAS,EACT,cAAsB,EACtB,aAAa,EACb,QAAQ,GACT,EAAE,kBAAkB,2CA6BpB;AAED,qGAAqG;AACrG,wBAAgB,cAAc,IAAI,KAAK,GAAG,IAAI,CAE7C"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createContext, useContext, useEffect, useState } from 'react';
|
|
3
|
+
import { Nexly } from './nexly.js';
|
|
4
|
+
const NexlyClientContext = createContext(null);
|
|
5
|
+
/**
|
|
6
|
+
* Initializes the Nexly client asynchronously (AsyncStorage prewarm) and exposes it
|
|
7
|
+
* via context. `useNexlyClient()` returns `null` until init completes or if credentials are missing.
|
|
8
|
+
*/
|
|
9
|
+
export function NexlyProvider({ collectUrl, appId, ingestKey, autoEngagement = false, initialScreen, children, }) {
|
|
10
|
+
const [client, setClient] = useState(null);
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const id = (appId ?? '').trim();
|
|
13
|
+
const k = (ingestKey ?? '').trim();
|
|
14
|
+
if (!id || !k) {
|
|
15
|
+
if (!id)
|
|
16
|
+
console.warn('[Nexly] Missing appId — tracking is disabled.');
|
|
17
|
+
if (!k)
|
|
18
|
+
console.warn('[Nexly] Missing ingestKey — tracking is disabled.');
|
|
19
|
+
setClient(null);
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
let cancelled = false;
|
|
23
|
+
Nexly.init({ collectUrl, appId: id, key: k }).then((instance) => {
|
|
24
|
+
if (cancelled)
|
|
25
|
+
return;
|
|
26
|
+
if (initialScreen)
|
|
27
|
+
instance.setScreen(initialScreen);
|
|
28
|
+
setClient(instance);
|
|
29
|
+
});
|
|
30
|
+
return () => {
|
|
31
|
+
cancelled = true;
|
|
32
|
+
};
|
|
33
|
+
}, [collectUrl, appId, ingestKey, initialScreen]);
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
if (!autoEngagement || !client)
|
|
36
|
+
return;
|
|
37
|
+
return client.startEngagement();
|
|
38
|
+
}, [autoEngagement, client]);
|
|
39
|
+
return _jsx(NexlyClientContext.Provider, { value: client, children: children });
|
|
40
|
+
}
|
|
41
|
+
/** Returns the configured {@link Nexly} client, or `null` if not yet ready / credentials missing. */
|
|
42
|
+
export function useNexlyClient() {
|
|
43
|
+
return useContext(NexlyClientContext);
|
|
44
|
+
}
|
package/dist/nexly.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { NexlyBase, type NexlyInit } from '@nexly/core';
|
|
2
|
+
/**
|
|
3
|
+
* React Native Nexly client. Implements transport via `fetch({ keepalive: true })`
|
|
4
|
+
* and metadata via `Platform` / `Dimensions` / `AsyncStorage`.
|
|
5
|
+
*
|
|
6
|
+
* Because `AsyncStorage` is asynchronous, visitor/session IDs are prewarmed during
|
|
7
|
+
* {@link Nexly.init} before the first event is sent.
|
|
8
|
+
*/
|
|
9
|
+
export declare class Nexly extends NexlyBase {
|
|
10
|
+
private currentScreen;
|
|
11
|
+
/**
|
|
12
|
+
* Creates (or replaces) the singleton **and awaits ID prewarming**.
|
|
13
|
+
* `NexlyProvider` awaits this before exposing a client via context.
|
|
14
|
+
*/
|
|
15
|
+
static init(options: NexlyInit): Promise<Nexly>;
|
|
16
|
+
/** Returns the singleton, or `null` if not yet initialized. */
|
|
17
|
+
static getInstance(): Nexly | null;
|
|
18
|
+
constructor(init: NexlyInit);
|
|
19
|
+
/** Sets the current screen name used as `path` for subsequent events. */
|
|
20
|
+
setScreen(name: string): void;
|
|
21
|
+
/** Convenience: set screen and send a pageview in one call. */
|
|
22
|
+
screenview(name: string): boolean;
|
|
23
|
+
protected transport(payload: Record<string, unknown>): boolean;
|
|
24
|
+
protected collectEventContext(): Record<string, unknown>;
|
|
25
|
+
protected collectSessionContext(): Record<string, unknown>;
|
|
26
|
+
getDefaultPath(): string;
|
|
27
|
+
startEngagement(): () => void;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=nexly.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"nexly.d.ts","sourceRoot":"","sources":["../src/nexly.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,SAAS,EAAE,MAAM,aAAa,CAAA;AAMvD;;;;;;GAMG;AACH,qBAAa,KAAM,SAAQ,SAAS;IAClC,OAAO,CAAC,aAAa,CAAM;IAE3B;;;OAGG;WACU,IAAI,CAAC,OAAO,EAAE,SAAS,GAAG,OAAO,CAAC,KAAK,CAAC;IASrD,+DAA+D;IAC/D,MAAM,CAAC,WAAW,IAAI,KAAK,GAAG,IAAI;gBAItB,IAAI,EAAE,SAAS;IAI3B,yEAAyE;IACzE,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI;IAK7B,+DAA+D;IAC/D,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO;cAKd,SAAS,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO;cAIpD,mBAAmB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;cAI9C,qBAAqB,IAAI,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAI1D,cAAc,IAAI,MAAM;IAIxB,eAAe,IAAI,MAAM,IAAI;CAQvC"}
|
package/dist/nexly.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { NexlyBase } from '@nexly/core';
|
|
2
|
+
import { collectEventMeta, collectSessionMeta } from './device-meta.js';
|
|
3
|
+
import { startEngagementTracking } from './engagement.js';
|
|
4
|
+
import { prewarmIds } from './session.js';
|
|
5
|
+
import { sendPayloadFetch } from './transport.js';
|
|
6
|
+
/**
|
|
7
|
+
* React Native Nexly client. Implements transport via `fetch({ keepalive: true })`
|
|
8
|
+
* and metadata via `Platform` / `Dimensions` / `AsyncStorage`.
|
|
9
|
+
*
|
|
10
|
+
* Because `AsyncStorage` is asynchronous, visitor/session IDs are prewarmed during
|
|
11
|
+
* {@link Nexly.init} before the first event is sent.
|
|
12
|
+
*/
|
|
13
|
+
export class Nexly extends NexlyBase {
|
|
14
|
+
currentScreen = '/';
|
|
15
|
+
/**
|
|
16
|
+
* Creates (or replaces) the singleton **and awaits ID prewarming**.
|
|
17
|
+
* `NexlyProvider` awaits this before exposing a client via context.
|
|
18
|
+
*/
|
|
19
|
+
static async init(options) {
|
|
20
|
+
const instance = new Nexly(options);
|
|
21
|
+
Nexly.setInstance(instance);
|
|
22
|
+
if (instance.appId && instance.key) {
|
|
23
|
+
await prewarmIds();
|
|
24
|
+
}
|
|
25
|
+
return instance;
|
|
26
|
+
}
|
|
27
|
+
/** Returns the singleton, or `null` if not yet initialized. */
|
|
28
|
+
static getInstance() {
|
|
29
|
+
return NexlyBase.getInstance() ?? null;
|
|
30
|
+
}
|
|
31
|
+
constructor(init) {
|
|
32
|
+
super(init);
|
|
33
|
+
}
|
|
34
|
+
/** Sets the current screen name used as `path` for subsequent events. */
|
|
35
|
+
setScreen(name) {
|
|
36
|
+
const trimmed = (name ?? '').trim();
|
|
37
|
+
this.currentScreen = trimmed || '/';
|
|
38
|
+
}
|
|
39
|
+
/** Convenience: set screen and send a pageview in one call. */
|
|
40
|
+
screenview(name) {
|
|
41
|
+
this.setScreen(name);
|
|
42
|
+
return this.pageview(this.currentScreen);
|
|
43
|
+
}
|
|
44
|
+
transport(payload) {
|
|
45
|
+
return sendPayloadFetch(this.collectUrl, payload);
|
|
46
|
+
}
|
|
47
|
+
collectEventContext() {
|
|
48
|
+
return collectEventMeta();
|
|
49
|
+
}
|
|
50
|
+
collectSessionContext() {
|
|
51
|
+
return collectSessionMeta();
|
|
52
|
+
}
|
|
53
|
+
getDefaultPath() {
|
|
54
|
+
return this.currentScreen;
|
|
55
|
+
}
|
|
56
|
+
startEngagement() {
|
|
57
|
+
if (this.disabled)
|
|
58
|
+
return () => { };
|
|
59
|
+
return startEngagementTracking({
|
|
60
|
+
collectUrl: this.collectUrl,
|
|
61
|
+
appId: this.appId,
|
|
62
|
+
apiToken: this.key,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Loads (or generates) the persistent visitor ID and current session ID from storage.
|
|
3
|
+
* Call once before sending any event. Subsequent calls are idempotent.
|
|
4
|
+
*/
|
|
5
|
+
export declare function prewarmIds(): Promise<{
|
|
6
|
+
visitorId: string;
|
|
7
|
+
sessionId: string;
|
|
8
|
+
}>;
|
|
9
|
+
/** Touch the session timestamp; if the session expired, roll a new session ID. */
|
|
10
|
+
export declare function touchSession(): Promise<void>;
|
|
11
|
+
export declare function getCachedVisitorId(): string | null;
|
|
12
|
+
export declare function getCachedSessionId(): string | null;
|
|
13
|
+
//# sourceMappingURL=session.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"session.d.ts","sourceRoot":"","sources":["../src/session.ts"],"names":[],"mappings":"AAwBA;;;GAGG;AACH,wBAAsB,UAAU,IAAI,OAAO,CAAC;IAAE,SAAS,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAAC,CAuBpF;AAED,kFAAkF;AAClF,wBAAsB,YAAY,IAAI,OAAO,CAAC,IAAI,CAAC,CAYlD;AAED,wBAAgB,kBAAkB,IAAI,MAAM,GAAG,IAAI,CAElD;AAED,wBAAgB,kBAAkB,IAAI,MAAM,GAAG,IAAI,CAElD"}
|
package/dist/session.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { storageGet, storageSet } from './storage.js';
|
|
2
|
+
const VISITOR_KEY = 'trk_visitor_id';
|
|
3
|
+
const SESSION_KEY = 'trk_session_id';
|
|
4
|
+
const SESSION_TS_KEY = 'trk_session_ts';
|
|
5
|
+
const SESSION_TIMEOUT_MS = 30 * 60 * 1000; // 30 minutes
|
|
6
|
+
function generateId() {
|
|
7
|
+
if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
|
|
8
|
+
return crypto.randomUUID();
|
|
9
|
+
}
|
|
10
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
11
|
+
const r = (Math.random() * 16) | 0;
|
|
12
|
+
return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
/** In-memory cache prewarmed by {@link prewarmIds}; safe to read sync after init. */
|
|
16
|
+
let cachedVisitorId = null;
|
|
17
|
+
let cachedSessionId = null;
|
|
18
|
+
/**
|
|
19
|
+
* Loads (or generates) the persistent visitor ID and current session ID from storage.
|
|
20
|
+
* Call once before sending any event. Subsequent calls are idempotent.
|
|
21
|
+
*/
|
|
22
|
+
export async function prewarmIds() {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
let visitorId = cachedVisitorId ?? (await storageGet(VISITOR_KEY));
|
|
25
|
+
if (!visitorId) {
|
|
26
|
+
visitorId = generateId();
|
|
27
|
+
await storageSet(VISITOR_KEY, visitorId);
|
|
28
|
+
}
|
|
29
|
+
cachedVisitorId = visitorId;
|
|
30
|
+
const [existing, tsRaw] = await Promise.all([storageGet(SESSION_KEY), storageGet(SESSION_TS_KEY)]);
|
|
31
|
+
const lastTs = tsRaw ? Number(tsRaw) : 0;
|
|
32
|
+
let sessionId;
|
|
33
|
+
if (existing && lastTs && now - lastTs < SESSION_TIMEOUT_MS) {
|
|
34
|
+
sessionId = existing;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
sessionId = generateId();
|
|
38
|
+
await storageSet(SESSION_KEY, sessionId);
|
|
39
|
+
}
|
|
40
|
+
await storageSet(SESSION_TS_KEY, String(now));
|
|
41
|
+
cachedSessionId = sessionId;
|
|
42
|
+
return { visitorId, sessionId };
|
|
43
|
+
}
|
|
44
|
+
/** Touch the session timestamp; if the session expired, roll a new session ID. */
|
|
45
|
+
export async function touchSession() {
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
const tsRaw = await storageGet(SESSION_TS_KEY);
|
|
48
|
+
const lastTs = tsRaw ? Number(tsRaw) : 0;
|
|
49
|
+
if (cachedSessionId && lastTs && now - lastTs < SESSION_TIMEOUT_MS) {
|
|
50
|
+
await storageSet(SESSION_TS_KEY, String(now));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const id = generateId();
|
|
54
|
+
cachedSessionId = id;
|
|
55
|
+
await storageSet(SESSION_KEY, id);
|
|
56
|
+
await storageSet(SESSION_TS_KEY, String(now));
|
|
57
|
+
}
|
|
58
|
+
export function getCachedVisitorId() {
|
|
59
|
+
return cachedVisitorId;
|
|
60
|
+
}
|
|
61
|
+
export function getCachedSessionId() {
|
|
62
|
+
return cachedSessionId;
|
|
63
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin wrapper around `@react-native-async-storage/async-storage` with an in-memory fallback
|
|
3
|
+
* so the SDK does not hard-crash if the peer dependency is missing (e.g. in tests).
|
|
4
|
+
*/
|
|
5
|
+
export declare function storageGet(key: string): Promise<string | null>;
|
|
6
|
+
export declare function storageSet(key: string, value: string): Promise<void>;
|
|
7
|
+
//# sourceMappingURL=storage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"storage.d.ts","sourceRoot":"","sources":["../src/storage.ts"],"names":[],"mappings":"AAAA;;;GAGG;AA8CH,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAOpE;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAO1E"}
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin wrapper around `@react-native-async-storage/async-storage` with an in-memory fallback
|
|
3
|
+
* so the SDK does not hard-crash if the peer dependency is missing (e.g. in tests).
|
|
4
|
+
*/
|
|
5
|
+
let storage = null;
|
|
6
|
+
let loadAttempted = false;
|
|
7
|
+
const memory = new Map();
|
|
8
|
+
const memoryStorage = {
|
|
9
|
+
async getItem(key) {
|
|
10
|
+
return memory.get(key) ?? null;
|
|
11
|
+
},
|
|
12
|
+
async setItem(key, value) {
|
|
13
|
+
memory.set(key, value);
|
|
14
|
+
},
|
|
15
|
+
async removeItem(key) {
|
|
16
|
+
memory.delete(key);
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
async function loadAsyncStorage() {
|
|
20
|
+
if (storage)
|
|
21
|
+
return storage;
|
|
22
|
+
if (loadAttempted)
|
|
23
|
+
return memoryStorage;
|
|
24
|
+
loadAttempted = true;
|
|
25
|
+
try {
|
|
26
|
+
const mod = (await import('@react-native-async-storage/async-storage'));
|
|
27
|
+
if (mod?.default) {
|
|
28
|
+
storage = mod.default;
|
|
29
|
+
return storage;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
// peer dep not installed — fall through
|
|
34
|
+
}
|
|
35
|
+
console.warn('[Nexly] @react-native-async-storage/async-storage is not installed; visitor/session IDs will not persist.');
|
|
36
|
+
storage = memoryStorage;
|
|
37
|
+
return storage;
|
|
38
|
+
}
|
|
39
|
+
export async function storageGet(key) {
|
|
40
|
+
const s = await loadAsyncStorage();
|
|
41
|
+
try {
|
|
42
|
+
return await s.getItem(key);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
export async function storageSet(key, value) {
|
|
49
|
+
const s = await loadAsyncStorage();
|
|
50
|
+
try {
|
|
51
|
+
await s.setItem(key, value);
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// ignore
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fire-and-forget JSON POST to the collector. Uses `fetch` with `keepalive: true`
|
|
3
|
+
* so requests survive app backgrounding on supported runtimes; errors are swallowed
|
|
4
|
+
* (analytics must not crash the host app).
|
|
5
|
+
*/
|
|
6
|
+
export declare function sendPayloadFetch(url: string, payload: Record<string, unknown>): boolean;
|
|
7
|
+
//# sourceMappingURL=transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transport.d.ts","sourceRoot":"","sources":["../src/transport.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAavF"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fire-and-forget JSON POST to the collector. Uses `fetch` with `keepalive: true`
|
|
3
|
+
* so requests survive app backgrounding on supported runtimes; errors are swallowed
|
|
4
|
+
* (analytics must not crash the host app).
|
|
5
|
+
*/
|
|
6
|
+
export function sendPayloadFetch(url, payload) {
|
|
7
|
+
if (!url)
|
|
8
|
+
return false;
|
|
9
|
+
try {
|
|
10
|
+
void fetch(url, {
|
|
11
|
+
method: 'POST',
|
|
12
|
+
headers: { 'Content-Type': 'application/json' },
|
|
13
|
+
body: JSON.stringify(payload),
|
|
14
|
+
keepalive: true,
|
|
15
|
+
}).catch(() => { });
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@nexly/react-native",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "React Native bindings for Nexly: fetch transport, AsyncStorage IDs, AppState engagement on top of @nexly/core",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"nexly",
|
|
8
|
+
"react-native",
|
|
9
|
+
"analytics",
|
|
10
|
+
"tracking",
|
|
11
|
+
"mobile",
|
|
12
|
+
"ingest"
|
|
13
|
+
],
|
|
14
|
+
"type": "module",
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"files": [
|
|
17
|
+
"dist",
|
|
18
|
+
"README.md",
|
|
19
|
+
"LICENSE"
|
|
20
|
+
],
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public"
|
|
23
|
+
},
|
|
24
|
+
"main": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js",
|
|
30
|
+
"default": "./dist/index.js"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc -p tsconfig.build.json",
|
|
35
|
+
"clean": "rm -rf dist"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"node": ">=20"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"@react-native-async-storage/async-storage": ">=1.21.0",
|
|
42
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
43
|
+
"react-native": ">=0.72.0"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@nexly/core": "0.9.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@react-native-async-storage/async-storage": "^2.1.2",
|
|
50
|
+
"@types/react": "^19.2.14",
|
|
51
|
+
"react": "^19.1.0",
|
|
52
|
+
"react-native": "^0.76.5",
|
|
53
|
+
"typescript": "~6.0.2"
|
|
54
|
+
}
|
|
55
|
+
}
|