@happyvertical/smrt-analytics 0.30.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 (72) hide show
  1. package/AGENTS.md +68 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +7 -0
  4. package/README.md +131 -0
  5. package/dist/__smrt-register__.d.ts +2 -0
  6. package/dist/__smrt-register__.d.ts.map +1 -0
  7. package/dist/collections/AnalyticsDataStreamCollection.d.ts +69 -0
  8. package/dist/collections/AnalyticsDataStreamCollection.d.ts.map +1 -0
  9. package/dist/collections/AnalyticsEventCollection.d.ts +131 -0
  10. package/dist/collections/AnalyticsEventCollection.d.ts.map +1 -0
  11. package/dist/collections/AnalyticsPropertyCollection.d.ts +66 -0
  12. package/dist/collections/AnalyticsPropertyCollection.d.ts.map +1 -0
  13. package/dist/collections/AnalyticsReportCollection.d.ts +69 -0
  14. package/dist/collections/AnalyticsReportCollection.d.ts.map +1 -0
  15. package/dist/collections/index.d.ts +8 -0
  16. package/dist/collections/index.d.ts.map +1 -0
  17. package/dist/index.d.ts +6 -0
  18. package/dist/index.d.ts.map +1 -0
  19. package/dist/index.js +1623 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/manifest.json +4161 -0
  22. package/dist/models/AnalyticsDataStream.d.ts +94 -0
  23. package/dist/models/AnalyticsDataStream.d.ts.map +1 -0
  24. package/dist/models/AnalyticsEvent.d.ts +142 -0
  25. package/dist/models/AnalyticsEvent.d.ts.map +1 -0
  26. package/dist/models/AnalyticsProperty.d.ts +142 -0
  27. package/dist/models/AnalyticsProperty.d.ts.map +1 -0
  28. package/dist/models/AnalyticsReport.d.ts +206 -0
  29. package/dist/models/AnalyticsReport.d.ts.map +1 -0
  30. package/dist/models/index.d.ts +8 -0
  31. package/dist/models/index.d.ts.map +1 -0
  32. package/dist/playground.d.ts +2 -0
  33. package/dist/playground.d.ts.map +1 -0
  34. package/dist/playground.js +99 -0
  35. package/dist/playground.js.map +1 -0
  36. package/dist/prompts.d.ts +57 -0
  37. package/dist/prompts.d.ts.map +1 -0
  38. package/dist/smrt-knowledge.json +1570 -0
  39. package/dist/svelte/AnalyticsSummary.svelte +63 -0
  40. package/dist/svelte/AnalyticsSummary.svelte.d.ts +8 -0
  41. package/dist/svelte/AnalyticsSummary.svelte.d.ts.map +1 -0
  42. package/dist/svelte/EventsTable.svelte +161 -0
  43. package/dist/svelte/EventsTable.svelte.d.ts +16 -0
  44. package/dist/svelte/EventsTable.svelte.d.ts.map +1 -0
  45. package/dist/svelte/PropertyInfo.svelte +139 -0
  46. package/dist/svelte/PropertyInfo.svelte.d.ts +12 -0
  47. package/dist/svelte/PropertyInfo.svelte.d.ts.map +1 -0
  48. package/dist/svelte/PropertyStatusBadge.svelte +65 -0
  49. package/dist/svelte/PropertyStatusBadge.svelte.d.ts +7 -0
  50. package/dist/svelte/PropertyStatusBadge.svelte.d.ts.map +1 -0
  51. package/dist/svelte/StatCard.svelte +63 -0
  52. package/dist/svelte/StatCard.svelte.d.ts +11 -0
  53. package/dist/svelte/StatCard.svelte.d.ts.map +1 -0
  54. package/dist/svelte/TrendBadge.svelte +49 -0
  55. package/dist/svelte/TrendBadge.svelte.d.ts +8 -0
  56. package/dist/svelte/TrendBadge.svelte.d.ts.map +1 -0
  57. package/dist/svelte/i18n.d.ts +10 -0
  58. package/dist/svelte/i18n.d.ts.map +1 -0
  59. package/dist/svelte/i18n.js +10 -0
  60. package/dist/svelte/index.d.ts +29 -0
  61. package/dist/svelte/index.d.ts.map +1 -0
  62. package/dist/svelte/index.js +39 -0
  63. package/dist/svelte/playground.d.ts +88 -0
  64. package/dist/svelte/playground.d.ts.map +1 -0
  65. package/dist/svelte/playground.js +95 -0
  66. package/dist/types/index.d.ts +267 -0
  67. package/dist/types/index.d.ts.map +1 -0
  68. package/dist/ui.d.ts +10 -0
  69. package/dist/ui.d.ts.map +1 -0
  70. package/dist/ui.js +79 -0
  71. package/dist/ui.js.map +1 -0
  72. package/package.json +95 -0
