@rovela-ai/sdk 0.4.3 → 0.5.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.
Files changed (141) 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/index.d.ts +1 -1
  14. package/dist/admin/index.d.ts.map +1 -1
  15. package/dist/admin/index.js +1 -1
  16. package/dist/admin/index.js.map +1 -1
  17. package/dist/admin/styles/admin-theme.css +11 -0
  18. package/dist/analytics/api/dashboard.d.ts +16 -0
  19. package/dist/analytics/api/dashboard.d.ts.map +1 -0
  20. package/dist/analytics/api/dashboard.js +37 -0
  21. package/dist/analytics/api/dashboard.js.map +1 -0
  22. package/dist/analytics/api/events.d.ts +23 -0
  23. package/dist/analytics/api/events.d.ts.map +1 -0
  24. package/dist/analytics/api/events.js +55 -0
  25. package/dist/analytics/api/events.js.map +1 -0
  26. package/dist/analytics/api/index.d.ts +15 -0
  27. package/dist/analytics/api/index.d.ts.map +1 -0
  28. package/dist/analytics/api/index.js +15 -0
  29. package/dist/analytics/api/index.js.map +1 -0
  30. package/dist/analytics/api/track.d.ts +20 -0
  31. package/dist/analytics/api/track.d.ts.map +1 -0
  32. package/dist/analytics/api/track.js +233 -0
  33. package/dist/analytics/api/track.js.map +1 -0
  34. package/dist/analytics/api/visitors.d.ts +19 -0
  35. package/dist/analytics/api/visitors.d.ts.map +1 -0
  36. package/dist/analytics/api/visitors.js +49 -0
  37. package/dist/analytics/api/visitors.js.map +1 -0
  38. package/dist/analytics/client/tracker.d.ts +51 -0
  39. package/dist/analytics/client/tracker.d.ts.map +1 -0
  40. package/dist/analytics/client/tracker.js +208 -0
  41. package/dist/analytics/client/tracker.js.map +1 -0
  42. package/dist/analytics/components/AnalyticsDashboard.d.ts +2 -0
  43. package/dist/analytics/components/AnalyticsDashboard.d.ts.map +1 -0
  44. package/dist/analytics/components/AnalyticsDashboard.js +26 -0
  45. package/dist/analytics/components/AnalyticsDashboard.js.map +1 -0
  46. package/dist/analytics/components/AnalyticsPeriodContext.d.ts +13 -0
  47. package/dist/analytics/components/AnalyticsPeriodContext.d.ts.map +1 -0
  48. package/dist/analytics/components/AnalyticsPeriodContext.js +28 -0
  49. package/dist/analytics/components/AnalyticsPeriodContext.js.map +1 -0
  50. package/dist/analytics/components/AnalyticsProvider.d.ts +22 -0
  51. package/dist/analytics/components/AnalyticsProvider.d.ts.map +1 -0
  52. package/dist/analytics/components/AnalyticsProvider.js +152 -0
  53. package/dist/analytics/components/AnalyticsProvider.js.map +1 -0
  54. package/dist/analytics/components/AnalyticsTabNav.d.ts +17 -0
  55. package/dist/analytics/components/AnalyticsTabNav.d.ts.map +1 -0
  56. package/dist/analytics/components/AnalyticsTabNav.js +42 -0
  57. package/dist/analytics/components/AnalyticsTabNav.js.map +1 -0
  58. package/dist/analytics/hooks/useAnalytics.d.ts +9 -0
  59. package/dist/analytics/hooks/useAnalytics.d.ts.map +1 -0
  60. package/dist/analytics/hooks/useAnalytics.js +8 -0
  61. package/dist/analytics/hooks/useAnalytics.js.map +1 -0
  62. package/dist/analytics/hooks/useAnalyticsDashboard.d.ts +9 -0
  63. package/dist/analytics/hooks/useAnalyticsDashboard.d.ts.map +1 -0
  64. package/dist/analytics/hooks/useAnalyticsDashboard.js +45 -0
  65. package/dist/analytics/hooks/useAnalyticsDashboard.js.map +1 -0
  66. package/dist/analytics/hooks/useEventsLog.d.ts +24 -0
  67. package/dist/analytics/hooks/useEventsLog.d.ts.map +1 -0
  68. package/dist/analytics/hooks/useEventsLog.js +81 -0
  69. package/dist/analytics/hooks/useEventsLog.js.map +1 -0
  70. package/dist/analytics/hooks/useVisitorsList.d.ts +20 -0
  71. package/dist/analytics/hooks/useVisitorsList.d.ts.map +1 -0
  72. package/dist/analytics/hooks/useVisitorsList.js +69 -0
  73. package/dist/analytics/hooks/useVisitorsList.js.map +1 -0
  74. package/dist/analytics/index.d.ts +44 -0
  75. package/dist/analytics/index.d.ts.map +1 -0
  76. package/dist/analytics/index.js +39 -0
  77. package/dist/analytics/index.js.map +1 -0
  78. package/dist/analytics/server/index.d.ts +10 -0
  79. package/dist/analytics/server/index.d.ts.map +1 -0
  80. package/dist/analytics/server/index.js +9 -0
  81. package/dist/analytics/server/index.js.map +1 -0
  82. package/dist/analytics/server/normalize.d.ts +23 -0
  83. package/dist/analytics/server/normalize.d.ts.map +1 -0
  84. package/dist/analytics/server/normalize.js +75 -0
  85. package/dist/analytics/server/normalize.js.map +1 -0
  86. package/dist/analytics/server/queries.d.ts +74 -0
  87. package/dist/analytics/server/queries.d.ts.map +1 -0
  88. package/dist/analytics/server/queries.js +470 -0
  89. package/dist/analytics/server/queries.js.map +1 -0
  90. package/dist/analytics/types.d.ts +186 -0
  91. package/dist/analytics/types.d.ts.map +1 -0
  92. package/dist/analytics/types.js +16 -0
  93. package/dist/analytics/types.js.map +1 -0
  94. package/dist/analytics/views/DashboardsView.d.ts +6 -0
  95. package/dist/analytics/views/DashboardsView.d.ts.map +1 -0
  96. package/dist/analytics/views/DashboardsView.js +93 -0
  97. package/dist/analytics/views/DashboardsView.js.map +1 -0
  98. package/dist/analytics/views/EventsView.d.ts +6 -0
  99. package/dist/analytics/views/EventsView.d.ts.map +1 -0
  100. package/dist/analytics/views/EventsView.js +85 -0
  101. package/dist/analytics/views/EventsView.js.map +1 -0
  102. package/dist/analytics/views/VisitorsView.d.ts +6 -0
  103. package/dist/analytics/views/VisitorsView.d.ts.map +1 -0
  104. package/dist/analytics/views/VisitorsView.js +57 -0
  105. package/dist/analytics/views/VisitorsView.js.map +1 -0
  106. package/dist/cart/store.d.ts.map +1 -1
  107. package/dist/cart/store.js +12 -0
  108. package/dist/cart/store.js.map +1 -1
  109. package/dist/checkout/components/CheckoutFlow.d.ts.map +1 -1
  110. package/dist/checkout/components/CheckoutFlow.js +26 -2
  111. package/dist/checkout/components/CheckoutFlow.js.map +1 -1
  112. package/dist/checkout/components/ShippingForm.js +10 -5
  113. package/dist/checkout/components/ShippingForm.js.map +1 -1
  114. package/dist/checkout/hooks/useCheckout.d.ts.map +1 -1
  115. package/dist/checkout/hooks/useCheckout.js +12 -1
  116. package/dist/checkout/hooks/useCheckout.js.map +1 -1
  117. package/dist/checkout/server/handle-webhook.js +15 -0
  118. package/dist/checkout/server/handle-webhook.js.map +1 -1
  119. package/dist/checkout/types.d.ts +13 -0
  120. package/dist/checkout/types.d.ts.map +1 -1
  121. package/dist/core/db/client.d.ts +14 -0
  122. package/dist/core/db/client.d.ts.map +1 -1
  123. package/dist/core/db/client.js +12 -0
  124. package/dist/core/db/client.js.map +1 -1
  125. package/dist/core/db/index.d.ts +2 -1
  126. package/dist/core/db/index.d.ts.map +1 -1
  127. package/dist/core/db/index.js +1 -1
  128. package/dist/core/db/index.js.map +1 -1
  129. package/dist/core/db/queries.d.ts +8 -7
  130. package/dist/core/db/queries.d.ts.map +1 -1
  131. package/dist/core/db/queries.js +80 -0
  132. package/dist/core/db/queries.js.map +1 -1
  133. package/dist/core/db/schema.d.ts +340 -4
  134. package/dist/core/db/schema.d.ts.map +1 -1
  135. package/dist/core/db/schema.js +55 -1
  136. package/dist/core/db/schema.js.map +1 -1
  137. package/dist/core/server/index.d.ts +2 -2
  138. package/dist/core/server/index.d.ts.map +1 -1
  139. package/dist/core/server/index.js +2 -0
  140. package/dist/core/server/index.js.map +1 -1
  141. package/package.json +25 -1
