@iann29/rastro 0.1.0-alpha.1 → 0.1.0-alpha.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/README.md +693 -69
  2. package/agent/integration.md +801 -0
  3. package/agent/manifest.json +205 -0
  4. package/agent/manifest.schema.json +444 -0
  5. package/dist/client/federation.d.ts +381 -0
  6. package/dist/client/federation.d.ts.map +1 -0
  7. package/dist/client/federation.js +274 -0
  8. package/dist/client/federation.js.map +1 -0
  9. package/dist/client/index.d.ts +2673 -15
  10. package/dist/client/index.d.ts.map +1 -1
  11. package/dist/client/index.js +565 -33
  12. package/dist/client/index.js.map +1 -1
  13. package/dist/component/_generated/api.d.ts +22 -0
  14. package/dist/component/_generated/api.d.ts.map +1 -1
  15. package/dist/component/_generated/api.js.map +1 -1
  16. package/dist/component/_generated/component.d.ts +315 -4
  17. package/dist/component/_generated/component.d.ts.map +1 -1
  18. package/dist/component/_generated/server.d.ts +4 -0
  19. package/dist/component/_generated/server.d.ts.map +1 -1
  20. package/dist/component/_generated/server.js.map +1 -1
  21. package/dist/component/affiliates.d.ts.map +1 -1
  22. package/dist/component/affiliates.js +6 -2
  23. package/dist/component/affiliates.js.map +1 -1
  24. package/dist/component/cardinality.d.ts +12 -0
  25. package/dist/component/cardinality.d.ts.map +1 -0
  26. package/dist/component/cardinality.js +94 -0
  27. package/dist/component/cardinality.js.map +1 -0
  28. package/dist/component/constants.d.ts +28 -1
  29. package/dist/component/constants.d.ts.map +1 -1
  30. package/dist/component/constants.js +44 -1
  31. package/dist/component/constants.js.map +1 -1
  32. package/dist/component/convex.config.d.ts +6 -1
  33. package/dist/component/convex.config.js +9 -1
  34. package/dist/component/convex.config.js.map +1 -1
  35. package/dist/component/coverage.d.ts +27 -0
  36. package/dist/component/coverage.d.ts.map +1 -0
  37. package/dist/component/coverage.js +56 -0
  38. package/dist/component/coverage.js.map +1 -0
  39. package/dist/component/diagnostics.d.ts +9 -0
  40. package/dist/component/diagnostics.d.ts.map +1 -0
  41. package/dist/component/diagnostics.js +47 -0
  42. package/dist/component/diagnostics.js.map +1 -0
  43. package/dist/component/errors.d.ts +1 -1
  44. package/dist/component/errors.d.ts.map +1 -1
  45. package/dist/component/errors.js.map +1 -1
  46. package/dist/component/eventStore.d.ts +21 -9
  47. package/dist/component/eventStore.d.ts.map +1 -1
  48. package/dist/component/eventStore.js +142 -152
  49. package/dist/component/eventStore.js.map +1 -1
  50. package/dist/component/funnels.d.ts.map +1 -1
  51. package/dist/component/funnels.js +5 -3
  52. package/dist/component/funnels.js.map +1 -1
  53. package/dist/component/geo.d.ts +73 -0
  54. package/dist/component/geo.d.ts.map +1 -0
  55. package/dist/component/geo.js +648 -0
  56. package/dist/component/geo.js.map +1 -0
  57. package/dist/component/goals.d.ts.map +1 -1
  58. package/dist/component/goals.js +8 -5
  59. package/dist/component/goals.js.map +1 -1
  60. package/dist/component/guards.d.ts.map +1 -1
  61. package/dist/component/guards.js.map +1 -1
  62. package/dist/component/http.d.ts.map +1 -1
  63. package/dist/component/http.js +281 -62
  64. package/dist/component/http.js.map +1 -1
  65. package/dist/component/identity.d.ts +13 -0
  66. package/dist/component/identity.d.ts.map +1 -0
  67. package/dist/component/identity.js +58 -0
  68. package/dist/component/identity.js.map +1 -0
  69. package/dist/component/ingest.d.ts +3 -1
  70. package/dist/component/ingest.d.ts.map +1 -1
  71. package/dist/component/ingest.js +498 -95
  72. package/dist/component/ingest.js.map +1 -1
  73. package/dist/component/live.d.ts.map +1 -1
  74. package/dist/component/live.js +7 -6
  75. package/dist/component/live.js.map +1 -1
  76. package/dist/component/localTime.d.ts +25 -0
  77. package/dist/component/localTime.d.ts.map +1 -0
  78. package/dist/component/localTime.js +126 -0
  79. package/dist/component/localTime.js.map +1 -0
  80. package/dist/component/reports.d.ts +266 -10
  81. package/dist/component/reports.d.ts.map +1 -1
  82. package/dist/component/reports.js +1243 -137
  83. package/dist/component/reports.js.map +1 -1
  84. package/dist/component/retention.d.ts +75 -1
  85. package/dist/component/retention.d.ts.map +1 -1
  86. package/dist/component/retention.js +561 -38
  87. package/dist/component/retention.js.map +1 -1
  88. package/dist/component/rollupStore.d.ts +320 -0
  89. package/dist/component/rollupStore.d.ts.map +1 -0
  90. package/dist/component/rollupStore.js +596 -0
  91. package/dist/component/rollupStore.js.map +1 -0
  92. package/dist/component/rollups.d.ts +20 -0
  93. package/dist/component/rollups.d.ts.map +1 -0
  94. package/dist/component/rollups.js +73 -0
  95. package/dist/component/rollups.js.map +1 -0
  96. package/dist/component/sanitize.d.ts +24 -1
  97. package/dist/component/sanitize.d.ts.map +1 -1
  98. package/dist/component/sanitize.js +96 -16
  99. package/dist/component/sanitize.js.map +1 -1
  100. package/dist/component/schema.d.ts +687 -65
  101. package/dist/component/schema.js +187 -20
  102. package/dist/component/schema.js.map +1 -1
  103. package/dist/component/sites.d.ts +12 -0
  104. package/dist/component/sites.d.ts.map +1 -1
  105. package/dist/component/sites.js +41 -7
  106. package/dist/component/sites.js.map +1 -1
  107. package/dist/component/useragent.d.ts +9 -0
  108. package/dist/component/useragent.d.ts.map +1 -0
  109. package/dist/component/useragent.js +152 -0
  110. package/dist/component/useragent.js.map +1 -0
  111. package/dist/component/validators.d.ts +124 -69
  112. package/dist/component/validators.d.ts.map +1 -1
  113. package/dist/component/validators.js +35 -14
  114. package/dist/component/validators.js.map +1 -1
  115. package/dist/component/visitors.d.ts +19 -0
  116. package/dist/component/visitors.d.ts.map +1 -0
  117. package/dist/component/visitors.js +86 -0
  118. package/dist/component/visitors.js.map +1 -0
  119. package/dist/component/vitals.d.ts +41 -0
  120. package/dist/component/vitals.d.ts.map +1 -0
  121. package/dist/component/vitals.js +115 -0
  122. package/dist/component/vitals.js.map +1 -0
  123. package/dist/react/index.d.ts.map +1 -1
  124. package/dist/react/index.js.map +1 -1
  125. package/dist/tracker/generated.d.ts +11 -4
  126. package/dist/tracker/generated.d.ts.map +1 -1
  127. package/dist/tracker/generated.js +11 -4
  128. package/dist/tracker/generated.js.map +1 -1
  129. package/dist/tracker/tracker.d.ts +1 -1
  130. package/dist/tracker/tracker.d.ts.map +1 -1
  131. package/dist/tracker/tracker.js +57 -20
  132. package/dist/tracker/tracker.js.map +1 -1
  133. package/dist/tracker/vitals.d.ts +10 -0
  134. package/dist/tracker/vitals.d.ts.map +1 -0
  135. package/dist/tracker/vitals.js +140 -0
  136. package/dist/tracker/vitals.js.map +1 -0
  137. package/dist/tracker.min.js +1 -1
  138. package/dist/vitals.min.js +1 -0
  139. package/docs/benchmarks/2026-08-20-realistic.md +76 -76
  140. package/docs/benchmarks/2026-08-21-formal-certification.md +353 -0
  141. package/docs/benchmarks/2026-08-30-alpha6-recertification.md +206 -0
  142. package/docs/federation-setup.md +464 -0
  143. package/docs/federation.md +352 -0
  144. package/docs/upgrading.md +344 -0
  145. package/llms.txt +72 -0
  146. package/package.json +55 -11
  147. package/scripts/benchmark-ingest.mjs +175 -73
  148. package/scripts/generate-federation-keys.mjs +20 -0
  149. package/src/component/_generated/api.ts +22 -0
  150. package/src/component/_generated/component.ts +381 -4
  151. package/src/component/_generated/server.ts +4 -0
  152. package/src/component/affiliates.ts +20 -5
  153. package/src/component/cardinality.ts +117 -0
  154. package/src/component/constants.ts +44 -1
  155. package/src/component/convex.config.ts +11 -1
  156. package/src/component/coverage.ts +71 -0
  157. package/src/component/diagnostics.ts +65 -0
  158. package/src/component/errors.ts +2 -1
  159. package/src/component/eventStore.ts +217 -193
  160. package/src/component/funnels.ts +19 -16
  161. package/src/component/geo.ts +835 -0
  162. package/src/component/goals.ts +29 -21
  163. package/src/component/guards.ts +3 -1
  164. package/src/component/http.ts +404 -70
  165. package/src/component/identity.ts +74 -0
  166. package/src/component/ingest.ts +894 -188
  167. package/src/component/live.ts +13 -7
  168. package/src/component/localTime.ts +167 -0
  169. package/src/component/reports.ts +1872 -197
  170. package/src/component/retention.ts +788 -96
  171. package/src/component/rollupStore.ts +799 -0
  172. package/src/component/rollups.ts +82 -0
  173. package/src/component/sanitize.ts +144 -29
  174. package/src/component/schema.ts +217 -21
  175. package/src/component/sites.ts +59 -12
  176. package/src/component/useragent.ts +171 -0
  177. package/src/component/validators.ts +49 -14
  178. package/src/component/visitors.ts +116 -0
  179. package/src/component/vitals.ts +146 -0
