luchy 0.1.0 → 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 +59 -0
- package/dist/react-router.d.ts +164 -0
- package/dist/react-router.d.ts.map +1 -0
- package/dist/react-router.js +107 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -203,6 +203,65 @@ client can be regenerated without a dashboard running anywhere. `schema.d.ts`
|
|
|
203
203
|
is generated too — never edit either by hand. Change the zod schemas in
|
|
204
204
|
`apps/dash`, re-run both steps, commit the result.
|
|
205
205
|
|
|
206
|
+
## React Router on Cloudflare (`luchy/react-router`)
|
|
207
|
+
|
|
208
|
+
A React Router app already tells you what it did — in the request. Every
|
|
209
|
+
console mutation is a form POST whose `intent` field names it (`rotate`,
|
|
210
|
+
`invite-member`, `create-api-key`, …), every JSON API mutation is
|
|
211
|
+
discriminated by its method, and every auth verb has a path that names the
|
|
212
|
+
operation. So the event does not need to be emitted by hand from a route
|
|
213
|
+
module, where it is only tracked once somebody remembers to instrument it: the
|
|
214
|
+
Worker *derives* it from the request it is already holding. One hook, zero
|
|
215
|
+
per-route code, and a new intent is tracked the day it is written.
|
|
216
|
+
|
|
217
|
+
Two lines in the Worker's `fetch`:
|
|
218
|
+
|
|
219
|
+
```ts
|
|
220
|
+
import { createRequestTracker } from 'luchy/react-router';
|
|
221
|
+
|
|
222
|
+
const tracker = createRequestTracker({
|
|
223
|
+
apiKey: LUCHY_API_KEY,
|
|
224
|
+
enabled: env.APP_ENV === 'production'
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
export default {
|
|
228
|
+
async fetch(request, env, ctx) {
|
|
229
|
+
const finish = tracker.begin(request);
|
|
230
|
+
const response = await requestHandler(request, loadContext);
|
|
231
|
+
finish(response, ctx);
|
|
232
|
+
return response;
|
|
233
|
+
}
|
|
234
|
+
} satisfies ExportedHandler<Env>;
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
`begin` has to run *before* React Router gets the request: it is what clones
|
|
238
|
+
the body, and once the handler has read it, it is gone. `finish` schedules
|
|
239
|
+
everything else on `ctx.waitUntil`, so nothing about analytics is on the
|
|
240
|
+
response's critical path, and nothing it does can throw into your handler.
|
|
241
|
+
|
|
242
|
+
Events come out named `route:intent` — `team/:id:invite-member`,
|
|
243
|
+
`applications:create` — with ids collapsed to `:id`, React Router's
|
|
244
|
+
single-fetch `.data` suffix stripped (so a hydrated and a non-hydrated
|
|
245
|
+
submission report the same name), and `/` mapped to `home`. The payload always
|
|
246
|
+
carries `status`.
|
|
247
|
+
|
|
248
|
+
| Option | Default | What it does |
|
|
249
|
+
| --- | --- | --- |
|
|
250
|
+
| `apiKey` | — | Luchy API key. The public ingest key is fine. |
|
|
251
|
+
| `endpoint` | hosted API | API root, without a trailing slash. |
|
|
252
|
+
| `enabled` | `true` | When false, everything is a no-op — no clone, no network. |
|
|
253
|
+
| `ignorePrefixes` | `['/__manifest']` | Raw-pathname prefixes to drop. Yours are added to the default, not swapped for it. |
|
|
254
|
+
| `ignoreRouteSuffixes` | `[]` | Normalized-route suffixes to drop, e.g. `/user-keys/validate`. |
|
|
255
|
+
| `ignoreEvents` | `[]` | Fully-formed event names to drop, e.g. `notifications:markRead`. |
|
|
256
|
+
| `trackFailures` | `false` | When true, 4xx/5xx are tracked too (tell them apart by `status`). |
|
|
257
|
+
| `methodSuffix` | `false` | When true, an intent-less mutation is `route:post` / `route:delete` instead of bare `route`. |
|
|
258
|
+
| `payload` | — | `(request, response) => payload`, merged over `status`. Runs inside `waitUntil`; a rejection costs the payload, not the event. |
|
|
259
|
+
| `onError` | — | The only way to see failures. |
|
|
260
|
+
|
|
261
|
+
The pieces are exported on their own too — `isMutatingMethod`,
|
|
262
|
+
`carriesIntent`, `normalizeRoute`, `serverEventName` — if you want the naming
|
|
263
|
+
without the hook.
|
|
264
|
+
|
|
206
265
|
## Development
|
|
207
266
|
|
|
208
267
|
### Build Scripts
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* React Router (v7+) on Cloudflare Workers — server events derived from
|
|
3
|
+
* requests.
|
|
4
|
+
*
|
|
5
|
+
* A React Router app already says what it did, in the request: every console
|
|
6
|
+
* mutation is a form POST whose `intent` field names it (`rotate`,
|
|
7
|
+
* `invite-member`, `create-api-key`, …), every JSON API mutation is
|
|
8
|
+
* discriminated by its method, and every auth verb has a path that names the
|
|
9
|
+
* operation. So instead of sprinkling `trackEvent` calls across route modules
|
|
10
|
+
* — where a new intent is only tracked once somebody remembers to instrument
|
|
11
|
+
* it — the Worker derives the event from the request it is already holding:
|
|
12
|
+
* normalized route + intent (or method). One hook, zero per-route code, and a
|
|
13
|
+
* new intent is tracked the day it is written.
|
|
14
|
+
*
|
|
15
|
+
* This module is server-only but not Cloudflare-only: the flush context is
|
|
16
|
+
* structural (`{ waitUntil }`), so anything with that shape works.
|
|
17
|
+
*
|
|
18
|
+
* @module
|
|
19
|
+
*/
|
|
20
|
+
/** A property bag. Nested objects are not supported by the ingest API. */
|
|
21
|
+
export type LuchyRequestPayload = Record<string, string | number | boolean>;
|
|
22
|
+
/**
|
|
23
|
+
* True for the methods worth an event.
|
|
24
|
+
*
|
|
25
|
+
* Reads are the browser tracker's job; only mutations describe what the app
|
|
26
|
+
* did.
|
|
27
|
+
*
|
|
28
|
+
* ```ts
|
|
29
|
+
* isMutatingMethod('post'); // true
|
|
30
|
+
* isMutatingMethod('GET'); // false
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export declare function isMutatingMethod(method: string): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* True when the request body is worth parsing for an `intent` field.
|
|
36
|
+
*
|
|
37
|
+
* Only urlencoded bodies are read: a multipart upload could be megabytes, and
|
|
38
|
+
* no route reads an intent out of one.
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* carriesIntent(request.headers.get('content-type'));
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function carriesIntent(contentType: string | null | undefined): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Turns a pathname into a route name: `/users/aBc123.../identities` →
|
|
47
|
+
* `users/:id/identities`.
|
|
48
|
+
*
|
|
49
|
+
* Client-side submissions land on React Router's single-fetch URL
|
|
50
|
+
* (`/users/abc.data`), so the `.data` suffix is stripped — otherwise the same
|
|
51
|
+
* action would report under two names depending on whether the page was
|
|
52
|
+
* hydrated. `/` and the single-fetch root (`/_root.data`) both become `home`.
|
|
53
|
+
*/
|
|
54
|
+
export declare function normalizeRoute(pathname: string): string;
|
|
55
|
+
/** What `serverEventName` needs to know about a finished request. */
|
|
56
|
+
export type ServerEventInput = {
|
|
57
|
+
method: string;
|
|
58
|
+
/** The raw pathname, before normalization. */
|
|
59
|
+
pathname: string;
|
|
60
|
+
/** The response status. Only consulted when `trackFailures` is false. */
|
|
61
|
+
status: number;
|
|
62
|
+
/** The submitted `intent`, if the body carried one. */
|
|
63
|
+
intent?: string | null;
|
|
64
|
+
};
|
|
65
|
+
/** The naming and filtering knobs, shared by the tracker and the pure helper. */
|
|
66
|
+
export type ServerEventNameConfig = Pick<RequestTrackerOptions, 'ignorePrefixes' | 'ignoreRouteSuffixes' | 'ignoreEvents' | 'trackFailures' | 'methodSuffix'>;
|
|
67
|
+
/**
|
|
68
|
+
* The event name for a mutation, or `null` when the request is not worth an
|
|
69
|
+
* event (a read, machine traffic, an ignored route, a failure the app does not
|
|
70
|
+
* track).
|
|
71
|
+
*
|
|
72
|
+
* Names are `route:intent` — an intent alone is ambiguous (`delete` and
|
|
73
|
+
* `update` appear on several routes), the pair is not. Without an intent the
|
|
74
|
+
* name is the bare route, or `route:post` / `route:delete` when
|
|
75
|
+
* `methodSuffix` is on, which keeps a REST route's methods distinct.
|
|
76
|
+
*
|
|
77
|
+
* ```ts
|
|
78
|
+
* serverEventName({ method: 'POST', pathname: '/team', status: 200, intent: 'invite' });
|
|
79
|
+
* // 'team:invite'
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export declare function serverEventName(input: ServerEventInput, config?: ServerEventNameConfig): string | null;
|
|
83
|
+
export type RequestTrackerOptions = {
|
|
84
|
+
/** A Luchy API key. The public ingest key is fine — it ships in the HTML. */
|
|
85
|
+
apiKey: string;
|
|
86
|
+
/** The API root, without a trailing slash. Defaults to the hosted API. */
|
|
87
|
+
endpoint?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Defaults to `true`. When false every call is a no-op — no clone, no
|
|
90
|
+
* request read, no network — so apps can pass
|
|
91
|
+
* `env.APP_ENV === 'production'` and leave the wiring in place.
|
|
92
|
+
*/
|
|
93
|
+
enabled?: boolean;
|
|
94
|
+
/**
|
|
95
|
+
* Raw-pathname prefixes to drop, matched before normalization. Merged with
|
|
96
|
+
* the built-in defaults (`/__manifest`) rather than replacing them.
|
|
97
|
+
*/
|
|
98
|
+
ignorePrefixes?: string[];
|
|
99
|
+
/**
|
|
100
|
+
* Normalized-route suffixes to drop, e.g. `/user-keys/validate`. Matched on
|
|
101
|
+
* the route so that literal app ids in the path do not each need an entry.
|
|
102
|
+
*/
|
|
103
|
+
ignoreRouteSuffixes?: string[];
|
|
104
|
+
/** Fully-formed event names to drop, e.g. `notifications:markRead`. */
|
|
105
|
+
ignoreEvents?: string[];
|
|
106
|
+
/**
|
|
107
|
+
* Defaults to `false`: a 4xx/5xx produces no event. Turn it on when failed
|
|
108
|
+
* mutations are themselves product signal (the `status` payload field tells
|
|
109
|
+
* them apart).
|
|
110
|
+
*/
|
|
111
|
+
trackFailures?: boolean;
|
|
112
|
+
/**
|
|
113
|
+
* Defaults to `false`: an intent-less mutation is named after its route
|
|
114
|
+
* alone. Turn it on for REST-ish routes, where `PATCH` and `DELETE` on the
|
|
115
|
+
* same path should report as `route:patch` and `route:delete`.
|
|
116
|
+
*/
|
|
117
|
+
methodSuffix?: boolean;
|
|
118
|
+
/**
|
|
119
|
+
* App enrichment — session ids, actor labels, plan names. It runs inside
|
|
120
|
+
* `waitUntil` (never on the request's critical path), its result is merged
|
|
121
|
+
* over the tracker's own `status` field, and a rejection is swallowed: a
|
|
122
|
+
* failed lookup costs the payload, not the event.
|
|
123
|
+
*/
|
|
124
|
+
payload?: (request: Request, response: Response) => Promise<LuchyRequestPayload> | LuchyRequestPayload;
|
|
125
|
+
/** Called when anything in the pipeline fails. Nothing else surfaces it. */
|
|
126
|
+
onError?: (error: unknown) => void;
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Flushes work after the response is sent. Structurally Cloudflare's
|
|
130
|
+
* `ExecutionContext`, declared here so this module imports no runtime types.
|
|
131
|
+
*/
|
|
132
|
+
export type FlushContext = {
|
|
133
|
+
waitUntil(promise: Promise<unknown>): void;
|
|
134
|
+
};
|
|
135
|
+
/** Ends a tracked request. Never throws, never blocks the response. */
|
|
136
|
+
export type FinishFn = (response: Response, ctx: FlushContext) => void;
|
|
137
|
+
export type RequestTracker = {
|
|
138
|
+
/**
|
|
139
|
+
* Called with the incoming request, before the React Router handler consumes
|
|
140
|
+
* its body. Returns the function to call with the response.
|
|
141
|
+
*/
|
|
142
|
+
begin(request: Request): FinishFn;
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Creates the request tracker: one hook in the Worker's `fetch`, and every
|
|
146
|
+
* mutation the app serves becomes an event.
|
|
147
|
+
*
|
|
148
|
+
* `begin` must run before the request is handed to React Router, because it is
|
|
149
|
+
* what clones the body — once the handler has read it, it is gone. `finish`
|
|
150
|
+
* does everything else on `ctx.waitUntil`, so the response is not held up by
|
|
151
|
+
* analytics.
|
|
152
|
+
*
|
|
153
|
+
* @example
|
|
154
|
+
* ```ts
|
|
155
|
+
* const tracker = createRequestTracker({ apiKey, enabled: env.APP_ENV === 'production' })
|
|
156
|
+
* // in fetch(request, env, ctx):
|
|
157
|
+
* const finish = tracker.begin(request)
|
|
158
|
+
* const response = await requestHandler(request, loadContext)
|
|
159
|
+
* finish(response, ctx)
|
|
160
|
+
* return response
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
export declare function createRequestTracker(options: RequestTrackerOptions): RequestTracker;
|
|
164
|
+
//# sourceMappingURL=react-router.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react-router.d.ts","sourceRoot":"","sources":["../src/react-router.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;GAkBG;AAEH,0EAA0E;AAC1E,MAAM,MAAM,mBAAmB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;AAe5E;;;;;;;;;;GAUG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAExD;AAED;;;;;;;;;GASG;AACH,wBAAgB,aAAa,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,OAAO,CAE7E;AAaD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAQvD;AAED,qEAAqE;AACrE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,MAAM,EAAE,MAAM,CAAC;IACf,8CAA8C;IAC9C,QAAQ,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,MAAM,EAAE,MAAM,CAAC;IACf,uDAAuD;IACvD,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB,CAAC;AAEF,iFAAiF;AACjF,MAAM,MAAM,qBAAqB,GAAG,IAAI,CACtC,qBAAqB,EACnB,gBAAgB,GAChB,qBAAqB,GACrB,cAAc,GACd,eAAe,GACf,cAAc,CACjB,CAAC;AAEF;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAC7B,KAAK,EAAE,gBAAgB,EACvB,MAAM,GAAE,qBAA0B,GACjC,MAAM,GAAG,IAAI,CA0Bf;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,6EAA6E;IAC7E,MAAM,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B;;;OAGG;IACH,mBAAmB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC/B,uEAAuE;IACvE,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB;;;;OAIG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;;;OAKG;IACH,OAAO,CAAC,EAAE,CACR,OAAO,EAAE,OAAO,EAChB,QAAQ,EAAE,QAAQ,KACf,OAAO,CAAC,mBAAmB,CAAC,GAAG,mBAAmB,CAAC;IACxD,4EAA4E;IAC5E,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,SAAS,CAAC,OAAO,EAAE,OAAO,CAAC,OAAO,CAAC,GAAG,IAAI,CAAC;CAC5C,CAAC;AAEF,uEAAuE;AACvE,MAAM,MAAM,QAAQ,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,GAAG,EAAE,YAAY,KAAK,IAAI,CAAC;AAEvE,MAAM,MAAM,cAAc,GAAG;IAC3B;;;OAGG;IACH,KAAK,CAAC,OAAO,EAAE,OAAO,GAAG,QAAQ,CAAC;CACnC,CAAC;AAKF;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,qBAAqB,GAC7B,cAAc,CAgFhB"}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { createLuchyClient } from "./api/index.js";
|
|
2
|
+
const MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
3
|
+
const DEFAULT_IGNORE_PREFIXES = ["/__manifest"];
|
|
4
|
+
const INTENT_PATTERN = /^[A-Za-z0-9_-]{1,40}$/;
|
|
5
|
+
function isMutatingMethod(method) {
|
|
6
|
+
return MUTATING_METHODS.has(method.toUpperCase());
|
|
7
|
+
}
|
|
8
|
+
function carriesIntent(contentType) {
|
|
9
|
+
return (contentType ?? "").includes("application/x-www-form-urlencoded");
|
|
10
|
+
}
|
|
11
|
+
function isIdSegment(segment) {
|
|
12
|
+
if (segment.length === 0) return false;
|
|
13
|
+
if (/^\d+$/.test(segment)) return true;
|
|
14
|
+
if (/^[0-9a-f]{8}-[0-9a-f]{4}-/i.test(segment)) return true;
|
|
15
|
+
return segment.length >= 16 && /\d/.test(segment);
|
|
16
|
+
}
|
|
17
|
+
function normalizeRoute(pathname) {
|
|
18
|
+
const segments = pathname.replace(/\.data$/, "").split("/").filter(Boolean);
|
|
19
|
+
if (segments.length === 0) return "home";
|
|
20
|
+
if (segments.length === 1 && segments[0] === "_root") return "home";
|
|
21
|
+
return segments.map((s) => isIdSegment(s) ? ":id" : s).join("/");
|
|
22
|
+
}
|
|
23
|
+
function serverEventName(input, config = {}) {
|
|
24
|
+
const method = input.method.toUpperCase();
|
|
25
|
+
if (!MUTATING_METHODS.has(method)) return null;
|
|
26
|
+
if (!config.trackFailures && input.status >= 400) return null;
|
|
27
|
+
const prefixes = [
|
|
28
|
+
...DEFAULT_IGNORE_PREFIXES,
|
|
29
|
+
...config.ignorePrefixes ?? []
|
|
30
|
+
];
|
|
31
|
+
if (prefixes.some((p) => input.pathname.startsWith(p))) return null;
|
|
32
|
+
const route = normalizeRoute(input.pathname);
|
|
33
|
+
if ((config.ignoreRouteSuffixes ?? []).some((s) => route.endsWith(s))) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const intent = input.intent && INTENT_PATTERN.test(input.intent) ? input.intent : null;
|
|
37
|
+
const name = intent ? `${route}:${intent}` : config.methodSuffix ? `${route}:${method.toLowerCase()}` : route;
|
|
38
|
+
if ((config.ignoreEvents ?? []).includes(name)) return null;
|
|
39
|
+
return name;
|
|
40
|
+
}
|
|
41
|
+
const NOOP_FINISH = () => {
|
|
42
|
+
};
|
|
43
|
+
function createRequestTracker(options) {
|
|
44
|
+
const enabled = options.enabled ?? true;
|
|
45
|
+
const prefixes = [
|
|
46
|
+
...DEFAULT_IGNORE_PREFIXES,
|
|
47
|
+
...options.ignorePrefixes ?? []
|
|
48
|
+
];
|
|
49
|
+
const client = createLuchyClient({
|
|
50
|
+
apiKey: options.apiKey,
|
|
51
|
+
endpoint: options.endpoint,
|
|
52
|
+
onError: options.onError
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
begin(request) {
|
|
56
|
+
if (!enabled) return NOOP_FINISH;
|
|
57
|
+
if (!isMutatingMethod(request.method)) return NOOP_FINISH;
|
|
58
|
+
const url = new URL(request.url);
|
|
59
|
+
if (prefixes.some((p) => url.pathname.startsWith(p))) return NOOP_FINISH;
|
|
60
|
+
const clone = carriesIntent(request.headers.get("content-type")) ? request.clone() : null;
|
|
61
|
+
return (response, ctx) => {
|
|
62
|
+
ctx.waitUntil(
|
|
63
|
+
(async () => {
|
|
64
|
+
const intent = clone ? new URLSearchParams(await clone.text()).get("intent") : null;
|
|
65
|
+
const name = serverEventName(
|
|
66
|
+
{
|
|
67
|
+
method: request.method,
|
|
68
|
+
pathname: url.pathname,
|
|
69
|
+
status: response.status,
|
|
70
|
+
intent
|
|
71
|
+
},
|
|
72
|
+
options
|
|
73
|
+
);
|
|
74
|
+
if (!name) return;
|
|
75
|
+
let payload = { status: response.status };
|
|
76
|
+
if (options.payload) {
|
|
77
|
+
try {
|
|
78
|
+
const extra = await options.payload(request, response);
|
|
79
|
+
payload = { ...payload, ...extra };
|
|
80
|
+
} catch (error) {
|
|
81
|
+
options.onError?.(error);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
await client.trackEvent({
|
|
85
|
+
name,
|
|
86
|
+
type: "server",
|
|
87
|
+
// The raw pathname, not the normalized route: the name carries
|
|
88
|
+
// the shape, the pathname keeps the detail.
|
|
89
|
+
pathname: url.pathname,
|
|
90
|
+
payload,
|
|
91
|
+
userAgent: request.headers.get("user-agent") ?? void 0
|
|
92
|
+
});
|
|
93
|
+
})().catch((error) => {
|
|
94
|
+
options.onError?.(error);
|
|
95
|
+
})
|
|
96
|
+
);
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export {
|
|
102
|
+
carriesIntent,
|
|
103
|
+
createRequestTracker,
|
|
104
|
+
isMutatingMethod,
|
|
105
|
+
normalizeRoute,
|
|
106
|
+
serverEventName
|
|
107
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "luchy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Luchy tracking - browser script + server-side tracker",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -14,6 +14,10 @@
|
|
|
14
14
|
"types": "./dist/server.d.ts",
|
|
15
15
|
"import": "./dist/server.js"
|
|
16
16
|
},
|
|
17
|
+
"./react-router": {
|
|
18
|
+
"types": "./dist/react-router.d.ts",
|
|
19
|
+
"import": "./dist/react-router.js"
|
|
20
|
+
},
|
|
17
21
|
"./api": {
|
|
18
22
|
"types": "./dist/api/index.d.ts",
|
|
19
23
|
"import": "./dist/api/index.js"
|