luchy 0.2.0 → 1.1.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 +253 -98
- package/dist/api/index.d.ts +44 -11
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +36 -8
- package/dist/api/schema.d.ts +862 -94
- 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/react-router.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createElement } from "react";
|
|
2
|
+
import { createContext } from "react-router";
|
|
3
|
+
import { DEFAULT_ENDPOINT, createLuchy, signIdentity } from "./server.js";
|
|
2
4
|
const MUTATING_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
3
5
|
const DEFAULT_IGNORE_PREFIXES = ["/__manifest"];
|
|
4
6
|
const INTENT_PATTERN = /^[A-Za-z0-9_-]{1,40}$/;
|
|
@@ -38,70 +40,205 @@ function serverEventName(input, config = {}) {
|
|
|
38
40
|
if ((config.ignoreEvents ?? []).includes(name)) return null;
|
|
39
41
|
return name;
|
|
40
42
|
}
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
const IDENTIFY_TTL_MS = 60 * 60 * 1e3;
|
|
44
|
+
const IDENTIFY_MAX_ENTRIES = 1e3;
|
|
45
|
+
const identified = /* @__PURE__ */ new Map();
|
|
46
|
+
function traitsHash(traits) {
|
|
47
|
+
const json = JSON.stringify(
|
|
48
|
+
Object.keys(traits).sort().map((key) => [key, traits[key]])
|
|
49
|
+
);
|
|
50
|
+
let hash = 2166136261;
|
|
51
|
+
for (let i = 0; i < json.length; i++) {
|
|
52
|
+
hash ^= json.charCodeAt(i);
|
|
53
|
+
hash = Math.imul(hash, 16777619);
|
|
54
|
+
}
|
|
55
|
+
return (hash >>> 0).toString(36);
|
|
56
|
+
}
|
|
57
|
+
function claimIdentify(key, traits, now) {
|
|
58
|
+
const hash = traitsHash(traits);
|
|
59
|
+
const seen = identified.get(key);
|
|
60
|
+
if (seen && seen.hash === hash && now - seen.at < IDENTIFY_TTL_MS) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
identified.delete(key);
|
|
64
|
+
if (identified.size >= IDENTIFY_MAX_ENTRIES) {
|
|
65
|
+
const oldest = identified.keys().next().value;
|
|
66
|
+
if (oldest !== void 0) identified.delete(oldest);
|
|
67
|
+
}
|
|
68
|
+
identified.set(key, { hash, at: now });
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
function resetIdentifyCache() {
|
|
72
|
+
identified.clear();
|
|
73
|
+
}
|
|
74
|
+
const luchyContext = createContext(null);
|
|
75
|
+
function clientInfo(request) {
|
|
76
|
+
const headers = request.headers;
|
|
77
|
+
const cf = request.cf;
|
|
78
|
+
const cfCountry = typeof cf?.country === "string" ? cf.country : void 0;
|
|
79
|
+
const language = headers.get("accept-language")?.split(",")[0]?.split(";")[0]?.trim();
|
|
80
|
+
return {
|
|
81
|
+
ip: headers.get("cf-connecting-ip") ?? void 0,
|
|
82
|
+
country: cfCountry ?? headers.get("cf-ipcountry") ?? void 0,
|
|
83
|
+
language: language || void 0,
|
|
84
|
+
userAgent: headers.get("user-agent") ?? void 0
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function luchyMiddleware(options) {
|
|
44
88
|
const enabled = options.enabled ?? true;
|
|
89
|
+
const endpoint = (options.endpoint || DEFAULT_ENDPOINT).replace(/\/+$/, "");
|
|
45
90
|
const prefixes = [
|
|
46
91
|
...DEFAULT_IGNORE_PREFIXES,
|
|
47
92
|
...options.ignorePrefixes ?? []
|
|
48
93
|
];
|
|
49
|
-
const
|
|
94
|
+
const luchy = createLuchy({
|
|
50
95
|
apiKey: options.apiKey,
|
|
51
|
-
|
|
96
|
+
secret: options.secret,
|
|
97
|
+
endpoint,
|
|
98
|
+
fetch: options.fetch,
|
|
52
99
|
onError: options.onError
|
|
53
100
|
});
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
101
|
+
function identify(args, user, traits) {
|
|
102
|
+
if (!claimIdentify(`${options.apiKey}\0${user}`, traits, Date.now())) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
options.waitUntil(args)(luchy.identify(user, traits));
|
|
107
|
+
} catch (error) {
|
|
108
|
+
options.onError?.(error);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function identityResolver(args) {
|
|
112
|
+
let memo;
|
|
113
|
+
async function resolve() {
|
|
114
|
+
if (!options.identity) {
|
|
115
|
+
return void 0;
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const resolved = await options.identity(args);
|
|
119
|
+
if (!resolved) {
|
|
120
|
+
return void 0;
|
|
121
|
+
}
|
|
122
|
+
const { props, traits, ...identity } = resolved;
|
|
123
|
+
if (enabled && traits) {
|
|
124
|
+
identify(args, identity.user, traits);
|
|
125
|
+
}
|
|
126
|
+
const token = options.secret ? await signIdentity(identity, options.secret) : void 0;
|
|
127
|
+
return { token, props };
|
|
128
|
+
} catch (error) {
|
|
129
|
+
options.onError?.(error);
|
|
130
|
+
return void 0;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return () => {
|
|
134
|
+
memo ?? (memo = resolve());
|
|
135
|
+
return memo;
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
return async (args, next) => {
|
|
139
|
+
const { request } = args;
|
|
140
|
+
const resolveIdentity = identityResolver(args);
|
|
141
|
+
args.context.set(luchyContext, {
|
|
142
|
+
apiKey: options.apiKey,
|
|
143
|
+
endpoint,
|
|
144
|
+
resolveIdentity
|
|
145
|
+
});
|
|
146
|
+
if (!enabled) {
|
|
147
|
+
return next();
|
|
148
|
+
}
|
|
149
|
+
if (!isMutatingMethod(request.method)) {
|
|
150
|
+
return next();
|
|
151
|
+
}
|
|
152
|
+
const url = new URL(request.url);
|
|
153
|
+
if (prefixes.some((p) => url.pathname.startsWith(p))) {
|
|
154
|
+
return next();
|
|
155
|
+
}
|
|
156
|
+
const clone = carriesIntent(request.headers.get("content-type")) ? request.clone() : null;
|
|
157
|
+
async function ship(status) {
|
|
158
|
+
const intent = clone ? new URLSearchParams(await clone.text()).get("intent") : null;
|
|
159
|
+
const name = serverEventName(
|
|
160
|
+
{ method: request.method, pathname: url.pathname, status, intent },
|
|
161
|
+
options
|
|
162
|
+
);
|
|
163
|
+
if (!name) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
const who = await resolveIdentity();
|
|
167
|
+
await luchy.track(
|
|
168
|
+
{
|
|
169
|
+
name,
|
|
170
|
+
type: "server",
|
|
171
|
+
// The raw pathname, not the normalized route: the name carries the
|
|
172
|
+
// shape, the pathname keeps the detail.
|
|
173
|
+
pathname: url.pathname,
|
|
174
|
+
// `status` last: it is the tracker's own field, props cannot fake it.
|
|
175
|
+
payload: { ...who?.props, status },
|
|
176
|
+
...clientInfo(request)
|
|
177
|
+
},
|
|
178
|
+
who?.token
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
function schedule(status) {
|
|
182
|
+
try {
|
|
183
|
+
options.waitUntil(args)(
|
|
184
|
+
ship(status).catch((error) => {
|
|
94
185
|
options.onError?.(error);
|
|
95
186
|
})
|
|
96
187
|
);
|
|
97
|
-
}
|
|
188
|
+
} catch (error) {
|
|
189
|
+
options.onError?.(error);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
let result;
|
|
193
|
+
try {
|
|
194
|
+
result = await next();
|
|
195
|
+
} catch (thrown) {
|
|
196
|
+
schedule(thrown instanceof Response ? thrown.status : 500);
|
|
197
|
+
throw thrown;
|
|
98
198
|
}
|
|
199
|
+
schedule(result instanceof Response ? result.status : 200);
|
|
200
|
+
return result;
|
|
99
201
|
};
|
|
100
202
|
}
|
|
203
|
+
async function getLuchy(context) {
|
|
204
|
+
const state = context.get(luchyContext);
|
|
205
|
+
if (!state) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
"getLuchy: luchyMiddleware is not installed on the root route"
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
const config = {
|
|
211
|
+
apiKey: state.apiKey,
|
|
212
|
+
endpoint: state.endpoint
|
|
213
|
+
};
|
|
214
|
+
const resolved = await state.resolveIdentity();
|
|
215
|
+
if (resolved?.token) {
|
|
216
|
+
config.identity = resolved.token;
|
|
217
|
+
}
|
|
218
|
+
if (resolved?.props) {
|
|
219
|
+
config.props = resolved.props;
|
|
220
|
+
}
|
|
221
|
+
return config;
|
|
222
|
+
}
|
|
223
|
+
const DEFAULT_SRC = "https://cdn.luchy.app/luchy.min.js";
|
|
224
|
+
function LuchyScript(props) {
|
|
225
|
+
return createElement("script", {
|
|
226
|
+
src: props.src ?? DEFAULT_SRC,
|
|
227
|
+
defer: true,
|
|
228
|
+
nonce: props.nonce,
|
|
229
|
+
"data-api-key": props.apiKey,
|
|
230
|
+
"data-endpoint": props.endpoint,
|
|
231
|
+
"data-identity": props.identity,
|
|
232
|
+
"data-props": props.props ? JSON.stringify(props.props) : void 0
|
|
233
|
+
});
|
|
234
|
+
}
|
|
101
235
|
export {
|
|
236
|
+
LuchyScript,
|
|
102
237
|
carriesIntent,
|
|
103
|
-
|
|
238
|
+
getLuchy,
|
|
104
239
|
isMutatingMethod,
|
|
240
|
+
luchyMiddleware,
|
|
105
241
|
normalizeRoute,
|
|
242
|
+
resetIdentifyCache,
|
|
106
243
|
serverEventName
|
|
107
244
|
};
|
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"}
|