@@ -1,7 +1,12 @@
1
1
  import { mutationGeneric, paginationOptsValidator, paginationResultValidator, queryGeneric, } from "convex/server";
2
- import { v } from "convex/values";
3
- import { eventPropertiesValidator, eventTypeValidator, funnelStepValidator, goalMatchTypeValidator, ingestContextValidator, ingestResultValidator, trackerEventValidator, } from "../component/validators.js";
4
- export { eventPropertiesValidator, eventTypeValidator, funnelStepValidator, goalMatchTypeValidator, ingestContextValidator, ingestResultValidator, trackerEventValidator, };
2
+ import { ConvexError, v } from "convex/values";
3
+ import { eventPropertiesValidator, eventTypeValidator, funnelStepValidator, goalMatchTypeValidator, ingestContextValidator, ingestResultValidator, trackerEventValidator, vitalMetricValidator, } from "../component/validators.js";
4
+ import { createFederatedAnalyticsConnectionAuthorizer, createFederatedAnalyticsManifest, FEDERATED_ANALYTICS_CONFIGURE_PERMISSION, FEDERATED_ANALYTICS_CONNECTOR_MANIFEST, FEDERATED_ANALYTICS_READ_PERMISSION, federatedAnalyticsConnectionStatusValidator, federatedAnalyticsManifestValidator, federatedSiteSummaryValidator, } from "./federation.js";
5
+ export { eventPropertiesValidator, eventTypeValidator, funnelStepValidator, goalMatchTypeValidator, ingestContextValidator, ingestResultValidator, trackerEventValidator, vitalMetricValidator, };
6
+ export { VITAL_METRICS, VITAL_THRESHOLDS } from "../component/vitals.js";
7
+ export { normalizeRoute } from "../component/sanitize.js";
8
+ export { localDayStart, nextLocalDayStart, previousLocalDayStart, } from "../component/localTime.js";
9
+ export { effectiveFederatedPermissions, FEDERATED_ANALYTICS_CLAIMS, FEDERATED_ANALYTICS_CONFIGURE_FUNCTIONS, FEDERATED_ANALYTICS_CONFIGURE_PERMISSION, FEDERATED_ANALYTICS_CONNECTOR_MANIFEST, FEDERATED_ANALYTICS_ERROR_CODES, FEDERATED_ANALYTICS_PERMISSIONS, FEDERATED_ANALYTICS_READ_PERMISSION, FEDERATED_ANALYTICS_REQUIRED_CAPABILITIES, FEDERATED_ANALYTICS_REQUIRED_FUNCTIONS, federatedAnalyticsPermissionValidator, } from "./federation.js";
5
10
  export class Rastro {
6
11
  component;
7
12
  constructor(component) {
@@ -16,6 +21,10 @@ export class Rastro {
16
21
  getSite(ctx, siteId) {
17
22
  return ctx.runQuery(this.component.sites.get, { siteId });
18
23
  }
24
+ /** The calendar days a site's daily overview answers in; see `sites.localDays`. */
25
+ localDays(ctx, siteId) {
26
+ return ctx.runQuery(this.component.sites.localDays, { siteId });
27
+ }
19
28
  listSites(ctx, args) {
20
29
  return ctx.runQuery(this.component.sites.listByOwner, args);
21
30
  }
@@ -25,6 +34,9 @@ export class Rastro {
25
34
  trackConversion(ctx, args) {
26
35
  return ctx.runMutation(this.component.ingest.trackConversion, args);
27
36
  }
37
+ linkVisitor(ctx, args) {
38
+ return ctx.runMutation(this.component.visitors.link, args);
39
+ }
28
40
  overview(ctx, args) {
29
41
  return ctx.runQuery(this.component.reports.overview, args);
30
42
  }
@@ -52,6 +64,12 @@ export class Rastro {
52
64
  affiliatesReport(ctx, args) {
53
65
  return ctx.runQuery(this.component.reports.affiliatesReport, args);
54
66
  }
67
+ vitalsReport(ctx, args) {
68
+ return ctx.runQuery(this.component.reports.vitalsReport, args);
69
+ }
70
+ siteMap(ctx, args) {
71
+ return ctx.runQuery(this.component.reports.siteMap, args);
72
+ }
55
73
  upsertGoal(ctx, args) {
56
74
  return ctx.runMutation(this.component.goals.upsert, args);
57
75
  }
@@ -77,11 +95,29 @@ export class Rastro {
77
95
  return ctx.runQuery(this.component.affiliates.list, { siteId });
78
96
  }
79
97
  removeAffiliate(ctx, siteId, affiliateId) {
80
- return ctx.runMutation(this.component.affiliates.remove, { siteId, affiliateId });
98
+ return ctx.runMutation(this.component.affiliates.remove, {
99
+ siteId,
100
+ affiliateId,
101
+ });
81
102
  }
82
103
  cleanup(ctx, args) {
83
104
  return ctx.runMutation(this.component.retention.cleanup, args);
84
105
  }
106
+ dataCoverage(ctx, args) {
107
+ return ctx.runQuery(this.component.reports.dataCoverage, args);
108
+ }
109
+ setRetentionPolicy(ctx, args) {
110
+ return ctx.runMutation(this.component.retention.setPolicy, args);
111
+ }
112
+ disableRetentionPolicy(ctx, siteId) {
113
+ return ctx.runMutation(this.component.retention.disablePolicy, { siteId });
114
+ }
115
+ retentionStatus(ctx, siteId) {
116
+ return ctx.runQuery(this.component.retention.status, { siteId });
117
+ }
118
+ runRetentionPolicy(ctx, args) {
119
+ return ctx.runMutation(this.component.retention.runPolicy, args);
120
+ }
85
121
  }
86
122
  const hostSiteValidator = v.object({
87
123
  _id: v.string(),
@@ -111,6 +147,7 @@ const hostSessionValidator = v.object({
111
147
  utmSource: v.optional(v.string()),
112
148
  utmMedium: v.optional(v.string()),
113
149
  utmCampaign: v.optional(v.string()),
150
+ visitorKey: v.optional(v.string()),
114
151
  country: v.optional(v.string()),
115
152
  city: v.optional(v.string()),
116
153
  latitude: v.optional(v.number()),
@@ -176,6 +213,8 @@ const hostLiveVisitorValidator = v.object({
176
213
  device: v.string(),
177
214
  pageviewCount: v.number(),
178
215
  eventCount: v.number(),
216
+ previousPath: v.optional(v.string()),
217
+ movedAt: v.optional(v.number()),
179
218
  });
180
219
  const hostConversionValidator = v.object({
181
220
  _id: v.string(),
@@ -206,12 +245,67 @@ const hostMetricTotalsValidator = v.object({
206
245
  visitors: v.number(),
207
246
  conversions: v.number(),
208
247
  revenueCents: v.number(),
248
+ durationMs: v.number(),
249
+ continuedSessions: v.number(),
250
+ newVisitors: v.number(),
209
251
  });
210
252
  const hostTopItemValidator = v.object({
211
253
  value: v.string(),
212
254
  count: v.number(),
213
255
  revenueCents: v.number(),
214
256
  });
257
+ const hostCoverageValidator = v.object({
258
+ state: v.union(v.literal("complete"), v.literal("partial"), v.literal("retained"), v.literal("unavailable")),
259
+ generation: v.union(v.number(), v.null()),
260
+ availableFrom: v.union(v.number(), v.null()),
261
+ availableThrough: v.union(v.number(), v.null()),
262
+ retainedBefore: v.union(v.number(), v.null()),
263
+ });
264
+ const hostRangeSemanticsValidator = v.object({
265
+ from: v.number(),
266
+ to: v.number(),
267
+ boundary: v.literal("inclusive"),
268
+ // "UTC", or the site timezone whose calendar days a daily overview used.
269
+ timezone: v.string(),
270
+ });
271
+ const hostReportMetadataValidator = v.object({
272
+ basis: v.union(v.literal("dailyRollup"), v.literal("boundedLegacy")),
273
+ coverage: hostCoverageValidator,
274
+ range: hostRangeSemanticsValidator,
275
+ });
276
+ const hostDataCoverageValidator = v.object({
277
+ siteId: v.string(),
278
+ from: v.number(),
279
+ to: v.number(),
280
+ datasets: v.array(v.object({
281
+ dataset: v.union(v.literal("overviewHour"), v.literal("overviewDay"), v.literal("overviewLocalDay"), v.literal("events"), v.literal("sessions"), v.literal("conversions"), v.literal("goals"), v.literal("funnels"), v.literal("affiliates"), v.literal("vitals"), v.literal("siteMap")),
282
+ coverage: hostCoverageValidator,
283
+ })),
284
+ });
285
+ const retentionKindValidator = v.union(v.literal("events"), v.literal("sessions"), v.literal("conversions"), v.literal("goalCompletions"), v.literal("aggregateHour"), v.literal("aggregateDay"), v.literal("funnelProgress"), v.literal("funnelSteps"), v.literal("affiliateStats"), v.literal("goalRollups"), v.literal("funnelRollups"), v.literal("affiliateRollups"), v.literal("vitalRollups"), v.literal("siteMapRollups"), v.literal("attributions"), v.literal("liveSessions"), v.literal("ingestWindows"), v.literal("siteIngestWindows"));
286
+ const hostRetentionPolicyValidator = v.object({
287
+ eventsDays: v.number(),
288
+ sessionsDays: v.number(),
289
+ conversionsDays: v.number(),
290
+ aggregatesDays: v.number(),
291
+ reportsDays: v.number(),
292
+ });
293
+ const hostRetentionStatusValidator = v.object({
294
+ policy: v.union(hostRetentionPolicyValidator.extend({ updatedAt: v.number() }), v.null()),
295
+ jobs: v.array(v.object({
296
+ kind: v.string(),
297
+ before: v.number(),
298
+ status: v.union(v.literal("pending"), v.literal("running"), v.literal("complete")),
299
+ deleted: v.number(),
300
+ scheduledAt: v.union(v.number(), v.null()),
301
+ updatedAt: v.number(),
302
+ })),
303
+ backlog: v.number(),
304
+ });
305
+ // See the component's breakdownBasisValidator: a breakdown's `count` is
306
+ // pageviews (pages) or sessions (every other dimension) once the range is
307
+ // fully covered by those counters, and raw event volume before that.
308
+ const hostBreakdownBasisValidator = v.union(v.literal("pageviews"), v.literal("sessions"), v.literal("eventVolume"));
215
309
  const hostOverviewValidator = v.object({
216
310
  from: v.number(),
217
311
  to: v.number(),
@@ -220,12 +314,41 @@ const hostOverviewValidator = v.object({
220
314
  totals: hostMetricTotalsValidator,
221
315
  timeSeries: v.array(hostMetricTotalsValidator.extend({ timestamp: v.number() })),
222
316
  topSources: v.array(hostTopItemValidator),
317
+ topCampaigns: v.array(hostTopItemValidator),
223
318
  topPages: v.array(hostTopItemValidator),
224
319
  topCountries: v.array(hostTopItemValidator),
225
320
  topDevices: v.array(hostTopItemValidator),
226
321
  topBrowsers: v.array(hostTopItemValidator),
227
322
  topEvents: v.array(hostTopItemValidator),
228
323
  topGoals: v.array(hostTopItemValidator),
324
+ metadata: v.object({
325
+ visitors: v.object({
326
+ basis: v.union(v.literal("estimatedUnique"), v.literal("bucketOccurrences")),
327
+ exact: v.boolean(),
328
+ legacyIncomplete: v.boolean(),
329
+ sketchVersion: v.union(v.number(), v.null()),
330
+ precision: v.union(v.number(), v.null()),
331
+ }),
332
+ breakdowns: v.object({
333
+ topSources: hostBreakdownBasisValidator,
334
+ topCampaigns: hostBreakdownBasisValidator,
335
+ topPages: hostBreakdownBasisValidator,
336
+ topCountries: hostBreakdownBasisValidator,
337
+ topDevices: hostBreakdownBasisValidator,
338
+ topBrowsers: hostBreakdownBasisValidator,
339
+ topEvents: v.literal("eventVolume"),
340
+ topGoals: v.literal("goalCompletionVolume"),
341
+ }),
342
+ engagement: v.union(v.literal("counted"), v.literal("unavailable")),
343
+ coverage: hostCoverageValidator,
344
+ range: hostRangeSemanticsValidator,
345
+ features: v.object({
346
+ sessionReplay: v.literal("unsupported"),
347
+ errorInsights: v.literal("unsupported"),
348
+ outboundLinks: v.literal("eventVolume"),
349
+ botDetection: v.literal("userAgent"),
350
+ }),
351
+ }),
229
352
  });
230
353
  const hostGoalValidator = v.object({
231
354
  _id: v.string(),
@@ -265,12 +388,22 @@ const hostAffiliateValidator = v.object({
265
388
  updatedAt: v.number(),
266
389
  });
267
390
  const hostGoalsReportValidator = v.object({
268
- goals: v.array(hostGoalValidator.extend({ completions: v.number(), valueCents: v.number() })),
391
+ goals: v.array(hostGoalValidator.extend({
392
+ completions: v.number(),
393
+ valueCents: v.number(),
394
+ })),
269
395
  truncated: v.boolean(),
396
+ metadata: hostReportMetadataValidator,
270
397
  });
271
398
  const hostFunnelsReportValidator = v.array(hostFunnelValidator.extend({
272
- stepsReport: v.array(v.object({ step: v.number(), name: v.string(), entrants: v.number(), completions: v.number() })),
399
+ stepsReport: v.array(v.object({
400
+ step: v.number(),
401
+ name: v.string(),
402
+ entrants: v.number(),
403
+ completions: v.number(),
404
+ })),
273
405
  truncated: v.boolean(),
406
+ metadata: hostReportMetadataValidator,
274
407
  }));
275
408
  const hostAffiliatesReportValidator = v.object({
276
409
  affiliates: v.array(hostAffiliateValidator.extend({
@@ -281,6 +414,65 @@ const hostAffiliatesReportValidator = v.object({
281
414
  commissionCents: v.number(),
282
415
  })),
283
416
  truncated: v.boolean(),
417
+ metadata: hostReportMetadataValidator.extend({
418
+ uniqueVisitors: v.object({
419
+ basis: v.union(v.literal("estimatedUnique"), v.literal("exactLegacy")),
420
+ exact: v.boolean(),
421
+ }),
422
+ }),
423
+ });
424
+ const hostVitalStatsValidator = v.object({
425
+ count: v.number(),
426
+ p75: v.number(),
427
+ average: v.number(),
428
+ good: v.number(),
429
+ needsImprovement: v.number(),
430
+ poor: v.number(),
431
+ });
432
+ const hostVitalsReportValidator = v.object({
433
+ metrics: v.array(hostVitalStatsValidator.extend({
434
+ metric: vitalMetricValidator,
435
+ devices: v.array(hostVitalStatsValidator.extend({ device: v.string() })),
436
+ pages: v.array(hostVitalStatsValidator.extend({ page: v.string() })),
437
+ timeSeries: v.array(v.object({
438
+ timestamp: v.number(),
439
+ count: v.number(),
440
+ p75: v.number(),
441
+ })),
442
+ })),
443
+ truncated: v.boolean(),
444
+ metadata: hostReportMetadataValidator,
445
+ });
446
+ const hostSiteMapValidator = v.object({
447
+ from: v.number(),
448
+ to: v.number(),
449
+ window: v.union(v.object({ from: v.number(), to: v.number() }), v.null()),
450
+ routes: v.array(v.object({
451
+ route: v.string(),
452
+ pageviews: v.number(),
453
+ visitors: v.number(),
454
+ entries: v.number(),
455
+ exits: v.number(),
456
+ bounces: v.number(),
457
+ transitionsIn: v.number(),
458
+ transitionsOut: v.number(),
459
+ dwellMs: v.number(),
460
+ dwellSamples: v.number(),
461
+ daily: v.array(v.object({ bucketStart: v.number(), pageviews: v.number() })),
462
+ })),
463
+ transitions: v.array(v.object({ from: v.string(), to: v.string(), count: v.number() })),
464
+ truncated: v.object({ routes: v.boolean(), transitions: v.boolean() }),
465
+ metadata: hostReportMetadataValidator.extend({
466
+ visitors: v.object({
467
+ basis: v.literal("estimatedUnique"),
468
+ exact: v.boolean(),
469
+ }),
470
+ limits: v.object({
471
+ routesPerDay: v.number(),
472
+ transitionSlots: v.number(),
473
+ rows: v.number(),
474
+ }),
475
+ }),
284
476
  });
285
477
  export function exposeAnalyticsApi(component, options) {
286
478
  const rastro = new Rastro(component);
@@ -297,7 +489,11 @@ export function exposeAnalyticsApi(component, options) {
297
489
  },
298
490
  returns: v.string(),
299
491
  handler: async (ctx, args) => {
300
- await options.authorize(ctx, { operation: "site.create", siteIds: [], ownerId: args.ownerId });
492
+ await options.authorize(ctx, {
493
+ operation: "site.create",
494
+ siteIds: [],
495
+ ownerId: args.ownerId,
496
+ });
301
497
  return await rastro.createSite(ctx, args);
302
498
  },
303
499
  }),
@@ -313,7 +509,10 @@ export function exposeAnalyticsApi(component, options) {
313
509
  },
314
510
  returns: v.null(),
315
511
  handler: async (ctx, args) => {
316
- await options.authorize(ctx, { operation: "site.update", siteIds: [args.siteId] });
512
+ await options.authorize(ctx, {
513
+ operation: "site.update",
514
+ siteIds: [args.siteId],
515
+ });
317
516
  return await rastro.updateSite(ctx, args);
318
517
  },
319
518
  }),
@@ -321,7 +520,10 @@ export function exposeAnalyticsApi(component, options) {
321
520
  args: { siteId: v.string() },
322
521
  returns: v.union(hostSiteValidator, v.null()),
323
522
  handler: async (ctx, args) => {
324
- await options.authorize(ctx, { operation: "site.get", siteIds: [args.siteId] });
523
+ await options.authorize(ctx, {
524
+ operation: "site.get",
525
+ siteIds: [args.siteId],
526
+ });
325
527
  return await rastro.getSite(ctx, args.siteId);
326
528
  },
327
529
  }),
@@ -329,7 +531,11 @@ export function exposeAnalyticsApi(component, options) {
329
531
  args: { ownerId: v.string(), limit: v.optional(v.number()) },
330
532
  returns: v.array(hostSiteValidator),
331
533
  handler: async (ctx, args) => {
332
- await options.authorize(ctx, { operation: "site.list", siteIds: [], ownerId: args.ownerId });
534
+ await options.authorize(ctx, {
535
+ operation: "site.list",
536
+ siteIds: [],
537
+ ownerId: args.ownerId,
538
+ });
333
539
  return await rastro.listSites(ctx, args);
334
540
  },
335
541
  }),
@@ -342,7 +548,10 @@ export function exposeAnalyticsApi(component, options) {
342
548
  },
343
549
  returns: ingestResultValidator,
344
550
  handler: async (ctx, args) => {
345
- await options.authorize(ctx, { operation: "ingest", siteIds: [args.siteId] });
551
+ await options.authorize(ctx, {
552
+ operation: "ingest",
553
+ siteIds: [args.siteId],
554
+ });
346
555
  return await rastro.ingest(ctx, args);
347
556
  },
348
557
  }),
@@ -361,10 +570,28 @@ export function exposeAnalyticsApi(component, options) {
361
570
  },
362
571
  returns: ingestResultValidator,
363
572
  handler: async (ctx, args) => {
364
- await options.authorize(ctx, { operation: "conversion.track", siteIds: [args.siteId] });
573
+ await options.authorize(ctx, {
574
+ operation: "conversion.track",
575
+ siteIds: [args.siteId],
576
+ });
365
577
  return await rastro.trackConversion(ctx, args);
366
578
  },
367
579
  }),
580
+ linkVisitor: mutationGeneric({
581
+ args: {
582
+ siteId: v.string(),
583
+ visitorId: v.string(),
584
+ previousVisitorId: v.string(),
585
+ },
586
+ returns: v.object({ linked: v.boolean(), aliasCount: v.number() }),
587
+ handler: async (ctx, args) => {
588
+ await options.authorize(ctx, {
589
+ operation: "visitor.link",
590
+ siteIds: [args.siteId],
591
+ });
592
+ return await rastro.linkVisitor(ctx, args);
593
+ },
594
+ }),
368
595
  overview: queryGeneric({
369
596
  args: {
370
597
  siteIds: v.array(v.string()),
@@ -374,7 +601,10 @@ export function exposeAnalyticsApi(component, options) {
374
601
  },
375
602
  returns: hostOverviewValidator,
376
603
  handler: async (ctx, args) => {
377
- await options.authorize(ctx, { operation: "report.overview", siteIds: args.siteIds });
604
+ await options.authorize(ctx, {
605
+ operation: "report.overview",
606
+ siteIds: args.siteIds,
607
+ });
378
608
  return await rastro.overview(ctx, args);
379
609
  },
380
610
  }),
@@ -386,7 +616,10 @@ export function exposeAnalyticsApi(component, options) {
386
616
  },
387
617
  returns: v.array(hostLiveVisitorValidator),
388
618
  handler: async (ctx, args) => {
389
- await options.authorize(ctx, { operation: "report.live", siteIds: args.siteIds });
619
+ await options.authorize(ctx, {
620
+ operation: "report.live",
621
+ siteIds: args.siteIds,
622
+ });
390
623
  return await rastro.liveVisitors(ctx, args);
391
624
  },
392
625
  }),
@@ -399,7 +632,10 @@ export function exposeAnalyticsApi(component, options) {
399
632
  },
400
633
  returns: paginationResultValidator(hostSessionValidator),
401
634
  handler: async (ctx, args) => {
402
- await options.authorize(ctx, { operation: "report.sessions", siteIds: [args.siteId] });
635
+ await options.authorize(ctx, {
636
+ operation: "report.sessions",
637
+ siteIds: [args.siteId],
638
+ });
403
639
  return await rastro.listSessions(ctx, args);
404
640
  },
405
641
  }),
@@ -413,7 +649,10 @@ export function exposeAnalyticsApi(component, options) {
413
649
  },
414
650
  returns: paginationResultValidator(hostEventValidator),
415
651
  handler: async (ctx, args) => {
416
- await options.authorize(ctx, { operation: "report.sessionJourney", siteIds: [args.siteId] });
652
+ await options.authorize(ctx, {
653
+ operation: "report.sessionJourney",
654
+ siteIds: [args.siteId],
655
+ });
417
656
  return await rastro.sessionJourney(ctx, args);
418
657
  },
419
658
  }),
@@ -426,7 +665,10 @@ export function exposeAnalyticsApi(component, options) {
426
665
  },
427
666
  returns: paginationResultValidator(hostConversionValidator),
428
667
  handler: async (ctx, args) => {
429
- await options.authorize(ctx, { operation: "report.conversions", siteIds: [args.siteId] });
668
+ await options.authorize(ctx, {
669
+ operation: "report.conversions",
670
+ siteIds: [args.siteId],
671
+ });
430
672
  return await rastro.listConversions(ctx, args);
431
673
  },
432
674
  }),
