@rovela-ai/sdk 0.4.3 → 0.5.2

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.
Files changed (144) hide show
  1. package/dist/admin/components/AdminNav.d.ts.map +1 -1
  2. package/dist/admin/components/AdminNav.js +10 -1
  3. package/dist/admin/components/AdminNav.js.map +1 -1
  4. package/dist/admin/components/ExampleContentBanner.js +2 -2
  5. package/dist/admin/components/ExampleContentBanner.js.map +1 -1
  6. package/dist/admin/components/SetupGuide.d.ts.map +1 -1
  7. package/dist/admin/components/SetupGuide.js +4 -4
  8. package/dist/admin/components/SetupGuide.js.map +1 -1
  9. package/dist/admin/components/index.d.ts +0 -1
  10. package/dist/admin/components/index.d.ts.map +1 -1
  11. package/dist/admin/components/index.js +0 -1
  12. package/dist/admin/components/index.js.map +1 -1
  13. package/dist/admin/hooks/fetchAdminApi.d.ts.map +1 -1
  14. package/dist/admin/hooks/fetchAdminApi.js +6 -0
  15. package/dist/admin/hooks/fetchAdminApi.js.map +1 -1
  16. package/dist/admin/index.d.ts +1 -1
  17. package/dist/admin/index.d.ts.map +1 -1
  18. package/dist/admin/index.js +1 -1
  19. package/dist/admin/index.js.map +1 -1
  20. package/dist/admin/styles/admin-theme.css +11 -0
  21. package/dist/analytics/api/dashboard.d.ts +16 -0
  22. package/dist/analytics/api/dashboard.d.ts.map +1 -0
  23. package/dist/analytics/api/dashboard.js +37 -0
  24. package/dist/analytics/api/dashboard.js.map +1 -0
  25. package/dist/analytics/api/events.d.ts +23 -0
  26. package/dist/analytics/api/events.d.ts.map +1 -0
  27. package/dist/analytics/api/events.js +55 -0
  28. package/dist/analytics/api/events.js.map +1 -0
  29. package/dist/analytics/api/index.d.ts +15 -0
  30. package/dist/analytics/api/index.d.ts.map +1 -0
  31. package/dist/analytics/api/index.js +15 -0
  32. package/dist/analytics/api/index.js.map +1 -0
  33. package/dist/analytics/api/track.d.ts +20 -0
  34. package/dist/analytics/api/track.d.ts.map +1 -0
  35. package/dist/analytics/api/track.js +233 -0
  36. package/dist/analytics/api/track.js.map +1 -0
  37. package/dist/analytics/api/visitors.d.ts +19 -0
  38. package/dist/analytics/api/visitors.d.ts.map +1 -0
  39. package/dist/analytics/api/visitors.js +49 -0
  40. package/dist/analytics/api/visitors.js.map +1 -0
  41. package/dist/analytics/client/tracker.d.ts +51 -0
  42. package/dist/analytics/client/tracker.d.ts.map +1 -0
  43. package/dist/analytics/client/tracker.js +208 -0
  44. package/dist/analytics/client/tracker.js.map +1 -0
  45. package/dist/analytics/components/AnalyticsDashboard.d.ts +2 -0
  46. package/dist/analytics/components/AnalyticsDashboard.d.ts.map +1 -0
  47. package/dist/analytics/components/AnalyticsDashboard.js +26 -0
  48. package/dist/analytics/components/AnalyticsDashboard.js.map +1 -0
  49. package/dist/analytics/components/AnalyticsPeriodContext.d.ts +13 -0
  50. package/dist/analytics/components/AnalyticsPeriodContext.d.ts.map +1 -0
  51. package/dist/analytics/components/AnalyticsPeriodContext.js +28 -0
  52. package/dist/analytics/components/AnalyticsPeriodContext.js.map +1 -0
  53. package/dist/analytics/components/AnalyticsProvider.d.ts +22 -0
  54. package/dist/analytics/components/AnalyticsProvider.d.ts.map +1 -0
  55. package/dist/analytics/components/AnalyticsProvider.js +152 -0
  56. package/dist/analytics/components/AnalyticsProvider.js.map +1 -0
  57. package/dist/analytics/components/AnalyticsTabNav.d.ts +14 -0
  58. package/dist/analytics/components/AnalyticsTabNav.d.ts.map +1 -0
  59. package/dist/analytics/components/AnalyticsTabNav.js +42 -0
  60. package/dist/analytics/components/AnalyticsTabNav.js.map +1 -0
  61. package/dist/analytics/hooks/useAnalytics.d.ts +9 -0
  62. package/dist/analytics/hooks/useAnalytics.d.ts.map +1 -0
  63. package/dist/analytics/hooks/useAnalytics.js +8 -0
  64. package/dist/analytics/hooks/useAnalytics.js.map +1 -0
  65. package/dist/analytics/hooks/useAnalyticsDashboard.d.ts +9 -0
  66. package/dist/analytics/hooks/useAnalyticsDashboard.d.ts.map +1 -0
  67. package/dist/analytics/hooks/useAnalyticsDashboard.js +45 -0
  68. package/dist/analytics/hooks/useAnalyticsDashboard.js.map +1 -0
  69. package/dist/analytics/hooks/useEventsLog.d.ts +24 -0
  70. package/dist/analytics/hooks/useEventsLog.d.ts.map +1 -0
  71. package/dist/analytics/hooks/useEventsLog.js +85 -0
  72. package/dist/analytics/hooks/useEventsLog.js.map +1 -0
  73. package/dist/analytics/hooks/useVisitorsList.d.ts +20 -0
  74. package/dist/analytics/hooks/useVisitorsList.d.ts.map +1 -0
  75. package/dist/analytics/hooks/useVisitorsList.js +73 -0
  76. package/dist/analytics/hooks/useVisitorsList.js.map +1 -0
  77. package/dist/analytics/index.d.ts +44 -0
  78. package/dist/analytics/index.d.ts.map +1 -0
  79. package/dist/analytics/index.js +39 -0
  80. package/dist/analytics/index.js.map +1 -0
  81. package/dist/analytics/server/index.d.ts +10 -0
  82. package/dist/analytics/server/index.d.ts.map +1 -0
  83. package/dist/analytics/server/index.js +9 -0
  84. package/dist/analytics/server/index.js.map +1 -0
  85. package/dist/analytics/server/normalize.d.ts +23 -0
  86. package/dist/analytics/server/normalize.d.ts.map +1 -0
  87. package/dist/analytics/server/normalize.js +75 -0
  88. package/dist/analytics/server/normalize.js.map +1 -0
  89. package/dist/analytics/server/queries.d.ts +74 -0
  90. package/dist/analytics/server/queries.d.ts.map +1 -0
  91. package/dist/analytics/server/queries.js +470 -0
  92. package/dist/analytics/server/queries.js.map +1 -0
  93. package/dist/analytics/types.d.ts +186 -0
  94. package/dist/analytics/types.d.ts.map +1 -0
  95. package/dist/analytics/types.js +16 -0
  96. package/dist/analytics/types.js.map +1 -0
  97. package/dist/analytics/views/DashboardsView.d.ts +6 -0
  98. package/dist/analytics/views/DashboardsView.d.ts.map +1 -0
  99. package/dist/analytics/views/DashboardsView.js +93 -0
  100. package/dist/analytics/views/DashboardsView.js.map +1 -0
  101. package/dist/analytics/views/EventsView.d.ts +6 -0
  102. package/dist/analytics/views/EventsView.d.ts.map +1 -0
  103. package/dist/analytics/views/EventsView.js +85 -0
  104. package/dist/analytics/views/EventsView.js.map +1 -0
  105. package/dist/analytics/views/VisitorsView.d.ts +6 -0
  106. package/dist/analytics/views/VisitorsView.d.ts.map +1 -0
  107. package/dist/analytics/views/VisitorsView.js +57 -0
  108. package/dist/analytics/views/VisitorsView.js.map +1 -0
  109. package/dist/cart/store.d.ts.map +1 -1
  110. package/dist/cart/store.js +12 -0
  111. package/dist/cart/store.js.map +1 -1
  112. package/dist/checkout/components/CheckoutFlow.d.ts.map +1 -1
  113. package/dist/checkout/components/CheckoutFlow.js +26 -2
  114. package/dist/checkout/components/CheckoutFlow.js.map +1 -1
  115. package/dist/checkout/components/ShippingForm.js +10 -5
  116. package/dist/checkout/components/ShippingForm.js.map +1 -1
  117. package/dist/checkout/hooks/useCheckout.d.ts.map +1 -1
  118. package/dist/checkout/hooks/useCheckout.js +12 -1
  119. package/dist/checkout/hooks/useCheckout.js.map +1 -1
  120. package/dist/checkout/server/handle-webhook.js +15 -0
  121. package/dist/checkout/server/handle-webhook.js.map +1 -1
  122. package/dist/checkout/types.d.ts +13 -0
  123. package/dist/checkout/types.d.ts.map +1 -1
  124. package/dist/core/db/client.d.ts +14 -0
  125. package/dist/core/db/client.d.ts.map +1 -1
  126. package/dist/core/db/client.js +12 -0
  127. package/dist/core/db/client.js.map +1 -1
  128. package/dist/core/db/index.d.ts +2 -1
  129. package/dist/core/db/index.d.ts.map +1 -1
  130. package/dist/core/db/index.js +1 -1
  131. package/dist/core/db/index.js.map +1 -1
  132. package/dist/core/db/queries.d.ts +8 -7
  133. package/dist/core/db/queries.d.ts.map +1 -1
  134. package/dist/core/db/queries.js +80 -0
  135. package/dist/core/db/queries.js.map +1 -1
  136. package/dist/core/db/schema.d.ts +340 -4
  137. package/dist/core/db/schema.d.ts.map +1 -1
  138. package/dist/core/db/schema.js +55 -1
  139. package/dist/core/db/schema.js.map +1 -1
  140. package/dist/core/server/index.d.ts +2 -2
  141. package/dist/core/server/index.d.ts.map +1 -1
  142. package/dist/core/server/index.js +2 -0
  143. package/dist/core/server/index.js.map +1 -1
  144. package/package.json +25 -1
