@lunardinos/tanstack-router 0.2.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 +94 -0
- package/dist/index.js +142 -0
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/provider.d.ts +18 -0
- package/dist/src/provider.d.ts.map +1 -0
- package/dist/src/provider.js +142 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
# @lunardinos/tanstack-router
|
|
2
|
+
|
|
3
|
+
[Lunar Dinos](https://lunardinos.com) session recording SDK for apps using [TanStack Router](https://tanstack.com/router).
|
|
4
|
+
|
|
5
|
+
Captures session replays, page navigations, clicks, inputs, and errors — with zero configuration beyond wrapping your app.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @lunardinos/tanstack-router
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Setup
|
|
14
|
+
|
|
15
|
+
Wrap your app with `LunarDinosProvider` inside your router context:
|
|
16
|
+
|
|
17
|
+
```tsx
|
|
18
|
+
import { LunarDinosProvider } from "@lunardinos/tanstack-router"
|
|
19
|
+
|
|
20
|
+
function App() {
|
|
21
|
+
return (
|
|
22
|
+
<LunarDinosProvider apiKey="your-api-key">
|
|
23
|
+
<Outlet />
|
|
24
|
+
</LunarDinosProvider>
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Route changes are tracked automatically via TanStack Router's `useMatches()`.
|
|
30
|
+
|
|
31
|
+
## Identifying users
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { useLunarDinos } from "@lunardinos/tanstack-router"
|
|
35
|
+
|
|
36
|
+
function Dashboard() {
|
|
37
|
+
const { identify } = useLunarDinos()
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
identify({
|
|
41
|
+
user: { id: "user-123", email: "jane@acme.com" },
|
|
42
|
+
account: { id: "acme", name: "Acme Inc" },
|
|
43
|
+
})
|
|
44
|
+
}, [])
|
|
45
|
+
|
|
46
|
+
return <div>...</div>
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Cookie consent
|
|
51
|
+
|
|
52
|
+
By default the recorder persists session and visitor IDs in cookies. If your site requires consent before setting cookies, start in `cookieless` mode and upgrade after consent:
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import { LunarDinosProvider, useLunarDinos } from "@lunardinos/tanstack-router"
|
|
56
|
+
|
|
57
|
+
// 1. Start without cookies — recording works, but sessions are not persisted
|
|
58
|
+
// across page loads. Identity is handled server-side via hashing.
|
|
59
|
+
function App() {
|
|
60
|
+
return (
|
|
61
|
+
<LunarDinosProvider apiKey="your-api-key" defaultPersistence="cookieless">
|
|
62
|
+
<Outlet />
|
|
63
|
+
</LunarDinosProvider>
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 2. After consent, switch to cookie-based persistence.
|
|
68
|
+
// The server automatically links the previous server-side identity
|
|
69
|
+
// to the new cookie-based one.
|
|
70
|
+
function CookieBanner() {
|
|
71
|
+
const { setPersistence } = useLunarDinos()
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<button onClick={() => setPersistence("cookie")}>
|
|
75
|
+
Accept cookies
|
|
76
|
+
</button>
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
In `cookieless` mode no data is stored on the user's device. Each page load gets a fresh session ID and the server stitches sessions together using a visitor hash, referrer, and timing. Note that recording and data collection still happen — only client-side storage is removed.
|
|
82
|
+
|
|
83
|
+
## Props
|
|
84
|
+
|
|
85
|
+
| Prop | Type | Default | Description |
|
|
86
|
+
|------|------|---------|-------------|
|
|
87
|
+
| `apiKey` | `string` | required | Your Lunar Dinos API key |
|
|
88
|
+
| `endpoint` | `string` | `https://ingest.lunardinos.com` | Custom ingestion endpoint |
|
|
89
|
+
| `enabled` | `boolean` | `true` | Disable recording (e.g. in dev) |
|
|
90
|
+
| `defaultPersistence` | `"cookie" \| "cookieless"` | `"cookie"` | Initial persistence mode — can be changed at runtime via `setPersistence()` |
|
|
91
|
+
|
|
92
|
+
## Learn more
|
|
93
|
+
|
|
94
|
+
Visit [lunardinos.com](https://lunardinos.com) to get started.
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// src/provider.tsx
|
|
2
|
+
import {
|
|
3
|
+
createLunarDinosRecorder
|
|
4
|
+
} from "@lunardinos/browser";
|
|
5
|
+
import { useMatches } from "@tanstack/react-router";
|
|
6
|
+
import {
|
|
7
|
+
Component,
|
|
8
|
+
createContext,
|
|
9
|
+
useContext,
|
|
10
|
+
useEffect,
|
|
11
|
+
useMemo,
|
|
12
|
+
useRef
|
|
13
|
+
} from "react";
|
|
14
|
+
import { jsx } from "react/jsx-runtime";
|
|
15
|
+
var RecorderContext = createContext(null);
|
|
16
|
+
function useLunarDinos() {
|
|
17
|
+
const ctx = useContext(RecorderContext);
|
|
18
|
+
if (!ctx)
|
|
19
|
+
throw new Error("useLunarDinos must be used within LunarDinosProvider");
|
|
20
|
+
return ctx;
|
|
21
|
+
}
|
|
22
|
+
function buildRoutePattern(matches) {
|
|
23
|
+
const deepest = matches[matches.length - 1];
|
|
24
|
+
return deepest?.fullPath ?? deepest?.id ?? "/";
|
|
25
|
+
}
|
|
26
|
+
function extractParams(matches) {
|
|
27
|
+
const deepest = matches[matches.length - 1];
|
|
28
|
+
const raw = deepest?.params;
|
|
29
|
+
if (!raw || Object.keys(raw).length === 0)
|
|
30
|
+
return null;
|
|
31
|
+
return Object.fromEntries(Object.entries(raw).filter(([, v]) => v != null).map(([k, v]) => [k, String(v)]));
|
|
32
|
+
}
|
|
33
|
+
var noopContextValue = {
|
|
34
|
+
identify: () => {},
|
|
35
|
+
setPersistence: () => {}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
class RecorderErrorBoundary extends Component {
|
|
39
|
+
state = { hasError: false };
|
|
40
|
+
static getDerivedStateFromError() {
|
|
41
|
+
return { hasError: true };
|
|
42
|
+
}
|
|
43
|
+
componentDidCatch(_error, _info) {}
|
|
44
|
+
render() {
|
|
45
|
+
return this.props.children;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function RecorderInner({
|
|
49
|
+
apiKey,
|
|
50
|
+
endpoint,
|
|
51
|
+
enabled = true,
|
|
52
|
+
defaultPersistence,
|
|
53
|
+
children
|
|
54
|
+
}) {
|
|
55
|
+
const matches = useMatches();
|
|
56
|
+
const recorderRef = useRef(null);
|
|
57
|
+
const prevRouteRef = useRef(null);
|
|
58
|
+
const matchesRef = useRef(matches);
|
|
59
|
+
matchesRef.current = matches;
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
if (!enabled)
|
|
62
|
+
return;
|
|
63
|
+
try {
|
|
64
|
+
const initMatches = matchesRef.current;
|
|
65
|
+
const routePattern = buildRoutePattern(initMatches);
|
|
66
|
+
const params = extractParams(initMatches);
|
|
67
|
+
const recorder = createLunarDinosRecorder({ apiKey, endpoint, defaultPersistence });
|
|
68
|
+
recorderRef.current = recorder;
|
|
69
|
+
prevRouteRef.current = routePattern;
|
|
70
|
+
recorder.start({
|
|
71
|
+
pageMetadata: {
|
|
72
|
+
href: window.location.href,
|
|
73
|
+
title: document.title,
|
|
74
|
+
routePattern,
|
|
75
|
+
params
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
const onBeforeUnload = () => {
|
|
79
|
+
try {
|
|
80
|
+
recorder.flush({ beacon: true });
|
|
81
|
+
recorder.stop();
|
|
82
|
+
} catch {}
|
|
83
|
+
};
|
|
84
|
+
window.addEventListener("beforeunload", onBeforeUnload);
|
|
85
|
+
return () => {
|
|
86
|
+
window.removeEventListener("beforeunload", onBeforeUnload);
|
|
87
|
+
try {
|
|
88
|
+
recorder.flush({ beacon: true });
|
|
89
|
+
recorder.stop();
|
|
90
|
+
} catch {}
|
|
91
|
+
recorderRef.current = null;
|
|
92
|
+
};
|
|
93
|
+
} catch {
|
|
94
|
+
recorderRef.current = null;
|
|
95
|
+
}
|
|
96
|
+
}, [apiKey, endpoint, enabled, defaultPersistence]);
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
try {
|
|
99
|
+
const recorder = recorderRef.current;
|
|
100
|
+
if (!enabled || !recorder || !recorder.isRecording())
|
|
101
|
+
return;
|
|
102
|
+
const routePattern = buildRoutePattern(matches);
|
|
103
|
+
if (routePattern === prevRouteRef.current)
|
|
104
|
+
return;
|
|
105
|
+
prevRouteRef.current = routePattern;
|
|
106
|
+
const params = extractParams(matches);
|
|
107
|
+
recorder.emitPageEvent({
|
|
108
|
+
href: window.location.href,
|
|
109
|
+
title: document.title,
|
|
110
|
+
routePattern,
|
|
111
|
+
params
|
|
112
|
+
});
|
|
113
|
+
} catch {}
|
|
114
|
+
}, [matches, enabled]);
|
|
115
|
+
const contextValue = useMemo(() => enabled ? {
|
|
116
|
+
identify: (payload) => {
|
|
117
|
+
try {
|
|
118
|
+
recorderRef.current?.identify(payload);
|
|
119
|
+
} catch {}
|
|
120
|
+
},
|
|
121
|
+
setPersistence: (mode) => {
|
|
122
|
+
try {
|
|
123
|
+
recorderRef.current?.setPersistence(mode);
|
|
124
|
+
} catch {}
|
|
125
|
+
}
|
|
126
|
+
} : noopContextValue, [enabled]);
|
|
127
|
+
return /* @__PURE__ */ jsx(RecorderContext.Provider, {
|
|
128
|
+
value: contextValue,
|
|
129
|
+
children
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
function LunarDinosProvider(props) {
|
|
133
|
+
return /* @__PURE__ */ jsx(RecorderErrorBoundary, {
|
|
134
|
+
children: /* @__PURE__ */ jsx(RecorderInner, {
|
|
135
|
+
...props
|
|
136
|
+
})
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
export {
|
|
140
|
+
useLunarDinos,
|
|
141
|
+
LunarDinosProvider
|
|
142
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AACjD,OAAO,EAAE,kBAAkB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { LunarDinosProvider, useLunarDinos } from "./provider";
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type IdentifyPayload, type PersistenceMode } from "@lunardinos/browser";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
export type { IdentifyPayload, PersistenceMode } from "@lunardinos/browser";
|
|
4
|
+
interface RecorderContextValue {
|
|
5
|
+
identify: (payload: IdentifyPayload) => void;
|
|
6
|
+
setPersistence: (mode: PersistenceMode) => void;
|
|
7
|
+
}
|
|
8
|
+
export declare function useLunarDinos(): RecorderContextValue;
|
|
9
|
+
interface RecorderProviderProps {
|
|
10
|
+
apiKey: string;
|
|
11
|
+
endpoint?: string;
|
|
12
|
+
enabled?: boolean;
|
|
13
|
+
/** Initial persistence mode: 'cookie' (default) or 'cookieless'. Can be changed at runtime via setPersistence(). */
|
|
14
|
+
defaultPersistence?: PersistenceMode;
|
|
15
|
+
children: ReactNode;
|
|
16
|
+
}
|
|
17
|
+
export declare function LunarDinosProvider(props: RecorderProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
18
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../../src/provider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,eAAe,EACpB,KAAK,eAAe,EAErB,MAAM,qBAAqB,CAAA;AAE5B,OAAO,EAIL,KAAK,SAAS,EAKf,MAAM,OAAO,CAAA;AAEd,YAAY,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAE3E,UAAU,oBAAoB;IAC5B,QAAQ,EAAE,CAAC,OAAO,EAAE,eAAe,KAAK,IAAI,CAAA;IAC5C,cAAc,EAAE,CAAC,IAAI,EAAE,eAAe,KAAK,IAAI,CAAA;CAChD;AAID,wBAAgB,aAAa,IAAI,oBAAoB,CAIpD;AAED,UAAU,qBAAqB;IAC7B,MAAM,EAAE,MAAM,CAAA;IACd,QAAQ,CAAC,EAAE,MAAM,CAAA;IACjB,OAAO,CAAC,EAAE,OAAO,CAAA;IACjB,oHAAoH;IACpH,kBAAkB,CAAC,EAAE,eAAe,CAAA;IACpC,QAAQ,EAAE,SAAS,CAAA;CACpB;AAuJD,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,qBAAqB,2CAM9D"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { createLunarDinosRecorder, } from "@lunardinos/browser";
|
|
3
|
+
import { useMatches } from "@tanstack/react-router";
|
|
4
|
+
import { Component, createContext, useContext, useEffect, useMemo, useRef, } from "react";
|
|
5
|
+
const RecorderContext = createContext(null);
|
|
6
|
+
export function useLunarDinos() {
|
|
7
|
+
const ctx = useContext(RecorderContext);
|
|
8
|
+
if (!ctx)
|
|
9
|
+
throw new Error("useLunarDinos must be used within LunarDinosProvider");
|
|
10
|
+
return ctx;
|
|
11
|
+
}
|
|
12
|
+
function buildRoutePattern(matches) {
|
|
13
|
+
const deepest = matches[matches.length - 1];
|
|
14
|
+
return deepest?.fullPath ?? deepest?.id ?? "/";
|
|
15
|
+
}
|
|
16
|
+
function extractParams(matches) {
|
|
17
|
+
const deepest = matches[matches.length - 1];
|
|
18
|
+
const raw = deepest?.params;
|
|
19
|
+
if (!raw || Object.keys(raw).length === 0)
|
|
20
|
+
return null;
|
|
21
|
+
return Object.fromEntries(Object.entries(raw)
|
|
22
|
+
.filter(([, v]) => v != null)
|
|
23
|
+
.map(([k, v]) => [k, String(v)]));
|
|
24
|
+
}
|
|
25
|
+
const noopContextValue = {
|
|
26
|
+
identify: () => { },
|
|
27
|
+
setPersistence: () => { },
|
|
28
|
+
};
|
|
29
|
+
/** Error boundary that swallows SDK errors and renders children unaffected. */
|
|
30
|
+
class RecorderErrorBoundary extends Component {
|
|
31
|
+
state = { hasError: false };
|
|
32
|
+
static getDerivedStateFromError() {
|
|
33
|
+
return { hasError: true };
|
|
34
|
+
}
|
|
35
|
+
componentDidCatch(_error, _info) {
|
|
36
|
+
// Silently swallow — the SDK should never crash the host app
|
|
37
|
+
}
|
|
38
|
+
render() {
|
|
39
|
+
return this.props.children;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function RecorderInner({ apiKey, endpoint, enabled = true, defaultPersistence, children, }) {
|
|
43
|
+
const matches = useMatches();
|
|
44
|
+
const recorderRef = useRef(null);
|
|
45
|
+
const prevRouteRef = useRef(null);
|
|
46
|
+
const matchesRef = useRef(matches);
|
|
47
|
+
matchesRef.current = matches;
|
|
48
|
+
// Initialize recorder on mount — pass initial page metadata so the
|
|
49
|
+
// first rrweb snapshot already carries route info (avoids race where
|
|
50
|
+
// rrweb hasn't set recording=true yet when emitPageEvent is called).
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (!enabled)
|
|
53
|
+
return;
|
|
54
|
+
try {
|
|
55
|
+
const initMatches = matchesRef.current;
|
|
56
|
+
const routePattern = buildRoutePattern(initMatches);
|
|
57
|
+
const params = extractParams(initMatches);
|
|
58
|
+
const recorder = createLunarDinosRecorder({ apiKey, endpoint, defaultPersistence });
|
|
59
|
+
recorderRef.current = recorder;
|
|
60
|
+
prevRouteRef.current = routePattern;
|
|
61
|
+
recorder.start({
|
|
62
|
+
pageMetadata: {
|
|
63
|
+
href: window.location.href,
|
|
64
|
+
title: document.title,
|
|
65
|
+
routePattern,
|
|
66
|
+
params,
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
const onBeforeUnload = () => {
|
|
70
|
+
try {
|
|
71
|
+
recorder.flush({ beacon: true });
|
|
72
|
+
recorder.stop();
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
/* swallow */
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
window.addEventListener("beforeunload", onBeforeUnload);
|
|
79
|
+
return () => {
|
|
80
|
+
window.removeEventListener("beforeunload", onBeforeUnload);
|
|
81
|
+
try {
|
|
82
|
+
recorder.flush({ beacon: true });
|
|
83
|
+
recorder.stop();
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
/* swallow */
|
|
87
|
+
}
|
|
88
|
+
recorderRef.current = null;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// SDK init failed — degrade silently
|
|
93
|
+
recorderRef.current = null;
|
|
94
|
+
}
|
|
95
|
+
}, [apiKey, endpoint, enabled, defaultPersistence]);
|
|
96
|
+
// Emit page event on subsequent navigations (initial page handled by start())
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
try {
|
|
99
|
+
const recorder = recorderRef.current;
|
|
100
|
+
if (!enabled || !recorder || !recorder.isRecording())
|
|
101
|
+
return;
|
|
102
|
+
const routePattern = buildRoutePattern(matches);
|
|
103
|
+
if (routePattern === prevRouteRef.current)
|
|
104
|
+
return;
|
|
105
|
+
prevRouteRef.current = routePattern;
|
|
106
|
+
const params = extractParams(matches);
|
|
107
|
+
recorder.emitPageEvent({
|
|
108
|
+
href: window.location.href,
|
|
109
|
+
title: document.title,
|
|
110
|
+
routePattern,
|
|
111
|
+
params,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
/* swallow */
|
|
116
|
+
}
|
|
117
|
+
}, [matches, enabled]);
|
|
118
|
+
const contextValue = useMemo(() => enabled
|
|
119
|
+
? {
|
|
120
|
+
identify: (payload) => {
|
|
121
|
+
try {
|
|
122
|
+
recorderRef.current?.identify(payload);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
/* swallow */
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
setPersistence: (mode) => {
|
|
129
|
+
try {
|
|
130
|
+
recorderRef.current?.setPersistence(mode);
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
/* swallow */
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
}
|
|
137
|
+
: noopContextValue, [enabled]);
|
|
138
|
+
return _jsx(RecorderContext.Provider, { value: contextValue, children: children });
|
|
139
|
+
}
|
|
140
|
+
export function LunarDinosProvider(props) {
|
|
141
|
+
return (_jsx(RecorderErrorBoundary, { children: _jsx(RecorderInner, { ...props }) }));
|
|
142
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lunardinos/tanstack-router",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"import": "./dist/index.js",
|
|
8
|
+
"types": "./dist/src/index.d.ts"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"dist",
|
|
13
|
+
"!dist/*.tsbuildinfo"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "rm -rf dist && bun run build.ts"
|
|
17
|
+
},
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@lunardinos/browser": "^0.1.9"
|
|
23
|
+
},
|
|
24
|
+
"devDependencies": {
|
|
25
|
+
"@types/react": "^19"
|
|
26
|
+
},
|
|
27
|
+
"peerDependencies": {
|
|
28
|
+
"@tanstack/react-router": ">=1",
|
|
29
|
+
"react": ">=18"
|
|
30
|
+
}
|
|
31
|
+
}
|