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 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). `luchy.schema()` documents the tables.
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
- `components`.
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 route schemas
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 zod schemas in
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`)
@@ -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
- /** The body accepted by `POST /analytics/query`. */
26
- export type QueryRequest = components['schemas']['AnalyticsQueryRequest'];
27
- /** The result of a successful `POST /analytics/query`. */
28
- export type QueryResponse = components['schemas']['AnalyticsQueryResponse'];
29
- /** The body accepted by `POST /analytics/sql`. */
30
- export type SqlRequest = components['schemas']['SqlRequest'];
31
- /** The result of a successful `POST /analytics/sql`. */
32
- export type SqlResponse = components['schemas']['SqlResponse'];
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/query', { body })` is checked
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`, `schema`, `health`), as opposed to the fire-and-forget
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.
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"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"}
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/query", {
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`). Throws
108
- * `LuchyApiError` on a non-2xx, including rejected statements (400).
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/sql", {
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
  *