@@ -440,7 +682,10 @@ export function exposeAnalyticsApi(component, options) {
440
682
  },
441
683
  returns: v.array(hostEventValidator),
442
684
  handler: async (ctx, args) => {
443
- await options.authorize(ctx, { operation: "report.visitorJourney", siteIds: args.siteIds });
685
+ await options.authorize(ctx, {
686
+ operation: "report.visitorJourney",
687
+ siteIds: args.siteIds,
688
+ });
444
689
  return await rastro.visitorJourney(ctx, args);
445
690
  },
446
691
  }),
@@ -448,7 +693,10 @@ export function exposeAnalyticsApi(component, options) {
448
693
  args: { siteId: v.string(), from: v.number(), to: v.number() },
449
694
  returns: hostGoalsReportValidator,
450
695
  handler: async (ctx, args) => {
451
- await options.authorize(ctx, { operation: "report.goals", siteIds: [args.siteId] });
696
+ await options.authorize(ctx, {
697
+ operation: "report.goals",
698
+ siteIds: [args.siteId],
699
+ });
452
700
  return await rastro.goalsReport(ctx, args);
453
701
  },
454
702
  }),
@@ -456,7 +704,10 @@ export function exposeAnalyticsApi(component, options) {
456
704
  args: { siteId: v.string(), from: v.number(), to: v.number() },
457
705
  returns: hostFunnelsReportValidator,
458
706
  handler: async (ctx, args) => {
459
- await options.authorize(ctx, { operation: "report.funnels", siteIds: [args.siteId] });
707
+ await options.authorize(ctx, {
708
+ operation: "report.funnels",
709
+ siteIds: [args.siteId],
710
+ });
460
711
  return await rastro.funnelsReport(ctx, args);
461
712
  },
462
713
  }),
