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/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": "
|
|
4
|
-
"description": "Luchy tracking - browser script
|
|
3
|
+
"version": "1.1.0",
|
|
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
|
}
|