luchy 0.0.1 → 0.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 ADDED
@@ -0,0 +1,314 @@
1
+ # Luchy Tracker
2
+
3
+ A lightweight, privacy-focused analytics tracker for web applications. Built for CDN deployment with automatic compression and versioning.
4
+
5
+ ## Features
6
+
7
+ - 🚀 **Lightweight**: ~3KB minified, ~1.5KB compressed
8
+ - 📊 **Privacy-focused**: No cookies, no personal data collection
9
+ - 🔄 **Auto-tracking**: Pageviews, outbound links, hash routing
10
+ - 📦 **CDN-ready**: Optimized for global distribution
11
+ - 🗜️ **Compressed**: Gzip and Brotli compression included
12
+ - 🏷️ **Versioned**: Commit-based versioning for safe deployments
13
+
14
+ ## Quick Start
15
+
16
+ ### Installation
17
+
18
+ ```bash
19
+ npm install luchy
20
+ ```
21
+
22
+ ### Basic Usage
23
+
24
+ ```html
25
+ <script
26
+ src="https://cdn.luchy.app/luchy.min.js"
27
+ data-api-key="your-api-key"
28
+ data-auto-pageviews
29
+ ></script>
30
+ ```
31
+
32
+ ### Advanced Usage
33
+
34
+ ```html
35
+ <script
36
+ src="https://cdn.luchy.app/luchy.min.js"
37
+ data-api-key="your-api-key"
38
+ data-endpoint="https://api.luchy.app/ingest"
39
+ data-auto-pageviews
40
+ data-auto-outbound
41
+ data-hash-routing
42
+ data-track-localhost
43
+ ></script>
44
+ ```
45
+
46
+ ## Data Attributes
47
+
48
+ | Attribute | Description | Required |
49
+ | ---------------------- | --------------------------------------- | -------- |
50
+ | `data-api-key` | Your Luchy API key | ✅ Yes |
51
+ | `data-endpoint` | Custom API endpoint | ❌ No |
52
+ | `data-auto-pageviews` | Enable automatic pageview tracking | ❌ No |
53
+ | `data-auto-outbound` | Enable automatic outbound link tracking | ❌ No |
54
+ | `data-auto-events` | Enable automatic data attribute event tracking | ❌ No |
55
+ | `data-hash-routing` | Enable hash routing support | ❌ No |
56
+ | `data-track-localhost` | Track localhost traffic | ❌ No |
57
+
58
+ ## Data Attribute Events
59
+
60
+ Track custom events without JavaScript by adding `data-luchy-event` to any HTML element. Clicks are detected automatically via event delegation.
61
+
62
+ ```html
63
+ <!-- Simple event -->
64
+ <button data-luchy-event="cta-click">Sign Up</button>
65
+
66
+ <!-- With a payload (data-luchy-payload-*) -->
67
+ <a data-luchy-event="post-click" data-luchy-payload-slug="hello-world" href="/blog/hello-world">
68
+ Read Post
69
+ </a>
70
+
71
+ <!-- Multiple keys, dashes convert to underscores -->
72
+ <!-- data-luchy-payload-plan-tier becomes { plan_tier: "enterprise" } -->
73
+ <button
74
+ data-luchy-event="purchase"
75
+ data-luchy-payload-plan-tier="enterprise"
76
+ data-luchy-payload-source="pricing-page"
77
+ >
78
+ Buy Now
79
+ </button>
80
+ ```
81
+
82
+ > `data-luchy-prop-*` is the original spelling and keeps working — it is written
83
+ > into pages we do not control, so it is never going away. New markup should use
84
+ > `data-luchy-payload-*`, which matches the field name on the wire and in the
85
+ > dashboard.
86
+
87
+ When an element has `data-luchy-event`, it takes priority over outbound link tracking to avoid duplicate events. Disable with `data-auto-events="false"` on the script tag.
88
+
89
+ ## Manual Tracking
90
+
91
+ ```javascript
92
+ // Track a pageview
93
+ window.luchy.trackPageview();
94
+
95
+ // Track a custom event. The second argument is the event payload —
96
+ // it is sent as `payload` and shows up as the event's properties.
97
+ window.luchy.trackEvent('Button Click', {
98
+ button: 'signup',
99
+ page: 'home'
100
+ });
101
+
102
+ // Enable auto-tracking features
103
+ window.luchy.enableAutoPageviews();
104
+ window.luchy.enableAutoOutboundTracking();
105
+ window.luchy.enableHashRouting();
106
+ ```
107
+
108
+ ## Server-Side Tracking
109
+
110
+ The browser script can only see what happens in a page. A server sees what the
111
+ application actually *did* — an order placed, a payment recorded, a webhook
112
+ processed — and those are usually the events worth having.
113
+
114
+ `luchy/server` carries no browser globals, so it runs in Node, Bun,
115
+ Deno and Cloudflare Workers:
116
+
117
+ ```ts
118
+ import { createServerTracker } from 'luchy/server';
119
+
120
+ const luchy = createServerTracker({
121
+ apiKey: process.env.LUCHY_API_KEY!,
122
+ // optional: point at a self-hosted dashboard
123
+ endpoint: 'https://dash.luchy.app/api/ingest',
124
+ onError: (error) => logger.warn('[luchy] event dropped', error)
125
+ });
126
+
127
+ await luchy.trackEvent({
128
+ name: 'order:placed',
129
+ pathname: '/checkout',
130
+ type: 'server',
131
+ payload: { plan: 'pro', amount: 29 }
132
+ });
133
+ ```
134
+
135
+ Nothing here ever rejects — analytics must not break the request it is
136
+ describing. Pass `onError` if you want failures in your logs.
137
+
138
+ Fire it after the response so it costs no latency. On Cloudflare Workers:
139
+
140
+ ```ts
141
+ ctx.waitUntil(luchy.trackEvent({ name: 'order:placed', pathname: '/checkout' }));
142
+ ```
143
+
144
+ ## API client (`luchy/api`)
145
+
146
+ `createServerTracker` covers the two calls most apps make. `luchy/api`
147
+ is the whole API: a typed client generated from the dashboard's own OpenAPI
148
+ document, with tracking *and* reading on it. It has no DOM globals and no node
149
+ built-ins, so the same import works on a server and in a browser.
150
+
151
+ ```ts
152
+ import { createLuchyClient } from 'luchy/api';
153
+
154
+ const luchy = createLuchyClient({
155
+ apiKey: LUCHY_API_KEY,
156
+ // optional: point at a self-hosted dashboard's API root
157
+ endpoint: 'https://dash.luchy.app/api',
158
+ onError: (error) => console.warn('[luchy] event dropped', error)
159
+ });
160
+
161
+ // Tracking never rejects, on either side of the wire.
162
+ await luchy.trackEvent({
163
+ name: 'order:placed',
164
+ pathname: '/checkout',
165
+ payload: { plan: 'pro', amount: 29 }
166
+ });
167
+
168
+ // Reads do — a chart with silently missing numbers is worse than an error.
169
+ const { results } = await luchy.query({
170
+ date_range: '30d',
171
+ metrics: ['pageviews', 'visitors'],
172
+ dimensions: ['event:page']
173
+ });
174
+ ```
175
+
176
+ Failed reads throw `LuchyApiError`, which carries the HTTP status and the
177
+ error body the API sent. For anything without a convenience method, `luchy.api`
178
+ is the underlying [`openapi-fetch`](https://openapi-ts.dev/openapi-fetch/)
179
+ client, typed against every documented endpoint:
180
+
181
+ ```ts
182
+ const { data, error } = await luchy.api.GET('/health');
183
+ ```
184
+
185
+ The request and response types come from the document too, so they are worth
186
+ importing rather than restating: `EventInput`, `PageviewInput`,
187
+ `IngestSuccess`, `QueryRequest`, `QueryResponse`, plus the raw `paths` and
188
+ `components`.
189
+
190
+ ### Where the types come from
191
+
192
+ ```
193
+ apps/dash zod route schemas
194
+ → bun run openapi:emit (in apps/dash)
195
+ → packages/tracker/openapi.json
196
+ → bun run generate (in packages/tracker)
197
+ → src/api/schema.d.ts
198
+ ```
199
+
200
+ `openapi.json` is checked in: it is the wire contract, so a change to the API's
201
+ shape shows up as a reviewable diff in the commit that caused it, and the
202
+ client can be regenerated without a dashboard running anywhere. `schema.d.ts`
203
+ is generated too — never edit either by hand. Change the zod schemas in
204
+ `apps/dash`, re-run both steps, commit the result.
205
+
206
+ ## Development
207
+
208
+ ### Build Scripts
209
+
210
+ ```bash
211
+ # Build the CDN bundle *and* the importable package entry points
212
+ npm run build
213
+
214
+ # Only the package entry points (dist/index.js, dist/server.js + types)
215
+ npm run build:lib
216
+
217
+ # Upload to R2 (requires Wrangler setup)
218
+ npm run upload
219
+
220
+ # Build and deploy everything
221
+ npm run deploy
222
+ ```
223
+
224
+ ### File Sizes
225
+
226
+ - **Minified**: 3.0 KB
227
+ - **Gzipped**: 1.7 KB (43% smaller)
228
+ - **Brotli**: 1.5 KB (50% smaller)
229
+
230
+ ### Generated Files
231
+
232
+ ```
233
+ dist/script/
234
+ ├── luchy.js (unminified)
235
+ ├── luchy.min.js (minified)
236
+ ├── luchy.js.gz (gzipped)
237
+ ├── luchy.min.js.gz (gzipped)
238
+ ├── luchy.js.br (brotli)
239
+ └── luchy.min.js.br (brotli)
240
+ ```
241
+
242
+ ## CDN Deployment
243
+
244
+ ### R2 Upload Structure
245
+
246
+ The deploy script uploads files to two locations:
247
+
248
+ **Root Level:**
249
+
250
+ ```
251
+ bucket/
252
+ ├── luchy.js
253
+ ├── luchy.min.js
254
+ ├── luchy.js.gz
255
+ ├── luchy.min.js.gz
256
+ ├── luchy.js.br
257
+ └── luchy.min.js.br
258
+ ```
259
+
260
+ **Versioned Level:**
261
+
262
+ ```
263
+ bucket/v/{commit-hash}/
264
+ ├── luchy.js
265
+ ├── luchy.min.js
266
+ ├── luchy.js.gz
267
+ ├── luchy.min.js.gz
268
+ ├── luchy.js.br
269
+ └── luchy.min.js.br
270
+ ```
271
+
272
+ ### CDN Usage Examples
273
+
274
+ ```html
275
+ <!-- Latest version -->
276
+ <script src="https://cdn.luchy.app/luchy.min.js"></script>
277
+
278
+ <!-- Specific version -->
279
+ <script src="https://cdn.luchy.app/v/abc123/luchy.min.js"></script>
280
+
281
+ <!-- With compression (automatic) -->
282
+ <script src="https://cdn.luchy.app/luchy.min.js"></script>
283
+ ```
284
+
285
+ ## Deploying
286
+
287
+ `bun run deploy` builds, uploads to R2, **purges the CDN cache** and then checks
288
+ what the edge actually serves.
289
+
290
+ The purge is not optional. R2 has the new bytes the instant the upload finishes,
291
+ but `cdn.luchy.app` is cached, so skipping it leaves every customer on the
292
+ previous build while the deploy prints nothing but green. For that reason the
293
+ script refuses to start when `CLOUDFLARE_API_TOKEN` is missing, rather than
294
+ uploading and half-shipping.
295
+
296
+ The verification step fetches `https://cdn.luchy.app/luchy.min.js` with no
297
+ cache-busting query string on purpose — a unique query string bypasses the edge
298
+ cache and would pass even when real visitors are still getting the old script.
299
+
300
+ Root paths are purged; `v/{commit-hash}/` is immutable and never needs it.
301
+
302
+ ## Environment Variables
303
+
304
+ - `R2_BUCKET`: R2 bucket name (default: `cdn-luchy-app`)
305
+ - `CLOUDFLARE_API_TOKEN`: **required.** Needs the Zone > Cache Purge permission
306
+ on the `luchy.app` zone.
307
+ - `CLOUDFLARE_ACCOUNT_ID`: defaults to the account that owns `cdn-luchy-app`.
308
+ Pinned because `wrangler r2 object put` refuses to pick between accounts in a
309
+ non-interactive shell.
310
+ - `CLOUDFLARE_ZONE_ID`: defaults to the `luchy.app` zone.
311
+
312
+ ## License
313
+
314
+ MIT
@@ -0,0 +1,112 @@
1
+ import { Client } from 'openapi-fetch';
2
+ import { components, paths } from './schema';
3
+ /**
4
+ * The typed Luchy API client.
5
+ *
6
+ * `schema.d.ts` next to this file is generated from `openapi.json`, which is
7
+ * itself emitted from the zod route definitions in `apps/dash`. Nothing in this
8
+ * package restates the wire format by hand — that is what let the
9
+ * `props`/`payload` split ship unnoticed — so a field renamed in the API turns
10
+ * into a type error here rather than into silently-dropped data in production.
11
+ *
12
+ * This module is isomorphic on purpose: no DOM globals, no node built-ins, no
13
+ * `import.meta.env`. It runs in browsers, Node, Bun, Deno and Cloudflare
14
+ * Workers, which is why the transport is injectable rather than assumed.
15
+ */
16
+ export type { components, paths };
17
+ /** The body accepted by `POST /ingest/event`. */
18
+ export type EventInput = components['schemas']['EventInput'];
19
+ /** The body accepted by `POST /ingest/pageview`. */
20
+ export type PageviewInput = components['schemas']['PageviewInput'];
21
+ /** What both ingest endpoints answer with on success. */
22
+ export type IngestSuccess = components['schemas']['SuccessResponse'];
23
+ /** The body accepted by `POST /query`. */
24
+ export type QueryRequest = components['schemas']['QueryRequest'];
25
+ /** The result of a successful `POST /query`. */
26
+ export type QueryResponse = components['schemas']['QueryResponse'];
27
+ /** The `GET /health` body. */
28
+ export type HealthResponse = components['schemas']['HealthResponse'];
29
+ /** A 400 from any endpoint. */
30
+ export type ValidationError = components['schemas']['ValidationErrorResponse'];
31
+ /** A 500 from any endpoint. */
32
+ export type ServerError = components['schemas']['ServerErrorResponse'];
33
+ /** The raw typed client, with every documented endpoint on it. */
34
+ export type LuchyApi = Client<paths>;
35
+ export type LuchyClientOptions = {
36
+ /** A Luchy API key. Sent as `Authorization: Bearer <key>`. */
37
+ apiKey: string;
38
+ /** The API root, without a trailing slash. Defaults to the hosted API. */
39
+ endpoint?: string;
40
+ /**
41
+ * Transport override. Useful for tests, for runtimes that hand you a scoped
42
+ * `fetch` (Cloudflare service bindings), and for retry/proxy wrappers.
43
+ */
44
+ fetch?: typeof globalThis.fetch;
45
+ /**
46
+ * Called when a tracking call fails. `trackEvent` and `trackPageview` never
47
+ * reject — analytics must not break the thing it is describing — so this is
48
+ * the only way to see those failures.
49
+ */
50
+ onError?: (error: unknown) => void;
51
+ };
52
+ export type LuchyClient = {
53
+ /**
54
+ * The underlying `openapi-fetch` client, pre-authenticated. Use it for
55
+ * anything the convenience methods below do not cover; it is typed against
56
+ * the full document, so `api.POST('/query', { body })` is checked end to end.
57
+ */
58
+ api: LuchyApi;
59
+ trackEvent(event: EventInput): Promise<IngestSuccess | null>;
60
+ trackPageview(pageview: PageviewInput): Promise<IngestSuccess | null>;
61
+ query(request: QueryRequest): Promise<QueryResponse>;
62
+ health(): Promise<HealthResponse>;
63
+ };
64
+ /**
65
+ * Thrown by the endpoints where failing loudly is the right answer — reads
66
+ * (`query`, `health`), as opposed to the fire-and-forget tracking calls.
67
+ *
68
+ * ```ts
69
+ * try {
70
+ * await luchy.query({ date_range: '7d', metrics: ['pageviews'] });
71
+ * } catch (error) {
72
+ * if (error instanceof LuchyApiError) {
73
+ * console.error(error.status, error.body);
74
+ * }
75
+ * }
76
+ * ```
77
+ */
78
+ export declare class LuchyApiError extends Error {
79
+ /** The HTTP status. Transport failures propagate as the runtime's own error, not this one. */
80
+ readonly status: number;
81
+ /** The parsed error body, when the API sent one. */
82
+ readonly body: ValidationError | ServerError | undefined;
83
+ constructor(message: string, status: number, body?: ValidationError | ServerError);
84
+ }
85
+ /**
86
+ * Creates a Luchy API client.
87
+ *
88
+ * The same import works on a server and in a browser, so an app that tracks
89
+ * from both ends has one client and one set of types instead of two.
90
+ *
91
+ * ```ts
92
+ * import { createLuchyClient } from 'luchy/api';
93
+ *
94
+ * const luchy = createLuchyClient({
95
+ * apiKey: process.env.LUCHY_API_KEY!,
96
+ * onError: (error) => console.warn('[luchy]', error)
97
+ * });
98
+ *
99
+ * await luchy.trackEvent({
100
+ * name: 'order:placed',
101
+ * pathname: '/checkout',
102
+ * payload: { plan: 'pro', amount: 29 }
103
+ * });
104
+ *
105
+ * const stats = await luchy.query({
106
+ * date_range: '7d',
107
+ * metrics: ['pageviews', 'visitors']
108
+ * });
109
+ * ```
110
+ */
111
+ export declare function createLuchyClient(options: LuchyClientOptions): LuchyClient;
112
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +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,yDAAyD;AACzD,MAAM,MAAM,aAAa,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,iBAAiB,CAAC,CAAC;AACrE,0CAA0C;AAC1C,MAAM,MAAM,YAAY,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,cAAc,CAAC,CAAC;AACjE,gDAAgD;AAChD,MAAM,MAAM,aAAa,GAAG,UAAU,CAAC,SAAS,CAAC,CAAC,eAAe,CAAC,CAAC;AACnE,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;;;;OAIG;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,MAAM,IAAI,OAAO,CAAC,cAAc,CAAC,CAAC;CACnC,CAAC;AAYF;;;;;;;;;;;;;GAaG;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,SAAS,CAAC;gBAGvD,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,MAAM,EACd,IAAI,CAAC,EAAE,eAAe,GAAG,WAAW;CAOvC;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,kBAAkB,GAAG,WAAW,CAoI1E"}
@@ -0,0 +1,125 @@
1
+ import createClient from "openapi-fetch";
2
+ const DEFAULT_ENDPOINT = "https://dash.luchy.app/api";
3
+ class LuchyApiError extends Error {
4
+ constructor(message, status, body) {
5
+ super(message);
6
+ this.name = "LuchyApiError";
7
+ this.status = status;
8
+ this.body = body;
9
+ }
10
+ }
11
+ function createLuchyClient(options) {
12
+ const { apiKey, onError } = options;
13
+ const api = createClient({
14
+ baseUrl: options.endpoint || DEFAULT_ENDPOINT,
15
+ fetch: options.fetch,
16
+ headers: { Authorization: `Bearer ${apiKey}` }
17
+ });
18
+ async function ingest(call) {
19
+ try {
20
+ const { data, error, response } = await call();
21
+ if (error || !data) {
22
+ onError?.(
23
+ new LuchyApiError(
24
+ `Luchy ingest failed with ${response.status}`,
25
+ response.status,
26
+ error
27
+ )
28
+ );
29
+ return null;
30
+ }
31
+ return data;
32
+ } catch (error) {
33
+ onError?.(error);
34
+ return null;
35
+ }
36
+ }
37
+ return {
38
+ api,
39
+ /**
40
+ * Records a custom event. Resolves to the ingest receipt, or to `null` if
41
+ * the call failed — it never rejects.
42
+ *
43
+ * ```ts
44
+ * await luchy.trackEvent({
45
+ * name: 'signup:completed',
46
+ * pathname: '/signup',
47
+ * type: 'server',
48
+ * payload: { plan: 'free' }
49
+ * });
50
+ * ```
51
+ */
52
+ trackEvent(event) {
53
+ return ingest(
54
+ () => api.POST("/ingest/event", {
55
+ // Lets the request outlive the page on `beforeunload`. Runtimes that
56
+ // do not support the flag ignore it rather than reject.
57
+ keepalive: true,
58
+ body: event
59
+ })
60
+ );
61
+ },
62
+ /**
63
+ * Records a pageview. Same contract as `trackEvent`: never rejects.
64
+ *
65
+ * ```ts
66
+ * await luchy.trackPageview({
67
+ * pathname: '/pricing',
68
+ * referrer: 'https://google.com'
69
+ * });
70
+ * ```
71
+ */
72
+ trackPageview(pageview) {
73
+ return ingest(
74
+ () => api.POST("/ingest/pageview", { keepalive: true, body: pageview })
75
+ );
76
+ },
77
+ /**
78
+ * Runs an analytics query. Unlike the tracking calls this throws
79
+ * `LuchyApiError` on a non-2xx, because a caller rendering a chart needs to
80
+ * know the numbers are missing.
81
+ *
82
+ * ```ts
83
+ * const { results } = await luchy.query({
84
+ * date_range: '30d',
85
+ * metrics: ['pageviews', 'visitors'],
86
+ * dimensions: ['event:page']
87
+ * });
88
+ * ```
89
+ */
90
+ async query(request) {
91
+ const { data, error, response } = await api.POST("/query", {
92
+ body: request
93
+ });
94
+ if (error || !data) {
95
+ throw new LuchyApiError(
96
+ `Luchy query failed with ${response.status}`,
97
+ response.status,
98
+ error
99
+ );
100
+ }
101
+ return data;
102
+ },
103
+ /**
104
+ * Pings the API. Throws `LuchyApiError` if it is not healthy.
105
+ *
106
+ * ```ts
107
+ * const { status, timestamp } = await luchy.health();
108
+ * ```
109
+ */
110
+ async health() {
111
+ const { data, response } = await api.GET("/health");
112
+ if (!data) {
113
+ throw new LuchyApiError(
114
+ `Luchy health check failed with ${response.status}`,
115
+ response.status
116
+ );
117
+ }
118
+ return data;
119
+ }
120
+ };
121
+ }
122
+ export {
123
+ LuchyApiError,
124
+ createLuchyClient
125
+ };