luchy 0.2.0 → 1.0.1
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 +245 -95
- package/dist/api/index.d.ts +21 -9
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +46 -8
- package/dist/api/schema.d.ts +367 -79
- package/dist/identity.d.ts +35 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/index.d.ts +23 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +61 -8
- package/dist/react-router.d.ts +121 -53
- package/dist/react-router.d.ts.map +1 -1
- package/dist/react-router.js +185 -48
- package/dist/script/luchy.js +62 -9
- package/dist/script/luchy.js.br +0 -0
- package/dist/script/luchy.js.gz +0 -0
- package/dist/script/luchy.min.js +62 -9
- package/dist/script/luchy.min.js.br +0 -0
- package/dist/script/luchy.min.js.gz +0 -0
- package/dist/server.d.ts +71 -39
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +114 -26
- package/package.json +17 -2
package/dist/script/luchy.js
CHANGED
|
@@ -62,9 +62,58 @@ var __async = (__this, __arguments, generator) => {
|
|
|
62
62
|
language: navigator.language || ((_a = navigator.languages) == null ? void 0 : _a[0]) || "en"
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
+
function parseProps(raw) {
|
|
66
|
+
if (!raw) return void 0;
|
|
67
|
+
try {
|
|
68
|
+
const value = JSON.parse(raw);
|
|
69
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
70
|
+
return void 0;
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
65
77
|
function luchy(options) {
|
|
66
78
|
const endpoint = options.endpoint || "https://api.luchy.app/ingest";
|
|
67
|
-
const { apiKey, trackLocalhost = false } = options;
|
|
79
|
+
const { apiKey, trackLocalhost = false, script } = options;
|
|
80
|
+
let identity = options.identity;
|
|
81
|
+
let props = options.props;
|
|
82
|
+
function currentIdentity() {
|
|
83
|
+
if (!script) return identity;
|
|
84
|
+
return script.getAttribute("data-identity") || void 0;
|
|
85
|
+
}
|
|
86
|
+
function currentProps() {
|
|
87
|
+
if (!script) return props;
|
|
88
|
+
return parseProps(script.getAttribute("data-props"));
|
|
89
|
+
}
|
|
90
|
+
function identify(token, nextProps) {
|
|
91
|
+
if (!script) {
|
|
92
|
+
identity = token;
|
|
93
|
+
props = nextProps;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
script.setAttribute("data-identity", token);
|
|
97
|
+
if (nextProps) {
|
|
98
|
+
script.setAttribute("data-props", JSON.stringify(nextProps));
|
|
99
|
+
} else {
|
|
100
|
+
script.removeAttribute("data-props");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function reset() {
|
|
104
|
+
if (!script) {
|
|
105
|
+
identity = void 0;
|
|
106
|
+
props = void 0;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
script.removeAttribute("data-identity");
|
|
110
|
+
script.removeAttribute("data-props");
|
|
111
|
+
}
|
|
112
|
+
function withProps(payload) {
|
|
113
|
+
const global = currentProps();
|
|
114
|
+
if (!global) return payload;
|
|
115
|
+
return __spreadValues(__spreadValues({}, global), payload);
|
|
116
|
+
}
|
|
68
117
|
const shouldTrack = () => {
|
|
69
118
|
if (trackLocalhost) return true;
|
|
70
119
|
return location.hostname !== "localhost" && location.hostname !== "127.0.0.1";
|
|
@@ -80,7 +129,9 @@ var __async = (__this, __arguments, generator) => {
|
|
|
80
129
|
apiKey,
|
|
81
130
|
data: __spreadValues({
|
|
82
131
|
pathname,
|
|
83
|
-
referrer
|
|
132
|
+
referrer,
|
|
133
|
+
payload: withProps(data == null ? void 0 : data.payload),
|
|
134
|
+
identity: currentIdentity()
|
|
84
135
|
}, browserData)
|
|
85
136
|
});
|
|
86
137
|
});
|
|
@@ -100,7 +151,8 @@ var __async = (__this, __arguments, generator) => {
|
|
|
100
151
|
// `payload`, NOT `props`. The ingest endpoint validates with a
|
|
101
152
|
// non-passthrough zod object, so an unknown key is stripped and the
|
|
102
153
|
// event is stored with no properties at all — 200, success, no data.
|
|
103
|
-
payload
|
|
154
|
+
payload: withProps(payload),
|
|
155
|
+
identity: currentIdentity()
|
|
104
156
|
}, browserData)
|
|
105
157
|
});
|
|
106
158
|
});
|
|
@@ -264,11 +316,9 @@ var __async = (__this, __arguments, generator) => {
|
|
|
264
316
|
const key = attr.name.slice(prefix.length).replace(/-/g, "_");
|
|
265
317
|
payload[key] = attr.value;
|
|
266
318
|
}
|
|
267
|
-
trackEvent(
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
{ type: "click" }
|
|
271
|
-
);
|
|
319
|
+
trackEvent(name, Object.keys(payload).length > 0 ? payload : void 0, {
|
|
320
|
+
type: "click"
|
|
321
|
+
});
|
|
272
322
|
};
|
|
273
323
|
document.addEventListener("click", dataEventClickListener);
|
|
274
324
|
}
|
|
@@ -313,6 +363,8 @@ var __async = (__this, __arguments, generator) => {
|
|
|
313
363
|
return {
|
|
314
364
|
trackPageview,
|
|
315
365
|
trackEvent,
|
|
366
|
+
identify,
|
|
367
|
+
reset,
|
|
316
368
|
setOptions
|
|
317
369
|
};
|
|
318
370
|
}
|
|
@@ -325,7 +377,8 @@ var __async = (__this, __arguments, generator) => {
|
|
|
325
377
|
const tracker = luchy({
|
|
326
378
|
apiKey,
|
|
327
379
|
endpoint,
|
|
328
|
-
trackLocalhost
|
|
380
|
+
trackLocalhost,
|
|
381
|
+
script
|
|
329
382
|
});
|
|
330
383
|
const autoPageviewsAttr = script.getAttribute("data-auto-pageviews");
|
|
331
384
|
const autoOutboundAttr = script.getAttribute("data-auto-outbound");
|
package/dist/script/luchy.js.br
CHANGED
|
Binary file
|
package/dist/script/luchy.js.gz
CHANGED
|
Binary file
|
package/dist/script/luchy.min.js
CHANGED
|
@@ -62,9 +62,58 @@ var __async = (__this, __arguments, generator) => {
|
|
|
62
62
|
language: navigator.language || ((_a = navigator.languages) == null ? void 0 : _a[0]) || "en"
|
|
63
63
|
};
|
|
64
64
|
}
|
|
65
|
+
function parseProps(raw) {
|
|
66
|
+
if (!raw) return void 0;
|
|
67
|
+
try {
|
|
68
|
+
const value = JSON.parse(raw);
|
|
69
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
70
|
+
return void 0;
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
} catch (e) {
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
65
77
|
function luchy(options) {
|
|
66
78
|
const endpoint = options.endpoint || "https://api.luchy.app/ingest";
|
|
67
|
-
const { apiKey, trackLocalhost = false } = options;
|
|
79
|
+
const { apiKey, trackLocalhost = false, script } = options;
|
|
80
|
+
let identity = options.identity;
|
|
81
|
+
let props = options.props;
|
|
82
|
+
function currentIdentity() {
|
|
83
|
+
if (!script) return identity;
|
|
84
|
+
return script.getAttribute("data-identity") || void 0;
|
|
85
|
+
}
|
|
86
|
+
function currentProps() {
|
|
87
|
+
if (!script) return props;
|
|
88
|
+
return parseProps(script.getAttribute("data-props"));
|
|
89
|
+
}
|
|
90
|
+
function identify(token, nextProps) {
|
|
91
|
+
if (!script) {
|
|
92
|
+
identity = token;
|
|
93
|
+
props = nextProps;
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
script.setAttribute("data-identity", token);
|
|
97
|
+
if (nextProps) {
|
|
98
|
+
script.setAttribute("data-props", JSON.stringify(nextProps));
|
|
99
|
+
} else {
|
|
100
|
+
script.removeAttribute("data-props");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function reset() {
|
|
104
|
+
if (!script) {
|
|
105
|
+
identity = void 0;
|
|
106
|
+
props = void 0;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
script.removeAttribute("data-identity");
|
|
110
|
+
script.removeAttribute("data-props");
|
|
111
|
+
}
|
|
112
|
+
function withProps(payload) {
|
|
113
|
+
const global = currentProps();
|
|
114
|
+
if (!global) return payload;
|
|
115
|
+
return __spreadValues(__spreadValues({}, global), payload);
|
|
116
|
+
}
|
|
68
117
|
const shouldTrack = () => {
|
|
69
118
|
if (trackLocalhost) return true;
|
|
70
119
|
return location.hostname !== "localhost" && location.hostname !== "127.0.0.1";
|
|
@@ -80,7 +129,9 @@ var __async = (__this, __arguments, generator) => {
|
|
|
80
129
|
apiKey,
|
|
81
130
|
data: __spreadValues({
|
|
82
131
|
pathname,
|
|
83
|
-
referrer
|
|
132
|
+
referrer,
|
|
133
|
+
payload: withProps(data == null ? void 0 : data.payload),
|
|
134
|
+
identity: currentIdentity()
|
|
84
135
|
}, browserData)
|
|
85
136
|
});
|
|
86
137
|
});
|
|
@@ -100,7 +151,8 @@ var __async = (__this, __arguments, generator) => {
|
|
|
100
151
|
// `payload`, NOT `props`. The ingest endpoint validates with a
|
|
101
152
|
// non-passthrough zod object, so an unknown key is stripped and the
|
|
102
153
|
// event is stored with no properties at all — 200, success, no data.
|
|
103
|
-
payload
|
|
154
|
+
payload: withProps(payload),
|
|
155
|
+
identity: currentIdentity()
|
|
104
156
|
}, browserData)
|
|
105
157
|
});
|
|
106
158
|
});
|
|
@@ -264,11 +316,9 @@ var __async = (__this, __arguments, generator) => {
|
|
|
264
316
|
const key = attr.name.slice(prefix.length).replace(/-/g, "_");
|
|
265
317
|
payload[key] = attr.value;
|
|
266
318
|
}
|
|
267
|
-
trackEvent(
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
{ type: "click" }
|
|
271
|
-
);
|
|
319
|
+
trackEvent(name, Object.keys(payload).length > 0 ? payload : void 0, {
|
|
320
|
+
type: "click"
|
|
321
|
+
});
|
|
272
322
|
};
|
|
273
323
|
document.addEventListener("click", dataEventClickListener);
|
|
274
324
|
}
|
|
@@ -313,6 +363,8 @@ var __async = (__this, __arguments, generator) => {
|
|
|
313
363
|
return {
|
|
314
364
|
trackPageview,
|
|
315
365
|
trackEvent,
|
|
366
|
+
identify,
|
|
367
|
+
reset,
|
|
316
368
|
setOptions
|
|
317
369
|
};
|
|
318
370
|
}
|
|
@@ -325,7 +377,8 @@ var __async = (__this, __arguments, generator) => {
|
|
|
325
377
|
const tracker = luchy({
|
|
326
378
|
apiKey,
|
|
327
379
|
endpoint,
|
|
328
|
-
trackLocalhost
|
|
380
|
+
trackLocalhost,
|
|
381
|
+
script
|
|
329
382
|
});
|
|
330
383
|
const autoPageviewsAttr = script.getAttribute("data-auto-pageviews");
|
|
331
384
|
const autoOutboundAttr = script.getAttribute("data-auto-outbound");
|
|
Binary file
|
|
Binary file
|
package/dist/server.d.ts
CHANGED
|
@@ -1,35 +1,61 @@
|
|
|
1
|
+
import { signIdentity, LuchyIdentity } from './identity.js';
|
|
1
2
|
/**
|
|
2
3
|
* Server-side tracking.
|
|
3
4
|
*
|
|
4
|
-
* The browser
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `location`, `navigator`), so it runs in Node, Bun, Deno and Cloudflare
|
|
9
|
-
* Workers.
|
|
5
|
+
* The browser script only sees what happens in a page. A server sees what the
|
|
6
|
+
* application actually *did* — an order placed, a payment recorded, a webhook
|
|
7
|
+
* processed — and those are usually the events worth having. No browser
|
|
8
|
+
* globals here, so it runs in Node, Bun, Deno and Cloudflare Workers.
|
|
10
9
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
10
|
+
* The wire format lives in `./api` (generated from the API's OpenAPI
|
|
11
|
+
* document); this module is the ergonomic face of it plus identity signing
|
|
12
|
+
* and person profiles (`identify`).
|
|
14
13
|
*
|
|
15
|
-
*
|
|
16
|
-
* document, so the wire format is no longer restated here at all. This module
|
|
17
|
-
* is the ergonomic face of it: two methods, no client object to hold.
|
|
14
|
+
* @module
|
|
18
15
|
*/
|
|
19
|
-
export type
|
|
16
|
+
export { signIdentity, type LuchyIdentity };
|
|
17
|
+
/** A property bag. Nested objects are not supported by the ingest API. */
|
|
18
|
+
export type LuchyPayload = Record<string, string | number | boolean>;
|
|
19
|
+
/**
|
|
20
|
+
* Person profile traits. Merged into the stored profile; `null` deletes a
|
|
21
|
+
* trait. The dashboard labels a person by `name`, else `email`, else the id.
|
|
22
|
+
*/
|
|
23
|
+
export type LuchyTraits = Record<string, string | number | boolean | null>;
|
|
24
|
+
export type LuchyOptions = {
|
|
20
25
|
apiKey: string;
|
|
21
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* The project's secret key (`lsk_…`, from the dashboard settings).
|
|
28
|
+
* Without it identities passed to `track`/`pageview` are not sent (ingest
|
|
29
|
+
* drops unsigned identities anyway, so sending one would only look like it
|
|
30
|
+
* worked) and `identify` is a no-op. Server-side only: never ship it.
|
|
31
|
+
*/
|
|
32
|
+
secret?: string;
|
|
33
|
+
/** The ingest base, without a trailing slash. Defaults to the hosted API. */
|
|
22
34
|
endpoint?: string;
|
|
23
35
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
|
|
36
|
+
* Defaults to `true`. When false every call is a no-op, so apps can pass
|
|
37
|
+
* `env.APP_ENV === 'production'` and leave the wiring in place.
|
|
38
|
+
*/
|
|
39
|
+
enabled?: boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Called when a hit fails to reach ingest. Analytics must never break the
|
|
42
|
+
* request it describes, so failures are swallowed unless you log them here.
|
|
27
43
|
*/
|
|
28
44
|
onError?: (error: unknown) => void;
|
|
45
|
+
/** Transport override, for tests and scoped runtimes (service bindings). */
|
|
46
|
+
fetch?: typeof globalThis.fetch;
|
|
29
47
|
};
|
|
30
|
-
/**
|
|
31
|
-
export type
|
|
32
|
-
|
|
48
|
+
/** What the end user's request looked like, for hits caused by one. */
|
|
49
|
+
export type LuchyRequestInfo = {
|
|
50
|
+
/** Forward the end user's UA so the hit joins their browser session. */
|
|
51
|
+
userAgent?: string;
|
|
52
|
+
/** The end user's IP. Only used for the visitor hash, never stored. */
|
|
53
|
+
ip?: string;
|
|
54
|
+
/** ISO country code. Wins over the sender's own `cf-ipcountry`. */
|
|
55
|
+
country?: string;
|
|
56
|
+
language?: string;
|
|
57
|
+
};
|
|
58
|
+
export type LuchyServerEvent = LuchyRequestInfo & {
|
|
33
59
|
/** The event name, e.g. `order:placed`. */
|
|
34
60
|
name: string;
|
|
35
61
|
/** Where it happened — a route, not a raw URL with ids in it. */
|
|
@@ -37,35 +63,41 @@ export type LuchyServerEvent = {
|
|
|
37
63
|
/** Free-form category, e.g. `server`, `webhook`, `cron`. */
|
|
38
64
|
type?: string;
|
|
39
65
|
payload?: LuchyPayload;
|
|
40
|
-
/** Forward the end user's UA when the event was caused by a request. */
|
|
41
|
-
userAgent?: string;
|
|
42
|
-
language?: string;
|
|
43
|
-
/** ISO country code. Cloudflare deployments can leave this to the server. */
|
|
44
|
-
country?: string;
|
|
45
66
|
};
|
|
46
|
-
export type LuchyServerPageview = {
|
|
67
|
+
export type LuchyServerPageview = LuchyRequestInfo & {
|
|
47
68
|
pathname: string;
|
|
48
69
|
referrer?: string;
|
|
49
|
-
|
|
50
|
-
language?: string;
|
|
51
|
-
country?: string;
|
|
70
|
+
payload?: LuchyPayload;
|
|
52
71
|
};
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
72
|
+
/**
|
|
73
|
+
* An identity to sign, or a token that is already signed (e.g. one rendered
|
|
74
|
+
* into the page and posted back).
|
|
75
|
+
*/
|
|
76
|
+
export type LuchyIdentityInput = LuchyIdentity | string;
|
|
77
|
+
export type Luchy = {
|
|
78
|
+
track(event: LuchyServerEvent, identity?: LuchyIdentityInput): Promise<void>;
|
|
79
|
+
pageview(pageview: LuchyServerPageview, identity?: LuchyIdentityInput): Promise<void>;
|
|
80
|
+
/**
|
|
81
|
+
* Stores profile traits for a user (`POST /ingest/identify`, authenticated
|
|
82
|
+
* with the secret key). Idempotent: traits merge, `null` deletes one.
|
|
83
|
+
*/
|
|
84
|
+
identify(user: string, traits: LuchyTraits): Promise<void>;
|
|
56
85
|
};
|
|
86
|
+
/** The hosted ingest base. */
|
|
87
|
+
export declare const DEFAULT_ENDPOINT = "https://dash.luchy.app/api/ingest";
|
|
57
88
|
/**
|
|
58
89
|
* Creates a server-side tracker. Every method resolves rather than rejects —
|
|
59
90
|
* see `onError`.
|
|
60
91
|
*
|
|
61
92
|
* ```ts
|
|
62
|
-
* const luchy =
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
93
|
+
* const luchy = createLuchy({ apiKey: env.LUCHY_API_KEY, secret: env.LUCHY_SECRET });
|
|
94
|
+
* ctx.waitUntil(
|
|
95
|
+
* luchy.track(
|
|
96
|
+
* { name: 'order:placed', pathname: '/checkout', payload: { plan: 'pro' } },
|
|
97
|
+
* { user: user.id, tenant: String(workspace.id) }
|
|
98
|
+
* )
|
|
99
|
+
* );
|
|
68
100
|
* ```
|
|
69
101
|
*/
|
|
70
|
-
export declare function
|
|
102
|
+
export declare function createLuchy(options: LuchyOptions): Luchy;
|
|
71
103
|
//# sourceMappingURL=server.d.ts.map
|
package/dist/server.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,eAAe,CAAC;AAEjE;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,YAAY,EAAE,KAAK,aAAa,EAAE,CAAC;AAE5C,0EAA0E;AAC1E,MAAM,MAAM,YAAY,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,CAAC;AAErE;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC,CAAC;AAE3E,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,EAAE,MAAM,CAAC;IACf;;;;;OAKG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,4EAA4E;IAC5E,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;CACjC,CAAC;AAEF,uEAAuE;AACvE,MAAM,MAAM,gBAAgB,GAAG;IAC7B,wEAAwE;IACxE,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,uEAAuE;IACvE,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,mEAAmE;IACnE,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG,gBAAgB,GAAG;IAChD,2CAA2C;IAC3C,IAAI,EAAE,MAAM,CAAC;IACb,iEAAiE;IACjE,QAAQ,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,YAAY,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG,gBAAgB,GAAG;IACnD,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,YAAY,CAAC;CACxB,CAAC;AAEF;;;GAGG;AACH,MAAM,MAAM,kBAAkB,GAAG,aAAa,GAAG,MAAM,CAAC;AAExD,MAAM,MAAM,KAAK,GAAG;IAClB,KAAK,CAAC,KAAK,EAAE,gBAAgB,EAAE,QAAQ,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,QAAQ,CACN,QAAQ,EAAE,mBAAmB,EAC7B,QAAQ,CAAC,EAAE,kBAAkB,GAC5B,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC5D,CAAC;AAEF,8BAA8B;AAC9B,eAAO,MAAM,gBAAgB,sCAAsC,CAAC;AAWpE;;;;;;;;;;;;;GAaG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,YAAY,GAAG,KAAK,CAiHxD"}
|
package/dist/server.js
CHANGED
|
@@ -1,40 +1,128 @@
|
|
|
1
|
-
import { createLuchyClient } from "./api/index.js";
|
|
2
|
-
const
|
|
1
|
+
import { createLuchyClient, LuchyApiError } from "./api/index.js";
|
|
2
|
+
const encoder = new TextEncoder();
|
|
3
|
+
function base64url(bytes) {
|
|
4
|
+
let binary = "";
|
|
5
|
+
for (const byte of bytes) {
|
|
6
|
+
binary += String.fromCharCode(byte);
|
|
7
|
+
}
|
|
8
|
+
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
9
|
+
}
|
|
10
|
+
async function signIdentity(identity, secret) {
|
|
11
|
+
const claims = { u: identity.user, a: identity.actor, t: identity.tenant };
|
|
12
|
+
const body = base64url(encoder.encode(JSON.stringify(claims)));
|
|
13
|
+
const key = await crypto.subtle.importKey(
|
|
14
|
+
"raw",
|
|
15
|
+
encoder.encode(secret),
|
|
16
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
17
|
+
false,
|
|
18
|
+
["sign"]
|
|
19
|
+
);
|
|
20
|
+
const signature = await crypto.subtle.sign("HMAC", key, encoder.encode(body));
|
|
21
|
+
return `${body}.${base64url(new Uint8Array(signature))}`;
|
|
22
|
+
}
|
|
23
|
+
const DEFAULT_ENDPOINT = "https://dash.luchy.app/api/ingest";
|
|
3
24
|
function apiBaseFrom(endpoint) {
|
|
4
|
-
|
|
5
|
-
return withoutSlash.replace(/\/ingest$/, "");
|
|
25
|
+
return endpoint.replace(/\/+$/, "").replace(/\/ingest$/, "");
|
|
6
26
|
}
|
|
7
|
-
function
|
|
27
|
+
function createLuchy(options) {
|
|
28
|
+
const enabled = options.enabled ?? true;
|
|
8
29
|
const client = createLuchyClient({
|
|
9
30
|
apiKey: options.apiKey,
|
|
10
31
|
endpoint: apiBaseFrom(options.endpoint || DEFAULT_ENDPOINT),
|
|
32
|
+
fetch: options.fetch,
|
|
11
33
|
onError: options.onError
|
|
12
34
|
});
|
|
35
|
+
const secretApi = options.secret ? createLuchyClient({
|
|
36
|
+
apiKey: options.secret,
|
|
37
|
+
endpoint: apiBaseFrom(options.endpoint || DEFAULT_ENDPOINT),
|
|
38
|
+
fetch: options.fetch
|
|
39
|
+
}).api : void 0;
|
|
40
|
+
let reportedMissingSecret = false;
|
|
41
|
+
async function tokenFor(identity) {
|
|
42
|
+
if (!identity) {
|
|
43
|
+
return void 0;
|
|
44
|
+
}
|
|
45
|
+
if (typeof identity === "string") {
|
|
46
|
+
return identity;
|
|
47
|
+
}
|
|
48
|
+
if (!options.secret) {
|
|
49
|
+
return void 0;
|
|
50
|
+
}
|
|
51
|
+
return signIdentity(identity, options.secret);
|
|
52
|
+
}
|
|
13
53
|
return {
|
|
14
|
-
async
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
54
|
+
async track(event, identity) {
|
|
55
|
+
if (!enabled) {
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
try {
|
|
59
|
+
await client.trackEvent({
|
|
60
|
+
name: event.name,
|
|
61
|
+
type: event.type,
|
|
62
|
+
pathname: event.pathname,
|
|
63
|
+
payload: event.payload,
|
|
64
|
+
identity: await tokenFor(identity),
|
|
65
|
+
userAgent: event.userAgent,
|
|
66
|
+
ip: event.ip,
|
|
67
|
+
country: event.country,
|
|
68
|
+
language: event.language
|
|
69
|
+
});
|
|
70
|
+
} catch (error) {
|
|
71
|
+
options.onError?.(error);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
async pageview(pageview, identity) {
|
|
75
|
+
if (!enabled) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
await client.trackPageview({
|
|
80
|
+
pathname: pageview.pathname,
|
|
81
|
+
referrer: pageview.referrer,
|
|
82
|
+
payload: pageview.payload,
|
|
83
|
+
identity: await tokenFor(identity),
|
|
84
|
+
userAgent: pageview.userAgent,
|
|
85
|
+
ip: pageview.ip,
|
|
86
|
+
country: pageview.country,
|
|
87
|
+
language: pageview.language
|
|
88
|
+
});
|
|
89
|
+
} catch (error) {
|
|
90
|
+
options.onError?.(error);
|
|
91
|
+
}
|
|
25
92
|
},
|
|
26
|
-
async
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
93
|
+
async identify(user, traits) {
|
|
94
|
+
if (!enabled) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (!secretApi) {
|
|
98
|
+
if (!reportedMissingSecret) {
|
|
99
|
+
reportedMissingSecret = true;
|
|
100
|
+
options.onError?.(
|
|
101
|
+
new Error("luchy: identify needs the `secret` option; skipped")
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const { error, response } = await secretApi.POST("/ingest/identify", {
|
|
108
|
+
body: { user, traits }
|
|
109
|
+
});
|
|
110
|
+
if (error || !response.ok) {
|
|
111
|
+
options.onError?.(
|
|
112
|
+
new LuchyApiError(
|
|
113
|
+
`Luchy identify failed with ${response.status}`,
|
|
114
|
+
response.status
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
options.onError?.(error);
|
|
120
|
+
}
|
|
35
121
|
}
|
|
36
122
|
};
|
|
37
123
|
}
|
|
38
124
|
export {
|
|
39
|
-
|
|
125
|
+
DEFAULT_ENDPOINT,
|
|
126
|
+
createLuchy,
|
|
127
|
+
signIdentity
|
|
40
128
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "luchy",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Luchy tracking - browser script
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "Luchy tracking - browser script, server tracker and React Router middleware",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"types": "./dist/index.d.ts",
|
|
@@ -55,8 +55,11 @@
|
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/node": "^24.7.0",
|
|
58
|
+
"@types/react": "^19.2.2",
|
|
58
59
|
"jsdom": "^25.0.1",
|
|
59
60
|
"openapi-typescript": "^7.13.0",
|
|
61
|
+
"react": "^19.2.0",
|
|
62
|
+
"react-router": "^7.14.0",
|
|
60
63
|
"terser": "^5.37.0",
|
|
61
64
|
"tsx": "^4.20.6",
|
|
62
65
|
"typescript": "^5.9.3",
|
|
@@ -68,5 +71,17 @@
|
|
|
68
71
|
},
|
|
69
72
|
"dependencies": {
|
|
70
73
|
"openapi-fetch": "^0.17.0"
|
|
74
|
+
},
|
|
75
|
+
"peerDependencies": {
|
|
76
|
+
"react": ">=18",
|
|
77
|
+
"react-router": ">=7.9.0"
|
|
78
|
+
},
|
|
79
|
+
"peerDependenciesMeta": {
|
|
80
|
+
"react": {
|
|
81
|
+
"optional": true
|
|
82
|
+
},
|
|
83
|
+
"react-router": {
|
|
84
|
+
"optional": true
|
|
85
|
+
}
|
|
71
86
|
}
|
|
72
87
|
}
|