@@ -464,10 +715,102 @@ export function exposeAnalyticsApi(component, options) {
464
715
  args: { siteId: v.string(), from: v.number(), to: v.number() },
465
716
  returns: hostAffiliatesReportValidator,
466
717
  handler: async (ctx, args) => {
467
- await options.authorize(ctx, { operation: "report.affiliates", siteIds: [args.siteId] });
718
+ await options.authorize(ctx, {
719
+ operation: "report.affiliates",
720
+ siteIds: [args.siteId],
721
+ });
468
722
  return await rastro.affiliatesReport(ctx, args);
469
723
  },
470
724
  }),
725
+ vitalsReport: queryGeneric({
726
+ args: { siteId: v.string(), from: v.number(), to: v.number() },
727
+ returns: hostVitalsReportValidator,
728
+ handler: async (ctx, args) => {
729
+ await options.authorize(ctx, {
730
+ operation: "report.vitals",
731
+ siteIds: [args.siteId],
732
+ });
733
+ return await rastro.vitalsReport(ctx, args);
734
+ },
735
+ }),
736
+ siteMap: queryGeneric({
737
+ args: {
738
+ siteIds: v.array(v.string()),
739
+ from: v.number(),
740
+ to: v.number(),
741
+ },
742
+ returns: hostSiteMapValidator,
743
+ handler: async (ctx, args) => {
744
+ await options.authorize(ctx, {
745
+ operation: "report.siteMap",
746
+ siteIds: args.siteIds,
747
+ });
748
+ return await rastro.siteMap(ctx, args);
749
+ },
750
+ }),
751
+ dataCoverage: queryGeneric({
752
+ args: { siteId: v.string(), from: v.number(), to: v.number() },
753
+ returns: hostDataCoverageValidator,
754
+ handler: async (ctx, args) => {
755
+ await options.authorize(ctx, {
756
+ operation: "coverage.read",
757
+ siteIds: [args.siteId],
758
+ });
759
+ return await rastro.dataCoverage(ctx, args);
760
+ },
761
+ }),
762
+ retentionStatus: queryGeneric({
763
+ args: { siteId: v.string() },
764
+ returns: hostRetentionStatusValidator,
765
+ handler: async (ctx, args) => {
766
+ await options.authorize(ctx, {
767
+ operation: "retention.status",
768
+ siteIds: [args.siteId],
769
+ });
770
+ return await rastro.retentionStatus(ctx, args.siteId);
771
+ },
772
+ }),
773
+ setRetentionPolicy: mutationGeneric({
774
+ args: { siteId: v.string(), ...hostRetentionPolicyValidator.fields },
775
+ returns: v.null(),
776
+ handler: async (ctx, args) => {
777
+ await options.authorize(ctx, {
778
+ operation: "retention.policy",
779
+ siteIds: [args.siteId],
780
+ });
781
+ return await rastro.setRetentionPolicy(ctx, args);
782
+ },
783
+ }),
784
+ disableRetentionPolicy: mutationGeneric({
785
+ args: { siteId: v.string() },
786
+ returns: v.null(),
787
+ handler: async (ctx, args) => {
788
+ await options.authorize(ctx, {
789
+ operation: "retention.policy",
790
+ siteIds: [args.siteId],
791
+ });
792
+ return await rastro.disableRetentionPolicy(ctx, args.siteId);
793
+ },
794
+ }),
795
+ runRetentionPolicy: mutationGeneric({
796
+ args: {
797
+ siteId: v.string(),
798
+ kind: retentionKindValidator,
799
+ batchSize: v.optional(v.number()),
800
+ },
801
+ returns: v.object({
802
+ deleted: v.number(),
803
+ hasMore: v.boolean(),
804
+ before: v.number(),
805
+ }),
806
+ handler: async (ctx, args) => {
807
+ await options.authorize(ctx, {
808
+ operation: "retention.policy",
809
+ siteIds: [args.siteId],
810
+ });
811
+ return await rastro.runRetentionPolicy(ctx, args);
812
+ },
813
+ }),
471
814
  upsertGoal: mutationGeneric({
472
815
  args: {
473
816
  siteId: v.string(),
@@ -481,7 +824,10 @@ export function exposeAnalyticsApi(component, options) {
481
824
  },
482
825
  returns: v.string(),
483
826
  handler: async (ctx, args) => {
484
- await options.authorize(ctx, { operation: "goal.upsert", siteIds: [args.siteId] });
827
+ await options.authorize(ctx, {
828
+ operation: "goal.upsert",
829
+ siteIds: [args.siteId],
830
+ });
485
831
  return await rastro.upsertGoal(ctx, args);
486
832
  },
487
833
  }),
@@ -489,7 +835,10 @@ export function exposeAnalyticsApi(component, options) {
489
835
  args: { siteId: v.string() },
490
836
  returns: v.array(hostGoalValidator),
491
837
  handler: async (ctx, args) => {
492
- await options.authorize(ctx, { operation: "goal.list", siteIds: [args.siteId] });
838
+ await options.authorize(ctx, {
839
+ operation: "goal.list",
840
+ siteIds: [args.siteId],
841
+ });
493
842
  return await rastro.listGoals(ctx, args.siteId);
494
843
  },
495
844
  }),
