luchy 1.0.1 → 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 CHANGED
@@ -254,7 +254,8 @@ const { current, delta } = await luchy.query({
254
254
  });
255
255
 
256
256
  // 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.
257
+ // database (not scoped to a project). The tables are documented in the
258
+ // method's description: https://dash.luchy.app/llms.txt
258
259
  const { rows } = await luchy.sql({
259
260
  query:
260
261
  'SELECT tenant, COUNT(*) AS n FROM events WHERE project_id = ? GROUP BY tenant',
@@ -274,13 +275,17 @@ const { data, error } = await luchy.api.GET('/health');
274
275
  The request and response types come from the document too, so they are worth
275
276
  importing rather than restating: `EventInput`, `PageviewInput`,
276
277
  `IngestSuccess`, `QueryRequest`, `QueryResponse`, `SqlRequest`, `SqlResponse`,
277
- plus the raw `paths` and
278
- `components`.
278
+ `MethodError`, plus the raw `paths` and `components`.
279
+
280
+ Reads use a query key (`wak_…`) minted in the dashboard under Keys. They call
281
+ the dashboard's analytics methods (`POST /api/analytics.query`,
282
+ `/api/analytics.sql`, …), the same ones its MCP server at
283
+ `https://dash.luchy.app/mcp` exposes to agents.
279
284
 
280
285
  ### Where the types come from
281
286
 
282
287
  ```
283
- apps/dash zod route schemas
288
+ apps/dash ingest routes (zod) + kit methods (app/kit)
284
289
  → bun run openapi:emit (in apps/dash)
285
290
  → packages/tracker/openapi.json
286
291
  → bun run generate (in packages/tracker)
@@ -290,7 +295,7 @@ apps/dash zod route schemas
290
295
  `openapi.json` is checked in: it is the wire contract, so a change to the API's
291
296
  shape shows up as a reviewable diff in the commit that caused it, and the
292
297
  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
298
+ is generated too — never edit either by hand. Change the schemas in
294
299
  `apps/dash`, re-run both steps, commit the result.
295
300
 
296
301
  ## 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
  *
@@ -185,65 +185,42 @@ export interface paths {
185
185
  patch?: never;
186
186
  trace?: never;
187
187
  };
188
- "/analytics/query": {
188
+ "/health": {
189
189
  parameters: {
190
190
  query?: never;
191
191
  header?: never;
192
192
  path?: never;
193
193
  cookie?: never;
194
194
  };
195
- get?: never;
196
- put?: never;
197
- /** @description Metrics for the key's project. */
198
- post: {
195
+ get: {
199
196
  parameters: {
200
197
  query?: never;
201
198
  header?: never;
202
199
  path?: never;
203
200
  cookie?: never;
204
201
  };
205
- requestBody?: {
206
- content: {
207
- "application/json": components["schemas"]["AnalyticsQueryRequest"];
208
- };
209
- };
202
+ requestBody?: never;
210
203
  responses: {
211
- /** @description Query result */
204
+ /** @description Health check endpoint */
212
205
  200: {
213
206
  headers: {
214
207
  [name: string]: unknown;
215
208
  };
216
209
  content: {
217
- "application/json": components["schemas"]["AnalyticsQueryResponse"];
218
- };
219
- };
220
- /** @description Invalid request */
221
- 400: {
222
- headers: {
223
- [name: string]: unknown;
224
- };
225
- content: {
226
- "application/json": components["schemas"]["ValidationErrorResponse"];
227
- };
228
- };
229
- /** @description Internal server error */
230
- 500: {
231
- headers: {
232
- [name: string]: unknown;
233
- };
234
- content: {
235
- "application/json": components["schemas"]["ServerErrorResponse"];
210
+ "application/json": components["schemas"]["HealthResponse"];
236
211
  };
237
212
  };
238
213
  };
239
214
  };
215
+ put?: never;
216
+ post?: never;
240
217
  delete?: never;
241
218
  options?: never;
242
219
  head?: never;
243
220
  patch?: never;
244
221
  trace?: never;
245
222
  };
246
- "/analytics/sql": {
223
+ "/analytics.query": {
247
224
  parameters: {
248
225
  query?: never;
249
226
  header?: never;
@@ -252,120 +229,199 @@ export interface paths {
252
229
  };
253
230
  get?: never;
254
231
  put?: never;
255
- /** @description Read-only SQL over the whole database — not scoped to the key’s project. Luchy is single-owner, so any valid query key may use it; filter on project_id yourself. */
256
- post: {
257
- parameters: {
258
- query?: never;
259
- header?: never;
260
- path?: never;
261
- cookie?: never;
262
- };
263
- requestBody?: {
264
- content: {
265
- "application/json": components["schemas"]["SqlRequest"];
266
- };
267
- };
268
- responses: {
269
- /** @description Rows */
270
- 200: {
271
- headers: {
272
- [name: string]: unknown;
273
- };
274
- content: {
275
- "application/json": components["schemas"]["SqlResponse"];
276
- };
277
- };
278
- /** @description Invalid request */
279
- 400: {
280
- headers: {
281
- [name: string]: unknown;
282
- };
283
- content: {
284
- "application/json": components["schemas"]["ValidationErrorResponse"];
285
- };
286
- };
287
- /** @description Internal server error */
288
- 500: {
289
- headers: {
290
- [name: string]: unknown;
291
- };
292
- content: {
293
- "application/json": components["schemas"]["ServerErrorResponse"];
294
- };
295
- };
296
- };
232
+ /**
233
+ * Metrics for the project over a time range: totals, an optional breakdown, time series and a comparison with another period.
234
+ * @description Every requested metric is computed. visitors = distinct coalesce(user, visitor_hash); users = distinct identified users; avg_duration in seconds; bounce_rate in %.
235
+ *
236
+ * Add `granularity` for a zero-filled time series and `compare: "previous_period"` to also get the period before with deltas; the result renders as a chart.
237
+ *
238
+ * Example — who used the app this week vs last: `{ metrics: ["users", "events"], granularity: "day", compare: "previous_period" }`.
239
+ *
240
+ * Anything this cannot express goes through `analytics.sql`.
241
+ *
242
+ * Requires the `analytics:read` permission.
243
+ */
244
+ post: operations["analytics.query"];
245
+ delete?: never;
246
+ options?: never;
247
+ head?: never;
248
+ patch?: never;
249
+ trace?: never;
250
+ };
251
+ "/analytics.sql": {
252
+ parameters: {
253
+ query?: never;
254
+ header?: never;
255
+ path?: never;
256
+ cookie?: never;
297
257
  };
258
+ get?: never;
259
+ put?: never;
260
+ /**
261
+ * Run one read-only SELECT or WITH statement over the analytics database, for anything `analytics.query` cannot express.
262
+ * @description Not scoped to a project: filter on `project_id = ?` and pass the current project's id (MCP: it's in the server instructions) in `params`. Rows are capped by `limit`.
263
+ *
264
+ * Luchy analytics database (SQLite / Cloudflare D1). Read-only: one SELECT or WITH statement.
265
+ * Timestamps are ISO-8601 UTC strings ('2025-01-31T12:00:00.000Z'); compare them as strings or use datetime()/strftime().
266
+ * Not scoped to a project: filter on project_id.
267
+ *
268
+ * projects — one tracked site/app
269
+ * id TEXT PK, name TEXT, created_at TEXT
270
+ *
271
+ * sessions — one visit by one (user, actor) or one anonymous visitor; 30 min idle timeout, 24 h cap
272
+ * id TEXT PK, project_id TEXT -> projects.id
273
+ * visitor_hash TEXT anonymous browser id (sha256 of a daily salt, project, ip, user agent); rotates daily
274
+ * user TEXT app-provided user id (signed identity), NULL when anonymous
275
+ * actor TEXT the real person when impersonating (user = who is viewed as), else NULL
276
+ * started_at TEXT, last_seen_at TEXT duration = (julianday(last_seen_at) - julianday(started_at)) * 86400 seconds
277
+ * browser TEXT, os TEXT, device TEXT ('desktop' | 'mobile' | 'tablet' | ...), country TEXT (ISO 3166-1 alpha-2), language TEXT
278
+ * entry_page TEXT pathname of the first event
279
+ *
280
+ * events — everything that happened, pageviews included
281
+ * id TEXT PK, project_id TEXT -> projects.id, session_id TEXT -> sessions.id
282
+ * type TEXT 'pageview' for pageviews; otherwise sender-defined ('server', 'click', ...) or NULL
283
+ * name TEXT 'pageview' for pageviews; event name otherwise ('__pageleave' marks a page exit)
284
+ * pathname TEXT, referrer TEXT
285
+ * tenant TEXT the consumer's client/workspace from the signed identity
286
+ * payload TEXT JSON object of props: json_extract(payload, '$.role')
287
+ * created_at TEXT
288
+ *
289
+ * people — person profiles sent via identify; one row per (project, user)
290
+ * project_id TEXT -> projects.id, user TEXT (same id as sessions.user)
291
+ * traits TEXT JSON object (name, email, ...): json_extract(traits, '$.email')
292
+ * created_at TEXT, updated_at TEXT
293
+ *
294
+ * Conventions
295
+ * visitors = COUNT(DISTINCT coalesce(s.user, s.visitor_hash)); users = COUNT(DISTINCT s.user)
296
+ * Join: FROM events e JOIN sessions s ON s.id = e.session_id
297
+ * Profiles: LEFT JOIN people p ON p.project_id = s.project_id AND p.user = s.user
298
+ * Alias duplicate column names: rows are objects keyed by column name.
299
+ *
300
+ * Requires the `analytics:read` permission.
301
+ */
302
+ post: operations["analytics.sql"];
298
303
  delete?: never;
299
304
  options?: never;
300
305
  head?: never;
301
306
  patch?: never;
302
307
  trace?: never;
303
308
  };
304
- "/analytics/schema": {
309
+ "/analytics.people": {
305
310
  parameters: {
306
311
  query?: never;
307
312
  header?: never;
308
313
  path?: never;
309
314
  cookie?: never;
310
315
  };
311
- /** @description Tables, columns and conventions for writing /analytics/sql queries. */
312
- get: {
313
- parameters: {
314
- query?: never;
315
- header?: never;
316
- path?: never;
317
- cookie?: never;
318
- };
319
- requestBody?: never;
320
- responses: {
321
- /** @description Schema documentation */
322
- 200: {
323
- headers: {
324
- [name: string]: unknown;
325
- };
326
- content: {
327
- "application/json": components["schemas"]["AnalyticsSchemaResponse"];
328
- };
329
- };
330
- };
316
+ get?: never;
317
+ put?: never;
318
+ /**
319
+ * Identified people active in the period or the one before, with sessions and actions this period vs the previous one. Quiet people (active before, not now) come last.
320
+ * @description Requires the `analytics:read` permission.
321
+ */
322
+ post: operations["analytics.people"];
323
+ delete?: never;
324
+ options?: never;
325
+ head?: never;
326
+ patch?: never;
327
+ trace?: never;
328
+ };
329
+ "/analytics.customers": {
330
+ parameters: {
331
+ query?: never;
332
+ header?: never;
333
+ path?: never;
334
+ cookie?: never;
331
335
  };
336
+ get?: never;
332
337
  put?: never;
333
- post?: never;
338
+ /**
339
+ * Customers (the `tenant` of events) active in the period or the one before: people and actions this period vs the previous one.
340
+ * @description Requires the `analytics:read` permission.
341
+ */
342
+ post: operations["analytics.customers"];
334
343
  delete?: never;
335
344
  options?: never;
336
345
  head?: never;
337
346
  patch?: never;
338
347
  trace?: never;
339
348
  };
340
- "/health": {
349
+ "/analytics.person": {
341
350
  parameters: {
342
351
  query?: never;
343
352
  header?: never;
344
353
  path?: never;
345
354
  cookie?: never;
346
355
  };
347
- get: {
348
- parameters: {
349
- query?: never;
350
- header?: never;
351
- path?: never;
352
- cookie?: never;
353
- };
354
- requestBody?: never;
355
- responses: {
356
- /** @description Health check endpoint */
357
- 200: {
358
- headers: {
359
- [name: string]: unknown;
360
- };
361
- content: {
362
- "application/json": components["schemas"]["HealthResponse"];
363
- };
364
- };
365
- };
356
+ get?: never;
357
+ put?: never;
358
+ /**
359
+ * One person, all time: profile traits, customers, who acted as them, and their latest sessions with what they did.
360
+ * @description Requires the `analytics:read` permission.
361
+ */
362
+ post: operations["analytics.person"];
363
+ delete?: never;
364
+ options?: never;
365
+ head?: never;
366
+ patch?: never;
367
+ trace?: never;
368
+ };
369
+ "/analytics.live": {
370
+ parameters: {
371
+ query?: never;
372
+ header?: never;
373
+ path?: never;
374
+ cookie?: never;
366
375
  };
376
+ get?: never;
367
377
  put?: never;
368
- post?: never;
378
+ /**
379
+ * Who is on the site right now: open sessions with their current page, top pages, countries and referrers.
380
+ * @description Requires the `analytics:read` permission.
381
+ */
382
+ post: operations["analytics.live"];
383
+ delete?: never;
384
+ options?: never;
385
+ head?: never;
386
+ patch?: never;
387
+ trace?: never;
388
+ };
389
+ "/admin.projects": {
390
+ parameters: {
391
+ query?: never;
392
+ header?: never;
393
+ path?: never;
394
+ cookie?: never;
395
+ };
396
+ get?: never;
397
+ put?: never;
398
+ /**
399
+ * List every project (tracked site or app) with its id. Use the id as ?project=<id> on /mcp or /api to switch project.
400
+ * @description Requires the `admin` permission.
401
+ */
402
+ post: operations["admin.projects"];
403
+ delete?: never;
404
+ options?: never;
405
+ head?: never;
406
+ patch?: never;
407
+ trace?: never;
408
+ };
409
+ "/admin.keys": {
410
+ parameters: {
411
+ query?: never;
412
+ header?: never;
413
+ path?: never;
414
+ cookie?: never;
415
+ };
416
+ get?: never;
417
+ put?: never;
418
+ /**
419
+ * The current project's ingest keys (public, for the browser script) and its secret key (signs identities, authenticates identify).
420
+ * @description Query keys (`wak_…`) live in the willy.im IdP and are managed from the dashboard; they are not listed here.
421
+ *
422
+ * Requires the `admin` permission.
423
+ */
424
+ post: operations["admin.keys"];
369
425
  delete?: never;
370
426
  options?: never;
371
427
  head?: never;
@@ -568,158 +624,8 @@ export interface components {
568
624
  * }
569
625
  */
570
626
  traits: {
571
- [key: string]: string | number | boolean | unknown | unknown;
572
- };
573
- };
574
- AnalyticsPeriod: {
575
- from: string;
576
- to: string;
577
- totals: {
578
- events?: number;
579
- pageviews?: number;
580
- sessions?: number;
581
- visitors?: number;
582
- users?: number;
583
- avg_duration?: number;
584
- bounce_rate?: number;
585
- };
586
- rows?: {
587
- events?: number;
588
- pageviews?: number;
589
- sessions?: number;
590
- visitors?: number;
591
- users?: number;
592
- avg_duration?: number;
593
- bounce_rate?: number;
594
- key: string | number | unknown;
595
- }[];
596
- series?: {
597
- events?: number;
598
- pageviews?: number;
599
- sessions?: number;
600
- visitors?: number;
601
- users?: number;
602
- avg_duration?: number;
603
- bounce_rate?: number;
604
- bucket: string;
605
- }[];
606
- };
607
- AnalyticsQueryResponse: {
608
- current: components["schemas"]["AnalyticsPeriod"];
609
- previous?: components["schemas"]["AnalyticsPeriod"];
610
- delta?: {
611
- events?: {
612
- abs: number;
613
- /** @description null when previous is 0 */
614
- pct: number | null;
615
- };
616
- pageviews?: {
617
- abs: number;
618
- /** @description null when previous is 0 */
619
- pct: number | null;
620
- };
621
- sessions?: {
622
- abs: number;
623
- /** @description null when previous is 0 */
624
- pct: number | null;
625
- };
626
- visitors?: {
627
- abs: number;
628
- /** @description null when previous is 0 */
629
- pct: number | null;
630
- };
631
- users?: {
632
- abs: number;
633
- /** @description null when previous is 0 */
634
- pct: number | null;
635
- };
636
- avg_duration?: {
637
- abs: number;
638
- /** @description null when previous is 0 */
639
- pct: number | null;
640
- };
641
- bounce_rate?: {
642
- abs: number;
643
- /** @description null when previous is 0 */
644
- pct: number | null;
645
- };
646
- };
647
- };
648
- AnalyticsQueryRequest: {
649
- /**
650
- * @description Inclusive start (ISO 8601)
651
- * @example 2025-01-01T00:00:00Z
652
- */
653
- from: string;
654
- /**
655
- * @description Exclusive end (ISO 8601)
656
- * @example 2025-02-01T00:00:00Z
657
- */
658
- to: string;
659
- /**
660
- * @description IANA timezone for buckets, hour and weekday. Default UTC.
661
- * @example America/Santo_Domingo
662
- */
663
- tz?: string;
664
- /**
665
- * @description Every metric is computed. visitors = distinct coalesce(user, visitor_hash); avg_duration in seconds; bounce_rate in %.
666
- * @example [
667
- * "pageviews",
668
- * "visitors"
669
- * ]
670
- */
671
- metrics: ("events" | "pageviews" | "sessions" | "visitors" | "users" | "avg_duration" | "bounce_rate")[];
672
- /**
673
- * @description A fixed field or `props.<key>` (a key of the event payload). `hour` and `weekday` (0 = Sunday) are in `tz`.
674
- * @example tenant
675
- */
676
- breakdown?: ("page" | "event" | "type" | "source" | "referrer" | "country" | "device" | "browser" | "os" | "tenant" | "user" | "actor" | "hour" | "weekday") | string;
677
- /**
678
- * @description [field, op, value]; `in` takes a list.
679
- * @example [
680
- * [
681
- * "props.role",
682
- * "eq",
683
- * "admin"
684
- * ]
685
- * ]
686
- */
687
- filters?: ((("page" | "event" | "type" | "source" | "referrer" | "country" | "device" | "browser" | "os" | "tenant" | "user" | "actor" | "hour" | "weekday") | string) | ("eq" | "neq" | "in") | (string | number | boolean | unknown | (string | number | boolean | unknown | unknown)[] | unknown))[][];
688
- /**
689
- * @description Adds a zero-filled time series
690
- * @enum {string}
691
- */
692
- granularity?: "hour" | "day" | "week" | "month";
693
- /** @description Also runs the query on another range and returns deltas */
694
- compare?: "previous_period" | {
695
- /** @example 2025-01-01T00:00:00Z */
696
- from: string;
697
- /** @example 2025-02-01T00:00:00Z */
698
- to: string;
627
+ [key: string]: string | number | boolean | null;
699
628
  };
700
- /** @description Max breakdown rows (default 100) */
701
- limit?: number;
702
- };
703
- SqlResponse: {
704
- columns: string[];
705
- rows: {
706
- [key: string]: unknown;
707
- }[];
708
- truncated: boolean;
709
- };
710
- SqlRequest: {
711
- /**
712
- * @description One SELECT or WITH statement. See GET /analytics/schema.
713
- * @example SELECT tenant, COUNT(*) AS actions FROM events WHERE project_id = ? GROUP BY tenant
714
- */
715
- query: string;
716
- /** @description Bound to `?` placeholders in order */
717
- params?: (string | number | boolean | unknown | unknown)[];
718
- /** @description Max rows (default 1000) */
719
- limit?: number;
720
- };
721
- AnalyticsSchemaResponse: {
722
- doc: string;
723
629
  };
724
630
  /** @description Health check response */
725
631
  HealthResponse: {
@@ -734,12 +640,586 @@ export interface components {
734
640
  */
735
641
  timestamp: string;
736
642
  };
643
+ Error: {
644
+ error: string;
645
+ };
646
+ InvalidInput: {
647
+ error: string;
648
+ fields: {
649
+ [key: string]: string[];
650
+ };
651
+ };
652
+ };
653
+ responses: {
654
+ /** @description The input doesn't match the method's schema. `fields` maps each field to its errors. */
655
+ InvalidInput: {
656
+ headers: {
657
+ [name: string]: unknown;
658
+ };
659
+ content: {
660
+ "application/json": components["schemas"]["InvalidInput"];
661
+ };
662
+ };
663
+ /** @description No credentials, or invalid or expired ones. */
664
+ Unauthorized: {
665
+ headers: {
666
+ [name: string]: unknown;
667
+ };
668
+ content: {
669
+ "application/json": components["schemas"]["Error"];
670
+ };
671
+ };
672
+ /** @description The caller lacks the permission. */
673
+ Forbidden: {
674
+ headers: {
675
+ [name: string]: unknown;
676
+ };
677
+ content: {
678
+ "application/json": components["schemas"]["Error"];
679
+ };
680
+ };
681
+ /** @description No such record. */
682
+ NotFound: {
683
+ headers: {
684
+ [name: string]: unknown;
685
+ };
686
+ content: {
687
+ "application/json": components["schemas"]["Error"];
688
+ };
689
+ };
690
+ /** @description The record's state doesn't allow this. */
691
+ Conflict: {
692
+ headers: {
693
+ [name: string]: unknown;
694
+ };
695
+ content: {
696
+ "application/json": components["schemas"]["Error"];
697
+ };
698
+ };
737
699
  };
738
- responses: never;
739
700
  parameters: never;
740
701
  requestBodies: never;
741
702
  headers: never;
742
703
  pathItems: never;
743
704
  }
744
705
  export type $defs = Record<string, never>;
745
- export type operations = Record<string, never>;
706
+ export interface operations {
707
+ "analytics.query": {
708
+ parameters: {
709
+ query?: never;
710
+ header?: never;
711
+ path?: never;
712
+ cookie?: never;
713
+ };
714
+ requestBody: {
715
+ content: {
716
+ "application/json": {
717
+ /** @description Inclusive start (ISO 8601). Default: 7 days before `to`. */
718
+ from?: string;
719
+ /** @description Exclusive end (ISO 8601). Default: now. */
720
+ to?: string;
721
+ /** @description IANA timezone for buckets, hour and weekday. Default UTC. */
722
+ tz?: string;
723
+ /** @description e.g. ["pageviews", "visitors"] */
724
+ metrics: ("events" | "pageviews" | "sessions" | "visitors" | "users" | "avg_duration" | "bounce_rate")[];
725
+ /** @description A fixed field or `props.<key>` (a key of the event payload). `hour` and `weekday` (0 = Sunday) are in `tz`. */
726
+ breakdown?: ("page" | "event" | "type" | "source" | "referrer" | "country" | "device" | "browser" | "os" | "tenant" | "user" | "actor" | "hour" | "weekday") | string;
727
+ /** @description [field, op, value]; `in` takes a list. e.g. [["props.role", "eq", "admin"]] */
728
+ filters?: [
729
+ ("page" | "event" | "type" | "source" | "referrer" | "country" | "device" | "browser" | "os" | "tenant" | "user" | "actor" | "hour" | "weekday") | string,
730
+ "eq" | "neq" | "in",
731
+ (string | number | boolean | null) | (string | number | boolean | null)[]
732
+ ][];
733
+ /**
734
+ * @description Adds a zero-filled time series
735
+ * @enum {string}
736
+ */
737
+ granularity?: "hour" | "day" | "week" | "month";
738
+ /** @description Also runs the query on another range and returns deltas */
739
+ compare?: "previous_period" | {
740
+ from: string;
741
+ to: string;
742
+ };
743
+ /** @description Max breakdown rows (default 100) */
744
+ limit?: number;
745
+ };
746
+ };
747
+ };
748
+ responses: {
749
+ /** @description OK */
750
+ 200: {
751
+ headers: {
752
+ [name: string]: unknown;
753
+ };
754
+ content: {
755
+ "application/json": {
756
+ /** @description The totals in one line, for reading aloud */
757
+ summary: string;
758
+ current: {
759
+ from: string;
760
+ to: string;
761
+ totals: {
762
+ events?: number;
763
+ pageviews?: number;
764
+ sessions?: number;
765
+ visitors?: number;
766
+ users?: number;
767
+ avg_duration?: number;
768
+ bounce_rate?: number;
769
+ };
770
+ rows?: {
771
+ events?: number;
772
+ pageviews?: number;
773
+ sessions?: number;
774
+ visitors?: number;
775
+ users?: number;
776
+ avg_duration?: number;
777
+ bounce_rate?: number;
778
+ key: (string | number) | null;
779
+ }[];
780
+ series?: {
781
+ events?: number;
782
+ pageviews?: number;
783
+ sessions?: number;
784
+ visitors?: number;
785
+ users?: number;
786
+ avg_duration?: number;
787
+ bounce_rate?: number;
788
+ bucket: string;
789
+ }[];
790
+ };
791
+ previous?: {
792
+ from: string;
793
+ to: string;
794
+ totals: {
795
+ events?: number;
796
+ pageviews?: number;
797
+ sessions?: number;
798
+ visitors?: number;
799
+ users?: number;
800
+ avg_duration?: number;
801
+ bounce_rate?: number;
802
+ };
803
+ rows?: {
804
+ events?: number;
805
+ pageviews?: number;
806
+ sessions?: number;
807
+ visitors?: number;
808
+ users?: number;
809
+ avg_duration?: number;
810
+ bounce_rate?: number;
811
+ key: (string | number) | null;
812
+ }[];
813
+ series?: {
814
+ events?: number;
815
+ pageviews?: number;
816
+ sessions?: number;
817
+ visitors?: number;
818
+ users?: number;
819
+ avg_duration?: number;
820
+ bounce_rate?: number;
821
+ bucket: string;
822
+ }[];
823
+ };
824
+ delta?: {
825
+ events?: {
826
+ abs: number;
827
+ /** @description null when the previous value is 0 */
828
+ pct: number | null;
829
+ };
830
+ pageviews?: {
831
+ abs: number;
832
+ /** @description null when the previous value is 0 */
833
+ pct: number | null;
834
+ };
835
+ sessions?: {
836
+ abs: number;
837
+ /** @description null when the previous value is 0 */
838
+ pct: number | null;
839
+ };
840
+ visitors?: {
841
+ abs: number;
842
+ /** @description null when the previous value is 0 */
843
+ pct: number | null;
844
+ };
845
+ users?: {
846
+ abs: number;
847
+ /** @description null when the previous value is 0 */
848
+ pct: number | null;
849
+ };
850
+ avg_duration?: {
851
+ abs: number;
852
+ /** @description null when the previous value is 0 */
853
+ pct: number | null;
854
+ };
855
+ bounce_rate?: {
856
+ abs: number;
857
+ /** @description null when the previous value is 0 */
858
+ pct: number | null;
859
+ };
860
+ };
861
+ };
862
+ };
863
+ };
864
+ 400: components["responses"]["InvalidInput"];
865
+ 401: components["responses"]["Unauthorized"];
866
+ 403: components["responses"]["Forbidden"];
867
+ 404: components["responses"]["NotFound"];
868
+ 409: components["responses"]["Conflict"];
869
+ };
870
+ };
871
+ "analytics.sql": {
872
+ parameters: {
873
+ query?: never;
874
+ header?: never;
875
+ path?: never;
876
+ cookie?: never;
877
+ };
878
+ requestBody: {
879
+ content: {
880
+ "application/json": {
881
+ /** @description e.g. SELECT tenant, COUNT(*) AS actions FROM events WHERE project_id = ? GROUP BY tenant */
882
+ query: string;
883
+ /** @description Bound to `?` placeholders in order */
884
+ params?: (string | number | boolean | null)[];
885
+ /** @description Max rows (default 1000) */
886
+ limit?: number;
887
+ };
888
+ };
889
+ };
890
+ responses: {
891
+ /** @description OK */
892
+ 200: {
893
+ headers: {
894
+ [name: string]: unknown;
895
+ };
896
+ content: {
897
+ "application/json": {
898
+ columns: string[];
899
+ rows: {
900
+ [key: string]: unknown;
901
+ }[];
902
+ truncated: boolean;
903
+ };
904
+ };
905
+ };
906
+ 400: components["responses"]["InvalidInput"];
907
+ 401: components["responses"]["Unauthorized"];
908
+ 403: components["responses"]["Forbidden"];
909
+ 404: components["responses"]["NotFound"];
910
+ 409: components["responses"]["Conflict"];
911
+ };
912
+ };
913
+ "analytics.people": {
914
+ parameters: {
915
+ query?: never;
916
+ header?: never;
917
+ path?: never;
918
+ cookie?: never;
919
+ };
920
+ requestBody?: {
921
+ content: {
922
+ "application/json": {
923
+ /** @description Inclusive start (ISO 8601). Default: 7 days before `to`. */
924
+ from?: string;
925
+ /** @description Exclusive end (ISO 8601). Default: now. */
926
+ to?: string;
927
+ /** @description IANA timezone for buckets, hour and weekday. Default UTC. */
928
+ tz?: string;
929
+ /** @description Only people acting for this customer */
930
+ tenant?: string;
931
+ limit?: number;
932
+ };
933
+ };
934
+ };
935
+ responses: {
936
+ /** @description OK */
937
+ 200: {
938
+ headers: {
939
+ [name: string]: unknown;
940
+ };
941
+ content: {
942
+ "application/json": {
943
+ from: string;
944
+ to: string;
945
+ people: {
946
+ user: string;
947
+ name: string;
948
+ email: string | null;
949
+ tenant: string | null;
950
+ lastSeen: string;
951
+ sessions: number;
952
+ actions: number;
953
+ pageviews: number;
954
+ helpedSessions: number;
955
+ previousActions: number;
956
+ quiet: boolean;
957
+ }[];
958
+ };
959
+ };
960
+ };
961
+ 400: components["responses"]["InvalidInput"];
962
+ 401: components["responses"]["Unauthorized"];
963
+ 403: components["responses"]["Forbidden"];
964
+ 404: components["responses"]["NotFound"];
965
+ 409: components["responses"]["Conflict"];
966
+ };
967
+ };
968
+ "analytics.customers": {
969
+ parameters: {
970
+ query?: never;
971
+ header?: never;
972
+ path?: never;
973
+ cookie?: never;
974
+ };
975
+ requestBody?: {
976
+ content: {
977
+ "application/json": {
978
+ /** @description Inclusive start (ISO 8601). Default: 7 days before `to`. */
979
+ from?: string;
980
+ /** @description Exclusive end (ISO 8601). Default: now. */
981
+ to?: string;
982
+ /** @description IANA timezone for buckets, hour and weekday. Default UTC. */
983
+ tz?: string;
984
+ limit?: number;
985
+ };
986
+ };
987
+ };
988
+ responses: {
989
+ /** @description OK */
990
+ 200: {
991
+ headers: {
992
+ [name: string]: unknown;
993
+ };
994
+ content: {
995
+ "application/json": {
996
+ from: string;
997
+ to: string;
998
+ customers: {
999
+ tenant: string;
1000
+ people: number;
1001
+ previousPeople: number;
1002
+ actions: number;
1003
+ previousActions: number;
1004
+ helpedSessions: number;
1005
+ lastSeen: string;
1006
+ quiet: boolean;
1007
+ }[];
1008
+ };
1009
+ };
1010
+ };
1011
+ 400: components["responses"]["InvalidInput"];
1012
+ 401: components["responses"]["Unauthorized"];
1013
+ 403: components["responses"]["Forbidden"];
1014
+ 404: components["responses"]["NotFound"];
1015
+ 409: components["responses"]["Conflict"];
1016
+ };
1017
+ };
1018
+ "analytics.person": {
1019
+ parameters: {
1020
+ query?: never;
1021
+ header?: never;
1022
+ path?: never;
1023
+ cookie?: never;
1024
+ };
1025
+ requestBody: {
1026
+ content: {
1027
+ "application/json": {
1028
+ /** @description The app-provided user id */
1029
+ user: string;
1030
+ /** @description Latest sessions to include (default 30) */
1031
+ sessions?: number;
1032
+ };
1033
+ };
1034
+ };
1035
+ responses: {
1036
+ /** @description OK */
1037
+ 200: {
1038
+ headers: {
1039
+ [name: string]: unknown;
1040
+ };
1041
+ content: {
1042
+ "application/json": {
1043
+ user: string;
1044
+ name: string;
1045
+ email: string | null;
1046
+ traits: {
1047
+ [key: string]: string | number | boolean;
1048
+ };
1049
+ firstSeen: string;
1050
+ lastSeen: string;
1051
+ sessions: number;
1052
+ actions: number;
1053
+ tenants: {
1054
+ tenant: string;
1055
+ actions: number;
1056
+ }[];
1057
+ actors: {
1058
+ id: string;
1059
+ name: string;
1060
+ email: string | null;
1061
+ }[];
1062
+ timeline: {
1063
+ id: string;
1064
+ startedAt: string;
1065
+ lastSeenAt: string | null;
1066
+ actor: {
1067
+ id: string;
1068
+ name: string;
1069
+ email: string | null;
1070
+ } | null;
1071
+ browser: string | null;
1072
+ os: string | null;
1073
+ device: string | null;
1074
+ country: string | null;
1075
+ events: {
1076
+ id: string;
1077
+ type: string | null;
1078
+ name: string;
1079
+ pathname: string;
1080
+ tenant: string | null;
1081
+ props: {
1082
+ [key: string]: unknown;
1083
+ };
1084
+ createdAt: string;
1085
+ }[];
1086
+ }[];
1087
+ };
1088
+ };
1089
+ };
1090
+ 400: components["responses"]["InvalidInput"];
1091
+ 401: components["responses"]["Unauthorized"];
1092
+ 403: components["responses"]["Forbidden"];
1093
+ 404: components["responses"]["NotFound"];
1094
+ 409: components["responses"]["Conflict"];
1095
+ };
1096
+ };
1097
+ "analytics.live": {
1098
+ parameters: {
1099
+ query?: never;
1100
+ header?: never;
1101
+ path?: never;
1102
+ cookie?: never;
1103
+ };
1104
+ requestBody?: {
1105
+ content: {
1106
+ "application/json": {
1107
+ /** @description How recent a pageview counts as live (default 5) */
1108
+ windowMinutes?: number;
1109
+ };
1110
+ };
1111
+ };
1112
+ responses: {
1113
+ /** @description OK */
1114
+ 200: {
1115
+ headers: {
1116
+ [name: string]: unknown;
1117
+ };
1118
+ content: {
1119
+ "application/json": {
1120
+ /** @description People on the site right now */
1121
+ count: number;
1122
+ sessions: {
1123
+ id: string;
1124
+ user: {
1125
+ id: string;
1126
+ name: string;
1127
+ email: string | null;
1128
+ } | null;
1129
+ actor: {
1130
+ id: string;
1131
+ name: string;
1132
+ email: string | null;
1133
+ } | null;
1134
+ country: string | null;
1135
+ browser: string | null;
1136
+ os: string | null;
1137
+ device: string | null;
1138
+ currentPage: string;
1139
+ referrer: string | null;
1140
+ startedAt: string;
1141
+ }[];
1142
+ pages: {
1143
+ count: number;
1144
+ pathname: string;
1145
+ }[];
1146
+ countries: {
1147
+ count: number;
1148
+ country: string;
1149
+ }[];
1150
+ referrers: {
1151
+ count: number;
1152
+ referrer: string;
1153
+ }[];
1154
+ };
1155
+ };
1156
+ };
1157
+ 400: components["responses"]["InvalidInput"];
1158
+ 401: components["responses"]["Unauthorized"];
1159
+ 403: components["responses"]["Forbidden"];
1160
+ 404: components["responses"]["NotFound"];
1161
+ 409: components["responses"]["Conflict"];
1162
+ };
1163
+ };
1164
+ "admin.projects": {
1165
+ parameters: {
1166
+ query?: never;
1167
+ header?: never;
1168
+ path?: never;
1169
+ cookie?: never;
1170
+ };
1171
+ requestBody?: never;
1172
+ responses: {
1173
+ /** @description OK */
1174
+ 200: {
1175
+ headers: {
1176
+ [name: string]: unknown;
1177
+ };
1178
+ content: {
1179
+ "application/json": {
1180
+ projects: {
1181
+ id: string;
1182
+ name: string;
1183
+ createdAt: string;
1184
+ }[];
1185
+ };
1186
+ };
1187
+ };
1188
+ 401: components["responses"]["Unauthorized"];
1189
+ 403: components["responses"]["Forbidden"];
1190
+ 404: components["responses"]["NotFound"];
1191
+ 409: components["responses"]["Conflict"];
1192
+ };
1193
+ };
1194
+ "admin.keys": {
1195
+ parameters: {
1196
+ query?: never;
1197
+ header?: never;
1198
+ path?: never;
1199
+ cookie?: never;
1200
+ };
1201
+ requestBody?: never;
1202
+ responses: {
1203
+ /** @description OK */
1204
+ 200: {
1205
+ headers: {
1206
+ [name: string]: unknown;
1207
+ };
1208
+ content: {
1209
+ "application/json": {
1210
+ ingestKeys: {
1211
+ id: string;
1212
+ name: string | null;
1213
+ createdAt: string;
1214
+ }[];
1215
+ secretKey: string | null;
1216
+ };
1217
+ };
1218
+ };
1219
+ 401: components["responses"]["Unauthorized"];
1220
+ 403: components["responses"]["Forbidden"];
1221
+ 404: components["responses"]["NotFound"];
1222
+ 409: components["responses"]["Conflict"];
1223
+ };
1224
+ };
1225
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "luchy",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "Luchy tracking - browser script, server tracker and React Router middleware",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",