@iann29/rastro 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +199 -50
- package/agent/integration.md +26 -10
- package/agent/manifest.json +10 -5
- package/agent/manifest.schema.json +14 -5
- package/dist/client/federation.d.ts +28 -8
- package/dist/client/federation.d.ts.map +1 -1
- package/dist/client/federation.js +12 -1
- package/dist/client/federation.js.map +1 -1
- package/dist/client/index.d.ts +262 -9
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +119 -2
- package/dist/client/index.js.map +1 -1
- package/dist/component/_generated/api.d.ts +6 -0
- package/dist/component/_generated/api.d.ts.map +1 -1
- package/dist/component/_generated/api.js.map +1 -1
- package/dist/component/_generated/component.d.ts +72 -1
- package/dist/component/_generated/component.d.ts.map +1 -1
- package/dist/component/_generated/server.d.ts +1 -0
- package/dist/component/_generated/server.d.ts.map +1 -1
- package/dist/component/_generated/server.js.map +1 -1
- package/dist/component/constants.d.ts +9 -2
- package/dist/component/constants.d.ts.map +1 -1
- package/dist/component/constants.js +31 -3
- package/dist/component/constants.js.map +1 -1
- package/dist/component/convex.config.d.ts +1 -0
- package/dist/component/convex.config.js +3 -0
- package/dist/component/convex.config.js.map +1 -1
- package/dist/component/coverage.d.ts +1 -0
- package/dist/component/coverage.d.ts.map +1 -1
- package/dist/component/coverage.js +6 -1
- package/dist/component/coverage.js.map +1 -1
- package/dist/component/eventStore.d.ts +2 -0
- package/dist/component/eventStore.d.ts.map +1 -1
- package/dist/component/geo.d.ts +1 -0
- package/dist/component/geo.d.ts.map +1 -1
- package/dist/component/geo.js +1 -1
- package/dist/component/geo.js.map +1 -1
- package/dist/component/http.d.ts.map +1 -1
- package/dist/component/http.js +65 -2
- package/dist/component/http.js.map +1 -1
- package/dist/component/identity.d.ts +12 -0
- package/dist/component/identity.d.ts.map +1 -1
- package/dist/component/identity.js +30 -2
- package/dist/component/identity.js.map +1 -1
- package/dist/component/ingest.d.ts +36 -0
- package/dist/component/ingest.d.ts.map +1 -1
- package/dist/component/ingest.js +111 -20
- package/dist/component/ingest.js.map +1 -1
- package/dist/component/live.d.ts.map +1 -1
- package/dist/component/live.js +34 -4
- package/dist/component/live.js.map +1 -1
- package/dist/component/origin.d.ts +70 -0
- package/dist/component/origin.d.ts.map +1 -0
- package/dist/component/origin.js +230 -0
- package/dist/component/origin.js.map +1 -0
- package/dist/component/platforms.d.ts +33 -0
- package/dist/component/platforms.d.ts.map +1 -0
- package/dist/component/platforms.js +328 -0
- package/dist/component/platforms.js.map +1 -0
- package/dist/component/reports.d.ts +27 -72
- package/dist/component/reports.d.ts.map +1 -1
- package/dist/component/reports.js +72 -16
- package/dist/component/reports.js.map +1 -1
- package/dist/component/rollupStore.d.ts +7 -4
- package/dist/component/rollupStore.d.ts.map +1 -1
- package/dist/component/rollupStore.js +37 -17
- package/dist/component/rollupStore.js.map +1 -1
- package/dist/component/rollups.d.ts +27 -8
- package/dist/component/rollups.d.ts.map +1 -1
- package/dist/component/rollups.js +110 -32
- package/dist/component/rollups.js.map +1 -1
- package/dist/component/sanitize.d.ts +5 -0
- package/dist/component/sanitize.d.ts.map +1 -1
- package/dist/component/sanitize.js +33 -1
- package/dist/component/sanitize.js.map +1 -1
- package/dist/component/schema.d.ts +68 -9
- package/dist/component/schema.js +21 -3
- package/dist/component/schema.js.map +1 -1
- package/dist/component/trackedLinks.d.ts +91 -0
- package/dist/component/trackedLinks.d.ts.map +1 -0
- package/dist/component/trackedLinks.js +314 -0
- package/dist/component/trackedLinks.js.map +1 -0
- package/dist/component/useragent.d.ts +6 -0
- package/dist/component/useragent.d.ts.map +1 -1
- package/dist/component/useragent.js +9 -0
- package/dist/component/useragent.js.map +1 -1
- package/dist/component/validators.d.ts +51 -11
- package/dist/component/validators.d.ts.map +1 -1
- package/dist/component/validators.js +35 -1
- package/dist/component/validators.js.map +1 -1
- package/dist/tracker/generated.d.ts +11 -11
- package/dist/tracker/generated.d.ts.map +1 -1
- package/dist/tracker/generated.js +11 -11
- package/dist/tracker/generated.js.map +1 -1
- package/dist/tracker/tracker.d.ts +1 -0
- package/dist/tracker/tracker.d.ts.map +1 -1
- package/dist/tracker/tracker.js +104 -47
- package/dist/tracker/tracker.js.map +1 -1
- package/dist/tracker/vitals.d.ts.map +1 -1
- package/dist/tracker/vitals.js +22 -4
- package/dist/tracker/vitals.js.map +1 -1
- package/dist/tracker.min.js +1 -1
- package/dist/vitals.min.js +1 -1
- package/docs/upgrading.md +234 -45
- package/llms.txt +9 -4
- package/package.json +2 -2
- package/src/component/_generated/api.ts +6 -0
- package/src/component/_generated/component.ts +153 -1
- package/src/component/_generated/server.ts +1 -0
- package/src/component/constants.ts +31 -3
- package/src/component/convex.config.ts +3 -0
- package/src/component/coverage.ts +7 -1
- package/src/component/geo.ts +1 -1
- package/src/component/http.ts +71 -2
- package/src/component/identity.ts +41 -2
- package/src/component/ingest.ts +146 -33
- package/src/component/live.ts +51 -4
- package/src/component/origin.ts +273 -0
- package/src/component/platforms.ts +359 -0
- package/src/component/reports.ts +98 -18
- package/src/component/rollupStore.ts +51 -20
- package/src/component/rollups.ts +141 -32
- package/src/component/sanitize.ts +39 -1
- package/src/component/schema.ts +24 -3
- package/src/component/trackedLinks.ts +384 -0
- package/src/component/useragent.ts +11 -0
- package/src/component/validators.ts +71 -0
- package/src/tracker/generated.ts +11 -11
package/dist/component/ingest.js
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
import { v } from "convex/values";
|
|
2
2
|
import { mutation } from "./_generated/server.js";
|
|
3
3
|
import { internal } from "./_generated/api.js";
|
|
4
|
-
import { AGGREGATE_SHARDS, MAX_BATCH_EVENT_GROUPS, DAY_MS, HOURLY_AGGREGATE_SHARDS, DIMENSION_SLOTS, HOUR_MS, LIVE_LEAVE_GRACE_MS, LIVE_SESSION_TTL_MS, LIVE_SWEEP_INTERVAL_MS, MAX_BATCH_EVENTS, MAX_SITE_BYTES_PER_SHARD_WINDOW, MAX_SITE_EVENTS_PER_SHARD_WINDOW, MAX_BYTES_PER_SESSION_WINDOW, MAX_EVENTS_PER_SESSION_WINDOW, MAX_FUNNELS_PER_SITE, MAX_GOALS_PER_SITE, RATE_LIMIT_WINDOW_MS, REPORT_ROLLUP_SHARDS, SECONDARY_AGGREGATE_SHARDS, SITE_INGEST_SHARDS, } from "./constants.js";
|
|
4
|
+
import { AGGREGATE_SHARDS, MAX_BATCH_EVENT_GROUPS, DAY_MS, HOURLY_AGGREGATE_SHARDS, DIMENSION_SLOTS, HOUR_MS, LIVE_LEAVE_GRACE_MS, LIVE_SESSION_TTL_MS, LIVE_SWEEP_INTERVAL_MS, MAX_BATCH_EVENTS, MAX_BYTES_PER_CLIENT_WINDOW, MAX_EVENTS_PER_CLIENT_WINDOW, MAX_SITE_BYTES_PER_SHARD_WINDOW, MAX_SITE_EVENTS_PER_SHARD_WINDOW, MAX_BYTES_PER_SESSION_WINDOW, MAX_EVENTS_PER_SESSION_WINDOW, MAX_FUNNELS_PER_SITE, MAX_GOALS_PER_SITE, RATE_LIMIT_WINDOW_MS, REPORT_ROLLUP_SHARDS, SECONDARY_AGGREGATE_SHARDS, SITE_INGEST_SHARDS, } from "./constants.js";
|
|
5
5
|
import { addVisitor, createVisitorSketch, unionVisitorSketches, } from "./cardinality.js";
|
|
6
6
|
import { addToVitalHistogram, emptyVitalHistogram, vitalDevice, VITAL_ALL, } from "./vitals.js";
|
|
7
7
|
import { fail } from "./errors.js";
|
|
8
8
|
import { ensureAnalyticsControl } from "./coverage.js";
|
|
9
9
|
import { bufferEventBatch, eventBatchKey, eventBucketStart, findTelemetry, flushEventBatches, insertEventBatch, inspectTelemetryMinute, } from "./eventStore.js";
|
|
10
|
-
import { cleanString, isValidMoney, measureBatchBytes, normalizeRoute, originAllowed, publicUrlHost, sanitizeContext, sanitizeCurrency, sanitizeEvent, sanitizeOpaqueId, sanitizePath, sanitizeProperties, sanitizeSlug, sourceFromReferrer, stableHash, } from "./sanitize.js";
|
|
10
|
+
import { cleanString, externalReferrer, isValidMoney, measureBatchBytes, normalizeRoute, originAllowed, publicUrlHost, sanitizeContext, sanitizeCurrency, sanitizeEvent, sanitizeOpaqueId, sanitizePath, sanitizeProperties, sanitizeSlug, sourceFromReferrer, stableHash, } from "./sanitize.js";
|
|
11
11
|
import { ingestContextValidator, ingestResultValidator, trackerEventValidator, trustedConversionValidator, } from "./validators.js";
|
|
12
12
|
import { createReportRollupDeltas, enqueueReportRollups, reportRollupCoordinates, } from "./rollupStore.js";
|
|
13
13
|
import { localDayStart, localDayTimezone } from "./localTime.js";
|
|
14
|
+
import { classifyOrigin, platformDimensionValue } from "./origin.js";
|
|
14
15
|
function furtherReach(current, next) {
|
|
15
16
|
if (!next)
|
|
16
17
|
return current;
|
|
@@ -26,6 +27,8 @@ export const ingestBatch = mutation({
|
|
|
26
27
|
origin: v.string(),
|
|
27
28
|
events: v.array(trackerEventValidator),
|
|
28
29
|
context: v.optional(ingestContextValidator),
|
|
30
|
+
// The HTTP action's salted daily client-network key (see identity.ts).
|
|
31
|
+
clientKey: v.optional(v.string()),
|
|
29
32
|
},
|
|
30
33
|
returns: ingestResultValidator,
|
|
31
34
|
handler: async (ctx, args) => {
|
|
@@ -51,6 +54,9 @@ export const ingestBatch = mutation({
|
|
|
51
54
|
}
|
|
52
55
|
const now = Date.now();
|
|
53
56
|
await ensureAnalyticsControl(ctx, args.siteId, now);
|
|
57
|
+
if (args.clientKey !== undefined) {
|
|
58
|
+
await consumeClientRateLimit(ctx, args.siteId, args.clientKey, eventByteCounts, now);
|
|
59
|
+
}
|
|
54
60
|
await consumeSiteRateLimit(ctx, args.siteId, args.events, eventByteCounts, now);
|
|
55
61
|
const context = sanitizeContext(args.context);
|
|
56
62
|
const timezone = localDayTimezone(site.timezone);
|
|
@@ -155,11 +161,10 @@ export const ingestBatch = mutation({
|
|
|
155
161
|
rejected += 1;
|
|
156
162
|
continue;
|
|
157
163
|
}
|
|
158
|
-
|
|
159
|
-
//
|
|
160
|
-
//
|
|
161
|
-
|
|
162
|
-
foldSessionDuration(aggregateDeltas, args.siteId, event, durationDeltaMs, timezone);
|
|
164
|
+
// A heartbeat is presence, not telemetry: it moves the session's
|
|
165
|
+
// lastSeenAt and live row only. The time it proves reaches the
|
|
166
|
+
// duration counters once, when the visitor departs (live.ts).
|
|
167
|
+
await accountHeartbeat(ctx, args.siteId, existingSession, event, context, now, batchState);
|
|
163
168
|
accepted += 1;
|
|
164
169
|
continue;
|
|
165
170
|
}
|
|
@@ -190,7 +195,7 @@ export const ingestBatch = mutation({
|
|
|
190
195
|
durationMs,
|
|
191
196
|
};
|
|
192
197
|
sessionRow.dirty = true;
|
|
193
|
-
foldSessionDuration(aggregateDeltas, args.siteId, event, durationMs - existingSession.durationMs, timezone);
|
|
198
|
+
foldSessionDuration(aggregateDeltas, args.siteId, event.sessionId, event.timestamp, durationMs - existingSession.durationMs, timezone);
|
|
194
199
|
}
|
|
195
200
|
const expiresAt = Math.min(liveSession.expiresAt, now + LIVE_LEAVE_GRACE_MS);
|
|
196
201
|
if (expiresAt < liveSession.expiresAt) {
|
|
@@ -252,6 +257,7 @@ export const ingestBatch = mutation({
|
|
|
252
257
|
reportRollupDeltas,
|
|
253
258
|
recordTelemetry: true,
|
|
254
259
|
timezone,
|
|
260
|
+
siteDomains: site.domains,
|
|
255
261
|
financialConversion: false,
|
|
256
262
|
trustedGoalKey: undefined,
|
|
257
263
|
now,
|
|
@@ -353,6 +359,7 @@ export const trackConversion = mutation({
|
|
|
353
359
|
reportRollupDeltas,
|
|
354
360
|
recordTelemetry: existingTelemetry === null && !telemetry.saturated,
|
|
355
361
|
timezone: localDayTimezone(site.timezone),
|
|
362
|
+
siteDomains: site.domains,
|
|
356
363
|
financialConversion: true,
|
|
357
364
|
trustedGoalKey,
|
|
358
365
|
now: Date.now(),
|
|
@@ -433,7 +440,7 @@ function withoutSystemFields(document) {
|
|
|
433
440
|
async function accountEvent(ctx, input) {
|
|
434
441
|
const { event } = input;
|
|
435
442
|
const session = input.recordTelemetry
|
|
436
|
-
? await upsertSession(ctx, input.siteId, event, input.context, input.existingSession, input.financialConversion, input.reportRollupDeltas, input.batchState)
|
|
443
|
+
? await upsertSession(ctx, input.siteId, event, input.context, input.siteDomains, input.existingSession, input.financialConversion, input.reportRollupDeltas, input.batchState)
|
|
437
444
|
: await accountTrustedSession(ctx, input.existingSession, event);
|
|
438
445
|
if (input.recordTelemetry && event.type === "pageview") {
|
|
439
446
|
// `existingSession` is the snapshot from before `upsertSession`, so it
|
|
@@ -510,6 +517,23 @@ async function accountEvent(ctx, input) {
|
|
|
510
517
|
const outboundHost = event.type === "outbound" ? publicUrlHost(event.href) : undefined;
|
|
511
518
|
const dimensions = [
|
|
512
519
|
{ type: "source", value: session.source },
|
|
520
|
+
// A session recorded before origins carries none, and fills no slot.
|
|
521
|
+
...(session.platform && session.channel && session.evidence
|
|
522
|
+
? [
|
|
523
|
+
{
|
|
524
|
+
type: "platform",
|
|
525
|
+
// The value carries the channel too when it is not the
|
|
526
|
+
// platform's default, so the pair survives without its own
|
|
527
|
+
// dimension (splitPlatformDimension reads it back).
|
|
528
|
+
value: platformDimensionValue({
|
|
529
|
+
platform: session.platform,
|
|
530
|
+
channel: session.channel,
|
|
531
|
+
}),
|
|
532
|
+
},
|
|
533
|
+
{ type: "channel", value: session.channel },
|
|
534
|
+
{ type: "evidence", value: session.evidence },
|
|
535
|
+
]
|
|
536
|
+
: []),
|
|
513
537
|
...campaignDimensions,
|
|
514
538
|
...(outboundHost
|
|
515
539
|
? [{ type: "outbound", value: outboundHost }]
|
|
@@ -550,6 +574,9 @@ async function accountTrustedSession(ctx, existing, event) {
|
|
|
550
574
|
continued: false,
|
|
551
575
|
startedAt: existing.startedAt,
|
|
552
576
|
source: existing.source,
|
|
577
|
+
platform: existing.platform,
|
|
578
|
+
channel: existing.channel,
|
|
579
|
+
evidence: existing.evidence,
|
|
553
580
|
utmCampaign: existing.utmCampaign,
|
|
554
581
|
utmMedium: existing.utmMedium,
|
|
555
582
|
entryPath: existing.entryPath,
|
|
@@ -568,7 +595,7 @@ async function accountTrustedSession(ctx, existing, event) {
|
|
|
568
595
|
affiliateExpiresAt: existing.affiliateExpiresAt,
|
|
569
596
|
};
|
|
570
597
|
}
|
|
571
|
-
async function upsertSession(ctx, siteId, event, context, existing, financialConversion, reportRollupDeltas, batchState) {
|
|
598
|
+
async function upsertSession(ctx, siteId, event, context, siteDomains, existing, financialConversion, reportRollupDeltas, batchState) {
|
|
572
599
|
// A host-supplied id is the identity; anonymous sessions count through the
|
|
573
600
|
// daily key derived by the HTTP action, or once per session without one.
|
|
574
601
|
const visitorKey = existing?.visitorKey ??
|
|
@@ -641,6 +668,9 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
|
|
|
641
668
|
continued: event.type === "pageview" && existing.pageviewCount === 1,
|
|
642
669
|
startedAt,
|
|
643
670
|
source: existing.source,
|
|
671
|
+
platform: existing.platform,
|
|
672
|
+
channel: existing.channel,
|
|
673
|
+
evidence: existing.evidence,
|
|
644
674
|
utmCampaign: existing.utmCampaign,
|
|
645
675
|
utmMedium: existing.utmMedium,
|
|
646
676
|
entryPath: existing.entryPath,
|
|
@@ -684,6 +714,17 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
|
|
|
684
714
|
if (alias)
|
|
685
715
|
isNewVisitor = false;
|
|
686
716
|
}
|
|
717
|
+
// First touch only, like `source`: the session keeps where it came from.
|
|
718
|
+
// An affiliate channel needs this landing's own `ref` to have resolved; a
|
|
719
|
+
// visitor's standing attribution alone is not how this visit arrived.
|
|
720
|
+
const origin = classifyOrigin({
|
|
721
|
+
utmSource,
|
|
722
|
+
utmMedium,
|
|
723
|
+
clickIds: event.clid,
|
|
724
|
+
referrer: externalReferrer(event.referrer, siteDomains),
|
|
725
|
+
inApp: context.inApp,
|
|
726
|
+
affiliate: event.affiliateSlug !== undefined && attribution !== undefined,
|
|
727
|
+
});
|
|
687
728
|
const fields = {
|
|
688
729
|
siteId,
|
|
689
730
|
sessionId: event.sessionId,
|
|
@@ -696,6 +737,7 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
|
|
|
696
737
|
newVisitor: isNewVisitor,
|
|
697
738
|
...(event.referrer ? { referrer: event.referrer } : {}),
|
|
698
739
|
source,
|
|
740
|
+
...origin,
|
|
699
741
|
...(utmSource ? { utmSource } : {}),
|
|
700
742
|
...(utmMedium ? { utmMedium } : {}),
|
|
701
743
|
...(utmCampaign ? { utmCampaign } : {}),
|
|
@@ -745,6 +787,7 @@ async function upsertSession(ctx, siteId, event, context, existing, financialCon
|
|
|
745
787
|
continued: false,
|
|
746
788
|
startedAt: event.timestamp,
|
|
747
789
|
source,
|
|
790
|
+
...origin,
|
|
748
791
|
utmCampaign,
|
|
749
792
|
utmMedium,
|
|
750
793
|
entryPath: event.path,
|
|
@@ -868,6 +911,12 @@ async function attributionForEvent(ctx, siteId, event, existingSession, batchSta
|
|
|
868
911
|
affiliateExpiresAt: expiresAt,
|
|
869
912
|
};
|
|
870
913
|
}
|
|
914
|
+
/**
|
|
915
|
+
* Moves the session's lastSeenAt and its live row. `durationMs` stays: it is
|
|
916
|
+
* what the duration counters already hold, and the time a heartbeat proves
|
|
917
|
+
* is counted when the visitor departs (live.ts), so an idle tab never
|
|
918
|
+
* rewrites an aggregate bucket.
|
|
919
|
+
*/
|
|
871
920
|
async function accountHeartbeat(ctx, siteId, session, event, context, now, batchState) {
|
|
872
921
|
const startedAt = Math.min(session.startedAt, event.timestamp);
|
|
873
922
|
const lastSeenAt = Math.max(session.lastSeenAt, event.timestamp);
|
|
@@ -875,7 +924,6 @@ async function accountHeartbeat(ctx, siteId, session, event, context, now, batch
|
|
|
875
924
|
startedAt,
|
|
876
925
|
lastSeenAt,
|
|
877
926
|
exitPath: event.timestamp >= session.lastSeenAt ? event.path : session.exitPath,
|
|
878
|
-
durationMs: Math.max(0, lastSeenAt - startedAt),
|
|
879
927
|
...(context.country ? { country: context.country } : {}),
|
|
880
928
|
...(context.city ? { city: context.city } : {}),
|
|
881
929
|
...(context.latitude !== undefined ? { latitude: context.latitude } : {}),
|
|
@@ -896,7 +944,6 @@ async function accountHeartbeat(ctx, siteId, session, event, context, now, batch
|
|
|
896
944
|
else {
|
|
897
945
|
await ctx.db.patch("sessions", session._id, updates);
|
|
898
946
|
}
|
|
899
|
-
const durationDeltaMs = Math.max(0, updates.durationMs - session.durationMs);
|
|
900
947
|
await updateLiveSession(ctx, siteId, event, context, {
|
|
901
948
|
isNewSession: false,
|
|
902
949
|
isNewVisitorHour: false,
|
|
@@ -918,7 +965,42 @@ async function accountHeartbeat(ctx, siteId, session, event, context, now, batch
|
|
|
918
965
|
conversionCount: session.conversionCount,
|
|
919
966
|
newVisitor: session.newVisitor,
|
|
920
967
|
}, now, batchState);
|
|
921
|
-
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* One client network's budget per site and minute, kept in the session
|
|
971
|
+
* windows' table under `~client:<key>`, a key no sanitized session ID can take.
|
|
972
|
+
* The key is the salted daily hash the HTTP action derived; the address never
|
|
973
|
+
* reaches this mutation. It is one row per client, so only that client's own
|
|
974
|
+
* concurrent requests contend on it.
|
|
975
|
+
*/
|
|
976
|
+
async function consumeClientRateLimit(ctx, siteId, clientKey, eventByteCounts, now) {
|
|
977
|
+
const windowStart = Math.floor(now / RATE_LIMIT_WINDOW_MS) * RATE_LIMIT_WINDOW_MS;
|
|
978
|
+
const sessionId = `~client:${clientKey}`;
|
|
979
|
+
const rows = await ctx.db
|
|
980
|
+
.query("ingestWindows")
|
|
981
|
+
.withIndex("by_siteId_and_sessionId_and_windowStart", (range) => range.eq("siteId", siteId).eq("sessionId", sessionId))
|
|
982
|
+
.order("desc")
|
|
983
|
+
.take(1);
|
|
984
|
+
const window = rows[0] ?? null;
|
|
985
|
+
const current = window?.windowStart === windowStart ? window : null;
|
|
986
|
+
const eventCount = (current?.eventCount ?? 0) + eventByteCounts.length;
|
|
987
|
+
const byteCount = (current?.byteCount ?? 0) +
|
|
988
|
+
eventByteCounts.reduce((total, bytes) => total + bytes, 0);
|
|
989
|
+
if (eventCount > MAX_EVENTS_PER_CLIENT_WINDOW ||
|
|
990
|
+
byteCount > MAX_BYTES_PER_CLIENT_WINDOW) {
|
|
991
|
+
fail("RATE_LIMITED", "client ingest budget exceeded", {
|
|
992
|
+
windowMs: RATE_LIMIT_WINDOW_MS,
|
|
993
|
+
eventLimitPerClient: MAX_EVENTS_PER_CLIENT_WINDOW,
|
|
994
|
+
byteLimitPerClient: MAX_BYTES_PER_CLIENT_WINDOW,
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
const fields = { windowStart, eventCount, byteCount, updatedAt: now };
|
|
998
|
+
if (window) {
|
|
999
|
+
await ctx.db.patch("ingestWindows", window._id, fields);
|
|
1000
|
+
}
|
|
1001
|
+
else {
|
|
1002
|
+
await ctx.db.insert("ingestWindows", { siteId, sessionId, ...fields });
|
|
1003
|
+
}
|
|
922
1004
|
}
|
|
923
1005
|
async function consumeSiteRateLimit(ctx, siteId, events, eventByteCounts, now) {
|
|
924
1006
|
const windowStart = Math.floor(now / RATE_LIMIT_WINDOW_MS) * RATE_LIMIT_WINDOW_MS;
|
|
@@ -1227,9 +1309,11 @@ async function advanceFunnel(ctx, siteId, funnel, event, reportRollupDeltas, bat
|
|
|
1227
1309
|
foldFunnelRollup(reportRollupDeltas, siteId, funnel._id, event, 0, false);
|
|
1228
1310
|
return { funnelId: funnel._id, step: 1, steps: funnel.steps.length };
|
|
1229
1311
|
}
|
|
1230
|
-
if (
|
|
1231
|
-
event.timestamp < progress.lastStepAt)
|
|
1312
|
+
if (event.timestamp < progress.lastStepAt)
|
|
1232
1313
|
return;
|
|
1314
|
+
// An expired window restarts on a first-step event even after a completion,
|
|
1315
|
+
// so a returning buyer can complete again; inside the window a completed
|
|
1316
|
+
// funnel stays done.
|
|
1233
1317
|
if (event.timestamp - progress.startedAt > funnel.conversionWindowMs) {
|
|
1234
1318
|
if (!matchesFirst)
|
|
1235
1319
|
return;
|
|
@@ -1258,6 +1342,8 @@ async function advanceFunnel(ctx, siteId, funnel, event, reportRollupDeltas, bat
|
|
|
1258
1342
|
foldFunnelRollup(reportRollupDeltas, siteId, funnel._id, event, 0, false);
|
|
1259
1343
|
return { funnelId: funnel._id, step: 1, steps: funnel.steps.length };
|
|
1260
1344
|
}
|
|
1345
|
+
if (progress.completedAt !== undefined)
|
|
1346
|
+
return;
|
|
1261
1347
|
const nextStep = funnel.steps[progress.currentStep];
|
|
1262
1348
|
if (!nextStep || !stepMatches(nextStep, event))
|
|
1263
1349
|
return;
|
|
@@ -1608,14 +1694,19 @@ function aggregateBucketStart(granularity, timestamp, timezone) {
|
|
|
1608
1694
|
const interval = granularity === "hour" ? HOUR_MS : DAY_MS;
|
|
1609
1695
|
return Math.floor(timestamp / interval) * interval;
|
|
1610
1696
|
}
|
|
1611
|
-
/**
|
|
1612
|
-
|
|
1697
|
+
/**
|
|
1698
|
+
* Folds session duration growth without telemetry: a departure's final
|
|
1699
|
+
* interval, or the heartbeat time of a visitor the live sweep retired. The
|
|
1700
|
+
* shard follows `shardKey`: the session for a departure, the site for a
|
|
1701
|
+
* sweep.
|
|
1702
|
+
*/
|
|
1703
|
+
export function foldSessionDuration(aggregateDeltas, siteId, shardKey, timestamp, durationDeltaMs, timezone) {
|
|
1613
1704
|
if (durationDeltaMs <= 0)
|
|
1614
1705
|
return;
|
|
1615
1706
|
for (const granularity of aggregateGranularities(timezone)) {
|
|
1616
|
-
const shard = stableHash(
|
|
1707
|
+
const shard = stableHash(shardKey) %
|
|
1617
1708
|
(granularity === "hour" ? HOURLY_AGGREGATE_SHARDS : AGGREGATE_SHARDS);
|
|
1618
|
-
const bucketStart = aggregateBucketStart(granularity,
|
|
1709
|
+
const bucketStart = aggregateBucketStart(granularity, timestamp, timezone);
|
|
1619
1710
|
const key = `${siteId}:${granularity}:${bucketStart}:${shard}`;
|
|
1620
1711
|
const delta = aggregateDeltas.get(key) ?? {
|
|
1621
1712
|
siteId,
|
|
@@ -1641,7 +1732,7 @@ function foldSessionDuration(aggregateDeltas, siteId, event, durationDeltaMs, ti
|
|
|
1641
1732
|
aggregateDeltas.set(key, delta);
|
|
1642
1733
|
}
|
|
1643
1734
|
}
|
|
1644
|
-
async function flushAggregates(ctx, aggregateDeltas) {
|
|
1735
|
+
export async function flushAggregates(ctx, aggregateDeltas) {
|
|
1645
1736
|
for (const delta of aggregateDeltas.values()) {
|
|
1646
1737
|
const bucket = await ctx.db
|
|
1647
1738
|
.query("aggregateBuckets")
|