@@ -497,7 +846,10 @@ export function exposeAnalyticsApi(component, options) {
497
846
  args: { siteId: v.string(), goalId: v.string() },
498
847
  returns: v.null(),
499
848
  handler: async (ctx, args) => {
500
- await options.authorize(ctx, { operation: "goal.remove", siteIds: [args.siteId] });
849
+ await options.authorize(ctx, {
850
+ operation: "goal.remove",
851
+ siteIds: [args.siteId],
852
+ });
501
853
  return await rastro.removeGoal(ctx, args.siteId, args.goalId);
502
854
  },
503
855
  }),
@@ -513,7 +865,10 @@ export function exposeAnalyticsApi(component, options) {
513
865
  },
514
866
  returns: v.string(),
515
867
  handler: async (ctx, args) => {
516
- await options.authorize(ctx, { operation: "funnel.upsert", siteIds: [args.siteId] });
868
+ await options.authorize(ctx, {
869
+ operation: "funnel.upsert",
870
+ siteIds: [args.siteId],
871
+ });
517
872
  return await rastro.upsertFunnel(ctx, args);
518
873
  },
519
874
  }),
@@ -521,7 +876,10 @@ export function exposeAnalyticsApi(component, options) {
521
876
  args: { siteId: v.string() },
522
877
  returns: v.array(hostFunnelValidator),
523
878
  handler: async (ctx, args) => {
524
- await options.authorize(ctx, { operation: "funnel.list", siteIds: [args.siteId] });
879
+ await options.authorize(ctx, {
880
+ operation: "funnel.list",
881
+ siteIds: [args.siteId],
882
+ });
525
883
  return await rastro.listFunnels(ctx, args.siteId);
526
884
  },
527
885
  }),
