@liiift-studio/sanity-visitor-insights 0.2.2 → 0.3.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 +7 -0
- package/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +26 -7
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +26 -7
- package/dist/index.mjs.map +1 -1
- package/dist/{ranges-DSFapooC.d.mts → ranges-Bh9WCyYL.d.mts} +45 -9
- package/dist/{ranges-DSFapooC.d.ts → ranges-Bh9WCyYL.d.ts} +45 -9
- package/dist/server.d.mts +4 -4
- package/dist/server.d.ts +4 -4
- package/dist/server.js +52 -9
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +49 -9
- package/dist/server.mjs.map +1 -1
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/{vercel-BMEO-Jcw.d.mts → vercel-B6d1Ng48.d.mts} +5 -1
- package/dist/{vercel-BMEO-Jcw.d.ts → vercel-B6d1Ng48.d.ts} +5 -1
- package/package.json +2 -2
- package/src/core/core.test.ts +65 -1
- package/src/core/cutover.ts +113 -19
- package/src/server.ts +1 -1
- package/src/studio/Figure.tsx +1 -0
- package/src/types.ts +2 -0
|
@@ -11,6 +11,8 @@ type UnavailableReason =
|
|
|
11
11
|
| 'before_cutover'
|
|
12
12
|
/** GA4 suppressed the row for privacy thresholding. A real number exists; we may not see it. */
|
|
13
13
|
| 'suppressed'
|
|
14
|
+
/** The event exists in the code but stopped reaching GA4 for this period. Not a real zero. */
|
|
15
|
+
| 'outage'
|
|
14
16
|
/** The upstream source errored or timed out. Retryable, unlike the others. */
|
|
15
17
|
| 'source_error'
|
|
16
18
|
/** The metric is not defined for this site at all (e.g. a flow that does not exist there). */
|
|
@@ -220,39 +222,71 @@ interface TypefaceInterestData {
|
|
|
220
222
|
}
|
|
221
223
|
|
|
222
224
|
/**
|
|
223
|
-
* Event instrumentation
|
|
225
|
+
* Event instrumentation history — when an event started, and when it stopped.
|
|
224
226
|
*
|
|
225
227
|
* GA4 cannot backfill events: data from before an event was instrumented does not exist and never
|
|
226
228
|
* will. A range spanning a cutover therefore mixes two measurement regimes, and a chart drawn
|
|
227
229
|
* across it reads as a change in visitor behaviour when it is really a change in what was counted.
|
|
228
230
|
*
|
|
231
|
+
* Outages are the same problem arriving from the other direction, and are easy to miss because the
|
|
232
|
+
* event still exists in the code. Darden is the worked example: its ecommerce events fired normally
|
|
233
|
+
* until a script-loading change on 2025-11-20 stopped them reaching GA4 for nine months. Every one
|
|
234
|
+
* of those days returns a real, honest zero from the API — a zero that means "not recorded", not
|
|
235
|
+
* "no sales". Without an outage recorded here, a yearly funnel would render that as a collapse in
|
|
236
|
+
* trade rather than as a gap in measurement.
|
|
237
|
+
*
|
|
229
238
|
* This lives in the package rather than in each site repo so all three foundries share one model
|
|
230
239
|
* and one set of semantics, and so the Studio panel and the server handler agree by construction.
|
|
231
240
|
*/
|
|
232
241
|
|
|
233
|
-
/** An event that predates this work — long-running, with no discontinuity to mark. */
|
|
242
|
+
/** An event that predates this work — long-running, with no start discontinuity to mark. */
|
|
234
243
|
declare const PREEXISTING: "preexisting";
|
|
235
244
|
/**
|
|
236
|
-
*
|
|
245
|
+
* A period during which an event stopped reaching GA4 despite still existing in the code.
|
|
246
|
+
* `until: null` means it is still broken as of now.
|
|
247
|
+
*/
|
|
248
|
+
interface EventOutage {
|
|
249
|
+
/** First ISO date with no data. */
|
|
250
|
+
start: string;
|
|
251
|
+
/** ISO date it resumed, or null if unresolved. */
|
|
252
|
+
until: string | null;
|
|
253
|
+
/** Short human-readable cause, shown in the UI next to the affected figure. */
|
|
254
|
+
reason?: string;
|
|
255
|
+
}
|
|
256
|
+
/** An event with a start date and, optionally, periods where it stopped firing. */
|
|
257
|
+
interface EventHistory {
|
|
258
|
+
/** When it began firing: `PREEXISTING`, or an ISO `YYYY-MM-DD` deploy date. */
|
|
259
|
+
from: typeof PREEXISTING | string;
|
|
260
|
+
/** Periods where it stopped. Order does not matter. */
|
|
261
|
+
outages?: EventOutage[];
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* When an event fired on a site.
|
|
237
265
|
* `PREEXISTING` = live before this work began. `null` = planned, not yet deployed.
|
|
238
|
-
* An ISO `YYYY-MM-DD` string = the deploy date
|
|
266
|
+
* An ISO `YYYY-MM-DD` string = the deploy date. An `EventHistory` adds outages.
|
|
239
267
|
*/
|
|
240
|
-
type EventCutover = typeof PREEXISTING | string | null;
|
|
268
|
+
type EventCutover = typeof PREEXISTING | string | null | EventHistory;
|
|
241
269
|
/** How completely an event covers a requested range. */
|
|
242
270
|
type Coverage = {
|
|
243
271
|
status: 'full';
|
|
244
272
|
} | {
|
|
245
273
|
status: 'partial';
|
|
246
|
-
|
|
274
|
+
reason: 'spans_cutover' | 'spans_outage';
|
|
275
|
+
/** Present when the range starts before the event was instrumented. */
|
|
276
|
+
cutover?: string;
|
|
277
|
+
/** Outages overlapping the range. Empty for a pure cutover overlap. */
|
|
278
|
+
outages: EventOutage[];
|
|
247
279
|
} | {
|
|
248
280
|
status: 'none';
|
|
249
|
-
reason: 'not_instrumented' | 'before_cutover' | 'unknown_event';
|
|
281
|
+
reason: 'not_instrumented' | 'before_cutover' | 'unknown_event' | 'outage';
|
|
250
282
|
cutover?: string;
|
|
283
|
+
/** Present when the whole range fell inside an outage. */
|
|
284
|
+
outages?: EventOutage[];
|
|
251
285
|
};
|
|
252
286
|
/**
|
|
253
287
|
* Determine how well an event covers a date range.
|
|
254
288
|
*
|
|
255
|
-
* @param cutovers - the site's event
|
|
289
|
+
* @param cutovers - the site's event history map
|
|
256
290
|
* @param eventName - GA4 event name to look up
|
|
257
291
|
* @param range - the requested range
|
|
258
292
|
*/
|
|
@@ -267,6 +301,8 @@ declare function coverageForRange(cutovers: Readonly<Record<string, EventCutover
|
|
|
267
301
|
* @param coverage - result of coverageForRange for the same event and range
|
|
268
302
|
*/
|
|
269
303
|
declare function applyCoverage(count: number | null, coverage: Coverage): MetricValue;
|
|
304
|
+
/** One-line description of an outage, for display beside an affected figure. */
|
|
305
|
+
declare function describeOutage(outage: EventOutage | undefined): string;
|
|
270
306
|
/**
|
|
271
307
|
* Human-readable notices for every event in a range that is not fully covered.
|
|
272
308
|
* Surfaced in the report envelope so the panel can show caveats without recomputing them.
|
|
@@ -393,4 +429,4 @@ declare function provisionalDates(range: DateRange, now?: Date): string[];
|
|
|
393
429
|
*/
|
|
394
430
|
declare function provisionalNotice(range: DateRange, now?: Date): string | null;
|
|
395
431
|
|
|
396
|
-
export { type AcquisitionData as A,
|
|
432
|
+
export { type AcquisitionData as A, type TypefaceInterestRow as B, type Coverage as C, type DiagnosticReport as D, type EventCutover as E, applyCoverage as F, GA4_PROCESSING_LAG_DAYS as G, assertValidSiteConfig as H, coverageNotices as I, type JourneyData as J, daysBetween as K, describeOutage as L, type MetricValue as M, formatInTimeZone as N, type OrdersConfig as O, PREEXISTING as P, isValidTimeZone as Q, type ReportEnvelope as R, type SiteAnalyticsConfig as S, type TypefaceInterestData as T, type UnavailableReason as U, type VercelConfig as V, provisionalDates as W, provisionalNotice as X, shiftDays as Y, type ReportName as a, type RangeKey as b, type MeasurementHealthData as c, type DateRange as d, REPORT_NAMES as e, type ReportError as f, type SourceName as g, type SourceStatus as h, coverageForRange as i, isReportName as j, previousRange as k, valueOrNull as l, type CheckStatus as m, type ConfigProblem as n, ok as o, partial as p, type DiagnosticCheck as q, resolveRange as r, type EventHistory as s, type EventOutage as t, unavailable as u, validateSiteConfig as v, type ExitPage as w, type Ga4Config as x, type JourneyStep as y, type SourceRow as z };
|
package/dist/server.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { S as SiteAnalyticsConfig, D as DiagnosticReport } from './ranges-
|
|
2
|
-
export { A as AcquisitionData, m as CheckStatus, n as ConfigProblem, C as Coverage, d as DateRange, q as DiagnosticCheck, E as EventCutover, s as ExitPage, G as GA4_PROCESSING_LAG_DAYS,
|
|
3
|
-
import { S as SanityQueryClient, G as Ga4Client, V as VercelClient } from './vercel-
|
|
4
|
-
export { a as Ga4Report, b as Ga4ReportRequest, c as ServiceAccountKey, d as VercelPageviews, e as createGa4Client, f as createVercelClient, p as parseServiceAccountKey } from './vercel-
|
|
1
|
+
import { S as SiteAnalyticsConfig, D as DiagnosticReport } from './ranges-Bh9WCyYL.mjs';
|
|
2
|
+
export { A as AcquisitionData, m as CheckStatus, n as ConfigProblem, C as Coverage, d as DateRange, q as DiagnosticCheck, E as EventCutover, s as EventHistory, t as EventOutage, w as ExitPage, G as GA4_PROCESSING_LAG_DAYS, x as Ga4Config, J as JourneyData, y as JourneyStep, c as MeasurementHealthData, M as MetricValue, O as OrdersConfig, P as PREEXISTING, e as REPORT_NAMES, b as RangeKey, R as ReportEnvelope, f as ReportError, a as ReportName, g as SourceName, z as SourceRow, h as SourceStatus, T as TypefaceInterestData, B as TypefaceInterestRow, U as UnavailableReason, V as VercelConfig, F as applyCoverage, H as assertValidSiteConfig, i as coverageForRange, I as coverageNotices, K as daysBetween, L as describeOutage, N as formatInTimeZone, j as isReportName, Q as isValidTimeZone, o as ok, p as partial, k as previousRange, W as provisionalDates, X as provisionalNotice, r as resolveRange, Y as shiftDays, u as unavailable, v as validateSiteConfig, l as valueOrNull } from './ranges-Bh9WCyYL.mjs';
|
|
3
|
+
import { S as SanityQueryClient, G as Ga4Client, V as VercelClient } from './vercel-B6d1Ng48.mjs';
|
|
4
|
+
export { a as Ga4Report, b as Ga4ReportRequest, c as ServiceAccountKey, d as VercelPageviews, e as createGa4Client, f as createVercelClient, g as eventNameFilter, p as parseServiceAccountKey, s as sumFirstMetric } from './vercel-B6d1Ng48.mjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Request authentication and CORS for the report handler.
|
package/dist/server.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { S as SiteAnalyticsConfig, D as DiagnosticReport } from './ranges-
|
|
2
|
-
export { A as AcquisitionData, m as CheckStatus, n as ConfigProblem, C as Coverage, d as DateRange, q as DiagnosticCheck, E as EventCutover, s as ExitPage, G as GA4_PROCESSING_LAG_DAYS,
|
|
3
|
-
import { S as SanityQueryClient, G as Ga4Client, V as VercelClient } from './vercel-
|
|
4
|
-
export { a as Ga4Report, b as Ga4ReportRequest, c as ServiceAccountKey, d as VercelPageviews, e as createGa4Client, f as createVercelClient, p as parseServiceAccountKey } from './vercel-
|
|
1
|
+
import { S as SiteAnalyticsConfig, D as DiagnosticReport } from './ranges-Bh9WCyYL.js';
|
|
2
|
+
export { A as AcquisitionData, m as CheckStatus, n as ConfigProblem, C as Coverage, d as DateRange, q as DiagnosticCheck, E as EventCutover, s as EventHistory, t as EventOutage, w as ExitPage, G as GA4_PROCESSING_LAG_DAYS, x as Ga4Config, J as JourneyData, y as JourneyStep, c as MeasurementHealthData, M as MetricValue, O as OrdersConfig, P as PREEXISTING, e as REPORT_NAMES, b as RangeKey, R as ReportEnvelope, f as ReportError, a as ReportName, g as SourceName, z as SourceRow, h as SourceStatus, T as TypefaceInterestData, B as TypefaceInterestRow, U as UnavailableReason, V as VercelConfig, F as applyCoverage, H as assertValidSiteConfig, i as coverageForRange, I as coverageNotices, K as daysBetween, L as describeOutage, N as formatInTimeZone, j as isReportName, Q as isValidTimeZone, o as ok, p as partial, k as previousRange, W as provisionalDates, X as provisionalNotice, r as resolveRange, Y as shiftDays, u as unavailable, v as validateSiteConfig, l as valueOrNull } from './ranges-Bh9WCyYL.js';
|
|
3
|
+
import { S as SanityQueryClient, G as Ga4Client, V as VercelClient } from './vercel-B6d1Ng48.js';
|
|
4
|
+
export { a as Ga4Report, b as Ga4ReportRequest, c as ServiceAccountKey, d as VercelPageviews, e as createGa4Client, f as createVercelClient, g as eventNameFilter, p as parseServiceAccountKey, s as sumFirstMetric } from './vercel-B6d1Ng48.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Request authentication and CORS for the report handler.
|
package/dist/server.js
CHANGED
|
@@ -36,6 +36,8 @@ __export(server_exports, {
|
|
|
36
36
|
createVercelClient: () => createVercelClient,
|
|
37
37
|
createVisitorInsightsHandler: () => createVisitorInsightsHandler,
|
|
38
38
|
daysBetween: () => daysBetween,
|
|
39
|
+
describeOutage: () => describeOutage,
|
|
40
|
+
eventNameFilter: () => eventNameFilter,
|
|
39
41
|
formatInTimeZone: () => formatInTimeZone,
|
|
40
42
|
isReportName: () => isReportName,
|
|
41
43
|
isValidTimeZone: () => isValidTimeZone,
|
|
@@ -49,6 +51,7 @@ __export(server_exports, {
|
|
|
49
51
|
resolveRange: () => resolveRange,
|
|
50
52
|
runDiagnostics: () => runDiagnostics,
|
|
51
53
|
shiftDays: () => shiftDays,
|
|
54
|
+
sumFirstMetric: () => sumFirstMetric,
|
|
52
55
|
unavailable: () => unavailable,
|
|
53
56
|
validateSiteConfig: () => validateSiteConfig,
|
|
54
57
|
valueOrNull: () => valueOrNull,
|
|
@@ -144,19 +147,41 @@ ${detail}`);
|
|
|
144
147
|
|
|
145
148
|
// src/core/cutover.ts
|
|
146
149
|
var PREEXISTING = "preexisting";
|
|
150
|
+
function toHistory(cutover) {
|
|
151
|
+
if (cutover === null || cutover === void 0) return null;
|
|
152
|
+
if (typeof cutover === "string") return { from: cutover };
|
|
153
|
+
if (!cutover.from) return null;
|
|
154
|
+
return cutover;
|
|
155
|
+
}
|
|
156
|
+
function overlaps(aStart, aEnd, bStart, bEnd) {
|
|
157
|
+
return aStart <= bEnd && (aEnd === null || aEnd >= bStart);
|
|
158
|
+
}
|
|
147
159
|
function coverageForRange(cutovers, eventName, range) {
|
|
148
160
|
if (!(eventName in cutovers)) return { status: "none", reason: "unknown_event" };
|
|
149
|
-
const
|
|
150
|
-
if (
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
if (
|
|
154
|
-
|
|
155
|
-
|
|
161
|
+
const history = toHistory(cutovers[eventName]);
|
|
162
|
+
if (!history) return { status: "none", reason: "not_instrumented" };
|
|
163
|
+
let cutover;
|
|
164
|
+
let startsBeforeCutover = false;
|
|
165
|
+
if (history.from !== PREEXISTING) {
|
|
166
|
+
const cutoverTime = Date.parse(history.from);
|
|
167
|
+
if (Number.isNaN(cutoverTime)) return { status: "none", reason: "unknown_event" };
|
|
168
|
+
cutover = history.from;
|
|
169
|
+
if (Date.parse(range.end) < cutoverTime) return { status: "none", reason: "before_cutover", cutover };
|
|
170
|
+
startsBeforeCutover = Date.parse(range.start) < cutoverTime;
|
|
171
|
+
}
|
|
172
|
+
const hit = (history.outages ?? []).filter((o) => overlaps(o.start, o.until, range.start, range.end));
|
|
173
|
+
const swallowed = hit.find((o) => o.start <= range.start && (o.until === null || o.until > range.end));
|
|
174
|
+
if (swallowed) return { status: "none", reason: "outage", outages: [swallowed], cutover };
|
|
175
|
+
if (hit.length > 0) return { status: "partial", reason: "spans_outage", outages: hit, cutover };
|
|
176
|
+
if (startsBeforeCutover) return { status: "partial", reason: "spans_cutover", outages: [], cutover };
|
|
156
177
|
return { status: "full" };
|
|
157
178
|
}
|
|
158
179
|
function applyCoverage(count, coverage) {
|
|
159
180
|
if (coverage.status === "none") {
|
|
181
|
+
if (coverage.reason === "outage") {
|
|
182
|
+
const outage = coverage.outages?.[0];
|
|
183
|
+
return unavailable("outage", outage ? describeOutage(outage) : void 0);
|
|
184
|
+
}
|
|
160
185
|
return unavailable(
|
|
161
186
|
coverage.reason === "before_cutover" ? "before_cutover" : "not_instrumented",
|
|
162
187
|
coverage.cutover ? `Instrumented from ${coverage.cutover}` : void 0
|
|
@@ -164,16 +189,31 @@ function applyCoverage(count, coverage) {
|
|
|
164
189
|
}
|
|
165
190
|
if (count === null) return unavailable("source_error");
|
|
166
191
|
if (coverage.status === "partial") {
|
|
167
|
-
|
|
192
|
+
if (coverage.reason === "spans_outage") {
|
|
193
|
+
const outage = coverage.outages[0];
|
|
194
|
+
const from2 = outage?.until ?? outage?.start ?? coverage.cutover ?? "";
|
|
195
|
+
return partial(count, from2, `Undercounted: ${describeOutage(outage)}`);
|
|
196
|
+
}
|
|
197
|
+
const from = coverage.cutover ?? "";
|
|
198
|
+
return partial(count, from, `Only counted from ${from}, when this event was added`);
|
|
168
199
|
}
|
|
169
200
|
return { status: "ok", value: count };
|
|
170
201
|
}
|
|
202
|
+
function describeOutage(outage) {
|
|
203
|
+
if (!outage) return "an outage affected part of this range";
|
|
204
|
+
const period = outage.until ? `${outage.start} to ${outage.until}` : `${outage.start} onwards`;
|
|
205
|
+
return outage.reason ? `not recorded ${period} (${outage.reason})` : `not recorded ${period}`;
|
|
206
|
+
}
|
|
171
207
|
function coverageNotices(cutovers, eventNames, range) {
|
|
172
208
|
const notices = [];
|
|
173
209
|
for (const name of eventNames) {
|
|
174
210
|
const coverage = coverageForRange(cutovers, name, range);
|
|
175
|
-
if (coverage.status === "partial") {
|
|
211
|
+
if (coverage.status === "partial" && coverage.reason === "spans_outage") {
|
|
212
|
+
notices.push(`${name} was ${describeOutage(coverage.outages[0])}, so figures for this range are too low \u2014 not a real decline.`);
|
|
213
|
+
} else if (coverage.status === "partial") {
|
|
176
214
|
notices.push(`${name} was instrumented on ${coverage.cutover}; figures before that date are missing, not zero.`);
|
|
215
|
+
} else if (coverage.status === "none" && coverage.reason === "outage") {
|
|
216
|
+
notices.push(`${name} was ${describeOutage(coverage.outages?.[0])}, covering this whole range. The figure is unavailable, not zero.`);
|
|
177
217
|
} else if (coverage.status === "none" && coverage.reason === "not_instrumented") {
|
|
178
218
|
notices.push(`${name} is not instrumented on this site, so it cannot be reported for any range.`);
|
|
179
219
|
} else if (coverage.status === "none" && coverage.reason === "before_cutover") {
|
|
@@ -1210,6 +1250,8 @@ async function runReport(report, ctx) {
|
|
|
1210
1250
|
createVercelClient,
|
|
1211
1251
|
createVisitorInsightsHandler,
|
|
1212
1252
|
daysBetween,
|
|
1253
|
+
describeOutage,
|
|
1254
|
+
eventNameFilter,
|
|
1213
1255
|
formatInTimeZone,
|
|
1214
1256
|
isReportName,
|
|
1215
1257
|
isValidTimeZone,
|
|
@@ -1223,6 +1265,7 @@ async function runReport(report, ctx) {
|
|
|
1223
1265
|
resolveRange,
|
|
1224
1266
|
runDiagnostics,
|
|
1225
1267
|
shiftDays,
|
|
1268
|
+
sumFirstMetric,
|
|
1226
1269
|
unavailable,
|
|
1227
1270
|
validateSiteConfig,
|
|
1228
1271
|
valueOrNull,
|