@@ -0,0 +1,233 @@
1
+ /**
2
+ * @rovela-ai/sdk/analytics/api/track
3
+ *
4
+ * POST /api/analytics/track
5
+ *
6
+ * Public endpoint — no auth (events are pseudonymous). Mounted by the store
7
+ * template via `export { POST } from '@rovela-ai/sdk/analytics/api/track'`.
8
+ *
9
+ * Discipline:
10
+ * - Bot UA → 204 (silent drop)
11
+ * - Admin-cookie present → 204 (no self-bias from merchant)
12
+ * - Validation failure → 400 (still safe; only thing it leaks is "bad shape")
13
+ * - DB insert failure → 204 (analytics MUST NEVER break the visitor flow;
14
+ * recordEvent swallows internally)
15
+ *
16
+ * sendBeacon() from the client sends Content-Type: text/plain by default;
17
+ * we parse JSON manually rather than rely on Next.js request.json().
18
+ */
19
+ import { NextResponse } from 'next/server';
20
+ import { recordEvent } from '../server/queries';
21
+ import { isBotUserAgent, deviceFromUserAgent, referrerHost, normalizePath, clip, safeCountry, } from '../server/normalize';
22
+ // =============================================================================
23
+ // Inline validation (no Zod dep — keep SDK lean; the rules are explicit and
24
+ // the surface is bounded by AnalyticsEventKind)
25
+ // =============================================================================
26
+ const VALID_KINDS = new Set([
27
+ 'page_view',
28
+ 'session_start',
29
+ 'session_heartbeat',
30
+ 'session_end',
31
+ 'view_item_list',
32
+ 'view_item',
33
+ 'select_item',
34
+ 'add_to_cart',
35
+ 'view_cart',
36
+ 'begin_checkout',
37
+ 'add_shipping_info',
38
+ 'purchase',
39
+ 'refund',
40
+ ]);
41
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
42
+ function isString(v) {
43
+ return typeof v === 'string' && v.length > 0;
44
+ }
45
+ function isUuid(v) {
46
+ return isString(v) && UUID_RE.test(v);
47
+ }
48
+ function isNumber(v) {
49
+ return typeof v === 'number' && Number.isFinite(v);
50
+ }
51
+ function validate(raw) {
52
+ if (!raw || typeof raw !== 'object')
53
+ return { ok: false, error: 'body must be an object' };
54
+ const o = raw;
55
+ if (!isUuid(o.visitor_id))
56
+ return { ok: false, error: 'visitor_id must be a uuid' };
57
+ if (!isUuid(o.session_id))
58
+ return { ok: false, error: 'session_id must be a uuid' };
59
+ if (!isString(o.event) || !VALID_KINDS.has(o.event)) {
60
+ return { ok: false, error: 'event must be a known kind' };
61
+ }
62
+ if (!isString(o.path))
63
+ return { ok: false, error: 'path must be a non-empty string' };
64
+ // Per-kind required fields (skip server-side fields like ts/device/country)
65
+ const kind = o.event;
66
+ switch (kind) {
67
+ case 'view_item':
68
+ case 'view_cart':
69
+ case 'begin_checkout':
70
+ case 'add_shipping_info':
71
+ if (!isNumber(o.value_cents) || !isString(o.currency)) {
72
+ return { ok: false, error: `${kind} requires value_cents + currency` };
73
+ }
74
+ if (kind === 'view_item' && !isUuid(o.product_id)) {
75
+ return { ok: false, error: 'view_item requires product_id' };
76
+ }
77
+ break;
78
+ case 'view_item_list':
79
+ if (!isUuid(o.category_id)) {
80
+ return { ok: false, error: 'view_item_list requires category_id' };
81
+ }
82
+ break;
83
+ case 'select_item':
84
+ if (!isUuid(o.product_id)) {
85
+ return { ok: false, error: 'select_item requires product_id' };
86
+ }
87
+ break;
88
+ case 'add_to_cart':
89
+ if (!isUuid(o.product_id) ||
90
+ !isNumber(o.quantity) ||
91
+ !isNumber(o.value_cents) ||
92
+ !isString(o.currency)) {
93
+ return {
94
+ ok: false,
95
+ error: 'add_to_cart requires product_id, quantity, value_cents, currency',
96
+ };
97
+ }
98
+ break;
99
+ case 'purchase':
100
+ case 'refund':
101
+ // Server-fired only — client posts to track are rejected. Stripe webhook
102
+ // wrapper calls recordEvent() directly, bypassing this validation path.
103
+ return { ok: false, error: `${kind} is server-fired only` };
104
+ // page_view + session_start — identity + path is enough
105
+ }
106
+ // Build the validated event with explicit field selection.
107
+ const base = {
108
+ visitor_id: o.visitor_id,
109
+ session_id: o.session_id,
110
+ path: normalizePath(o.path),
111
+ referrer: referrerHost(o.referrer ?? null),
112
+ utm_source: clip(o.utm_source, 100),
113
+ utm_medium: clip(o.utm_medium, 100),
114
+ utm_campaign: clip(o.utm_campaign, 200),
115
+ };
116
+ let payload;
117
+ switch (kind) {
118
+ case 'page_view':
119
+ case 'session_start':
120
+ case 'session_heartbeat':
121
+ case 'session_end':
122
+ payload = { event: kind, ...base };
123
+ break;
124
+ case 'view_item_list':
125
+ payload = {
126
+ event: 'view_item_list',
127
+ category_id: o.category_id,
128
+ ...base,
129
+ };
130
+ break;
131
+ case 'view_item':
132
+ payload = {
133
+ event: 'view_item',
134
+ product_id: o.product_id,
135
+ value_cents: Math.round(o.value_cents),
136
+ currency: o.currency.slice(0, 3).toUpperCase(),
137
+ ...base,
138
+ };
139
+ break;
140
+ case 'select_item':
141
+ payload = {
142
+ event: 'select_item',
143
+ product_id: o.product_id,
144
+ category_id: isUuid(o.category_id) ? o.category_id : null,
145
+ ...base,
146
+ };
147
+ break;
148
+ case 'add_to_cart':
149
+ payload = {
150
+ event: 'add_to_cart',
151
+ product_id: o.product_id,
152
+ variant_id: isUuid(o.variant_id) ? o.variant_id : null,
153
+ quantity: Math.round(o.quantity),
154
+ value_cents: Math.round(o.value_cents),
155
+ currency: o.currency.slice(0, 3).toUpperCase(),
156
+ ...base,
157
+ };
158
+ break;
159
+ case 'view_cart':
160
+ payload = {
161
+ event: 'view_cart',
162
+ value_cents: Math.round(o.value_cents),
163
+ currency: o.currency.slice(0, 3).toUpperCase(),
164
+ ...base,
165
+ };
166
+ break;
167
+ case 'begin_checkout':
168
+ payload = {
169
+ event: 'begin_checkout',
170
+ value_cents: Math.round(o.value_cents),
171
+ currency: o.currency.slice(0, 3).toUpperCase(),
172
+ ...base,
173
+ };
174
+ break;
175
+ case 'add_shipping_info':
176
+ payload = {
177
+ event: 'add_shipping_info',
178
+ value_cents: Math.round(o.value_cents),
179
+ currency: o.currency.slice(0, 3).toUpperCase(),
180
+ ...base,
181
+ };
182
+ break;
183
+ default:
184
+ // Exhaustiveness — purchase/refund handled above
185
+ return { ok: false, error: 'unsupported kind' };
186
+ }
187
+ return { ok: true, event: { ...base, ...payload } };
188
+ }
189
+ // =============================================================================
190
+ // Admin-cookie sniff (exclude merchant self-traffic)
191
+ // =============================================================================
192
+ const ADMIN_COOKIE_NAMES = [
193
+ '__Secure-rovela.admin.session-token',
194
+ 'rovela.admin.session-token',
195
+ ];
196
+ function hasAdminSession(cookieHeader) {
197
+ if (!cookieHeader)
198
+ return false;
199
+ return ADMIN_COOKIE_NAMES.some((n) => cookieHeader.includes(`${n}=`));
200
+ }
201
+ // =============================================================================
202
+ // Handler
203
+ // =============================================================================
204
+ export async function POST(request) {
205
+ // sendBeacon defaults to text/plain; parse JSON manually.
206
+ let raw;
207
+ try {
208
+ const text = await request.text();
209
+ raw = text ? JSON.parse(text) : null;
210
+ }
211
+ catch {
212
+ return new NextResponse(null, { status: 204 });
213
+ }
214
+ // Bot check — silent drop
215
+ const ua = request.headers.get('user-agent');
216
+ if (isBotUserAgent(ua)) {
217
+ return new NextResponse(null, { status: 204 });
218
+ }
219
+ // Admin-cookie sniff — silent drop (don't bias merchant's own metrics)
220
+ if (hasAdminSession(request.headers.get('cookie'))) {
221
+ return new NextResponse(null, { status: 204 });
222
+ }
223
+ const validation = validate(raw);
224
+ if (!validation.ok) {
225
+ return NextResponse.json({ error: validation.error }, { status: 400 });
226
+ }
227
+ const device = deviceFromUserAgent(ua);
228
+ const country = safeCountry(request.headers.get('x-vercel-ip-country'));
229
+ // Best-effort insert — recordEvent swallows errors internally.
230
+ await recordEvent({ event: validation.event, device, country });
231
+ return new NextResponse(null, { status: 204 });
232
+ }
233
+ //# sourceMappingURL=track.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"track.js","sourceRoot":"","sources":["../../../src/analytics/api/track.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @rovela-ai/sdk/analytics/api/visitors
3
+ *
4
+ * GET /api/admin/analytics/visitors
5
+ *
6
+ * Offset-paginated per-visitor aggregate. Admin-gated, settings.read.
7
+ *
8
+ * Query params:
9
+ * period: today | 7d | 30d | 90d (default 30d)
10
+ * cursor: stringified offset from previous page's nextCursor
11
+ * limit: 1–200 (default 50)
12
+ * sortBy: last_seen | first_seen | sessions | pageviews | country (default last_seen)
13
+ * sortDir: asc | desc (default desc)
14
+ */
15
+ import { NextRequest, NextResponse } from 'next/server';
16
+ import type { AnalyticsVisitorsPage } from '../types';
17
+ import type { AdminApiError } from '../../admin/types';
18
+ export declare function GET(request: NextRequest): Promise<NextResponse<AnalyticsVisitorsPage | AdminApiError>>;
19
+ //# sourceMappingURL=visitors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"visitors.d.ts","sourceRoot":"","sources":["../../../src/analytics/api/visitors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAGvD,OAAO,KAAK,EAGV,qBAAqB,EACtB,MAAM,UAAU,CAAA;AACjB,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAA;AAUtD,wBAAsB,GAAG,CACvB,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,YAAY,CAAC,qBAAqB,GAAG,aAAa,CAAC,CAAC,CAoC9D"}
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @rovela-ai/sdk/analytics/api/visitors
3
+ *
4
+ * GET /api/admin/analytics/visitors
5
+ *
6
+ * Offset-paginated per-visitor aggregate. Admin-gated, settings.read.
7
+ *
8
+ * Query params:
9
+ * period: today | 7d | 30d | 90d (default 30d)
10
+ * cursor: stringified offset from previous page's nextCursor
11
+ * limit: 1–200 (default 50)
12
+ * sortBy: last_seen | first_seen | sessions | pageviews | country (default last_seen)
13
+ * sortDir: asc | desc (default desc)
14
+ */
15
+ import { NextResponse } from 'next/server';
16
+ import { requireAdmin } from '../../admin/server/admin-session';
17
+ import { getVisitorsList } from '../server/queries';
18
+ const VALID_PERIODS = new Set(['today', '7d', '30d', '90d']);
19
+ function parsePeriod(raw) {
20
+ return raw && VALID_PERIODS.has(raw)
21
+ ? raw
22
+ : '30d';
23
+ }
24
+ export async function GET(request) {
25
+ const guard = await requireAdmin({ permission: 'settings.read' });
26
+ if (!guard.ok) {
27
+ return guard.response;
28
+ }
29
+ try {
30
+ const sp = request.nextUrl.searchParams;
31
+ const limitParam = sp.get('limit');
32
+ const limit = limitParam ? Math.max(1, Number(limitParam)) : 50;
33
+ const sortBy = sp.get('sortBy') ?? 'last_seen';
34
+ const sortDir = sp.get('sortDir') === 'asc' ? 'asc' : 'desc';
35
+ const payload = await getVisitorsList({
36
+ period: parsePeriod(sp.get('period')),
37
+ cursor: sp.get('cursor'),
38
+ limit,
39
+ sortBy,
40
+ sortDir,
41
+ });
42
+ return NextResponse.json(payload);
43
+ }
44
+ catch (error) {
45
+ console.error('[Analytics Visitors API] GET error:', error);
46
+ return NextResponse.json({ error: 'Failed to fetch visitors list', code: 'INTERNAL_ERROR' }, { status: 500 });
47
+ }
48
+ }
49
+ //# sourceMappingURL=visitors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"visitors.js","sourceRoot":"","sources":["../../../src/analytics/api/visitors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,OAAO,EAAe,YAAY,EAAE,MAAM,aAAa,CAAA;AACvD,OAAO,EAAE,YAAY,EAAE,MAAM,kCAAkC,CAAA;AAC/D,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAQnD,MAAM,aAAa,GAAG,IAAI,GAAG,CAAkB,CAAC,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC,CAAA;AAE7E,SAAS,WAAW,CAAC,GAAkB;IACrC,OAAO,GAAG,IAAI,aAAa,CAAC,GAAG,CAAC,GAAsB,CAAC;QACrD,CAAC,CAAE,GAAuB;QAC1B,CAAC,CAAC,KAAK,CAAA;AACX,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,GAAG,CACvB,OAAoB;IAEpB,MAAM,KAAK,GAAG,MAAM,YAAY,CAAC,EAAE,UAAU,EAAE,eAAe,EAAE,CAAC,CAAA;IACjE,IAAI,CAAC,KAAK,CAAC,EAAE,EAAE,CAAC;QACd,OAAO,KAAK,CAAC,QAAQ,CAAA;IACvB,CAAC;IAED,IAAI,CAAC;QACH,MAAM,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,YAAY,CAAA;QACvC,MAAM,UAAU,GAAG,EAAE,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;QAClC,MAAM,KAAK,GAAG,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAA;QAC/D,MAAM,MAAM,GACT,EAAE,CAAC,GAAG,CAAC,QAAQ,CAMP,IAAI,WAAW,CAAA;QAC1B,MAAM,OAAO,GACX,EAAE,CAAC,GAAG,CAAC,SAAS,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,CAAA;QAE9C,MAAM,OAAO,GAAG,MAAM,eAAe,CAAC;YACpC,MAAM,EAAE,WAAW,CAAC,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YACrC,MAAM,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC;YACxB,KAAK;YACL,MAAM;YACN,OAAO;SACR,CAAC,CAAA;QACF,OAAO,YAAY,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IACnC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,qCAAqC,EAAE,KAAK,CAAC,CAAA;QAC3D,OAAO,YAAY,CAAC,IAAI,CACtB,EAAE,KAAK,EAAE,+BAA+B,EAAE,IAAI,EAAE,gBAAgB,EAAE,EAClE,EAAE,MAAM,EAAE,GAAG,EAAE,CAChB,CAAA;IACH,CAAC;AACH,CAAC"}
@@ -0,0 +1,51 @@
1
+ /**
2
+ * @rovela-ai/sdk/analytics/client/tracker
3
+ *
4
+ * Browser-side analytics primitives — pseudonymous identity + transport.
5
+ * No React, no hooks; safe to import from anywhere on the client (cart store,
6
+ * checkout flow, server-component-rendered client islands).
7
+ *
8
+ * Identity model:
9
+ * - visitor_id: persistent UUID in localStorage. Never expires.
10
+ * - session_id: per-tab UUID in sessionStorage. Rotates after 30 min of
11
+ * inactivity (matches GA4's default session boundary).
12
+ *
13
+ * Transport:
14
+ * - sendBeacon() when available (survives page unload, fire-and-forget).
15
+ * - fetch({keepalive:true}) fallback for browsers without sendBeacon.
16
+ * - Both are unblocking; the call returns synchronously, the network
17
+ * happens in the background, errors are swallowed.
18
+ *
19
+ * SSR safety:
20
+ * - Every helper short-circuits when `typeof window === 'undefined'`. Safe
21
+ * to call from a Server Component import chain that incidentally pulls
22
+ * this file in.
23
+ */
24
+ import type { AnalyticsPayload } from '../types';
25
+ /** Read or mint the visitor UUID. Persisted in localStorage. */
26
+ export declare function getVisitorId(): string;
27
+ /** Read or mint the session UUID; rotate after 30 min idle. */
28
+ export declare function getSessionId(): {
29
+ id: string;
30
+ isNew: boolean;
31
+ };
32
+ interface Utm {
33
+ utm_source: string | null;
34
+ utm_medium: string | null;
35
+ utm_campaign: string | null;
36
+ }
37
+ export declare function getUtm(): Utm;
38
+ /**
39
+ * Fire a single event. Identity (visitor/session ids), UTM, and path are
40
+ * derived automatically from the browser; the caller supplies only the
41
+ * event-specific payload.
42
+ *
43
+ * Returns immediately — network happens in the background.
44
+ *
45
+ * Caller is responsible for consent gating (`useCookieConsent().analytics`)
46
+ * before calling this. The track endpoint also drops events from admin
47
+ * sessions + bots as a server-side safety net.
48
+ */
49
+ export declare function emitEvent(payload: AnalyticsPayload): void;
50
+ export {};
51
+ //# sourceMappingURL=tracker.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracker.d.ts","sourceRoot":"","sources":["../../../src/analytics/client/tracker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,gBAAgB,EAAsB,MAAM,UAAU,CAAA;AAiCpE,gEAAgE;AAChE,wBAAgB,YAAY,IAAI,MAAM,CAcrC;AAOD,+DAA+D;AAC/D,wBAAgB,YAAY,IAAI;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAwB7D;AAMD,UAAU,GAAG;IACX,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,YAAY,EAAE,MAAM,GAAG,IAAI,CAAA;CAC5B;AAED,wBAAgB,MAAM,IAAI,GAAG,CAY5B;AAuED;;;;;;;;;;GAUG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,IAAI,CA2BzD"}
@@ -0,0 +1,208 @@
1
+ /**
2
+ * @rovela-ai/sdk/analytics/client/tracker
3
+ *
4
+ * Browser-side analytics primitives — pseudonymous identity + transport.
5
+ * No React, no hooks; safe to import from anywhere on the client (cart store,
6
+ * checkout flow, server-component-rendered client islands).
7
+ *
8
+ * Identity model:
9
+ * - visitor_id: persistent UUID in localStorage. Never expires.
10
+ * - session_id: per-tab UUID in sessionStorage. Rotates after 30 min of
11
+ * inactivity (matches GA4's default session boundary).
12
+ *
13
+ * Transport:
14
+ * - sendBeacon() when available (survives page unload, fire-and-forget).
15
+ * - fetch({keepalive:true}) fallback for browsers without sendBeacon.
16
+ * - Both are unblocking; the call returns synchronously, the network
17
+ * happens in the background, errors are swallowed.
18
+ *
19
+ * SSR safety:
20
+ * - Every helper short-circuits when `typeof window === 'undefined'`. Safe
21
+ * to call from a Server Component import chain that incidentally pulls
22
+ * this file in.
23
+ */
24
+ const VISITOR_KEY = 'rovela:av'; // localStorage
25
+ const SESSION_KEY = 'rovela:as'; // sessionStorage — { id, ts }
26
+ const TRACK_ENDPOINT = '/api/analytics/track';
27
+ const SESSION_TTL_MS = 30 * 60 * 1000;
28
+ const SSR = typeof window === 'undefined';
29
+ // =============================================================================
30
+ // UUID — prefer crypto.randomUUID, fall back to a v4 from getRandomValues
31
+ // =============================================================================
32
+ function uuidv4() {
33
+ if (!SSR && typeof crypto !== 'undefined') {
34
+ if (typeof crypto.randomUUID === 'function')
35
+ return crypto.randomUUID();
36
+ // Fallback: RFC 4122 v4 from getRandomValues
37
+ const bytes = new Uint8Array(16);
38
+ crypto.getRandomValues(bytes);
39
+ bytes[6] = (bytes[6] & 0x0f) | 0x40;
40
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
41
+ const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
42
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
43
+ }
44
+ // Server-side never gets here for an outbound event, but the API needs a
45
+ // type-safe string return. This is unreachable in practice.
46
+ return '00000000-0000-4000-8000-000000000000';
47
+ }
48
+ // =============================================================================
49
+ // Visitor + session identity
50
+ // =============================================================================
51
+ /** Read or mint the visitor UUID. Persisted in localStorage. */
52
+ export function getVisitorId() {
53
+ if (SSR)
54
+ return '';
55
+ try {
56
+ const existing = localStorage.getItem(VISITOR_KEY);
57
+ if (existing && /^[0-9a-f-]{36}$/i.test(existing))
58
+ return existing;
59
+ const fresh = uuidv4();
60
+ localStorage.setItem(VISITOR_KEY, fresh);
61
+ return fresh;
62
+ }
63
+ catch {
64
+ // localStorage blocked (Safari private mode, third-party iframe).
65
+ // Fall back to a per-pageload UUID so events still flow with sane
66
+ // session-level grouping (will be uniqued per page, never aggregated).
67
+ return uuidv4();
68
+ }
69
+ }
70
+ /** Read or mint the session UUID; rotate after 30 min idle. */
71
+ export function getSessionId() {
72
+ if (SSR)
73
+ return { id: '', isNew: false };
74
+ try {
75
+ const now = Date.now();
76
+ const raw = sessionStorage.getItem(SESSION_KEY);
77
+ if (raw) {
78
+ const parsed = JSON.parse(raw);
79
+ if (parsed?.id &&
80
+ /^[0-9a-f-]{36}$/i.test(parsed.id) &&
81
+ typeof parsed.ts === 'number' &&
82
+ now - parsed.ts < SESSION_TTL_MS) {
83
+ // Bump the activity timestamp on every access.
84
+ sessionStorage.setItem(SESSION_KEY, JSON.stringify({ id: parsed.id, ts: now }));
85
+ return { id: parsed.id, isNew: false };
86
+ }
87
+ }
88
+ const fresh = uuidv4();
89
+ sessionStorage.setItem(SESSION_KEY, JSON.stringify({ id: fresh, ts: now }));
90
+ return { id: fresh, isNew: true };
91
+ }
92
+ catch {
93
+ return { id: uuidv4(), isNew: true };
94
+ }
95
+ }
96
+ export function getUtm() {
97
+ if (SSR)
98
+ return { utm_source: null, utm_medium: null, utm_campaign: null };
99
+ try {
100
+ const params = new URLSearchParams(window.location.search);
101
+ return {
102
+ utm_source: params.get('utm_source'),
103
+ utm_medium: params.get('utm_medium'),
104
+ utm_campaign: params.get('utm_campaign'),
105
+ };
106
+ }
107
+ catch {
108
+ return { utm_source: null, utm_medium: null, utm_campaign: null };
109
+ }
110
+ }
111
+ /**
112
+ * Read the persisted consent from localStorage and return whether analytics
113
+ * is granted. Returns `true` when the key is missing — the CookieConsent
114
+ * provider grants analytics by default when the banner is disabled (non-EU
115
+ * stores), and storefronts that DO show the banner write the persisted
116
+ * value as soon as the user clicks anything. The window between mount and
117
+ * first click is rare and brief; defaulting to true here preserves data
118
+ * from real visitors while EU-enabled stores' banner blocks events at the
119
+ * provider level (AnalyticsProvider only mounts `track` when consent is
120
+ * granted via useCookieConsent()).
121
+ *
122
+ * This second gate exists for callers OUTSIDE React (cart Zustand store,
123
+ * checkout hook) that can't reach useCookieConsent(). For React components,
124
+ * prefer useAnalytics() which composes both gates.
125
+ */
126
+ function hasAnalyticsConsent() {
127
+ if (SSR)
128
+ return false;
129
+ try {
130
+ const raw = localStorage.getItem('rovela-cookie-consent');
131
+ if (!raw)
132
+ return true; // banner disabled OR not yet decided — see comment
133
+ const parsed = JSON.parse(raw);
134
+ return parsed.analytics === true;
135
+ }
136
+ catch {
137
+ return true;
138
+ }
139
+ }
140
+ // =============================================================================
141
+ // Transport — fire-and-forget, never throws
142
+ // =============================================================================
143
+ function postEvent(event) {
144
+ if (SSR)
145
+ return;
146
+ const body = JSON.stringify(event);
147
+ try {
148
+ if (navigator.sendBeacon) {
149
+ // sendBeacon needs a Blob to set Content-Type; the API parses text/plain
150
+ // either way, but using a JSON blob keeps DevTools readable.
151
+ const blob = new Blob([body], { type: 'application/json' });
152
+ navigator.sendBeacon(TRACK_ENDPOINT, blob);
153
+ return;
154
+ }
155
+ // Fallback: fetch with keepalive so the request survives page unload.
156
+ void fetch(TRACK_ENDPOINT, {
157
+ method: 'POST',
158
+ body,
159
+ headers: { 'Content-Type': 'application/json' },
160
+ keepalive: true,
161
+ }).catch(() => undefined);
162
+ }
163
+ catch {
164
+ // Defensive — never bubble. Analytics MUST NEVER break customer flows.
165
+ }
166
+ }
167
+ // =============================================================================
168
+ // Public API — emit a typed event
169
+ // =============================================================================
170
+ /**
171
+ * Fire a single event. Identity (visitor/session ids), UTM, and path are
172
+ * derived automatically from the browser; the caller supplies only the
173
+ * event-specific payload.
174
+ *
175
+ * Returns immediately — network happens in the background.
176
+ *
177
+ * Caller is responsible for consent gating (`useCookieConsent().analytics`)
178
+ * before calling this. The track endpoint also drops events from admin
179
+ * sessions + bots as a server-side safety net.
180
+ */
181
+ export function emitEvent(payload) {
182
+ if (SSR)
183
+ return;
184
+ // Belt-and-suspenders consent gate — useAnalytics already gates React
185
+ // callers, but emitEvent is also called from the cart Zustand store and
186
+ // the checkout hook (non-React surfaces).
187
+ if (!hasAnalyticsConsent())
188
+ return;
189
+ const visitorId = getVisitorId();
190
+ const { id: sessionId } = getSessionId();
191
+ const utm = getUtm();
192
+ const fallbackPath = window.location.pathname || '/';
193
+ const fallbackReferrer = typeof document !== 'undefined' && document.referrer ? document.referrer : null;
194
+ // Caller-supplied path / referrer / utm win (e.g. AnalyticsProvider's
195
+ // per-route path); fall through to browser defaults otherwise.
196
+ const wire = {
197
+ visitor_id: visitorId,
198
+ session_id: sessionId,
199
+ ...payload,
200
+ path: payload.path ?? fallbackPath,
201
+ referrer: payload.referrer ?? fallbackReferrer,
202
+ utm_source: payload.utm_source ?? utm.utm_source,
203
+ utm_medium: payload.utm_medium ?? utm.utm_medium,
204
+ utm_campaign: payload.utm_campaign ?? utm.utm_campaign,
205
+ };
206
+ postEvent(wire);
207
+ }
208
+ //# sourceMappingURL=tracker.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tracker.js","sourceRoot":"","sources":["../../../src/analytics/client/tracker.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function AnalyticsDashboard(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=AnalyticsDashboard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AnalyticsDashboard.d.ts","sourceRoot":"","sources":["../../../src/analytics/components/AnalyticsDashboard.tsx"],"names":[],"mappings":"AAwBA,wBAAgB,kBAAkB,4CAWjC"}
@@ -0,0 +1,26 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ /**
4
+ * @rovela-ai/sdk/analytics/components/AnalyticsDashboard
5
+ *
6
+ * Back-compat shim. The original single-page dashboard has been split into
7
+ * a tab shell (AnalyticsTabNav) + three views (DashboardsView, EventsView,
8
+ * VisitorsView) wired to route-per-tab pages.
9
+ *
10
+ * This component still works — it renders the AnalyticsTabNav + the
11
+ * DashboardsView. Templates that haven't migrated to the route-per-tab
12
+ * layout get the Dashboards tab content with the tab navigation visible.
13
+ *
14
+ * New templates should mount `<DashboardsView period={period} />` directly
15
+ * from the route page and `<AnalyticsTabNav baseHref="/admin/analytics" />`
16
+ * from the shared layout.
17
+ */
18
+ import { useState } from 'react';
19
+ import { AnalyticsTabNav } from './AnalyticsTabNav';
20
+ import { PeriodSelector } from '../../admin/components/PeriodSelector';
21
+ import { DashboardsView } from '../views/DashboardsView';
22
+ export function AnalyticsDashboard() {
23
+ const [period, setPeriod] = useState('30d');
24
+ return (_jsxs("div", { className: "space-y-6", children: [_jsx(AnalyticsTabNav, { baseHref: "/admin/analytics", trailing: _jsx(PeriodSelector, { value: period, onChange: setPeriod }) }), _jsx(DashboardsView, { period: period })] }));
25
+ }
26
+ //# sourceMappingURL=AnalyticsDashboard.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AnalyticsDashboard.js","sourceRoot":"","sources":["../../../src/analytics/components/AnalyticsDashboard.tsx"],"names":[],"mappings":"AAAA,YAAY,CAAA;;AAEZ;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAA;AAChC,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AACnD,OAAO,EAAE,cAAc,EAAE,MAAM,uCAAuC,CAAA;AACtE,OAAO,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAA;AAGxD,MAAM,UAAU,kBAAkB;IAChC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,GAAG,QAAQ,CAAkB,KAAK,CAAC,CAAA;IAC5D,OAAO,CACL,eAAK,SAAS,EAAC,WAAW,aACxB,KAAC,eAAe,IACd,QAAQ,EAAC,kBAAkB,EAC3B,QAAQ,EAAE,KAAC,cAAc,IAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,GAAI,GAChE,EACF,KAAC,cAAc,IAAC,MAAM,EAAE,MAAM,GAAI,IAC9B,CACP,CAAA;AACH,CAAC"}
@@ -0,0 +1,13 @@
1
+ import type { AnalyticsPeriod } from '../types';
2
+ interface AnalyticsPeriodContextValue {
3
+ period: AnalyticsPeriod;
4
+ setPeriod: (p: AnalyticsPeriod) => void;
5
+ }
6
+ export interface AnalyticsPeriodProviderProps {
7
+ children: React.ReactNode;
8
+ defaultPeriod?: AnalyticsPeriod;
9
+ }
10
+ export declare function AnalyticsPeriodProvider({ children, defaultPeriod, }: AnalyticsPeriodProviderProps): import("react/jsx-runtime").JSX.Element;
11
+ export declare function useAnalyticsPeriod(): AnalyticsPeriodContextValue;
12
+ export {};
13
+ //# sourceMappingURL=AnalyticsPeriodContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AnalyticsPeriodContext.d.ts","sourceRoot":"","sources":["../../../src/analytics/components/AnalyticsPeriodContext.tsx"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,UAAU,CAAA;AAE/C,UAAU,2BAA2B;IACnC,MAAM,EAAE,eAAe,CAAA;IACvB,SAAS,EAAE,CAAC,CAAC,EAAE,eAAe,KAAK,IAAI,CAAA;CACxC;AAMD,MAAM,WAAW,4BAA4B;IAC3C,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAA;IACzB,aAAa,CAAC,EAAE,eAAe,CAAA;CAChC;AAED,wBAAgB,uBAAuB,CAAC,EACtC,QAAQ,EACR,aAAqB,GACtB,EAAE,4BAA4B,2CAO9B;AAED,wBAAgB,kBAAkB,IAAI,2BAA2B,CAShE"}