@@ -529,7 +887,10 @@ export function exposeAnalyticsApi(component, options) {
529
887
  args: { siteId: v.string(), funnelId: v.string() },
530
888
  returns: v.null(),
531
889
  handler: async (ctx, args) => {
532
- await options.authorize(ctx, { operation: "funnel.remove", siteIds: [args.siteId] });
890
+ await options.authorize(ctx, {
891
+ operation: "funnel.remove",
892
+ siteIds: [args.siteId],
893
+ });
533
894
  return await rastro.removeFunnel(ctx, args.siteId, args.funnelId);
534
895
  },
535
896
  }),
@@ -545,7 +906,10 @@ export function exposeAnalyticsApi(component, options) {
545
906
  },
546
907
  returns: v.string(),
547
908
  handler: async (ctx, args) => {
548
- await options.authorize(ctx, { operation: "affiliate.upsert", siteIds: [args.siteId] });
909
+ await options.authorize(ctx, {
910
+ operation: "affiliate.upsert",
911
+ siteIds: [args.siteId],
912
+ });
549
913
  return await rastro.upsertAffiliate(ctx, args);
550
914
  },
551
915
  }),
@@ -553,7 +917,10 @@ export function exposeAnalyticsApi(component, options) {
553
917
  args: { siteId: v.string() },
554
918
  returns: v.array(hostAffiliateValidator),
555
919
  handler: async (ctx, args) => {
556
- await options.authorize(ctx, { operation: "affiliate.list", siteIds: [args.siteId] });
920
+ await options.authorize(ctx, {
921
+ operation: "affiliate.list",
922
+ siteIds: [args.siteId],
923
+ });
557
924
  return await rastro.listAffiliates(ctx, args.siteId);
558
925
  },
559
926
  }),
