@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 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,3 @@
1
+ export type { IdentifyPayload } from "./provider";
2
+ export { LunarDinosProvider, useLunarDinos } from "./provider";
3
+ //# sourceMappingURL=index.d.ts.map
@@ -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
+ }