@happyvertical/smrt-analytics 0.37.2 → 0.37.4

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