@@ -0,0 +1,470 @@
1
+ /**
2
+ * @rovela-ai/sdk/analytics/server/queries
3
+ *
4
+ * Per-store analytics read + write helpers. Reads from the store's own Neon
5
+ * branch via `getDb()` (no tenant filtering). Each read returns a typed
6
+ * payload ready for the dashboard.
7
+ *
8
+ * Writes are best-effort: `recordEvent` swallows failures so an analytics
9
+ * outage NEVER breaks customer flows (checkout, cart, browse). The endpoint
10
+ * + webhook callers rely on that contract.
11
+ */
12
+ import { sql } from 'drizzle-orm';
13
+ import { getDb } from '../../core/db/client';
14
+ import { ensureAnalyticsTable } from '../../core/db/queries';
15
+ // =============================================================================
16
+ // DB resolution — every read helper accepts an optional `db` param so the
17
+ // platform side (which iterates over merchant stores) can pass a per-store
18
+ // Drizzle client built via `createDb(project.databaseUrl)`. Defaults to
19
+ // `getDb()` (env's DATABASE_URL) for the storefront-side admin dashboard.
20
+ // =============================================================================
21
+ function resolveDb(db) {
22
+ return db ?? getDb();
23
+ }
24
+ // =============================================================================
25
+ // Period helpers
26
+ // =============================================================================
27
+ const PERIOD_DAYS = {
28
+ today: 1,
29
+ '7d': 7,
30
+ '30d': 30,
31
+ '90d': 90,
32
+ };
33
+ export function periodToDays(period) {
34
+ return PERIOD_DAYS[period];
35
+ }
36
+ function pctDelta(current, prior) {
37
+ if (prior === 0)
38
+ return null;
39
+ return (current - prior) / prior;
40
+ }
41
+ /**
42
+ * Insert a single event. Never throws — analytics MUST NEVER break the
43
+ * customer flow. The caller is the track endpoint OR the Stripe webhook
44
+ * wrapper; both have a "don't fail the user request on analytics failure"
45
+ * contract.
46
+ *
47
+ * For `purchase` / `refund` events, the partial unique index on `order_id`
48
+ * absorbs Stripe at-least-once webhook redelivery via ON CONFLICT.
49
+ */
50
+ export async function recordEvent(input) {
51
+ try {
52
+ await ensureAnalyticsTable();
53
+ const db = getDb();
54
+ const e = input.event;
55
+ const isDedupable = e.event === 'purchase' || e.event === 'refund';
56
+ // Sparse-column extraction — TS narrowing keeps the union types honest.
57
+ const productId = 'product_id' in e ? e.product_id : null;
58
+ const variantId = 'variant_id' in e ? e.variant_id ?? null : null;
59
+ const categoryId = 'category_id' in e ? e.category_id ?? null : null;
60
+ const orderId = 'order_id' in e ? e.order_id : null;
61
+ const valueCents = 'value_cents' in e ? e.value_cents : null;
62
+ const quantity = 'quantity' in e ? e.quantity : null;
63
+ const currency = 'currency' in e ? e.currency : null;
64
+ if (isDedupable && orderId) {
65
+ // ON CONFLICT DO NOTHING via the partial unique index keyed on
66
+ // (order_id) WHERE event = 'purchase' OR 'refund'.
67
+ await db.execute(sql `
68
+ INSERT INTO analytics_events
69
+ (visitor_id, session_id, event, path, referrer,
70
+ product_id, variant_id, category_id, order_id,
71
+ value_cents, quantity, currency,
72
+ utm_source, utm_medium, utm_campaign, device, country)
73
+ VALUES
74
+ (${e.visitor_id}, ${e.session_id}, ${e.event}, ${e.path}, ${e.referrer ?? null},
75
+ ${productId}, ${variantId}, ${categoryId}, ${orderId},
76
+ ${valueCents}, ${quantity}, ${currency},
77
+ ${e.utm_source ?? null}, ${e.utm_medium ?? null}, ${e.utm_campaign ?? null},
78
+ ${input.device}, ${input.country})
79
+ ON CONFLICT DO NOTHING
80
+ `);
81
+ }
82
+ else {
83
+ await db.execute(sql `
84
+ INSERT INTO analytics_events
85
+ (visitor_id, session_id, event, path, referrer,
86
+ product_id, variant_id, category_id, order_id,
87
+ value_cents, quantity, currency,
88
+ utm_source, utm_medium, utm_campaign, device, country)
89
+ VALUES
90
+ (${e.visitor_id}, ${e.session_id}, ${e.event}, ${e.path}, ${e.referrer ?? null},
91
+ ${productId}, ${variantId}, ${categoryId}, ${orderId},
92
+ ${valueCents}, ${quantity}, ${currency},
93
+ ${e.utm_source ?? null}, ${e.utm_medium ?? null}, ${e.utm_campaign ?? null},
94
+ ${input.device}, ${input.country})
95
+ `);
96
+ }
97
+ }
98
+ catch (err) {
99
+ // Best-effort: never bubble to the caller. The track endpoint catches and
100
+ // returns 204 even on insert failure, so client tracking remains lossless
101
+ // from the visitor's perspective.
102
+ console.error('[analytics.recordEvent] insert failed:', err);
103
+ }
104
+ }
105
+ async function fetchOverview(start, end, db) {
106
+ const rows = (await db.execute(sql `
107
+ SELECT
108
+ COUNT(DISTINCT visitor_id) FILTER (WHERE event = 'page_view') AS visitors,
109
+ COUNT(DISTINCT session_id) FILTER (WHERE event = 'page_view') AS sessions,
110
+ COUNT(*) FILTER (WHERE event = 'page_view') AS pageviews,
111
+ COUNT(*) FILTER (WHERE event = 'purchase') AS purchases
112
+ FROM analytics_events
113
+ WHERE ts >= ${start.toISOString()} AND ts < ${end.toISOString()}
114
+ `));
115
+ const r = rows.rows?.[0] ?? { visitors: 0, sessions: 0, pageviews: 0, purchases: 0 };
116
+ const sessions = Number(r.sessions);
117
+ const purchases = Number(r.purchases);
118
+ return {
119
+ visitors: Number(r.visitors),
120
+ sessions,
121
+ pageviews: Number(r.pageviews),
122
+ conversionRate: sessions === 0 ? null : purchases / sessions,
123
+ };
124
+ }
125
+ async function fetchTrend(start, end, db) {
126
+ const rows = (await db.execute(sql `
127
+ SELECT
128
+ date_trunc('day', ts)::date::text AS date,
129
+ COUNT(DISTINCT visitor_id) AS visitors
130
+ FROM analytics_events
131
+ WHERE event = 'page_view'
132
+ AND ts >= ${start.toISOString()} AND ts < ${end.toISOString()}
133
+ GROUP BY 1
134
+ ORDER BY 1
135
+ `));
136
+ return (rows.rows ?? []).map((r) => ({
137
+ date: r.date,
138
+ visitors: Number(r.visitors),
139
+ }));
140
+ }
141
+ async function fetchTopPages(start, end, db) {
142
+ const rows = (await db.execute(sql `
143
+ SELECT path, COUNT(*) AS views
144
+ FROM analytics_events
145
+ WHERE event = 'page_view'
146
+ AND ts >= ${start.toISOString()} AND ts < ${end.toISOString()}
147
+ GROUP BY path
148
+ ORDER BY views DESC
149
+ LIMIT 10
150
+ `));
151
+ return (rows.rows ?? []).map((r) => ({ path: r.path, views: Number(r.views) }));
152
+ }
153
+ async function fetchTopProducts(start, end, db) {
154
+ const rows = (await db.execute(sql `
155
+ SELECT
156
+ product_id::text AS product_id,
157
+ COUNT(*) FILTER (WHERE event = 'view_item') AS views,
158
+ COUNT(*) FILTER (WHERE event = 'add_to_cart') AS adds,
159
+ COUNT(*) FILTER (WHERE event = 'purchase') AS purchases
160
+ FROM analytics_events
161
+ WHERE product_id IS NOT NULL
162
+ AND ts >= ${start.toISOString()} AND ts < ${end.toISOString()}
163
+ GROUP BY product_id
164
+ ORDER BY views DESC, adds DESC
165
+ LIMIT 10
166
+ `));
167
+ return (rows.rows ?? []).map((r) => ({
168
+ productId: r.product_id,
169
+ views: Number(r.views),
170
+ addsToCart: Number(r.adds),
171
+ purchases: Number(r.purchases),
172
+ }));
173
+ }
174
+ async function fetchSources(start, end, db) {
175
+ // Group by UTM source if present, else by referrer host, else 'direct'.
176
+ const rows = (await db.execute(sql `
177
+ SELECT
178
+ COALESCE(NULLIF(utm_source, ''), NULLIF(referrer, ''), 'direct') AS source,
179
+ COUNT(DISTINCT visitor_id) AS visitors
180
+ FROM analytics_events
181
+ WHERE event = 'page_view'
182
+ AND ts >= ${start.toISOString()} AND ts < ${end.toISOString()}
183
+ GROUP BY 1
184
+ ORDER BY visitors DESC
185
+ LIMIT 8
186
+ `));
187
+ return (rows.rows ?? []).map((r) => ({
188
+ source: r.source,
189
+ visitors: Number(r.visitors),
190
+ }));
191
+ }
192
+ async function fetchDevices(start, end, db) {
193
+ const rows = (await db.execute(sql `
194
+ SELECT
195
+ COALESCE(NULLIF(device, ''), 'unknown') AS device,
196
+ COUNT(DISTINCT visitor_id) AS visitors
197
+ FROM analytics_events
198
+ WHERE event = 'page_view'
199
+ AND ts >= ${start.toISOString()} AND ts < ${end.toISOString()}
200
+ GROUP BY 1
201
+ ORDER BY visitors DESC
202
+ `));
203
+ return (rows.rows ?? []).map((r) => ({
204
+ device: r.device,
205
+ visitors: Number(r.visitors),
206
+ }));
207
+ }
208
+ /**
209
+ * Dashboard payload. Period-scoped current + prior windows for delta-vs-prior
210
+ * KPIs. All seven panels fan out in one Promise.all (~150ms warm Neon).
211
+ *
212
+ * @param db Optional Drizzle client. Default: `getDb()` (env DATABASE_URL).
213
+ * Platform-side passes `createDb(project.databaseUrl)` to read
214
+ * from a merchant's branch.
215
+ */
216
+ export async function getAnalyticsDashboard(period, db) {
217
+ const dbRef = resolveDb(db);
218
+ // Pre-provisioned platform stores already have the table; the self-heal
219
+ // here is a no-op then. New stores get it created on first call.
220
+ // Skipped when caller passes a `db` — caller (platform) shouldn't trigger
221
+ // schema mutations against arbitrary merchant branches from a Rovela
222
+ // platform request. The storefront-side AnalyticsProvider track endpoint
223
+ // already runs the ensure path on first write.
224
+ if (!db) {
225
+ await ensureAnalyticsTable();
226
+ }
227
+ const days = periodToDays(period);
228
+ const now = new Date();
229
+ const start = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
230
+ const priorStart = new Date(start.getTime() - days * 24 * 60 * 60 * 1000);
231
+ const [current, prior, trend, topPages, topProducts, sources, devices] = await Promise.all([
232
+ fetchOverview(start, now, dbRef),
233
+ fetchOverview(priorStart, start, dbRef),
234
+ fetchTrend(start, now, dbRef),
235
+ fetchTopPages(start, now, dbRef),
236
+ fetchTopProducts(start, now, dbRef),
237
+ fetchSources(start, now, dbRef),
238
+ fetchDevices(start, now, dbRef),
239
+ ]);
240
+ const overview = {
241
+ current,
242
+ prior,
243
+ deltas: {
244
+ visitors: pctDelta(current.visitors, prior.visitors),
245
+ sessions: pctDelta(current.sessions, prior.sessions),
246
+ pageviews: pctDelta(current.pageviews, prior.pageviews),
247
+ conversionRate: current.conversionRate === null || prior.conversionRate === null
248
+ ? null
249
+ : pctDelta(current.conversionRate, prior.conversionRate),
250
+ },
251
+ };
252
+ return {
253
+ period,
254
+ overview,
255
+ trend,
256
+ topPages,
257
+ topProducts,
258
+ sources,
259
+ devices,
260
+ };
261
+ }
262
+ // =============================================================================
263
+ // Platform-side compact summary (per-store overview card)
264
+ // =============================================================================
265
+ /**
266
+ * 7-day visitor + conversion summary for the Rovela platform's per-store
267
+ * Traffic card. Fast (3 small aggregates) — the platform fan-outs across
268
+ * every store the user can see, so we keep this tight.
269
+ */
270
+ export async function getTrafficSummary7d(db) {
271
+ const dbRef = resolveDb(db);
272
+ if (!db) {
273
+ await ensureAnalyticsTable();
274
+ }
275
+ const now = new Date();
276
+ const sevenDaysAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
277
+ const fourteenDaysAgo = new Date(now.getTime() - 14 * 24 * 60 * 60 * 1000);
278
+ const [current, prior] = await Promise.all([
279
+ fetchOverview(sevenDaysAgo, now, dbRef),
280
+ fetchOverview(fourteenDaysAgo, sevenDaysAgo, dbRef),
281
+ ]);
282
+ return {
283
+ visitors7d: current.visitors,
284
+ visitorsPrior7d: prior.visitors,
285
+ delta: pctDelta(current.visitors, prior.visitors),
286
+ conversionRate7d: current.conversionRate,
287
+ };
288
+ }
289
+ const EVENT_LOG_SORT_COLUMNS = new Set([
290
+ 'ts',
291
+ 'event',
292
+ 'path',
293
+ 'referrer',
294
+ 'country',
295
+ ]);
296
+ export async function getEventsLog(options, db) {
297
+ const dbRef = resolveDb(db);
298
+ if (!db) {
299
+ await ensureAnalyticsTable();
300
+ }
301
+ const days = periodToDays(options.period);
302
+ const now = new Date();
303
+ const start = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
304
+ const limit = Math.min(Math.max(options.limit ?? 50, 1), 200);
305
+ // Fetch one extra row to detect "is there a next page". The extra row is
306
+ // sliced off before returning.
307
+ const fetchLimit = limit + 1;
308
+ const sortBy = EVENT_LOG_SORT_COLUMNS.has(options.sortBy)
309
+ ? options.sortBy
310
+ : 'ts';
311
+ const sortDir = options.sortDir === 'asc' ? 'asc' : 'desc';
312
+ // Cursor predicate — only meaningful for the default ts-desc sort because
313
+ // mixing sort + cursor on arbitrary columns is error-prone. For non-ts
314
+ // sorts we ignore the cursor and just return the first page; the UI
315
+ // disables paginate-by-cursor when a custom sort is active.
316
+ const cursorBigInt = options.cursor && /^\d+$/.test(options.cursor)
317
+ ? options.cursor
318
+ : null;
319
+ const orderClause = sortBy === 'ts'
320
+ ? sortDir === 'asc'
321
+ ? sql `ts ASC, id ASC`
322
+ : sql `ts DESC, id DESC`
323
+ : sortBy === 'event'
324
+ ? sortDir === 'asc'
325
+ ? sql `event ASC, ts DESC`
326
+ : sql `event DESC, ts DESC`
327
+ : sortBy === 'path'
328
+ ? sortDir === 'asc'
329
+ ? sql `path ASC, ts DESC`
330
+ : sql `path DESC, ts DESC`
331
+ : sortBy === 'referrer'
332
+ ? sortDir === 'asc'
333
+ ? sql `referrer ASC NULLS LAST, ts DESC`
334
+ : sql `referrer DESC NULLS LAST, ts DESC`
335
+ : sortDir === 'asc'
336
+ ? sql `country ASC NULLS LAST, ts DESC`
337
+ : sql `country DESC NULLS LAST, ts DESC`;
338
+ const cursorClause = sortBy === 'ts' && cursorBigInt
339
+ ? sortDir === 'desc'
340
+ ? sql `AND id < ${cursorBigInt}::bigint`
341
+ : sql `AND id > ${cursorBigInt}::bigint`
342
+ : sql ``;
343
+ const heartbeatClause = options.hideHeartbeats
344
+ ? sql `AND event <> 'session_heartbeat'`
345
+ : sql ``;
346
+ const result = (await dbRef.execute(sql `
347
+ SELECT id, ts, event, path, referrer, country, session_id, visitor_id,
348
+ product_id, value_cents, currency, device
349
+ FROM analytics_events
350
+ WHERE ts >= ${start.toISOString()} AND ts < ${now.toISOString()}
351
+ ${cursorClause}
352
+ ${heartbeatClause}
353
+ ORDER BY ${orderClause}
354
+ LIMIT ${fetchLimit}
355
+ `));
356
+ const allRows = result.rows ?? [];
357
+ const hasMore = allRows.length > limit;
358
+ const slice = hasMore ? allRows.slice(0, limit) : allRows;
359
+ // Cursor for next page is only stable for ts-sorted pagination.
360
+ const nextCursor = hasMore && sortBy === 'ts' ? String(slice[slice.length - 1].id) : null;
361
+ const mapped = slice.map((r) => ({
362
+ id: String(r.id),
363
+ ts: timestampToIso(r.ts),
364
+ event: r.event,
365
+ path: r.path,
366
+ referrer: r.referrer,
367
+ country: r.country,
368
+ sessionId: r.session_id,
369
+ visitorId: r.visitor_id,
370
+ productId: r.product_id,
371
+ valueCents: r.value_cents === null ? null : Number(r.value_cents),
372
+ currency: r.currency,
373
+ device: r.device,
374
+ }));
375
+ return { rows: mapped, nextCursor };
376
+ }
377
+ const VISITOR_SORT_COLUMNS = new Set([
378
+ 'last_seen',
379
+ 'first_seen',
380
+ 'sessions',
381
+ 'pageviews',
382
+ 'country',
383
+ ]);
384
+ function timestampToIso(value) {
385
+ if (value instanceof Date)
386
+ return value.toISOString();
387
+ return String(value ?? '');
388
+ }
389
+ export async function getVisitorsList(options, db) {
390
+ const dbRef = resolveDb(db);
391
+ if (!db) {
392
+ await ensureAnalyticsTable();
393
+ }
394
+ const days = periodToDays(options.period);
395
+ const now = new Date();
396
+ const start = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
397
+ const limit = Math.min(Math.max(options.limit ?? 50, 1), 200);
398
+ const fetchLimit = limit + 1;
399
+ const offset = options.cursor && /^\d+$/.test(options.cursor)
400
+ ? Number(options.cursor)
401
+ : 0;
402
+ const sortBy = VISITOR_SORT_COLUMNS.has(options.sortBy)
403
+ ? options.sortBy
404
+ : 'last_seen';
405
+ const sortDir = options.sortDir === 'asc' ? 'asc' : 'desc';
406
+ const orderClause = sortBy === 'last_seen'
407
+ ? sortDir === 'asc'
408
+ ? sql `last_seen ASC`
409
+ : sql `last_seen DESC`
410
+ : sortBy === 'first_seen'
411
+ ? sortDir === 'asc'
412
+ ? sql `first_seen ASC`
413
+ : sql `first_seen DESC`
414
+ : sortBy === 'sessions'
415
+ ? sortDir === 'asc'
416
+ ? sql `sessions ASC`
417
+ : sql `sessions DESC`
418
+ : sortBy === 'pageviews'
419
+ ? sortDir === 'asc'
420
+ ? sql `pageviews ASC`
421
+ : sql `pageviews DESC`
422
+ : sortDir === 'asc'
423
+ ? sql `country ASC NULLS LAST`
424
+ : sql `country DESC NULLS LAST`;
425
+ // First-touch attribution via a correlated subquery scoped to the same
426
+ // period window. Cheap because (visitor_id, ts) is indexed.
427
+ const result = (await dbRef.execute(sql `
428
+ SELECT
429
+ ae.visitor_id,
430
+ MIN(ae.ts) AS first_seen,
431
+ MAX(ae.ts) AS last_seen,
432
+ COUNT(DISTINCT ae.session_id) AS sessions,
433
+ COUNT(*) FILTER (WHERE ae.event = 'page_view') AS pageviews,
434
+ COUNT(*) FILTER (WHERE ae.event = 'purchase') AS purchases,
435
+ MAX(ae.country) AS country,
436
+ (
437
+ SELECT COALESCE(NULLIF(sub.utm_source, ''),
438
+ NULLIF(sub.referrer, ''),
439
+ 'direct')
440
+ FROM analytics_events sub
441
+ WHERE sub.visitor_id = ae.visitor_id
442
+ AND sub.ts >= ${start.toISOString()}
443
+ AND sub.ts < ${now.toISOString()}
444
+ ORDER BY sub.ts ASC
445
+ LIMIT 1
446
+ ) AS initial_source
447
+ FROM analytics_events ae
448
+ WHERE ae.ts >= ${start.toISOString()} AND ae.ts < ${now.toISOString()}
449
+ GROUP BY ae.visitor_id
450
+ ORDER BY ${orderClause}
451
+ LIMIT ${fetchLimit}
452
+ OFFSET ${offset}
453
+ `));
454
+ const allRows = result.rows ?? [];
455
+ const hasMore = allRows.length > limit;
456
+ const slice = hasMore ? allRows.slice(0, limit) : allRows;
457
+ const nextCursor = hasMore ? String(offset + limit) : null;
458
+ const mapped = slice.map((r) => ({
459
+ visitorId: r.visitor_id,
460
+ firstSeen: timestampToIso(r.first_seen),
461
+ lastSeen: timestampToIso(r.last_seen),
462
+ sessions: Number(r.sessions),
463
+ pageviews: Number(r.pageviews),
464
+ purchases: Number(r.purchases),
465
+ country: r.country,
466
+ initialSource: r.initial_source ?? 'direct',
467
+ }));
468
+ return { rows: mapped, nextCursor };
469
+ }
470
+ //# sourceMappingURL=queries.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queries.js","sourceRoot":"","sources":["../../../src/analytics/server/queries.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,186 @@
1
+ /**
2
+ * @rovela-ai/sdk/analytics/types
3
+ *
4
+ * Event vocabulary for the storefront analytics tracker. GA4-shaped.
5
+ *
6
+ * Discriminated union — every event type lists its required fields explicitly.
7
+ * The server's track endpoint validates against this shape; unknown events are
8
+ * rejected with 400. New events must be added BOTH to the union below AND to
9
+ * the CHECK constraint in `ensureAnalyticsTable()` (core/db/queries.ts).
10
+ *
11
+ * Identity fields (`visitor_id`, `session_id`) are supplied by the client
12
+ * tracker, not declared on each event. Server-derived fields (`device`,
13
+ * `country`, `ts`) are added during the INSERT and not part of the wire shape.
14
+ */
15
+ /** Event kinds. Keep in lockstep with the SQL CHECK constraint
16
+ * (core/db/queries.ts `ensureAnalyticsTable`). */
17
+ export type AnalyticsEventKind = 'page_view' | 'session_start' | 'session_heartbeat' | 'session_end' | 'view_item_list' | 'view_item' | 'select_item' | 'add_to_cart' | 'view_cart' | 'begin_checkout' | 'add_shipping_info' | 'purchase' | 'refund';
18
+ /** Identity envelope present on every wire event. */
19
+ export interface AnalyticsIdentity {
20
+ visitor_id: string;
21
+ session_id: string;
22
+ }
23
+ /** Shared optional context every event may carry. */
24
+ export interface AnalyticsCommon {
25
+ path: string;
26
+ referrer?: string | null;
27
+ utm_source?: string | null;
28
+ utm_medium?: string | null;
29
+ utm_campaign?: string | null;
30
+ }
31
+ /** Per-event-type payload, before identity + server-derived fields are merged. */
32
+ export type AnalyticsPayload = ({
33
+ event: 'page_view';
34
+ } & AnalyticsCommon) | ({
35
+ event: 'session_start';
36
+ } & AnalyticsCommon) | ({
37
+ event: 'session_heartbeat';
38
+ } & AnalyticsCommon) | ({
39
+ event: 'session_end';
40
+ } & AnalyticsCommon) | ({
41
+ event: 'view_item_list';
42
+ category_id: string;
43
+ } & AnalyticsCommon) | ({
44
+ event: 'view_item';
45
+ product_id: string;
46
+ value_cents: number;
47
+ currency: string;
48
+ } & AnalyticsCommon) | ({
49
+ event: 'select_item';
50
+ product_id: string;
51
+ category_id?: string | null;
52
+ } & AnalyticsCommon) | ({
53
+ event: 'add_to_cart';
54
+ product_id: string;
55
+ variant_id?: string | null;
56
+ quantity: number;
57
+ value_cents: number;
58
+ currency: string;
59
+ } & AnalyticsCommon) | ({
60
+ event: 'view_cart';
61
+ value_cents: number;
62
+ currency: string;
63
+ } & AnalyticsCommon) | ({
64
+ event: 'begin_checkout';
65
+ value_cents: number;
66
+ currency: string;
67
+ } & AnalyticsCommon) | ({
68
+ event: 'add_shipping_info';
69
+ value_cents: number;
70
+ currency: string;
71
+ } & AnalyticsCommon) | ({
72
+ event: 'purchase';
73
+ order_id: string;
74
+ value_cents: number;
75
+ currency: string;
76
+ } & AnalyticsCommon) | ({
77
+ event: 'refund';
78
+ order_id: string;
79
+ value_cents: number;
80
+ currency: string;
81
+ } & AnalyticsCommon);
82
+ /** Full wire event = identity + payload. */
83
+ export type AnalyticsWireEvent = AnalyticsIdentity & AnalyticsPayload;
84
+ export type AnalyticsPeriod = 'today' | '7d' | '30d' | '90d';
85
+ /** Primary KPI row — visitors / sessions / pageviews / conversion. */
86
+ export interface AnalyticsOverview {
87
+ visitors: number;
88
+ sessions: number;
89
+ pageviews: number;
90
+ /** Decimal ratio. `purchases / sessions`. null when sessions=0. */
91
+ conversionRate: number | null;
92
+ }
93
+ export interface AnalyticsOverviewWithDeltas {
94
+ current: AnalyticsOverview;
95
+ prior: AnalyticsOverview;
96
+ /** Decimal delta against prior (e.g. +0.18 = +18%). null when prior=0. */
97
+ deltas: {
98
+ visitors: number | null;
99
+ sessions: number | null;
100
+ pageviews: number | null;
101
+ conversionRate: number | null;
102
+ };
103
+ }
104
+ export interface AnalyticsTrendPoint {
105
+ date: string;
106
+ visitors: number;
107
+ }
108
+ export interface AnalyticsTopPage {
109
+ path: string;
110
+ views: number;
111
+ }
112
+ export interface AnalyticsTopProduct {
113
+ productId: string;
114
+ views: number;
115
+ addsToCart: number;
116
+ purchases: number;
117
+ }
118
+ export interface AnalyticsSourceBucket {
119
+ source: string;
120
+ visitors: number;
121
+ }
122
+ export interface AnalyticsDeviceBucket {
123
+ device: string;
124
+ visitors: number;
125
+ }
126
+ export interface AnalyticsDashboardPayload {
127
+ period: AnalyticsPeriod;
128
+ overview: AnalyticsOverviewWithDeltas;
129
+ trend: AnalyticsTrendPoint[];
130
+ topPages: AnalyticsTopPage[];
131
+ topProducts: AnalyticsTopProduct[];
132
+ sources: AnalyticsSourceBucket[];
133
+ devices: AnalyticsDeviceBucket[];
134
+ }
135
+ export interface AnalyticsTrafficSummary {
136
+ visitors7d: number;
137
+ visitorsPrior7d: number;
138
+ /** Decimal delta against prior 7d. null when prior=0. */
139
+ delta: number | null;
140
+ /** Decimal ratio. null when sessions=0. */
141
+ conversionRate7d: number | null;
142
+ }
143
+ export interface AnalyticsEventLogRow {
144
+ /** Row id — opaque, used for cursor pagination. Encoded as string in JSON to
145
+ * preserve precision for clients (bigserial > Number.MAX_SAFE_INTEGER one
146
+ * day, far away but cheap to future-proof). */
147
+ id: string;
148
+ ts: string;
149
+ event: AnalyticsEventKind;
150
+ path: string;
151
+ referrer: string | null;
152
+ country: string | null;
153
+ sessionId: string;
154
+ visitorId: string;
155
+ /** Optional commerce context — populated for view_item / add_to_cart /
156
+ * purchase / refund / etc. */
157
+ productId: string | null;
158
+ valueCents: number | null;
159
+ currency: string | null;
160
+ device: string | null;
161
+ }
162
+ export type AnalyticsSortDir = 'asc' | 'desc';
163
+ export interface AnalyticsEventsLogPage {
164
+ rows: AnalyticsEventLogRow[];
165
+ /** Cursor for the next page (the `id` of the last row, ascending sort order).
166
+ * null when there's no more data. */
167
+ nextCursor: string | null;
168
+ }
169
+ export interface AnalyticsVisitorRow {
170
+ visitorId: string;
171
+ firstSeen: string;
172
+ lastSeen: string;
173
+ sessions: number;
174
+ pageviews: number;
175
+ purchases: number;
176
+ country: string | null;
177
+ /** First touch attribution — utm_source if present, else referrer host,
178
+ * else 'direct'. */
179
+ initialSource: string;
180
+ }
181
+ export interface AnalyticsVisitorsPage {
182
+ rows: AnalyticsVisitorRow[];
183
+ /** Offset cursor (string for symmetry with events). null when no more. */
184
+ nextCursor: string | null;
185
+ }
186
+ //# sourceMappingURL=types.d.ts.map