@@ -561,30 +928,195 @@ export function exposeAnalyticsApi(component, options) {
561
928
  args: { siteId: v.string(), affiliateId: v.string() },
562
929
  returns: v.null(),
563
930
  handler: async (ctx, args) => {
564
- await options.authorize(ctx, { operation: "affiliate.remove", siteIds: [args.siteId] });
931
+ await options.authorize(ctx, {
932
+ operation: "affiliate.remove",
933
+ siteIds: [args.siteId],
934
+ });
565
935
  return await rastro.removeAffiliate(ctx, args.siteId, args.affiliateId);
566
936
  },
567
937
  }),
568
938
  cleanupAnalytics: mutationGeneric({
569
939
  args: {
570
940
  siteId: v.string(),
571
- kind: v.union(v.literal("events"), v.literal("sessions"), v.literal("conversions"), v.literal("goalCompletions"), v.literal("aggregateHour"), v.literal("aggregateDay"), v.literal("funnelProgress"), v.literal("funnelSteps"), v.literal("affiliateStats"), v.literal("attributions"), v.literal("liveSessions"), v.literal("ingestWindows"), v.literal("siteIngestWindows")),
941
+ kind: v.union(v.literal("events"), v.literal("sessions"), v.literal("conversions"), v.literal("goalCompletions"), v.literal("aggregateHour"), v.literal("aggregateDay"), v.literal("funnelProgress"), v.literal("funnelSteps"), v.literal("affiliateStats"), v.literal("goalRollups"), v.literal("funnelRollups"), v.literal("affiliateRollups"), v.literal("vitalRollups"), v.literal("attributions"), v.literal("liveSessions"), v.literal("ingestWindows"), v.literal("siteIngestWindows")),
572
942
  before: v.number(),
573
943
  batchSize: v.optional(v.number()),
574
944
  },
575
- returns: v.object({ deleted: v.number(), hasMore: v.boolean() }),
945
+ returns: v.object({
946
+ deleted: v.number(),
947
+ hasMore: v.boolean(),
948
+ before: v.number(),
949
+ }),
576
950
  handler: async (ctx, args) => {
577
- await options.authorize(ctx, { operation: "retention.cleanup", siteIds: [args.siteId] });
951
+ await options.authorize(ctx, {
952
+ operation: "retention.cleanup",
953
+ siteIds: [args.siteId],
954
+ });
578
955
  return await rastro.cleanup(ctx, args);
579
956
  },
580
957
  }),
581
958
  };
582
959
  }
