@happyvertical/smrt-analytics 0.32.0 → 0.32.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/collections/AnalyticsEventCollection.d.ts +76 -3
- package/dist/collections/AnalyticsEventCollection.d.ts.map +1 -1
- package/dist/index.js +156 -35
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +14 -2
- package/dist/smrt-knowledge.json +4 -4
- package/dist/svelte/AnalyticsSummary.svelte +2 -2
- package/dist/svelte/EventsTable.svelte +36 -22
- package/dist/svelte/EventsTable.svelte.d.ts.map +1 -1
- package/dist/svelte/PropertyInfo.svelte +7 -7
- package/dist/svelte/PropertyStatusBadge.svelte +10 -10
- package/dist/svelte/StatCard.svelte +8 -6
- package/dist/svelte/StatCard.svelte.d.ts +1 -1
- package/dist/svelte/StatCard.svelte.d.ts.map +1 -1
- package/dist/svelte/TrendBadge.svelte +13 -9
- package/dist/svelte/TrendBadge.svelte.d.ts +1 -1
- package/dist/svelte/TrendBadge.svelte.d.ts.map +1 -1
- package/dist/svelte/i18n.d.ts +1 -0
- package/dist/svelte/i18n.d.ts.map +1 -1
- package/dist/svelte/i18n.js +1 -0
- package/dist/types/index.d.ts +6 -1
- package/dist/types/index.d.ts.map +1 -1
- package/package.json +7 -7
|
@@ -3,6 +3,66 @@ import { AnalyticsEvent } from '../models/AnalyticsEvent.js';
|
|
|
3
3
|
import { PropertyStatsWithTrend, TrackingEventStatus } from '../types/index.js';
|
|
4
4
|
export declare class AnalyticsEventCollection extends SmrtCollection<AnalyticsEvent> {
|
|
5
5
|
static readonly _itemClass: typeof AnalyticsEvent;
|
|
6
|
+
/**
|
|
7
|
+
* Offset of `timeZone` at `instant`, in milliseconds (wall-clock minus UTC).
|
|
8
|
+
*
|
|
9
|
+
* Reads the zone's wall-clock Y/M/D h:m:s for `instant` via
|
|
10
|
+
* `Intl.DateTimeFormat` parts and subtracts the real UTC instant. Positive
|
|
11
|
+
* east of UTC, negative west (e.g. `America/Los_Angeles` returns roughly
|
|
12
|
+
* `-7h`/`-8h` depending on DST).
|
|
13
|
+
*
|
|
14
|
+
* @throws RangeError if `timeZone` is not a valid IANA identifier.
|
|
15
|
+
*/
|
|
16
|
+
private zoneOffsetMs;
|
|
17
|
+
/**
|
|
18
|
+
* Resolve the UTC instant marking the start of the calendar day (00:00) that
|
|
19
|
+
* `instant` falls on **within the given IANA time zone**.
|
|
20
|
+
*
|
|
21
|
+
* Day-over-day buckets ("today vs yesterday") must respect the property's
|
|
22
|
+
* configured `timeZone` (defaults to `America/Los_Angeles`), otherwise a
|
|
23
|
+
* pageview at 11:30pm local time — already the next UTC day — is bucketed
|
|
24
|
+
* into the wrong day. We read the wall-clock civil date for the zone, then
|
|
25
|
+
* map that date's local midnight back to a UTC instant, correcting for the
|
|
26
|
+
* zone offset (and re-correcting once across a DST boundary).
|
|
27
|
+
*
|
|
28
|
+
* Invalid/unknown zone identifiers fall back to UTC day boundaries (matching
|
|
29
|
+
* the previous behaviour) rather than throwing.
|
|
30
|
+
*
|
|
31
|
+
* @param instant - Reference instant.
|
|
32
|
+
* @param timeZone - IANA time zone (e.g. `America/Los_Angeles`).
|
|
33
|
+
* @returns UTC `Date` for local midnight of the day `instant` is in.
|
|
34
|
+
*/
|
|
35
|
+
protected startOfDayInZone(instant: Date, timeZone: string): Date;
|
|
36
|
+
/**
|
|
37
|
+
* Resolve the UTC instant for the start of the day *before* `todayStart`'s
|
|
38
|
+
* local day, in `timeZone`.
|
|
39
|
+
*
|
|
40
|
+
* Steps back 12h from local midnight (landing safely inside the previous
|
|
41
|
+
* civil day regardless of DST — a naive `- 24h` skips a day across
|
|
42
|
+
* spring-forward), then re-resolves start-of-day.
|
|
43
|
+
*
|
|
44
|
+
* @param todayStart - Local-midnight UTC instant from {@link startOfDayInZone}.
|
|
45
|
+
* @param timeZone - IANA time zone.
|
|
46
|
+
* @returns UTC `Date` for local midnight of the prior calendar day.
|
|
47
|
+
*/
|
|
48
|
+
protected startOfYesterdayInZone(todayStart: Date, timeZone: string): Date;
|
|
49
|
+
/**
|
|
50
|
+
* Classify a day-over-day change into a trend direction + percent.
|
|
51
|
+
*
|
|
52
|
+
* - `yesterday > 0`: percent = rounded delta; >5% up, <-5% down, else flat.
|
|
53
|
+
* - `yesterday === 0 && today > 0`: a brand-new surge from a zero baseline —
|
|
54
|
+
* classified `up` with a `null` percent (no finite percentage exists), so
|
|
55
|
+
* the UI renders "new" rather than a misleading flat 0%.
|
|
56
|
+
* - `yesterday === 0 && today === 0`: flat, 0%.
|
|
57
|
+
*
|
|
58
|
+
* @param today - Today's count.
|
|
59
|
+
* @param yesterday - Yesterday's count.
|
|
60
|
+
* @returns Trend direction and percent (null when growing from zero).
|
|
61
|
+
*/
|
|
62
|
+
protected classifyTrend(today: number, yesterday: number): {
|
|
63
|
+
trend: 'up' | 'down' | 'flat';
|
|
64
|
+
trendPercent: number | null;
|
|
65
|
+
};
|
|
6
66
|
/**
|
|
7
67
|
* Find events by property
|
|
8
68
|
*
|
|
@@ -112,20 +172,33 @@ export declare class AnalyticsEventCollection extends SmrtCollection<AnalyticsEv
|
|
|
112
172
|
*
|
|
113
173
|
* Compares today's pageview count against yesterday's to produce a
|
|
114
174
|
* trend direction and percentage change. A threshold of 5% is used
|
|
115
|
-
* to classify 'up' vs 'down' vs 'flat'
|
|
175
|
+
* to classify 'up' vs 'down' vs 'flat'; growth from a zero baseline is
|
|
176
|
+
* classified `up` with a `null` percent (see {@link classifyTrend}).
|
|
177
|
+
*
|
|
178
|
+
* Day boundaries are computed in `timeZone` (an IANA identifier such as the
|
|
179
|
+
* property's `AnalyticsProperty.timeZone`, which defaults to
|
|
180
|
+
* `America/Los_Angeles`) so an event near local midnight buckets into the
|
|
181
|
+
* correct calendar day. Defaults to `'UTC'` when omitted.
|
|
116
182
|
*
|
|
117
183
|
* @param propertyId - Property ID
|
|
118
184
|
* @param now - Optional current date (for testing)
|
|
185
|
+
* @param timeZone - IANA time zone for day boundaries (default `'UTC'`)
|
|
119
186
|
* @returns Stats with trend
|
|
120
187
|
*/
|
|
121
|
-
getPropertyStatsWithTrend(propertyId: string, now?: Date): Promise<PropertyStatsWithTrend>;
|
|
188
|
+
getPropertyStatsWithTrend(propertyId: string, now?: Date, timeZone?: string): Promise<PropertyStatsWithTrend>;
|
|
122
189
|
/**
|
|
123
190
|
* Get day-over-day stats for multiple properties in batch.
|
|
124
191
|
*
|
|
192
|
+
* Day boundaries are computed in `timeZone` (default `'UTC'`); see
|
|
193
|
+
* {@link getPropertyStatsWithTrend}. A single zone applies to the whole
|
|
194
|
+
* batch, so callers mixing properties with different `timeZone` values
|
|
195
|
+
* should batch per zone (or fall back to per-property calls).
|
|
196
|
+
*
|
|
125
197
|
* @param propertyIds - Array of property IDs
|
|
126
198
|
* @param now - Optional current date (for testing)
|
|
199
|
+
* @param timeZone - IANA time zone for day boundaries (default `'UTC'`)
|
|
127
200
|
* @returns Map of propertyId to stats
|
|
128
201
|
*/
|
|
129
|
-
getBatchPropertyStats(propertyIds: string[], now?: Date): Promise<Map<string, PropertyStatsWithTrend>>;
|
|
202
|
+
getBatchPropertyStats(propertyIds: string[], now?: Date, timeZone?: string): Promise<Map<string, PropertyStatsWithTrend>>;
|
|
130
203
|
}
|
|
131
204
|
//# sourceMappingURL=AnalyticsEventCollection.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AnalyticsEventCollection.d.ts","sourceRoot":"","sources":["../../src/collections/AnalyticsEventCollection.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,cAAc,EAAE,MAAM,0BAA0B,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,6BAA6B,CAAC;AAC7D,OAAO,EACL,KAAK,sBAAsB,EAC3B,mBAAmB,EACpB,MAAM,mBAAmB,CAAC;AAE3B,qBAAa,wBAAyB,SAAQ,cAAc,CAAC,cAAc,CAAC;IAC1E,MAAM,CAAC,QAAQ,CAAC,UAAU,wBAAkB;IAE5C;;;;;OAKG;IACG,cAAc,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAOnE;;;;;OAKG;IACG,eAAe,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAOnE;;;;;OAKG;IACG,cAAc,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAOjE;;;;;OAKG;IACG,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAO7D;;;;;OAKG;IACG,YAAY,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAO1E;;OAEG;IACG,WAAW,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAI9C;;OAEG;IACG,QAAQ,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAI3C;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAI7C;;;;;OAKG;IACG,YAAY,CAAC,UAAU,GAAE,MAAU,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAKrE;;;;;OAKG;IACG,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAU1E;;;;;;OAMG;IACG,eAAe,CACnB,SAAS,EAAE,IAAI,EACf,OAAO,EAAE,IAAI,GACZ,OAAO,CAAC,cAAc,EAAE,CAAC;IAU5B;;;;;OAKG;IACG,eAAe,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAcrE;;;;;OAKG;IACG,aAAa,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAWnE;;;;;OAKG;IACG,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAYxE;;;;;OAKG;IACG,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC;QAClD,KAAK,EAAE,MAAM,CAAC;QACd,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IAeF
|
|
1
|
+
{"version":3,"file":"AnalyticsEventCollection.d.ts","sourceRoot":"","sources":["../../src/collections/AnalyticsEventCollection.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -556,6 +556,134 @@ AnalyticsEvent = __decorateClass$2([
|
|
|
556
556
|
], AnalyticsEvent);
|
|
557
557
|
class AnalyticsEventCollection extends SmrtCollection {
|
|
558
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
|
+
}
|
|
559
687
|
/**
|
|
560
688
|
* Find events by property
|
|
561
689
|
*
|
|
@@ -745,22 +873,23 @@ class AnalyticsEventCollection extends SmrtCollection {
|
|
|
745
873
|
*
|
|
746
874
|
* Compares today's pageview count against yesterday's to produce a
|
|
747
875
|
* trend direction and percentage change. A threshold of 5% is used
|
|
748
|
-
* to classify 'up' vs 'down' vs 'flat'
|
|
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.
|
|
749
883
|
*
|
|
750
884
|
* @param propertyId - Property ID
|
|
751
885
|
* @param now - Optional current date (for testing)
|
|
886
|
+
* @param timeZone - IANA time zone for day boundaries (default `'UTC'`)
|
|
752
887
|
* @returns Stats with trend
|
|
753
888
|
*/
|
|
754
|
-
async getPropertyStatsWithTrend(propertyId, now) {
|
|
889
|
+
async getPropertyStatsWithTrend(propertyId, now, timeZone = "UTC") {
|
|
755
890
|
const currentTime = now || /* @__PURE__ */ new Date();
|
|
756
|
-
const todayStart =
|
|
757
|
-
|
|
758
|
-
currentTime.getUTCFullYear(),
|
|
759
|
-
currentTime.getUTCMonth(),
|
|
760
|
-
currentTime.getUTCDate()
|
|
761
|
-
)
|
|
762
|
-
);
|
|
763
|
-
const yesterdayStart = new Date(todayStart.getTime() - 864e5);
|
|
891
|
+
const todayStart = this.startOfDayInZone(currentTime, timeZone);
|
|
892
|
+
const yesterdayStart = this.startOfYesterdayInZone(todayStart, timeZone);
|
|
764
893
|
const allPageviewEvents = await this.list({
|
|
765
894
|
where: {
|
|
766
895
|
propertyId,
|
|
@@ -781,14 +910,10 @@ class AnalyticsEventCollection extends SmrtCollection {
|
|
|
781
910
|
);
|
|
782
911
|
const todayPageviews = todayPageviewEvents.length;
|
|
783
912
|
const yesterdayPageviews = yesterdayPageviewEvents.length;
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
trendPercent = Math.round(change);
|
|
789
|
-
if (change > 5) trend = "up";
|
|
790
|
-
else if (change < -5) trend = "down";
|
|
791
|
-
}
|
|
913
|
+
const { trend, trendPercent } = this.classifyTrend(
|
|
914
|
+
todayPageviews,
|
|
915
|
+
yesterdayPageviews
|
|
916
|
+
);
|
|
792
917
|
return {
|
|
793
918
|
todayPageviews,
|
|
794
919
|
todayUsers: todayClients.size,
|
|
@@ -801,21 +926,21 @@ class AnalyticsEventCollection extends SmrtCollection {
|
|
|
801
926
|
/**
|
|
802
927
|
* Get day-over-day stats for multiple properties in batch.
|
|
803
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
|
+
*
|
|
804
934
|
* @param propertyIds - Array of property IDs
|
|
805
935
|
* @param now - Optional current date (for testing)
|
|
936
|
+
* @param timeZone - IANA time zone for day boundaries (default `'UTC'`)
|
|
806
937
|
* @returns Map of propertyId to stats
|
|
807
938
|
*/
|
|
808
|
-
async getBatchPropertyStats(propertyIds, now) {
|
|
939
|
+
async getBatchPropertyStats(propertyIds, now, timeZone = "UTC") {
|
|
809
940
|
const results = /* @__PURE__ */ new Map();
|
|
810
941
|
const currentTime = now || /* @__PURE__ */ new Date();
|
|
811
|
-
const todayStart =
|
|
812
|
-
|
|
813
|
-
currentTime.getUTCFullYear(),
|
|
814
|
-
currentTime.getUTCMonth(),
|
|
815
|
-
currentTime.getUTCDate()
|
|
816
|
-
)
|
|
817
|
-
);
|
|
818
|
-
const yesterdayStart = new Date(todayStart.getTime() - 864e5);
|
|
942
|
+
const todayStart = this.startOfDayInZone(currentTime, timeZone);
|
|
943
|
+
const yesterdayStart = this.startOfYesterdayInZone(todayStart, timeZone);
|
|
819
944
|
const allEvents = await this.list({
|
|
820
945
|
where: {
|
|
821
946
|
eventName: "page_view",
|
|
@@ -841,14 +966,10 @@ class AnalyticsEventCollection extends SmrtCollection {
|
|
|
841
966
|
);
|
|
842
967
|
const todayPageviews = todayPageviewEvents.length;
|
|
843
968
|
const yesterdayPageviews = yesterdayPageviewEvents.length;
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
trendPercent = Math.round(change);
|
|
849
|
-
if (change > 5) trend = "up";
|
|
850
|
-
else if (change < -5) trend = "down";
|
|
851
|
-
}
|
|
969
|
+
const { trend, trendPercent } = this.classifyTrend(
|
|
970
|
+
todayPageviews,
|
|
971
|
+
yesterdayPageviews
|
|
972
|
+
);
|
|
852
973
|
results.set(propertyId, {
|
|
853
974
|
todayPageviews,
|
|
854
975
|
todayUsers: todayClients.size,
|