luchy 1.0.1 → 1.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 +22 -5
- package/dist/api/index.d.ts +34 -13
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +5 -15
- package/dist/api/schema.d.ts +749 -269
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +24 -1
- package/dist/script/luchy.js +25 -1
- package/dist/script/luchy.js.br +0 -0
- package/dist/script/luchy.js.gz +0 -0
- package/dist/script/luchy.min.js +25 -1
- package/dist/script/luchy.min.js.br +0 -0
- package/dist/script/luchy.min.js.gz +0 -0
- package/dist/utm.d.ts +6 -0
- package/dist/utm.d.ts.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -123,6 +123,15 @@ window.luchy.reset();
|
|
|
123
123
|
`identify`/`reset` write through to the script tag's `data-identity` /
|
|
124
124
|
`data-props`, so the tag stays the single source of truth.
|
|
125
125
|
|
|
126
|
+
### UTM attribution
|
|
127
|
+
|
|
128
|
+
Every pageview reads `utm_source`, `utm_medium`, `utm_campaign`, `utm_term`
|
|
129
|
+
and `utm_content` from `location.search` and adds the present ones to its
|
|
130
|
+
payload (trimmed, at most 200 characters). Nothing else from the query string
|
|
131
|
+
is sent, and a key in the pageview's own `payload` wins over the URL. Ingest
|
|
132
|
+
keeps the first `utm_source` / `utm_medium` / `utm_campaign` of a session as
|
|
133
|
+
its attribution.
|
|
134
|
+
|
|
126
135
|
## Identity
|
|
127
136
|
|
|
128
137
|
Luchy ties hits to your users only when your server vouches for them. An
|
|
@@ -218,6 +227,9 @@ await luchy.pageview({ pathname: '/pricing', payload: { plan: 'pro' } });
|
|
|
218
227
|
is the server.
|
|
219
228
|
- Nothing ever rejects — analytics must not break the request it is
|
|
220
229
|
describing. Pass `onError` if you want failures in your logs.
|
|
230
|
+
- UTM attribution is not read from any URL here: pass the same keys
|
|
231
|
+
(`utm_source`, `utm_medium`, `utm_campaign`, `utm_term`, `utm_content`) in
|
|
232
|
+
`payload` and the session is attributed exactly as for the browser script.
|
|
221
233
|
|
|
222
234
|
## API client (`luchy/api`)
|
|
223
235
|
|
|
@@ -254,7 +266,8 @@ const { current, delta } = await luchy.query({
|
|
|
254
266
|
});
|
|
255
267
|
|
|
256
268
|
// Anything the engine can't express: one read-only SELECT/WITH over the whole
|
|
257
|
-
// database (not scoped to a project).
|
|
269
|
+
// database (not scoped to a project). The tables are documented in the
|
|
270
|
+
// method's description: https://dash.luchy.app/llms.txt
|
|
258
271
|
const { rows } = await luchy.sql({
|
|
259
272
|
query:
|
|
260
273
|
'SELECT tenant, COUNT(*) AS n FROM events WHERE project_id = ? GROUP BY tenant',
|
|
@@ -274,13 +287,17 @@ const { data, error } = await luchy.api.GET('/health');
|
|
|
274
287
|
The request and response types come from the document too, so they are worth
|
|
275
288
|
importing rather than restating: `EventInput`, `PageviewInput`,
|
|
276
289
|
`IngestSuccess`, `QueryRequest`, `QueryResponse`, `SqlRequest`, `SqlResponse`,
|
|
277
|
-
plus the raw `paths` and
|
|
278
|
-
|
|
290
|
+
`MethodError`, plus the raw `paths` and `components`.
|
|
291
|
+
|
|
292
|
+
Reads use a query key (`wak_…`) minted in the dashboard under Keys. They call
|
|
293
|
+
the dashboard's analytics methods (`POST /api/analytics.query`,
|
|
294
|
+
`/api/analytics.sql`, …), the same ones its MCP server at
|
|
295
|
+
`https://dash.luchy.app/mcp` exposes to agents.
|
|
279
296
|
|
|
280
297
|
### Where the types come from
|
|
281
298
|
|
|
282
299
|
```
|
|
283
|
-
apps/dash zod
|
|
300
|
+
apps/dash ingest routes (zod) + kit methods (app/kit)
|
|
284
301
|
→ bun run openapi:emit (in apps/dash)
|
|
285
302
|
→ packages/tracker/openapi.json
|
|
286
303
|
→ bun run generate (in packages/tracker)
|
|
@@ -290,7 +307,7 @@ apps/dash zod route schemas
|
|
|
290
307
|
`openapi.json` is checked in: it is the wire contract, so a change to the API's
|
|
291
308
|
shape shows up as a reviewable diff in the commit that caused it, and the
|
|
292
309
|
client can be regenerated without a dashboard running anywhere. `schema.d.ts`
|
|
293
|
-
is generated too — never edit either by hand. Change the
|
|
310
|
+
is generated too — never edit either by hand. Change the schemas in
|
|
294
311
|
`apps/dash`, re-run both steps, commit the result.
|
|
295
312
|
|
|
296
313
|
## React Router (`luchy/react-router`)
|
package/dist/api/index.d.ts
CHANGED
|
@@ -22,14 +22,36 @@ export type PageviewInput = components['schemas']['PageviewInput'];
|
|
|
22
22
|
export type IdentifyInput = components['schemas']['IdentifyInput'];
|
|
23
23
|
/** What both ingest endpoints answer with on success. */
|
|
24
24
|
export type IngestSuccess = components['schemas']['SuccessResponse'];
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
25
|
+
type Body<P extends keyof paths> = paths[P] extends {
|
|
26
|
+
post: {
|
|
27
|
+
requestBody?: {
|
|
28
|
+
content: {
|
|
29
|
+
'application/json': infer B;
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
};
|
|
33
|
+
} ? B : never;
|
|
34
|
+
type Result<P extends keyof paths> = paths[P] extends {
|
|
35
|
+
post: {
|
|
36
|
+
responses: {
|
|
37
|
+
200: {
|
|
38
|
+
content: {
|
|
39
|
+
'application/json': infer R;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
} ? R : never;
|
|
45
|
+
/** The body accepted by `POST /analytics.query`. */
|
|
46
|
+
export type QueryRequest = Body<'/analytics.query'>;
|
|
47
|
+
/** The result of a successful `POST /analytics.query`. */
|
|
48
|
+
export type QueryResponse = Result<'/analytics.query'>;
|
|
49
|
+
/** The body accepted by `POST /analytics.sql`. */
|
|
50
|
+
export type SqlRequest = Body<'/analytics.sql'>;
|
|
51
|
+
/** The result of a successful `POST /analytics.sql`. */
|
|
52
|
+
export type SqlResponse = Result<'/analytics.sql'>;
|
|
53
|
+
/** A kit method's error body (`{ error }`, plus `fields` for invalid input). */
|
|
54
|
+
export type MethodError = components['schemas']['InvalidInput'];
|
|
33
55
|
/** The `GET /health` body. */
|
|
34
56
|
export type HealthResponse = components['schemas']['HealthResponse'];
|
|
35
57
|
/** A 400 from any endpoint. */
|
|
@@ -59,7 +81,7 @@ export type LuchyClient = {
|
|
|
59
81
|
/**
|
|
60
82
|
* The underlying `openapi-fetch` client, pre-authenticated. Use it for
|
|
61
83
|
* anything the convenience methods below do not cover; it is typed against
|
|
62
|
-
* the full document, so `api.POST('/analytics
|
|
84
|
+
* the full document, so `api.POST('/analytics.query', { body })` is checked
|
|
63
85
|
* end to end.
|
|
64
86
|
*/
|
|
65
87
|
api: LuchyApi;
|
|
@@ -67,12 +89,11 @@ export type LuchyClient = {
|
|
|
67
89
|
trackPageview(pageview: PageviewInput): Promise<IngestSuccess | null>;
|
|
68
90
|
query(request: QueryRequest): Promise<QueryResponse>;
|
|
69
91
|
sql(request: SqlRequest): Promise<SqlResponse>;
|
|
70
|
-
schema(): Promise<string>;
|
|
71
92
|
health(): Promise<HealthResponse>;
|
|
72
93
|
};
|
|
73
94
|
/**
|
|
74
95
|
* Thrown by the endpoints where failing loudly is the right answer — reads
|
|
75
|
-
* (`query`, `sql`, `
|
|
96
|
+
* (`query`, `sql`, `health`), as opposed to the fire-and-forget
|
|
76
97
|
* tracking calls.
|
|
77
98
|
*
|
|
78
99
|
* ```ts
|
|
@@ -89,8 +110,8 @@ export declare class LuchyApiError extends Error {
|
|
|
89
110
|
/** The HTTP status. Transport failures propagate as the runtime's own error, not this one. */
|
|
90
111
|
readonly status: number;
|
|
91
112
|
/** The parsed error body, when the API sent one. */
|
|
92
|
-
readonly body: ValidationError | ServerError | undefined;
|
|
93
|
-
constructor(message: string, status: number, body?: ValidationError | ServerError);
|
|
113
|
+
readonly body: ValidationError | ServerError | MethodError | undefined;
|
|
114
|
+
constructor(message: string, status: number, body?: ValidationError | ServerError | MethodError);
|
|
94
115
|
}
|
|
95
116
|
/**
|
|
96
117
|
* Creates a Luchy API client.
|
package/dist/api/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA,OAAqB,EAAE,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAElD;;;;;;;;;;;;GAYG;AAEH,YAAY,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;AAElC,iDAAiD;AACjD,MAAM,MAAM,UAAU,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,YAAY,CAAC,CAAC;AAC7D,oDAAoD;AACpD,MAAM,MAAM,aAAa,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,eAAe,CAAC,CAAC;AACnE,sEAAsE;AACtE,MAAM,MAAM,aAAa,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,eAAe,CAAC,CAAC;AACnE,yDAAyD;AACzD,MAAM,MAAM,aAAa,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,iBAAiB,CAAC,CAAC;AACrE,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA,OAAqB,EAAE,KAAK,MAAM,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,KAAK,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAElD;;;;;;;;;;;;GAYG;AAEH,YAAY,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;AAElC,iDAAiD;AACjD,MAAM,MAAM,UAAU,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,YAAY,CAAC,CAAC;AAC7D,oDAAoD;AACpD,MAAM,MAAM,aAAa,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,eAAe,CAAC,CAAC;AACnE,sEAAsE;AACtE,MAAM,MAAM,aAAa,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,eAAe,CAAC,CAAC;AACnE,yDAAyD;AACzD,MAAM,MAAM,aAAa,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,iBAAiB,CAAC,CAAC;AACrE,KAAK,IAAI,CAAC,CAAC,SAAS,MAAM,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,SAAS;IAClD,IAAI,EAAE;QAAE,WAAW,CAAC,EAAE;YAAE,OAAO,EAAE;gBAAE,kBAAkB,EAAE,MAAM,CAAC,CAAA;aAAE,CAAA;SAAE,CAAA;KAAE,CAAC;CACtE,GACG,CAAC,GACD,KAAK,CAAC;AACV,KAAK,MAAM,CAAC,CAAC,SAAS,MAAM,KAAK,IAAI,KAAK,CAAC,CAAC,CAAC,SAAS;IACpD,IAAI,EAAE;QAAE,SAAS,EAAE;YAAE,GAAG,EAAE;gBAAE,OAAO,EAAE;oBAAE,kBAAkB,EAAE,MAAM,CAAC,CAAA;iBAAE,CAAA;aAAE,CAAA;SAAE,CAAA;KAAE,CAAC;CAC5E,GACG,CAAC,GACD,KAAK,CAAC;AAEV,oDAAoD;AACpD,MAAM,MAAM,YAAY,GAAG,IAAI,CAAC,kBAAkB,CAAC,CAAC;AACpD,0DAA0D;AAC1D,MAAM,MAAM,aAAa,GAAG,MAAM,CAAC,kBAAkB,CAAC,CAAC;AACvD,kDAAkD;AAClD,MAAM,MAAM,UAAU,GAAG,IAAI,CAAC,gBAAgB,CAAC,CAAC;AAChD,wDAAwD;AACxD,MAAM,MAAM,WAAW,GAAG,MAAM,CAAC,gBAAgB,CAAC,CAAC;AACnD,gFAAgF;AAChF,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,cAAc,CAAC,CAAC;AAChE,8BAA8B;AAC9B,MAAM,MAAM,cAAc,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,gBAAgB,CAAC,CAAC;AACrE,+BAA+B;AAC/B,MAAM,MAAM,eAAe,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,yBAAyB,CAAC,CAAC;AAC/E,+BAA+B;AAC/B,MAAM,MAAM,WAAW,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,qBAAqB,CAAC,CAAC;AAEvE,kEAAkE;AAClE,MAAM,MAAM,QAAQ,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;AAErC,MAAM,MAAM,kBAAkB,GAAG;IAC/B,8DAA8D;IAC9D,MAAM,EAAE,MAAM,CAAC;IACf,0EAA0E;IAC1E,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,KAAK,CAAC,EAAE,OAAO,UAAU,CAAC,KAAK,CAAC;IAChC;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC,CAAC;AAEF,MAAM,MAAM,WAAW,GAAG;IACxB;;;;;OAKG;IACH,GAAG,EAAE,QAAQ,CAAC;IACd,UAAU,CAAC,KAAK,EAAE,UAAU,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IAC7D,aAAa,CAAC,QAAQ,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAC;IACtE,KAAK,CAAC,OAAO,EAAE,YAAY,GAAG,OAAO,CAAC,aAAa,CAAC,CAAC;IACrD,GAAG,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;IAC/C,MAAM,IAAI,OAAO,CAAC,cAAc,CAAC,CAAC;CACnC,CAAC;AAYF;;;;;;;;;;;;;;GAcG;AACH,qBAAa,aAAc,SAAQ,KAAK;IACtC,8FAA8F;IAC9F,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,oDAAoD;IACpD,QAAQ,CAAC,IAAI,EAAE,eAAe,GAAG,WAAW,GAAG,WAAW,GAAG,SAAS,CAAC;gBAGrE,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE,eAAe,GAAG,WAAW,GAAG,WAAW;CAOrD;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAmK1E"}
|
package/dist/api/index.js
CHANGED
|
@@ -90,7 +90,7 @@ function createLuchyClient(options) {
|
|
|
90
90
|
* ```
|
|
91
91
|
*/
|
|
92
92
|
async query(request) {
|
|
93
|
-
const { data, error, response } = await api.POST("/analytics
|
|
93
|
+
const { data, error, response } = await api.POST("/analytics.query", {
|
|
94
94
|
body: request
|
|
95
95
|
});
|
|
96
96
|
if (error || !data) {
|
|
@@ -104,8 +104,9 @@ function createLuchyClient(options) {
|
|
|
104
104
|
},
|
|
105
105
|
/**
|
|
106
106
|
* Runs one read-only `SELECT`/`WITH` statement over the whole database
|
|
107
|
-
* (not scoped to a project — filter on `project_id`).
|
|
108
|
-
* `
|
|
107
|
+
* (not scoped to a project — filter on `project_id`). The schema is in
|
|
108
|
+
* the method's description: `GET /llms.txt` or `/openapi.json` on the dash.
|
|
109
|
+
* Throws `LuchyApiError` on a non-2xx, including rejected statements (400).
|
|
109
110
|
*
|
|
110
111
|
* ```ts
|
|
111
112
|
* const { rows } = await luchy.sql({
|
|
@@ -115,7 +116,7 @@ function createLuchyClient(options) {
|
|
|
115
116
|
* ```
|
|
116
117
|
*/
|
|
117
118
|
async sql(request) {
|
|
118
|
-
const { data, error, response } = await api.POST("/analytics
|
|
119
|
+
const { data, error, response } = await api.POST("/analytics.sql", {
|
|
119
120
|
body: request
|
|
120
121
|
});
|
|
121
122
|
if (error || !data) {
|
|
@@ -127,17 +128,6 @@ function createLuchyClient(options) {
|
|
|
127
128
|
}
|
|
128
129
|
return data;
|
|
129
130
|
},
|
|
130
|
-
/** The tables, columns and conventions to write `sql` queries against. */
|
|
131
|
-
async schema() {
|
|
132
|
-
const { data, response } = await api.GET("/analytics/schema");
|
|
133
|
-
if (!data) {
|
|
134
|
-
throw new LuchyApiError(
|
|
135
|
-
`Luchy schema failed with ${response.status}`,
|
|
136
|
-
response.status
|
|
137
|
-
);
|
|
138
|
-
}
|
|
139
|
-
return data.doc;
|
|
140
|
-
},
|
|
141
131
|
/**
|
|
142
132
|
* Pings the API. Throws `LuchyApiError` if it is not healthy.
|
|
143
133
|
*
|