960
+ /**
961
+ * The federated operations that change a site, and so run only with the
962
+ * configure permission on both the token and the host's grant. Everything else
963
+ * on the federated surface is a read.
964
+ */
965
+ const FEDERATED_CONFIGURE_OPERATIONS = new Set([
966
+ "site.update",
967
+ "goal.upsert",
968
+ "goal.list",
969
+ "goal.remove",
970
+ "funnel.upsert",
971
+ "funnel.list",
972
+ "funnel.remove",
973
+ "affiliate.upsert",
974
+ "affiliate.list",
975
+ "affiliate.remove",
976
+ "retention.policy",
977
+ "retention.status",
978
+ ]);
979
+ const federatedSiteSettingsValidator = v.object({
980
+ siteId: v.string(),
981
+ name: v.string(),
982
+ domains: v.array(v.string()),
983
+ timezone: v.union(v.string(), v.null()),
984
+ currency: v.string(),
985
+ cookieless: v.boolean(),
986
+ });
987
+ /**
988
+ * Exposes the reactive read surface a federated Rastro dashboard subscribes to,
989
+ * plus the configuration functions behind the `configure` capability, which
990
+ * run only when the token and the host's grant both carry the configure
991
+ * permission.
992
+ */
993
+ export function exposeFederatedAnalyticsApi(component, options) {
994
+ const authorizeConnection = createFederatedAnalyticsConnectionAuthorizer(options);
995
+ const requiredPermission = (operation) => FEDERATED_CONFIGURE_OPERATIONS.has(operation)
996
+ ? FEDERATED_ANALYTICS_CONFIGURE_PERMISSION
997
+ : FEDERATED_ANALYTICS_READ_PERMISSION;
998
+ const api = exposeAnalyticsApi(component, {
999
+ authorize: async (ctx, request) => {
1000
+ await authorizeConnection(ctx, request.siteIds, requiredPermission(request.operation));
1001
+ },
1002
+ });
1003
+ const rastro = new Rastro(component);
1004
+ return {
1005
+ manifest: queryGeneric({
1006
+ args: {},
1007
+ returns: federatedAnalyticsManifestValidator,
1008
+ handler: () => createFederatedAnalyticsManifest(),
1009
+ }),
1010
+ connectionStatus: queryGeneric({
1011
+ args: {},
1012
+ returns: federatedAnalyticsConnectionStatusValidator,
1013
+ handler: async (ctx) => {
1014
+ const connection = await authorizeConnection(ctx);
1015
+ return {
1016
+ status: "connected",
1017
+ protocolVersion: FEDERATED_ANALYTICS_CONNECTOR_MANIFEST.protocolVersion,
1018
+ capabilities: [
1019
+ ...FEDERATED_ANALYTICS_CONNECTOR_MANIFEST.capabilities,
1020
+ ],
1021
+ permissions: connection.effectivePermissions,
1022
+ siteCount: connection.siteIds.length,
1023
+ };
1024
+ },
1025
+ }),
1026
+ listSites: queryGeneric({
1027
+ args: {},
1028
+ returns: v.array(federatedSiteSummaryValidator),
1029
+ handler: async (ctx) => {
1030
+ const connection = await authorizeConnection(ctx);
1031
+ const summaries = [];
1032
+ for (const siteId of connection.siteIds) {
1033
+ const site = await rastro.getSite(ctx, siteId);
1034
+ if (!site)
1035
+ continue;
1036
+ summaries.push({
1037
+ siteId: site._id,
1038
+ name: site.name,
1039
+ currency: site.currency ?? "USD",
1040
+ timezone: site.timezone ?? null,
1041
+ cookieless: site.cookieless,
1042
+ localDays: await rastro.localDays(ctx, siteId),
1043
+ });
1044
+ }
1045
+ return summaries;
1046
+ },
1047
+ }),
1048
+ overview: api.overview,
1049
+ liveVisitors: api.liveVisitors,
1050
+ listSessions: api.listSessions,
1051
+ sessionJourney: api.sessionJourney,
1052
+ listConversions: api.listConversions,
1053
+ visitorJourney: api.visitorJourney,
1054
+ goalsReport: api.goalsReport,
1055
+ funnelsReport: api.funnelsReport,
1056
+ affiliatesReport: api.affiliatesReport,
1057
+ vitalsReport: api.vitalsReport,
1058
+ siteMap: api.siteMap,
1059
+ dataCoverage: api.dataCoverage,
1060
+ // The configure surface. `listSites` stays redacted for every reader; the
1061
+ // domains a site accepts are shown only to a connection allowed to change
1062
+ // them, and the site's currency is reported here because the ledger
1063
+ // forbids changing it after the first conversion is accounted.
1064
+ siteSettings: queryGeneric({
1065
+ args: { siteId: v.string() },
1066
+ returns: federatedSiteSettingsValidator,
1067
+ handler: async (ctx, args) => {
1068
+ await authorizeConnection(ctx, [args.siteId], FEDERATED_ANALYTICS_CONFIGURE_PERMISSION);
1069
+ const site = await rastro.getSite(ctx, args.siteId);
1070
+ if (!site) {
1071
+ throw new ConvexError({
1072
+ code: "NOT_FOUND",
1073
+ message: "site not found",
1074
+ details: null,
1075
+ });
1076
+ }
1077
+ return {
1078
+ siteId: site._id,
1079
+ name: site.name,
1080
+ domains: site.domains,
1081
+ timezone: site.timezone ?? null,
1082
+ currency: site.currency ?? "USD",
1083
+ cookieless: site.cookieless,
1084
+ };
1085
+ },
1086
+ }),
1087
+ updateSite: mutationGeneric({
1088
+ args: {
1089
+ siteId: v.string(),
1090
+ name: v.optional(v.string()),
1091
+ domains: v.optional(v.array(v.string())),
1092
+ timezone: v.optional(v.union(v.string(), v.null())),
1093
+ },
1094
+ returns: v.null(),
1095
+ handler: async (ctx, args) => {
1096
+ await authorizeConnection(ctx, [args.siteId], FEDERATED_ANALYTICS_CONFIGURE_PERMISSION);
1097
+ return await rastro.updateSite(ctx, args);
1098
+ },
1099
+ }),
1100
+ listGoals: api.listGoals,
1101
+ upsertGoal: api.upsertGoal,
1102
+ removeGoal: api.removeGoal,
1103
+ listFunnels: api.listFunnels,
1104
+ upsertFunnel: api.upsertFunnel,
1105
+ removeFunnel: api.removeFunnel,
1106
+ listAffiliates: api.listAffiliates,
1107
+ upsertAffiliate: api.upsertAffiliate,
1108
+ removeAffiliate: api.removeAffiliate,
1109
+ retentionStatus: api.retentionStatus,
1110
+ setRetentionPolicy: api.setRetentionPolicy,
1111
+ disableRetentionPolicy: api.disableRetentionPolicy,
1112
+ };
1113
+ }
583
1114
  export function componentHttpPaths(prefix = "/rastro/") {
584
1115
  const normalized = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
585
1116
  return {
586
1117
  events: `${normalized}/events`,
587
1118
  tracker: `${normalized}/tracker.js`,
1119
+ vitals: `${normalized}/vitals.js`,
588
1120
  health: `${normalized}/health`,
589
1121
  };
590
1122
  }