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