package/dist/index.js ADDED
@@ -0,0 +1,1623 @@
1
+ import { ObjectRegistry, foreignKey, smrt, SmrtObject, SmrtCollection, field } from "@happyvertical/smrt-core";
2
+ import { definePrompt, resolvePrompt } from "@happyvertical/smrt-prompts";
3
+ import { tenantId, TenantScoped } from "@happyvertical/smrt-tenancy";
4
+ ObjectRegistry.registerPackageManifest(
5
+ new URL("./manifest.json", import.meta.url)
6
+ );
7
+ const smrtAnalyticsAnalyzePerformancePrompt = definePrompt({
8
+ key: "smrtAnalytics.property.analyzePerformance",
9
+ template: `Analyze the analytics performance for this property over the last {period}.
10
+ Consider: traffic trends, user engagement, conversion patterns.
11
+ Property: {propertyDisplayName} ({propertyProvider})`,
12
+ editable: {
13
+ template: true,
14
+ profile: true,
15
+ model: true,
16
+ params: true
17
+ }
18
+ });
19
+ const smrtAnalyticsAnalyzeResultsPrompt = definePrompt({
20
+ key: "smrtAnalytics.report.analyzeResults",
21
+ template: `Analyze these analytics report results and provide insights:
22
+
23
+ Report: {reportName}
24
+ Dimensions: {reportDimensions}
25
+ Metrics: {reportMetrics}
26
+ Date Range: {dateRangeStart} to {dateRangeEnd}
27
+ Row Count: {rowCount}
28
+
29
+ Data: {reportData}
30
+
31
+ Provide:
32
+ 1. Key findings
33
+ 2. Trends or patterns
34
+ 3. Actionable recommendations`,
35
+ editable: {
36
+ template: true,
37
+ profile: true,
38
+ model: true,
39
+ params: true
40
+ }
41
+ });
42
+ const smrtAnalyticsHasPositiveTrendsPrompt = definePrompt({
43
+ key: "smrtAnalytics.report.hasPositiveTrends",
44
+ template: `Based on these report results, are the metrics showing positive trends?
45
+
46
+ Metrics: {reportMetrics}
47
+ Data: {reportData}
48
+
49
+ Consider: user growth, engagement, conversions as positive indicators.
50
+
51
+ Begin your response with the single word "yes" or "no" (lower case), then
52
+ optionally provide a one-sentence explanation. Tooling parses the leading
53
+ word — "yes" or "true" means positive, anything else means negative.`,
54
+ editable: {
55
+ template: true,
56
+ profile: true,
57
+ model: true,
58
+ params: true
59
+ }
60
+ });
61
+ function promptMessageOptions(ai) {
62
+ return {
63
+ ...ai.params || {},
64
+ ...ai.model ? { model: ai.model } : {},
65
+ ...typeof ai.temperature === "number" ? { temperature: ai.temperature } : {},
66
+ ...typeof ai.maxTokens === "number" ? { maxTokens: ai.maxTokens } : {}
67
+ };
68
+ }
69
+ var AnalyticsProvider = /* @__PURE__ */ ((AnalyticsProvider2) => {
70
+ AnalyticsProvider2["GA4"] = "ga4";
71
+ AnalyticsProvider2["PLAUSIBLE"] = "plausible";
72
+ AnalyticsProvider2["MATOMO"] = "matomo";
73
+ return AnalyticsProvider2;
74
+ })(AnalyticsProvider || {});
75
+ var AnalyticsPropertyStatus = /* @__PURE__ */ ((AnalyticsPropertyStatus2) => {
76
+ AnalyticsPropertyStatus2["ACTIVE"] = "active";
77
+ AnalyticsPropertyStatus2["INACTIVE"] = "inactive";
78
+ AnalyticsPropertyStatus2["PENDING"] = "pending";
79
+ return AnalyticsPropertyStatus2;
80
+ })(AnalyticsPropertyStatus || {});
81
+ var DataStreamType = /* @__PURE__ */ ((DataStreamType2) => {
82
+ DataStreamType2["WEB"] = "WEB_DATA_STREAM";
83
+ DataStreamType2["ANDROID"] = "ANDROID_APP_DATA_STREAM";
84
+ DataStreamType2["IOS"] = "IOS_APP_DATA_STREAM";
85
+ return DataStreamType2;
86
+ })(DataStreamType || {});
87
+ var DataStreamStatus = /* @__PURE__ */ ((DataStreamStatus2) => {
88
+ DataStreamStatus2["ACTIVE"] = "active";
89
+ DataStreamStatus2["INACTIVE"] = "inactive";
90
+ return DataStreamStatus2;
91
+ })(DataStreamStatus || {});
92
+ var ReportStatus = /* @__PURE__ */ ((ReportStatus2) => {
93
+ ReportStatus2["DRAFT"] = "draft";
94
+ ReportStatus2["SCHEDULED"] = "scheduled";
95
+ ReportStatus2["RUNNING"] = "running";
96
+ ReportStatus2["COMPLETED"] = "completed";
97
+ ReportStatus2["FAILED"] = "failed";
98
+ return ReportStatus2;
99
+ })(ReportStatus || {});
100
+ var ReportFrequency = /* @__PURE__ */ ((ReportFrequency2) => {
101
+ ReportFrequency2["ONCE"] = "once";
102
+ ReportFrequency2["DAILY"] = "daily";
103
+ ReportFrequency2["WEEKLY"] = "weekly";
104
+ ReportFrequency2["MONTHLY"] = "monthly";
105
+ return ReportFrequency2;
106
+ })(ReportFrequency || {});
107
+ var CustomDimensionScope = /* @__PURE__ */ ((CustomDimensionScope2) => {
108
+ CustomDimensionScope2["EVENT"] = "EVENT";
109
+ CustomDimensionScope2["USER"] = "USER";
110
+ CustomDimensionScope2["ITEM"] = "ITEM";
111
+ return CustomDimensionScope2;
112
+ })(CustomDimensionScope || {});
113
+ var TrackingEventStatus = /* @__PURE__ */ ((TrackingEventStatus2) => {
114
+ TrackingEventStatus2["PENDING"] = "pending";
115
+ TrackingEventStatus2["SENT"] = "sent";
116
+ TrackingEventStatus2["FAILED"] = "failed";
117
+ return TrackingEventStatus2;
118
+ })(TrackingEventStatus || {});
119
+ var CountingMethod = /* @__PURE__ */ ((CountingMethod2) => {
120
+ CountingMethod2["ONCE_PER_EVENT"] = "ONCE_PER_EVENT";
121
+ CountingMethod2["ONCE_PER_SESSION"] = "ONCE_PER_SESSION";
122
+ return CountingMethod2;
123
+ })(CountingMethod || {});
124
+ var __defProp$3 = Object.defineProperty;
125
+ var __getOwnPropDesc$3 = Object.getOwnPropertyDescriptor;
126
+ var __decorateClass$3 = (decorators, target, key, kind) => {
127
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$3(target, key) : target;
128
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
129
+ if (decorator = decorators[i])
130
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
131
+ if (kind && result) __defProp$3(target, key, result);
132
+ return result;
133
+ };
134
+ let AnalyticsDataStream = class extends SmrtObject {
135
+ tenantId = null;
136
+ propertyId = "";
137
+ /**
138
+ * Human-readable display name
139
+ */
140
+ displayName = "";
141
+ /**
142
+ * Stream type (WEB, ANDROID, IOS)
143
+ */
144
+ streamType = DataStreamType.WEB;
145
+ /**
146
+ * External stream ID from provider
147
+ */
148
+ externalId = "";
149
+ /**
150
+ * Measurement ID for web streams (G-XXXXXXX)
151
+ */
152
+ measurementId = "";
153
+ /**
154
+ * Firebase App ID for app streams
155
+ */
156
+ firebaseAppId = "";
157
+ /**
158
+ * Default URI for web streams
159
+ */
160
+ defaultUri = "";
161
+ /**
162
+ * Bundle ID for iOS apps
163
+ */
164
+ bundleId = "";
165
+ /**
166
+ * Package name for Android apps
167
+ */
168
+ packageName = "";
169
+ /**
170
+ * Stream status
171
+ */
172
+ status = DataStreamStatus.ACTIVE;
173
+ /**
174
+ * Enhanced measurement enabled
175
+ */
176
+ enhancedMeasurement = true;
177
+ constructor(options = {}) {
178
+ super(options);
179
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
180
+ if (options.propertyId !== void 0) this.propertyId = options.propertyId;
181
+ if (options.displayName !== void 0)
182
+ this.displayName = options.displayName;
183
+ if (options.streamType !== void 0) this.streamType = options.streamType;
184
+ if (options.externalId !== void 0) this.externalId = options.externalId;
185
+ if (options.measurementId !== void 0)
186
+ this.measurementId = options.measurementId;
187
+ if (options.firebaseAppId !== void 0)
188
+ this.firebaseAppId = options.firebaseAppId;
189
+ if (options.defaultUri !== void 0) this.defaultUri = options.defaultUri;
190
+ if (options.bundleId !== void 0) this.bundleId = options.bundleId;
191
+ if (options.packageName !== void 0)
192
+ this.packageName = options.packageName;
193
+ if (options.status !== void 0) this.status = options.status;
194
+ if (options.enhancedMeasurement !== void 0)
195
+ this.enhancedMeasurement = options.enhancedMeasurement;
196
+ }
197
+ /**
198
+ * Check if this is a web stream
199
+ */
200
+ isWeb() {
201
+ return this.streamType === DataStreamType.WEB;
202
+ }
203
+ /**
204
+ * Check if this is an iOS app stream
205
+ */
206
+ isIOS() {
207
+ return this.streamType === DataStreamType.IOS;
208
+ }
209
+ /**
210
+ * Check if this is an Android app stream
211
+ */
212
+ isAndroid() {
213
+ return this.streamType === DataStreamType.ANDROID;
214
+ }
215
+ /**
216
+ * Check if this is a mobile app stream (iOS or Android)
217
+ */
218
+ isMobileApp() {
219
+ return this.isIOS() || this.isAndroid();
220
+ }
221
+ /**
222
+ * Get the platform identifier (measurementId for web, firebaseAppId for apps)
223
+ */
224
+ getPlatformId() {
225
+ if (this.isWeb()) {
226
+ return this.measurementId;
227
+ }
228
+ return this.firebaseAppId;
229
+ }
230
+ };
231
+ __decorateClass$3([
232
+ tenantId({ nullable: true })
233
+ ], AnalyticsDataStream.prototype, "tenantId", 2);
234
+ __decorateClass$3([
235
+ foreignKey("AnalyticsProperty")
236
+ ], AnalyticsDataStream.prototype, "propertyId", 2);
237
+ AnalyticsDataStream = __decorateClass$3([
238
+ TenantScoped({ mode: "optional" }),
239
+ smrt({
240
+ tableStrategy: "sti",
241
+ api: { include: ["list", "get", "create", "update"] },
242
+ mcp: { include: ["list", "get"] },
243
+ cli: true
244
+ })
245
+ ], AnalyticsDataStream);
246
+ class AnalyticsDataStreamCollection extends SmrtCollection {
247
+ static _itemClass = AnalyticsDataStream;
248
+ /**
249
+ * Find streams by property
250
+ *
251
+ * @param propertyId - Parent property ID
252
+ * @returns Array of data streams
253
+ */
254
+ async findByProperty(propertyId) {
255
+ return await this.list({
256
+ where: { propertyId },
257
+ orderBy: "displayName ASC"
258
+ });
259
+ }
260
+ /**
261
+ * Find stream by external ID
262
+ *
263
+ * @param externalId - External stream ID from provider
264
+ * @returns Matching stream or null
265
+ */
266
+ async findByExternalId(externalId) {
267
+ const results = await this.list({
268
+ where: { externalId },
269
+ limit: 1
270
+ });
271
+ return results.length > 0 ? results[0] : null;
272
+ }
273
+ /**
274
+ * Find stream by measurement ID (web streams)
275
+ *
276
+ * @param measurementId - GA4 measurement ID (G-XXXXXXX)
277
+ * @returns Matching stream or null
278
+ */
279
+ async findByMeasurementId(measurementId) {
280
+ const results = await this.list({
281
+ where: { measurementId },
282
+ limit: 1
283
+ });
284
+ return results.length > 0 ? results[0] : null;
285
+ }
286
+ /**
287
+ * Find streams by type
288
+ *
289
+ * @param streamType - Stream type (WEB, ANDROID, IOS)
290
+ * @returns Array of matching streams
291
+ */
292
+ async findByType(streamType) {
293
+ return await this.list({
294
+ where: { streamType },
295
+ orderBy: "displayName ASC"
296
+ });
297
+ }
298
+ /**
299
+ * Find all web streams
300
+ */
301
+ async findWebStreams() {
302
+ return await this.findByType(DataStreamType.WEB);
303
+ }
304
+ /**
305
+ * Find all iOS app streams
306
+ */
307
+ async findIOSStreams() {
308
+ return await this.findByType(DataStreamType.IOS);
309
+ }
310
+ /**
311
+ * Find all Android app streams
312
+ */
313
+ async findAndroidStreams() {
314
+ return await this.findByType(DataStreamType.ANDROID);
315
+ }
316
+ /**
317
+ * Find all mobile app streams (iOS + Android)
318
+ */
319
+ async findMobileStreams() {
320
+ const ios = await this.findIOSStreams();
321
+ const android = await this.findAndroidStreams();
322
+ return [...ios, ...android].sort(
323
+ (a, b) => a.displayName.localeCompare(b.displayName)
324
+ );
325
+ }
326
+ /**
327
+ * Find streams by status
328
+ *
329
+ * @param status - Stream status
330
+ * @returns Array of matching streams
331
+ */
332
+ async findByStatus(status) {
333
+ return await this.list({
334
+ where: { status },
335
+ orderBy: "displayName ASC"
336
+ });
337
+ }
338
+ /**
339
+ * Find all active streams
340
+ */
341
+ async findActive() {
342
+ return await this.findByStatus(DataStreamStatus.ACTIVE);
343
+ }
344
+ /**
345
+ * Find active streams for a property
346
+ *
347
+ * @param propertyId - Parent property ID
348
+ * @returns Array of active streams
349
+ */
350
+ async findActiveByProperty(propertyId) {
351
+ return await this.list({
352
+ where: {
353
+ propertyId,
354
+ status: DataStreamStatus.ACTIVE
355
+ },
356
+ orderBy: "displayName ASC"
357
+ });
358
+ }
359
+ }
360
+ var __defProp$2 = Object.defineProperty;
361
+ var __getOwnPropDesc$2 = Object.getOwnPropertyDescriptor;
362
+ var __decorateClass$2 = (decorators, target, key, kind) => {
363
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$2(target, key) : target;
364
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
365
+ if (decorator = decorators[i])
366
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
367
+ if (kind && result) __defProp$2(target, key, result);
368
+ return result;
369
+ };
370
+ let AnalyticsEvent = class extends SmrtObject {
371
+ tenantId = null;
372
+ propertyId = "";
373
+ /**
374
+ * Event name (e.g., 'purchase', 'page_view', 'sign_up')
375
+ */
376
+ eventName = "";
377
+ /**
378
+ * Client ID for anonymous tracking
379
+ */
380
+ clientId = "";
381
+ /**
382
+ * User ID for identified tracking
383
+ */
384
+ userId = "";
385
+ /**
386
+ * Event parameters (JSON)
387
+ */
388
+ params = "{}";
389
+ /**
390
+ * Event timestamp
391
+ */
392
+ eventTimestamp = /* @__PURE__ */ new Date();
393
+ /**
394
+ * Tracking status
395
+ */
396
+ status = TrackingEventStatus.PENDING;
397
+ /**
398
+ * Timestamp when sent to provider
399
+ */
400
+ sentAt = null;
401
+ /**
402
+ * Error message if sending failed
403
+ */
404
+ errorMessage = "";
405
+ /**
406
+ * Retry count
407
+ */
408
+ retryCount = 0;
409
+ /**
410
+ * Disable personalized ads
411
+ */
412
+ nonPersonalizedAds = false;
413
+ /**
414
+ * Session ID
415
+ */
416
+ sessionId = "";
417
+ /**
418
+ * Page path (for pageview events)
419
+ */
420
+ pagePath = "";
421
+ /**
422
+ * Page title (for pageview events)
423
+ */
424
+ pageTitle = "";
425
+ /**
426
+ * User agent
427
+ */
428
+ userAgent = "";
429
+ /**
430
+ * IP address (anonymized)
431
+ */
432
+ ipAddress = "";
433
+ constructor(options = {}) {
434
+ super(options);
435
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
436
+ if (options.propertyId !== void 0) this.propertyId = options.propertyId;
437
+ if (options.eventName !== void 0) this.eventName = options.eventName;
438
+ if (options.clientId !== void 0) this.clientId = options.clientId;
439
+ if (options.userId !== void 0) this.userId = options.userId;
440
+ if (options.params !== void 0) this.params = options.params;
441
+ if (options.eventTimestamp !== void 0)
442
+ this.eventTimestamp = options.eventTimestamp;
443
+ if (options.status !== void 0) this.status = options.status;
444
+ if (options.sentAt !== void 0) this.sentAt = options.sentAt;
445
+ if (options.errorMessage !== void 0)
446
+ this.errorMessage = options.errorMessage;
447
+ if (options.retryCount !== void 0) this.retryCount = options.retryCount;
448
+ if (options.nonPersonalizedAds !== void 0)
449
+ this.nonPersonalizedAds = options.nonPersonalizedAds;
450
+ if (options.sessionId !== void 0) this.sessionId = options.sessionId;
451
+ if (options.pagePath !== void 0) this.pagePath = options.pagePath;
452
+ if (options.pageTitle !== void 0) this.pageTitle = options.pageTitle;
453
+ if (options.userAgent !== void 0) this.userAgent = options.userAgent;
454
+ if (options.ipAddress !== void 0) this.ipAddress = options.ipAddress;
455
+ }
456
+ /**
457
+ * Get parsed event parameters
458
+ */
459
+ getParams() {
460
+ try {
461
+ return JSON.parse(this.params);
462
+ } catch {
463
+ return {};
464
+ }
465
+ }
466
+ /**
467
+ * Set event parameters
468
+ */
469
+ setParams(params) {
470
+ this.params = JSON.stringify(params);
471
+ }
472
+ /**
473
+ * Add a parameter to the event
474
+ */
475
+ addParam(key, value) {
476
+ const params = this.getParams();
477
+ params[key] = value;
478
+ this.setParams(params);
479
+ }
480
+ /**
481
+ * Check if this is a pageview event
482
+ */
483
+ isPageview() {
484
+ return this.eventName === "page_view" || this.eventName === "pageview";
485
+ }
486
+ /**
487
+ * Check if this is a conversion event
488
+ */
489
+ isConversion() {
490
+ const conversionEvents = [
491
+ "purchase",
492
+ "sign_up",
493
+ "generate_lead",
494
+ "begin_checkout"
495
+ ];
496
+ return conversionEvents.includes(this.eventName);
497
+ }
498
+ /**
499
+ * Mark as sent successfully
500
+ */
501
+ markSent() {
502
+ this.status = TrackingEventStatus.SENT;
503
+ this.sentAt = /* @__PURE__ */ new Date();
504
+ this.errorMessage = "";
505
+ }
506
+ /**
507
+ * Mark as failed
508
+ */
509
+ markFailed(error) {
510
+ this.status = TrackingEventStatus.FAILED;
511
+ this.errorMessage = error;
512
+ this.retryCount++;
513
+ }
514
+ /**
515
+ * Reset for retry
516
+ */
517
+ resetForRetry() {
518
+ this.status = TrackingEventStatus.PENDING;
519
+ this.sentAt = null;
520
+ }
521
+ /**
522
+ * Check if event should be retried
523
+ */
524
+ shouldRetry(maxRetries = 3) {
525
+ return this.status === TrackingEventStatus.FAILED && this.retryCount < maxRetries;
526
+ }
527
+ /**
528
+ * Build SDK TrackEvent object
529
+ */
530
+ toTrackEvent() {
531
+ return {
532
+ name: this.eventName,
533
+ params: this.getParams(),
534
+ clientId: this.clientId || void 0,
535
+ userId: this.userId || void 0,
536
+ timestamp: this.eventTimestamp.getTime() * 1e3,
537
+ // Convert to microseconds
538
+ nonPersonalizedAds: this.nonPersonalizedAds
539
+ };
540
+ }
541
+ };
542
+ __decorateClass$2([
543
+ tenantId({ nullable: true })
544
+ ], AnalyticsEvent.prototype, "tenantId", 2);
545
+ __decorateClass$2([
546
+ foreignKey("AnalyticsProperty")
547
+ ], AnalyticsEvent.prototype, "propertyId", 2);
548
+ AnalyticsEvent = __decorateClass$2([
549
+ TenantScoped({ mode: "optional" }),
550
+ smrt({
551
+ tableStrategy: "sti",
552
+ api: { include: ["list", "get", "create"] },
553
+ mcp: { include: ["list", "get", "track"] },
554
+ cli: true
555
+ })
556
+ ], AnalyticsEvent);
557
+ class AnalyticsEventCollection extends SmrtCollection {
558
+ static _itemClass = AnalyticsEvent;
559
+ /**
560
+ * Find events by property
561
+ *
562
+ * @param propertyId - Parent property ID
563
+ * @returns Array of events
564
+ */
565
+ async findByProperty(propertyId) {
566
+ return await this.list({
567
+ where: { propertyId },
568
+ orderBy: "eventTimestamp DESC"
569
+ });
570
+ }
571
+ /**
572
+ * Find events by event name
573
+ *
574
+ * @param eventName - Event name to filter by
575
+ * @returns Array of matching events
576
+ */
577
+ async findByEventName(eventName) {
578
+ return await this.list({
579
+ where: { eventName },
580
+ orderBy: "eventTimestamp DESC"
581
+ });
582
+ }
583
+ /**
584
+ * Find events by client ID
585
+ *
586
+ * @param clientId - Client ID
587
+ * @returns Array of events for this client
588
+ */
589
+ async findByClientId(clientId) {
590
+ return await this.list({
591
+ where: { clientId },
592
+ orderBy: "eventTimestamp DESC"
593
+ });
594
+ }
595
+ /**
596
+ * Find events by user ID
597
+ *
598
+ * @param userId - User ID
599
+ * @returns Array of events for this user
600
+ */
601
+ async findByUserId(userId) {
602
+ return await this.list({
603
+ where: { userId },
604
+ orderBy: "eventTimestamp DESC"
605
+ });
606
+ }
607
+ /**
608
+ * Find events by status
609
+ *
610
+ * @param status - Tracking event status
611
+ * @returns Array of matching events
612
+ */
613
+ async findByStatus(status) {
614
+ return await this.list({
615
+ where: { status },
616
+ orderBy: "eventTimestamp DESC"
617
+ });
618
+ }
619
+ /**
620
+ * Find all pending events
621
+ */
622
+ async findPending() {
623
+ return await this.findByStatus(TrackingEventStatus.PENDING);
624
+ }
625
+ /**
626
+ * Find all sent events
627
+ */
628
+ async findSent() {
629
+ return await this.findByStatus(TrackingEventStatus.SENT);
630
+ }
631
+ /**
632
+ * Find all failed events
633
+ */
634
+ async findFailed() {
635
+ return await this.findByStatus(TrackingEventStatus.FAILED);
636
+ }
637
+ /**
638
+ * Find events that should be retried
639
+ *
640
+ * @param maxRetries - Maximum retry count
641
+ * @returns Array of events eligible for retry
642
+ */
643
+ async findForRetry(maxRetries = 3) {
644
+ const failed = await this.findFailed();
645
+ return failed.filter((e) => e.shouldRetry(maxRetries));
646
+ }
647
+ /**
648
+ * Find pending events for a property
649
+ *
650
+ * @param propertyId - Parent property ID
651
+ * @returns Array of pending events
652
+ */
653
+ async findPendingByProperty(propertyId) {
654
+ return await this.list({
655
+ where: {
656
+ propertyId,
657
+ status: TrackingEventStatus.PENDING
658
+ },
659
+ orderBy: "eventTimestamp ASC"
660
+ // Process oldest first
661
+ });
662
+ }
663
+ /**
664
+ * Find events by date range
665
+ *
666
+ * @param startDate - Start date
667
+ * @param endDate - End date
668
+ * @returns Array of events in date range
669
+ */
670
+ async findByDateRange(startDate, endDate) {
671
+ return await this.list({
672
+ where: {
673
+ "eventTimestamp >=": startDate.toISOString(),
674
+ "eventTimestamp <=": endDate.toISOString()
675
+ },
676
+ orderBy: "eventTimestamp DESC"
677
+ });
678
+ }
679
+ /**
680
+ * Find conversion events
681
+ *
682
+ * @param propertyId - Optional property ID filter
683
+ * @returns Array of conversion events
684
+ */
685
+ async findConversions(propertyId) {
686
+ const conversionEvents = [
687
+ "purchase",
688
+ "sign_up",
689
+ "generate_lead",
690
+ "begin_checkout"
691
+ ];
692
+ const all = propertyId ? await this.findByProperty(propertyId) : await this.list({ orderBy: "eventTimestamp DESC" });
693
+ return all.filter((e) => conversionEvents.includes(e.eventName));
694
+ }
695
+ /**
696
+ * Find pageview events
697
+ *
698
+ * @param propertyId - Optional property ID filter
699
+ * @returns Array of pageview events
700
+ */
701
+ async findPageviews(propertyId) {
702
+ const where = { eventName: "page_view" };
703
+ if (propertyId) {
704
+ where.propertyId = propertyId;
705
+ }
706
+ return await this.list({
707
+ where,
708
+ orderBy: "eventTimestamp DESC"
709
+ });
710
+ }
711
+ /**
712
+ * Count events by event name for a property
713
+ *
714
+ * @param propertyId - Property ID
715
+ * @returns Map of event name to count
716
+ */
717
+ async countByEventName(propertyId) {
718
+ const events = await this.findByProperty(propertyId);
719
+ const counts = /* @__PURE__ */ new Map();
720
+ for (const event of events) {
721
+ const current = counts.get(event.eventName) || 0;
722
+ counts.set(event.eventName, current + 1);
723
+ }
724
+ return counts;
725
+ }
726
+ /**
727
+ * Get event stats for a property
728
+ *
729
+ * @param propertyId - Property ID
730
+ * @returns Event statistics
731
+ */
732
+ async getPropertyStats(propertyId) {
733
+ const events = await this.findByProperty(propertyId);
734
+ return {
735
+ total: events.length,
736
+ pending: events.filter((e) => e.status === TrackingEventStatus.PENDING).length,
737
+ sent: events.filter((e) => e.status === TrackingEventStatus.SENT).length,
738
+ failed: events.filter((e) => e.status === TrackingEventStatus.FAILED).length,
739
+ conversions: events.filter((e) => e.isConversion()).length,
740
+ pageviews: events.filter((e) => e.isPageview()).length
741
+ };
742
+ }
743
+ /**
744
+ * Get day-over-day pageview stats with trend for a property.
745
+ *
746
+ * Compares today's pageview count against yesterday's to produce a
747
+ * trend direction and percentage change. A threshold of 5% is used
748
+ * to classify 'up' vs 'down' vs 'flat'.
749
+ *
750
+ * @param propertyId - Property ID
751
+ * @param now - Optional current date (for testing)
752
+ * @returns Stats with trend
753
+ */
754
+ async getPropertyStatsWithTrend(propertyId, now) {
755
+ const currentTime = now || /* @__PURE__ */ new Date();
756
+ const todayStart = new Date(
757
+ Date.UTC(
758
+ currentTime.getUTCFullYear(),
759
+ currentTime.getUTCMonth(),
760
+ currentTime.getUTCDate()
761
+ )
762
+ );
763
+ const yesterdayStart = new Date(todayStart.getTime() - 864e5);
764
+ const allPageviewEvents = await this.list({
765
+ where: {
766
+ propertyId,
767
+ eventName: "page_view",
768
+ "eventTimestamp >=": yesterdayStart.toISOString(),
769
+ "eventTimestamp <=": currentTime.toISOString()
770
+ }
771
+ });
772
+ const todayPageviewEvents = allPageviewEvents.filter(
773
+ (e) => new Date(e.eventTimestamp) >= todayStart
774
+ );
775
+ const yesterdayPageviewEvents = allPageviewEvents.filter(
776
+ (e) => new Date(e.eventTimestamp) < todayStart
777
+ );
778
+ const todayClients = new Set(todayPageviewEvents.map((e) => e.clientId));
779
+ const yesterdayClients = new Set(
780
+ yesterdayPageviewEvents.map((e) => e.clientId)
781
+ );
782
+ const todayPageviews = todayPageviewEvents.length;
783
+ const yesterdayPageviews = yesterdayPageviewEvents.length;
784
+ let trend = "flat";
785
+ let trendPercent = 0;
786
+ if (yesterdayPageviews > 0) {
787
+ const change = (todayPageviews - yesterdayPageviews) / yesterdayPageviews * 100;
788
+ trendPercent = Math.round(change);
789
+ if (change > 5) trend = "up";
790
+ else if (change < -5) trend = "down";
791
+ }
792
+ return {
793
+ todayPageviews,
794
+ todayUsers: todayClients.size,
795
+ yesterdayPageviews,
796
+ yesterdayUsers: yesterdayClients.size,
797
+ trend,
798
+ trendPercent
799
+ };
800
+ }
801
+ /**
802
+ * Get day-over-day stats for multiple properties in batch.
803
+ *
804
+ * @param propertyIds - Array of property IDs
805
+ * @param now - Optional current date (for testing)
806
+ * @returns Map of propertyId to stats
807
+ */
808
+ async getBatchPropertyStats(propertyIds, now) {
809
+ const results = /* @__PURE__ */ new Map();
810
+ const currentTime = now || /* @__PURE__ */ new Date();
811
+ const todayStart = new Date(
812
+ Date.UTC(
813
+ currentTime.getUTCFullYear(),
814
+ currentTime.getUTCMonth(),
815
+ currentTime.getUTCDate()
816
+ )
817
+ );
818
+ const yesterdayStart = new Date(todayStart.getTime() - 864e5);
819
+ const allEvents = await this.list({
820
+ where: {
821
+ eventName: "page_view",
822
+ "eventTimestamp >=": yesterdayStart.toISOString(),
823
+ "eventTimestamp <=": currentTime.toISOString()
824
+ }
825
+ });
826
+ const todayByProperty = /* @__PURE__ */ new Map();
827
+ const yesterdayByProperty = /* @__PURE__ */ new Map();
828
+ for (const e of allEvents) {
829
+ const isToday = new Date(e.eventTimestamp) >= todayStart;
830
+ const map = isToday ? todayByProperty : yesterdayByProperty;
831
+ const list = map.get(e.propertyId);
832
+ if (list) list.push(e);
833
+ else map.set(e.propertyId, [e]);
834
+ }
835
+ for (const propertyId of propertyIds) {
836
+ const todayPageviewEvents = todayByProperty.get(propertyId) ?? [];
837
+ const yesterdayPageviewEvents = yesterdayByProperty.get(propertyId) ?? [];
838
+ const todayClients = new Set(todayPageviewEvents.map((e) => e.clientId));
839
+ const yesterdayClients = new Set(
840
+ yesterdayPageviewEvents.map((e) => e.clientId)
841
+ );
842
+ const todayPageviews = todayPageviewEvents.length;
843
+ const yesterdayPageviews = yesterdayPageviewEvents.length;
844
+ let trend = "flat";
845
+ let trendPercent = 0;
846
+ if (yesterdayPageviews > 0) {
847
+ const change = (todayPageviews - yesterdayPageviews) / yesterdayPageviews * 100;
848
+ trendPercent = Math.round(change);
849
+ if (change > 5) trend = "up";
850
+ else if (change < -5) trend = "down";
851
+ }
852
+ results.set(propertyId, {
853
+ todayPageviews,
854
+ todayUsers: todayClients.size,
855
+ yesterdayPageviews,
856
+ yesterdayUsers: yesterdayClients.size,
857
+ trend,
858
+ trendPercent
859
+ });
860
+ }
861
+ return results;
862
+ }
863
+ }
864
+ var __defProp$1 = Object.defineProperty;
865
+ var __getOwnPropDesc$1 = Object.getOwnPropertyDescriptor;
866
+ var __decorateClass$1 = (decorators, target, key, kind) => {
867
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$1(target, key) : target;
868
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
869
+ if (decorator = decorators[i])
870
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
871
+ if (kind && result) __defProp$1(target, key, result);
872
+ return result;
873
+ };
874
+ let AnalyticsProperty = class extends SmrtObject {
875
+ tenantId = null;
876
+ /**
877
+ * Internal name/identifier
878
+ */
879
+ name = "";
880
+ /**
881
+ * Human-readable display name
882
+ */
883
+ displayName = "";
884
+ /**
885
+ * Analytics provider (ga4, plausible, matomo)
886
+ */
887
+ provider = AnalyticsProvider.GA4;
888
+ /**
889
+ * External ID from the provider (e.g., "properties/123456789" for GA4,
890
+ * idSite for Matomo)
891
+ */
892
+ externalId = "";
893
+ /**
894
+ * Measurement ID for GA4 (G-XXXXXXXXXX)
895
+ */
896
+ measurementId = "";
897
+ apiSecret = "";
898
+ /**
899
+ * Site domain for Plausible/Matomo
900
+ */
901
+ siteDomain = "";
902
+ /**
903
+ * Property timezone
904
+ */
905
+ timeZone = "America/Los_Angeles";
906
+ /**
907
+ * Currency code (e.g., 'USD', 'EUR')
908
+ */
909
+ currencyCode = "USD";
910
+ /**
911
+ * Industry category
912
+ */
913
+ industryCategory = "";
914
+ /**
915
+ * Service level (STANDARD, PREMIUM)
916
+ */
917
+ serviceLevel = "STANDARD";
918
+ /**
919
+ * Property status
920
+ */
921
+ status = AnalyticsPropertyStatus.ACTIVE;
922
+ /**
923
+ * Last sync timestamp with provider
924
+ */
925
+ lastSyncAt = null;
926
+ providerMetadata = "{}";
927
+ constructor(options = {}) {
928
+ super(options);
929
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
930
+ if (options.name !== void 0) this.name = options.name;
931
+ if (options.displayName !== void 0)
932
+ this.displayName = options.displayName;
933
+ if (options.provider !== void 0) this.provider = options.provider;
934
+ if (options.externalId !== void 0) this.externalId = options.externalId;
935
+ if (options.measurementId !== void 0)
936
+ this.measurementId = options.measurementId;
937
+ if (options.apiSecret !== void 0) this.apiSecret = options.apiSecret;
938
+ if (options.siteDomain !== void 0) this.siteDomain = options.siteDomain;
939
+ if (options.timeZone !== void 0) this.timeZone = options.timeZone;
940
+ if (options.currencyCode !== void 0)
941
+ this.currencyCode = options.currencyCode;
942
+ if (options.industryCategory !== void 0)
943
+ this.industryCategory = options.industryCategory;
944
+ if (options.serviceLevel !== void 0)
945
+ this.serviceLevel = options.serviceLevel;
946
+ if (options.status !== void 0) this.status = options.status;
947
+ if (options.lastSyncAt !== void 0) this.lastSyncAt = options.lastSyncAt;
948
+ if (options.providerMetadata !== void 0)
949
+ this.providerMetadata = options.providerMetadata;
950
+ }
951
+ /**
952
+ * Check if this is a GA4 property
953
+ */
954
+ isGA4() {
955
+ return this.provider === AnalyticsProvider.GA4;
956
+ }
957
+ /**
958
+ * Check if this is a Plausible site
959
+ */
960
+ isPlausible() {
961
+ return this.provider === AnalyticsProvider.PLAUSIBLE;
962
+ }
963
+ /**
964
+ * Check if this is a Matomo site
965
+ */
966
+ isMatomo() {
967
+ return this.provider === AnalyticsProvider.MATOMO;
968
+ }
969
+ /**
970
+ * Get parsed provider metadata
971
+ */
972
+ getProviderMetadata() {
973
+ try {
974
+ return JSON.parse(this.providerMetadata);
975
+ } catch {
976
+ return {};
977
+ }
978
+ }
979
+ /**
980
+ * Set provider metadata
981
+ */
982
+ setProviderMetadata(metadata) {
983
+ this.providerMetadata = JSON.stringify(metadata);
984
+ }
985
+ /**
986
+ * Mark as synced with provider
987
+ */
988
+ markSynced() {
989
+ this.lastSyncAt = /* @__PURE__ */ new Date();
990
+ }
991
+ /**
992
+ * AI-powered: Analyze property performance.
993
+ *
994
+ * Uses the `smrtAnalytics.property.analyzePerformance` prompt registered
995
+ * via `@happyvertical/smrt-prompts`, allowing tenant- or instance-level
996
+ * overrides of the template, model, and parameters at runtime.
997
+ *
998
+ * Only non-PII fields (display name, provider label, requested period)
999
+ * are sent to the AI provider. Internal identifiers (`id`, `externalId`,
1000
+ * `measurementId`, `apiSecret`, `providerMetadata`) are intentionally
1001
+ * excluded — see `../prompts.ts` for the full exclusion rationale.
1002
+ */
1003
+ async analyzePerformance(options = {}) {
1004
+ const period = options.period || "30 days";
1005
+ const db = this.options.db ?? this.options.persistence;
1006
+ const resolvedPrompt = await resolvePrompt(
1007
+ smrtAnalyticsAnalyzePerformancePrompt.key,
1008
+ {
1009
+ db,
1010
+ variables: {
1011
+ period,
1012
+ propertyDisplayName: this.displayName || "",
1013
+ propertyProvider: this.provider || ""
1014
+ }
1015
+ }
1016
+ );
1017
+ const ai = await this.getAiClient();
1018
+ const analysis = await ai.message(
1019
+ resolvedPrompt.text,
1020
+ promptMessageOptions(resolvedPrompt.ai)
1021
+ );
1022
+ return {
1023
+ action: "analyzePerformance",
1024
+ period,
1025
+ analysis: analysis.trim()
1026
+ };
1027
+ }
1028
+ /**
1029
+ * AI-powered: Check if property is performing well
1030
+ */
1031
+ async isPerformingWell() {
1032
+ return await this.is(
1033
+ `
1034
+ Based on the property configuration and metadata:
1035
+ - Property: ${this.displayName}
1036
+ - Provider: ${this.provider}
1037
+ - Status: ${this.status}
1038
+ - Last sync: ${this.lastSyncAt}
1039
+
1040
+ Is this property properly configured and likely performing well?
1041
+ `,
1042
+ // Property fields hand-rolled above; skip is()'s object-data injection.
1043
+ { includeData: false }
1044
+ );
1045
+ }
1046
+ };
1047
+ __decorateClass$1([
1048
+ tenantId({ nullable: true })
1049
+ ], AnalyticsProperty.prototype, "tenantId", 2);
1050
+ __decorateClass$1([
1051
+ field({ sensitive: true })
1052
+ ], AnalyticsProperty.prototype, "apiSecret", 2);
1053
+ __decorateClass$1([
1054
+ field({ sensitive: true })
1055
+ ], AnalyticsProperty.prototype, "providerMetadata", 2);
1056
+ AnalyticsProperty = __decorateClass$1([
1057
+ TenantScoped({ mode: "optional" }),
1058
+ smrt({
1059
+ tableStrategy: "sti",
1060
+ api: { include: ["list", "get", "create", "update"] },
1061
+ mcp: { include: ["list", "get", "sync", "runReport"] },
1062
+ cli: true
1063
+ })
1064
+ ], AnalyticsProperty);
1065
+ class AnalyticsPropertyCollection extends SmrtCollection {
1066
+ static _itemClass = AnalyticsProperty;
1067
+ /**
1068
+ * Find property by external ID
1069
+ *
1070
+ * @param externalId - External ID from provider
1071
+ * @returns Matching property or null
1072
+ */
1073
+ async findByExternalId(externalId) {
1074
+ const results = await this.list({
1075
+ where: { externalId },
1076
+ limit: 1
1077
+ });
1078
+ return results.length > 0 ? results[0] : null;
1079
+ }
1080
+ /**
1081
+ * Find property by measurement ID (GA4)
1082
+ *
1083
+ * @param measurementId - GA4 measurement ID (G-XXXXXXXXXX)
1084
+ * @returns Matching property or null
1085
+ */
1086
+ async findByMeasurementId(measurementId) {
1087
+ const results = await this.list({
1088
+ where: { measurementId },
1089
+ limit: 1
1090
+ });
1091
+ return results.length > 0 ? results[0] : null;
1092
+ }
1093
+ /**
1094
+ * Find property by site domain and optional provider.
1095
+ *
1096
+ * @param siteDomain - Provider site domain
1097
+ * @param provider - Optional provider discriminator for migration/coexistence
1098
+ * @returns Matching property or null
1099
+ */
1100
+ async findBySiteDomain(siteDomain, provider) {
1101
+ const results = await this.list({
1102
+ where: provider ? { siteDomain, provider } : { siteDomain },
1103
+ limit: 1
1104
+ });
1105
+ return results.length > 0 ? results[0] : null;
1106
+ }
1107
+ /**
1108
+ * Find properties by provider
1109
+ *
1110
+ * @param provider - Analytics provider
1111
+ * @returns Array of matching properties
1112
+ */
1113
+ async findByProvider(provider) {
1114
+ return await this.list({
1115
+ where: { provider },
1116
+ orderBy: "displayName ASC"
1117
+ });
1118
+ }
1119
+ /**
1120
+ * Find all GA4 properties
1121
+ */
1122
+ async findGA4Properties() {
1123
+ return await this.findByProvider(AnalyticsProvider.GA4);
1124
+ }
1125
+ /**
1126
+ * Find all Plausible sites
1127
+ */
1128
+ async findPlausibleSites() {
1129
+ return await this.findByProvider(AnalyticsProvider.PLAUSIBLE);
1130
+ }
1131
+ /**
1132
+ * Find all Matomo sites
1133
+ */
1134
+ async findMatomoSites() {
1135
+ return await this.findByProvider(AnalyticsProvider.MATOMO);
1136
+ }
1137
+ /**
1138
+ * Find properties by status
1139
+ *
1140
+ * @param status - Property status
1141
+ * @returns Array of matching properties
1142
+ */
1143
+ async findByStatus(status) {
1144
+ return await this.list({
1145
+ where: { status },
1146
+ orderBy: "displayName ASC"
1147
+ });
1148
+ }
1149
+ /**
1150
+ * Find all active properties
1151
+ */
1152
+ async findActive() {
1153
+ return await this.findByStatus(AnalyticsPropertyStatus.ACTIVE);
1154
+ }
1155
+ /**
1156
+ * Find properties that need syncing (not synced in last N hours)
1157
+ *
1158
+ * @param hoursAgo - Hours since last sync
1159
+ * @returns Array of properties needing sync
1160
+ */
1161
+ async findNeedingSync(hoursAgo = 24) {
1162
+ const cutoff = /* @__PURE__ */ new Date();
1163
+ cutoff.setTime(cutoff.getTime() - hoursAgo * 60 * 60 * 1e3);
1164
+ const all = await this.findActive();
1165
+ return all.filter((p) => !p.lastSyncAt || p.lastSyncAt < cutoff);
1166
+ }
1167
+ }
1168
+ var __defProp = Object.defineProperty;
1169
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
1170
+ var __decorateClass = (decorators, target, key, kind) => {
1171
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
1172
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
1173
+ if (decorator = decorators[i])
1174
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
1175
+ if (kind && result) __defProp(target, key, result);
1176
+ return result;
1177
+ };
1178
+ let AnalyticsReport = class extends SmrtObject {
1179
+ tenantId = null;
1180
+ propertyId = "";
1181
+ /**
1182
+ * Report name
1183
+ */
1184
+ name = "";
1185
+ /**
1186
+ * Report description
1187
+ */
1188
+ description = "";
1189
+ /**
1190
+ * Dimensions to group by (JSON array)
1191
+ */
1192
+ dimensions = "[]";
1193
+ /**
1194
+ * Metrics to retrieve (JSON array)
1195
+ */
1196
+ metrics = "[]";
1197
+ /**
1198
+ * Date range start (relative or absolute)
1199
+ */
1200
+ dateRangeStart = "7daysAgo";
1201
+ /**
1202
+ * Date range end (relative or absolute)
1203
+ */
1204
+ dateRangeEnd = "today";
1205
+ /**
1206
+ * Dimension filter expression (JSON)
1207
+ */
1208
+ dimensionFilter = "";
1209
+ /**
1210
+ * Metric filter expression (JSON)
1211
+ */
1212
+ metricFilter = "";
1213
+ /**
1214
+ * Sort order (JSON array)
1215
+ */
1216
+ orderBy = "[]";
1217
+ /**
1218
+ * Maximum results to return
1219
+ */
1220
+ maxResults = 0;
1221
+ /**
1222
+ * Report status
1223
+ */
1224
+ status = ReportStatus.DRAFT;
1225
+ /**
1226
+ * Scheduling frequency
1227
+ */
1228
+ frequency = ReportFrequency.ONCE;
1229
+ /**
1230
+ * Last run timestamp
1231
+ */
1232
+ lastRunAt = null;
1233
+ /**
1234
+ * Next scheduled run
1235
+ */
1236
+ nextRunAt = null;
1237
+ /**
1238
+ * Cached result data (JSON)
1239
+ */
1240
+ resultData = "";
1241
+ /**
1242
+ * Row count from last run
1243
+ */
1244
+ rowCount = 0;
1245
+ /**
1246
+ * Error message from last failed run
1247
+ */
1248
+ lastError = "";
1249
+ constructor(options = {}) {
1250
+ super(options);
1251
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
1252
+ if (options.propertyId !== void 0) this.propertyId = options.propertyId;
1253
+ if (options.name !== void 0) this.name = options.name;
1254
+ if (options.description !== void 0)
1255
+ this.description = options.description;
1256
+ if (options.dimensions !== void 0) this.dimensions = options.dimensions;
1257
+ if (options.metrics !== void 0) this.metrics = options.metrics;
1258
+ if (options.dateRangeStart !== void 0)
1259
+ this.dateRangeStart = options.dateRangeStart;
1260
+ if (options.dateRangeEnd !== void 0)
1261
+ this.dateRangeEnd = options.dateRangeEnd;
1262
+ if (options.dimensionFilter !== void 0)
1263
+ this.dimensionFilter = options.dimensionFilter;
1264
+ if (options.metricFilter !== void 0)
1265
+ this.metricFilter = options.metricFilter;
1266
+ if (options.orderBy !== void 0) this.orderBy = options.orderBy;
1267
+ if (options.maxResults !== void 0) this.maxResults = options.maxResults;
1268
+ if (options.status !== void 0) this.status = options.status;
1269
+ if (options.frequency !== void 0) this.frequency = options.frequency;
1270
+ if (options.lastRunAt !== void 0) this.lastRunAt = options.lastRunAt;
1271
+ if (options.nextRunAt !== void 0) this.nextRunAt = options.nextRunAt;
1272
+ if (options.resultData !== void 0) this.resultData = options.resultData;
1273
+ if (options.rowCount !== void 0) this.rowCount = options.rowCount;
1274
+ if (options.lastError !== void 0) this.lastError = options.lastError;
1275
+ }
1276
+ /**
1277
+ * Get parsed dimensions
1278
+ */
1279
+ getDimensions() {
1280
+ try {
1281
+ return JSON.parse(this.dimensions);
1282
+ } catch {
1283
+ return [];
1284
+ }
1285
+ }
1286
+ /**
1287
+ * Set dimensions
1288
+ */
1289
+ setDimensions(dimensions) {
1290
+ this.dimensions = JSON.stringify(dimensions);
1291
+ }
1292
+ /**
1293
+ * Get parsed metrics
1294
+ */
1295
+ getMetrics() {
1296
+ try {
1297
+ return JSON.parse(this.metrics);
1298
+ } catch {
1299
+ return [];
1300
+ }
1301
+ }
1302
+ /**
1303
+ * Set metrics
1304
+ */
1305
+ setMetrics(metrics) {
1306
+ this.metrics = JSON.stringify(metrics);
1307
+ }
1308
+ /**
1309
+ * Get parsed result data
1310
+ */
1311
+ getResultData() {
1312
+ if (!this.resultData) return null;
1313
+ try {
1314
+ return JSON.parse(this.resultData);
1315
+ } catch {
1316
+ return null;
1317
+ }
1318
+ }
1319
+ /**
1320
+ * Set result data
1321
+ */
1322
+ setResultData(data) {
1323
+ this.resultData = JSON.stringify(data);
1324
+ }
1325
+ /**
1326
+ * Mark report as running
1327
+ */
1328
+ markRunning() {
1329
+ this.status = ReportStatus.RUNNING;
1330
+ this.lastError = "";
1331
+ }
1332
+ /**
1333
+ * Mark report as completed with results
1334
+ */
1335
+ markCompleted(rowCount) {
1336
+ this.status = ReportStatus.COMPLETED;
1337
+ this.lastRunAt = /* @__PURE__ */ new Date();
1338
+ this.rowCount = rowCount;
1339
+ this.lastError = "";
1340
+ this.calculateNextRun();
1341
+ }
1342
+ /**
1343
+ * Mark report as failed
1344
+ */
1345
+ markFailed(error) {
1346
+ this.status = ReportStatus.FAILED;
1347
+ this.lastRunAt = /* @__PURE__ */ new Date();
1348
+ this.lastError = error;
1349
+ this.calculateNextRun();
1350
+ }
1351
+ /**
1352
+ * Calculate next scheduled run based on frequency
1353
+ */
1354
+ calculateNextRun() {
1355
+ if (this.frequency === ReportFrequency.ONCE) {
1356
+ this.nextRunAt = null;
1357
+ return;
1358
+ }
1359
+ const now = /* @__PURE__ */ new Date();
1360
+ const next = new Date(now);
1361
+ switch (this.frequency) {
1362
+ case ReportFrequency.DAILY:
1363
+ next.setDate(next.getDate() + 1);
1364
+ break;
1365
+ case ReportFrequency.WEEKLY:
1366
+ next.setDate(next.getDate() + 7);
1367
+ break;
1368
+ case ReportFrequency.MONTHLY:
1369
+ next.setMonth(next.getMonth() + 1);
1370
+ break;
1371
+ }
1372
+ this.nextRunAt = next;
1373
+ }
1374
+ /**
1375
+ * Check if report is due to run
1376
+ */
1377
+ isDue() {
1378
+ if (this.frequency === ReportFrequency.ONCE) {
1379
+ return this.status === ReportStatus.SCHEDULED && !this.lastRunAt;
1380
+ }
1381
+ if (!this.nextRunAt) return false;
1382
+ return /* @__PURE__ */ new Date() >= this.nextRunAt;
1383
+ }
1384
+ /**
1385
+ * AI-powered: Analyze report results.
1386
+ *
1387
+ * Uses the `smrtAnalytics.report.analyzeResults` prompt registered via
1388
+ * `@happyvertical/smrt-prompts`, allowing tenant- or instance-level
1389
+ * overrides of the template, model, and parameters at runtime.
1390
+ *
1391
+ * Internal identifiers (`id`, `propertyId`, `tenantId`, `lastError`, raw
1392
+ * `dimensionFilter` / `metricFilter` JSON) are excluded from the prompt
1393
+ * variables — see `../prompts.ts` for the exclusion rationale.
1394
+ *
1395
+ * **`resultData` is FORWARDED VERBATIM.** The persisted result rows are
1396
+ * JSON-stringified into the `reportData` variable; this package cannot
1397
+ * strip PII because the row schema is determined by which dimensions /
1398
+ * metrics the caller asked the analytics provider to return. If the
1399
+ * persisted rows contain `userPseudoId`, `clientId`, IP-derived
1400
+ * geolocation, or any other identifier, those fields WILL reach the AI
1401
+ * provider. Callers are responsible for excluding PII-bearing dimensions
1402
+ * before persisting, applying a column allowlist at the call site, or
1403
+ * overriding the prompt template via `PromptOverride`. The forwarding is
1404
+ * pinned by a regression test in
1405
+ * `__tests__/analytics-report-prompt.test.ts`.
1406
+ *
1407
+ * The previous implementation issued a second freeform `this.do()` call
1408
+ * to re-summarize "top 3 insights"; that behaviour is now folded into
1409
+ * the single registered template (which already asks for findings,
1410
+ * trends, and recommendations) — `insights` mirrors `analysis` so the
1411
+ * return shape is preserved without a redundant AI round-trip.
1412
+ */
1413
+ async analyzeResults(_options = {}) {
1414
+ const resultData = this.getResultData();
1415
+ const db = this.options.db ?? this.options.persistence;
1416
+ const resolvedPrompt = await resolvePrompt(
1417
+ smrtAnalyticsAnalyzeResultsPrompt.key,
1418
+ {
1419
+ db,
1420
+ variables: {
1421
+ reportName: this.name || "",
1422
+ reportDimensions: this.dimensions || "[]",
1423
+ reportMetrics: this.metrics || "[]",
1424
+ dateRangeStart: this.dateRangeStart || "",
1425
+ dateRangeEnd: this.dateRangeEnd || "",
1426
+ rowCount: String(this.rowCount),
1427
+ reportData: JSON.stringify(resultData, null, 2)
1428
+ }
1429
+ }
1430
+ );
1431
+ const ai = await this.getAiClient();
1432
+ const analysis = (await ai.message(
1433
+ resolvedPrompt.text,
1434
+ promptMessageOptions(resolvedPrompt.ai)
1435
+ )).trim();
1436
+ return {
1437
+ action: "analyzeResults",
1438
+ analysis,
1439
+ insights: analysis
1440
+ };
1441
+ }
1442
+ /**
1443
+ * AI-powered: Check if results show positive trends.
1444
+ *
1445
+ * Uses the `smrtAnalytics.report.hasPositiveTrends` prompt registered
1446
+ * via `@happyvertical/smrt-prompts`. Only the metric labels and the
1447
+ * aggregate `resultData` JSON are sent to the AI provider — though as
1448
+ * with `analyzeResults`, `resultData` is forwarded verbatim and may
1449
+ * carry PII the caller persisted; see `analyzeResults` docstring and
1450
+ * `../prompts.ts`.
1451
+ *
1452
+ * Boolean coercion uses `/^\s*(yes|true)\b/i` against the trimmed
1453
+ * response. The registered prompt template explicitly instructs the
1454
+ * model to begin its answer with the literal word "yes" or "no" so
1455
+ * this regex is reliable; tenant overrides MUST preserve that leading-
1456
+ * word convention or the boolean will silently fall to `false`.
1457
+ */
1458
+ async hasPositiveTrends() {
1459
+ const resultData = this.getResultData();
1460
+ const db = this.options.db ?? this.options.persistence;
1461
+ const resolvedPrompt = await resolvePrompt(
1462
+ smrtAnalyticsHasPositiveTrendsPrompt.key,
1463
+ {
1464
+ db,
1465
+ variables: {
1466
+ reportMetrics: this.metrics || "[]",
1467
+ reportData: JSON.stringify(resultData)
1468
+ }
1469
+ }
1470
+ );
1471
+ const ai = await this.getAiClient();
1472
+ const response = (await ai.message(
1473
+ resolvedPrompt.text,
1474
+ promptMessageOptions(resolvedPrompt.ai)
1475
+ )).trim();
1476
+ return /^\s*(yes|true)\b/i.test(response);
1477
+ }
1478
+ };
1479
+ __decorateClass([
1480
+ tenantId({ nullable: true })
1481
+ ], AnalyticsReport.prototype, "tenantId", 2);
1482
+ __decorateClass([
1483
+ foreignKey("AnalyticsProperty")
1484
+ ], AnalyticsReport.prototype, "propertyId", 2);
1485
+ AnalyticsReport = __decorateClass([
1486
+ TenantScoped({ mode: "optional" }),
1487
+ smrt({
1488
+ tableStrategy: "sti",
1489
+ api: { include: ["list", "get", "create", "update", "run"] },
1490
+ mcp: { include: ["list", "get", "run", "analyze"] },
1491
+ cli: true
1492
+ })
1493
+ ], AnalyticsReport);
1494
+ class AnalyticsReportCollection extends SmrtCollection {
1495
+ static _itemClass = AnalyticsReport;
1496
+ /**
1497
+ * Find reports by property
1498
+ *
1499
+ * @param propertyId - Parent property ID
1500
+ * @returns Array of reports
1501
+ */
1502
+ async findByProperty(propertyId) {
1503
+ return await this.list({
1504
+ where: { propertyId },
1505
+ orderBy: "name ASC"
1506
+ });
1507
+ }
1508
+ /**
1509
+ * Find reports by status
1510
+ *
1511
+ * @param status - Report status
1512
+ * @returns Array of matching reports
1513
+ */
1514
+ async findByStatus(status) {
1515
+ return await this.list({
1516
+ where: { status },
1517
+ orderBy: "name ASC"
1518
+ });
1519
+ }
1520
+ /**
1521
+ * Find all draft reports
1522
+ */
1523
+ async findDrafts() {
1524
+ return await this.findByStatus(ReportStatus.DRAFT);
1525
+ }
1526
+ /**
1527
+ * Find all scheduled reports
1528
+ */
1529
+ async findScheduled() {
1530
+ return await this.findByStatus(ReportStatus.SCHEDULED);
1531
+ }
1532
+ /**
1533
+ * Find all completed reports
1534
+ */
1535
+ async findCompleted() {
1536
+ return await this.findByStatus(ReportStatus.COMPLETED);
1537
+ }
1538
+ /**
1539
+ * Find all failed reports
1540
+ */
1541
+ async findFailed() {
1542
+ return await this.findByStatus(ReportStatus.FAILED);
1543
+ }
1544
+ /**
1545
+ * Find reports by frequency
1546
+ *
1547
+ * @param frequency - Report frequency
1548
+ * @returns Array of matching reports
1549
+ */
1550
+ async findByFrequency(frequency) {
1551
+ return await this.list({
1552
+ where: { frequency },
1553
+ orderBy: "name ASC"
1554
+ });
1555
+ }
1556
+ /**
1557
+ * Find all recurring reports (not one-time)
1558
+ */
1559
+ async findRecurring() {
1560
+ const all = await this.list({ orderBy: "name ASC" });
1561
+ return all.filter((r) => r.frequency !== ReportFrequency.ONCE);
1562
+ }
1563
+ /**
1564
+ * Find reports due to run
1565
+ *
1566
+ * @returns Array of reports that are due
1567
+ */
1568
+ async findDue() {
1569
+ const all = await this.list({
1570
+ where: { status: ReportStatus.SCHEDULED }
1571
+ });
1572
+ return all.filter((r) => r.isDue());
1573
+ }
1574
+ /**
1575
+ * Find reports for a property by status
1576
+ *
1577
+ * @param propertyId - Parent property ID
1578
+ * @param status - Report status
1579
+ * @returns Array of matching reports
1580
+ */
1581
+ async findByPropertyAndStatus(propertyId, status) {
1582
+ return await this.list({
1583
+ where: { propertyId, status },
1584
+ orderBy: "name ASC"
1585
+ });
1586
+ }
1587
+ /**
1588
+ * Find recently run reports
1589
+ *
1590
+ * @param hoursAgo - Hours since last run
1591
+ * @returns Array of recently run reports
1592
+ */
1593
+ async findRecentlyRun(hoursAgo = 24) {
1594
+ const cutoff = /* @__PURE__ */ new Date();
1595
+ cutoff.setTime(cutoff.getTime() - hoursAgo * 60 * 60 * 1e3);
1596
+ const all = await this.list({ orderBy: "lastRunAt DESC" });
1597
+ return all.filter((r) => r.lastRunAt && r.lastRunAt >= cutoff);
1598
+ }
1599
+ }
1600
+ export {
1601
+ AnalyticsDataStream,
1602
+ AnalyticsDataStreamCollection,
1603
+ AnalyticsEvent,
1604
+ AnalyticsEventCollection,
1605
+ AnalyticsProperty,
1606
+ AnalyticsPropertyCollection,
1607
+ AnalyticsPropertyStatus,
1608
+ AnalyticsProvider,
1609
+ AnalyticsReport,
1610
+ AnalyticsReportCollection,
1611
+ CountingMethod,
1612
+ CustomDimensionScope,
1613
+ DataStreamStatus,
1614
+ DataStreamType,
1615
+ ReportFrequency,
1616
+ ReportStatus,
1617
+ TrackingEventStatus,
1618
+ promptMessageOptions,
1619
+ smrtAnalyticsAnalyzePerformancePrompt,
1620
+ smrtAnalyticsAnalyzeResultsPrompt,
1621
+ smrtAnalyticsHasPositiveTrendsPrompt
1622
+ };
1623
+ //# sourceMappingURL=index.js.map