@liiift-studio/sanity-visitor-insights 0.44.0 → 0.45.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/dist/index.d.mts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/{ranges-Cc3Bv5-l.d.mts → ranges-CWGvBi3C.d.mts} +1 -1
- package/dist/{ranges-Cc3Bv5-l.d.ts → ranges-CWGvBi3C.d.ts} +1 -1
- package/dist/server.d.mts +4 -4
- package/dist/server.d.ts +4 -4
- package/dist/server.js +48 -15
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +48 -15
- package/dist/server.mjs.map +1 -1
- package/dist/testing.d.mts +1 -1
- package/dist/testing.d.ts +1 -1
- package/dist/{vercel-Da9lu56v.d.mts → vercel-CGDmz9Bt.d.mts} +4 -0
- package/dist/{vercel-Da9lu56v.d.ts → vercel-CGDmz9Bt.d.ts} +4 -0
- package/package.json +1 -1
- package/src/core/capture.ts +30 -5
- package/src/server/diagnostics.ts +7 -2
- package/src/server/mailchimp.ts +43 -6
- package/src/server/reports/measurementHealth.ts +22 -5
- package/src/server/vercel.test.ts +66 -0
package/dist/index.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as sanity from 'sanity';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, c as MeasurementHealthData, D as DiagnosticReport, J as JourneyData, T as TypefaceInterestData } from './ranges-
|
|
4
|
-
export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DailyPoint, j as DateRange, k as DiagnosticCheck, E as EmailCampaign, l as EventCutover, m as JourneyOutcome, n as JourneyStep, L as LandingPage, o as LicenceTierRow, P as PREEXISTING, p as REPORT_NAMES, q as ReportError, S as SiteAnalyticsConfig, r as SourceName, s as SourceRow, t as SourceStatus, u as TimelineEvent, v as TypefaceInterestRow, U as UnavailableReason, w as coverageForRange, x as estimated, y as isReportName, z as ok, B as partial, F as previousRange, G as resolveRange, H as unavailable, I as validateSiteConfig, K as valueOrNull } from './ranges-
|
|
3
|
+
import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, c as MeasurementHealthData, D as DiagnosticReport, J as JourneyData, T as TypefaceInterestData } from './ranges-CWGvBi3C.mjs';
|
|
4
|
+
export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DailyPoint, j as DateRange, k as DiagnosticCheck, E as EmailCampaign, l as EventCutover, m as JourneyOutcome, n as JourneyStep, L as LandingPage, o as LicenceTierRow, P as PREEXISTING, p as REPORT_NAMES, q as ReportError, S as SiteAnalyticsConfig, r as SourceName, s as SourceRow, t as SourceStatus, u as TimelineEvent, v as TypefaceInterestRow, U as UnavailableReason, w as coverageForRange, x as estimated, y as isReportName, z as ok, B as partial, F as previousRange, G as resolveRange, H as unavailable, I as validateSiteConfig, K as valueOrNull } from './ranges-CWGvBi3C.mjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Data hook for the Studio panels.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as sanity from 'sanity';
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, c as MeasurementHealthData, D as DiagnosticReport, J as JourneyData, T as TypefaceInterestData } from './ranges-
|
|
4
|
-
export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DailyPoint, j as DateRange, k as DiagnosticCheck, E as EmailCampaign, l as EventCutover, m as JourneyOutcome, n as JourneyStep, L as LandingPage, o as LicenceTierRow, P as PREEXISTING, p as REPORT_NAMES, q as ReportError, S as SiteAnalyticsConfig, r as SourceName, s as SourceRow, t as SourceStatus, u as TimelineEvent, v as TypefaceInterestRow, U as UnavailableReason, w as coverageForRange, x as estimated, y as isReportName, z as ok, B as partial, F as previousRange, G as resolveRange, H as unavailable, I as validateSiteConfig, K as valueOrNull } from './ranges-
|
|
3
|
+
import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, c as MeasurementHealthData, D as DiagnosticReport, J as JourneyData, T as TypefaceInterestData } from './ranges-CWGvBi3C.js';
|
|
4
|
+
export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as CrossSourceDay, i as DailyPoint, j as DateRange, k as DiagnosticCheck, E as EmailCampaign, l as EventCutover, m as JourneyOutcome, n as JourneyStep, L as LandingPage, o as LicenceTierRow, P as PREEXISTING, p as REPORT_NAMES, q as ReportError, S as SiteAnalyticsConfig, r as SourceName, s as SourceRow, t as SourceStatus, u as TimelineEvent, v as TypefaceInterestRow, U as UnavailableReason, w as coverageForRange, x as estimated, y as isReportName, z as ok, B as partial, F as previousRange, G as resolveRange, H as unavailable, I as validateSiteConfig, K as valueOrNull } from './ranges-CWGvBi3C.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Data hook for the Studio panels.
|
|
@@ -227,7 +227,7 @@ declare function fromPageviews(ga4Pageviews: number, vercelPageviews: number): C
|
|
|
227
227
|
* single known source, so a shortfall here that the others do not show points at attribution — a
|
|
228
228
|
* tagging or redirect problem — rather than at measurement generally.
|
|
229
229
|
*/
|
|
230
|
-
declare function fromEmail(
|
|
230
|
+
declare function fromEmail(ga4Users: number, mailchimpClicks: number): CaptureEstimate | null;
|
|
231
231
|
/** The combined view of GA4's capture rate. */
|
|
232
232
|
interface CaptureModel {
|
|
233
233
|
/** Every estimate that had a usable denominator, best first. */
|
|
@@ -227,7 +227,7 @@ declare function fromPageviews(ga4Pageviews: number, vercelPageviews: number): C
|
|
|
227
227
|
* single known source, so a shortfall here that the others do not show points at attribution — a
|
|
228
228
|
* tagging or redirect problem — rather than at measurement generally.
|
|
229
229
|
*/
|
|
230
|
-
declare function fromEmail(
|
|
230
|
+
declare function fromEmail(ga4Users: number, mailchimpClicks: number): CaptureEstimate | null;
|
|
231
231
|
/** The combined view of GA4's capture rate. */
|
|
232
232
|
interface CaptureModel {
|
|
233
233
|
/** Every estimate that had a usable denominator, best first. */
|
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, C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, N as ConfigProblem, g as Coverage, h as CrossSourceDay, O as CustomRangeError, i as DailyPoint, j as DateRange, k as DiagnosticCheck, E as EmailCampaign, l as EventCutover, Q as EventHistory, V as EventNameMap, W as EventOutage, X as GA4_PROCESSING_LAG_DAYS, Y as Ga4Config, J as JourneyData, m as JourneyOutcome, n as JourneyStep, L as LandingPage, o as LicenceTierRow, Z as MAX_CUSTOM_RANGE_DAYS, c as MeasurementHealthData, M as MetricValue, _ as OrdersConfig, P as PREEXISTING, $ as RANGE_DAYS, p as REPORT_NAMES, b as RangeKey, R as ReportEnvelope, q as ReportError, a as ReportName, r as SourceName, s as SourceRow, t as SourceStatus, u as TimelineEvent, T as TypefaceInterestData, v as TypefaceInterestRow, U as UnavailableReason, a0 as VercelConfig, a1 as applyCoverage, a2 as assertValidSiteConfig, a3 as captureModel, a4 as coverageForAny, w as coverageForRange, a5 as coverageNotices, a6 as daysBetween, a7 as describeOutage, x as estimated, a8 as formatInTimeZone, a9 as fromEmail, aa as fromOrders, ab as fromPageviews, ac as grossUp, y as isReportName, ad as isValidTimeZone, z as ok, B as partial, F as previousRange, ae as provisionalDates, af as provisionalNotice, ag as resolveCustomRange, G as resolveRange, ah as shiftDays, H as unavailable, I as validateSiteConfig, K as valueOrNull, ai as zonedDayEndUtc, aj as zonedDayStartUtc } from './ranges-
|
|
3
|
-
import { S as SanityQueryClient, G as Ga4Client, V as VercelClient } from './vercel-
|
|
4
|
-
export { a as Ga4Report, b as Ga4ReportRequest, L as LicenceFieldMap, c as LicenceTier, M as MailchimpAudience, d as MailchimpCampaign, e as MailchimpClient, O as OrderCounts, f as OrderQueryOptions, g as ServiceAccountKey, T as TypefaceOrderCounts, U as UNKNOWN_STATUS, h as VercelPageviews, i as countLicenceTiers, j as countOrders, k as countOrdersByTypeface, l as createGa4Client, m as createMailchimpClient, n as createVercelClient, o as datacenterFromKey, p as eventNameFilter, q as orderQueryOptions, r as parseServiceAccountKey, s as sumFirstMetric } from './vercel-
|
|
1
|
+
import { S as SiteAnalyticsConfig, D as DiagnosticReport } from './ranges-CWGvBi3C.mjs';
|
|
2
|
+
export { A as AcquisitionData, C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, N as ConfigProblem, g as Coverage, h as CrossSourceDay, O as CustomRangeError, i as DailyPoint, j as DateRange, k as DiagnosticCheck, E as EmailCampaign, l as EventCutover, Q as EventHistory, V as EventNameMap, W as EventOutage, X as GA4_PROCESSING_LAG_DAYS, Y as Ga4Config, J as JourneyData, m as JourneyOutcome, n as JourneyStep, L as LandingPage, o as LicenceTierRow, Z as MAX_CUSTOM_RANGE_DAYS, c as MeasurementHealthData, M as MetricValue, _ as OrdersConfig, P as PREEXISTING, $ as RANGE_DAYS, p as REPORT_NAMES, b as RangeKey, R as ReportEnvelope, q as ReportError, a as ReportName, r as SourceName, s as SourceRow, t as SourceStatus, u as TimelineEvent, T as TypefaceInterestData, v as TypefaceInterestRow, U as UnavailableReason, a0 as VercelConfig, a1 as applyCoverage, a2 as assertValidSiteConfig, a3 as captureModel, a4 as coverageForAny, w as coverageForRange, a5 as coverageNotices, a6 as daysBetween, a7 as describeOutage, x as estimated, a8 as formatInTimeZone, a9 as fromEmail, aa as fromOrders, ab as fromPageviews, ac as grossUp, y as isReportName, ad as isValidTimeZone, z as ok, B as partial, F as previousRange, ae as provisionalDates, af as provisionalNotice, ag as resolveCustomRange, G as resolveRange, ah as shiftDays, H as unavailable, I as validateSiteConfig, K as valueOrNull, ai as zonedDayEndUtc, aj as zonedDayStartUtc } from './ranges-CWGvBi3C.mjs';
|
|
3
|
+
import { S as SanityQueryClient, G as Ga4Client, V as VercelClient } from './vercel-CGDmz9Bt.mjs';
|
|
4
|
+
export { a as Ga4Report, b as Ga4ReportRequest, L as LicenceFieldMap, c as LicenceTier, M as MailchimpAudience, d as MailchimpCampaign, e as MailchimpClient, O as OrderCounts, f as OrderQueryOptions, g as ServiceAccountKey, T as TypefaceOrderCounts, U as UNKNOWN_STATUS, h as VercelPageviews, i as countLicenceTiers, j as countOrders, k as countOrdersByTypeface, l as createGa4Client, m as createMailchimpClient, n as createVercelClient, o as datacenterFromKey, p as eventNameFilter, q as orderQueryOptions, r as parseServiceAccountKey, s as sumFirstMetric } from './vercel-CGDmz9Bt.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, C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, N as ConfigProblem, g as Coverage, h as CrossSourceDay, O as CustomRangeError, i as DailyPoint, j as DateRange, k as DiagnosticCheck, E as EmailCampaign, l as EventCutover, Q as EventHistory, V as EventNameMap, W as EventOutage, X as GA4_PROCESSING_LAG_DAYS, Y as Ga4Config, J as JourneyData, m as JourneyOutcome, n as JourneyStep, L as LandingPage, o as LicenceTierRow, Z as MAX_CUSTOM_RANGE_DAYS, c as MeasurementHealthData, M as MetricValue, _ as OrdersConfig, P as PREEXISTING, $ as RANGE_DAYS, p as REPORT_NAMES, b as RangeKey, R as ReportEnvelope, q as ReportError, a as ReportName, r as SourceName, s as SourceRow, t as SourceStatus, u as TimelineEvent, T as TypefaceInterestData, v as TypefaceInterestRow, U as UnavailableReason, a0 as VercelConfig, a1 as applyCoverage, a2 as assertValidSiteConfig, a3 as captureModel, a4 as coverageForAny, w as coverageForRange, a5 as coverageNotices, a6 as daysBetween, a7 as describeOutage, x as estimated, a8 as formatInTimeZone, a9 as fromEmail, aa as fromOrders, ab as fromPageviews, ac as grossUp, y as isReportName, ad as isValidTimeZone, z as ok, B as partial, F as previousRange, ae as provisionalDates, af as provisionalNotice, ag as resolveCustomRange, G as resolveRange, ah as shiftDays, H as unavailable, I as validateSiteConfig, K as valueOrNull, ai as zonedDayEndUtc, aj as zonedDayStartUtc } from './ranges-
|
|
3
|
-
import { S as SanityQueryClient, G as Ga4Client, V as VercelClient } from './vercel-
|
|
4
|
-
export { a as Ga4Report, b as Ga4ReportRequest, L as LicenceFieldMap, c as LicenceTier, M as MailchimpAudience, d as MailchimpCampaign, e as MailchimpClient, O as OrderCounts, f as OrderQueryOptions, g as ServiceAccountKey, T as TypefaceOrderCounts, U as UNKNOWN_STATUS, h as VercelPageviews, i as countLicenceTiers, j as countOrders, k as countOrdersByTypeface, l as createGa4Client, m as createMailchimpClient, n as createVercelClient, o as datacenterFromKey, p as eventNameFilter, q as orderQueryOptions, r as parseServiceAccountKey, s as sumFirstMetric } from './vercel-
|
|
1
|
+
import { S as SiteAnalyticsConfig, D as DiagnosticReport } from './ranges-CWGvBi3C.js';
|
|
2
|
+
export { A as AcquisitionData, C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, N as ConfigProblem, g as Coverage, h as CrossSourceDay, O as CustomRangeError, i as DailyPoint, j as DateRange, k as DiagnosticCheck, E as EmailCampaign, l as EventCutover, Q as EventHistory, V as EventNameMap, W as EventOutage, X as GA4_PROCESSING_LAG_DAYS, Y as Ga4Config, J as JourneyData, m as JourneyOutcome, n as JourneyStep, L as LandingPage, o as LicenceTierRow, Z as MAX_CUSTOM_RANGE_DAYS, c as MeasurementHealthData, M as MetricValue, _ as OrdersConfig, P as PREEXISTING, $ as RANGE_DAYS, p as REPORT_NAMES, b as RangeKey, R as ReportEnvelope, q as ReportError, a as ReportName, r as SourceName, s as SourceRow, t as SourceStatus, u as TimelineEvent, T as TypefaceInterestData, v as TypefaceInterestRow, U as UnavailableReason, a0 as VercelConfig, a1 as applyCoverage, a2 as assertValidSiteConfig, a3 as captureModel, a4 as coverageForAny, w as coverageForRange, a5 as coverageNotices, a6 as daysBetween, a7 as describeOutage, x as estimated, a8 as formatInTimeZone, a9 as fromEmail, aa as fromOrders, ab as fromPageviews, ac as grossUp, y as isReportName, ad as isValidTimeZone, z as ok, B as partial, F as previousRange, ae as provisionalDates, af as provisionalNotice, ag as resolveCustomRange, G as resolveRange, ah as shiftDays, H as unavailable, I as validateSiteConfig, K as valueOrNull, ai as zonedDayEndUtc, aj as zonedDayStartUtc } from './ranges-CWGvBi3C.js';
|
|
3
|
+
import { S as SanityQueryClient, G as Ga4Client, V as VercelClient } from './vercel-CGDmz9Bt.js';
|
|
4
|
+
export { a as Ga4Report, b as Ga4ReportRequest, L as LicenceFieldMap, c as LicenceTier, M as MailchimpAudience, d as MailchimpCampaign, e as MailchimpClient, O as OrderCounts, f as OrderQueryOptions, g as ServiceAccountKey, T as TypefaceOrderCounts, U as UNKNOWN_STATUS, h as VercelPageviews, i as countLicenceTiers, j as countOrders, k as countOrdersByTypeface, l as createGa4Client, m as createMailchimpClient, n as createVercelClient, o as datacenterFromKey, p as eventNameFilter, q as orderQueryOptions, r as parseServiceAccountKey, s as sumFirstMetric } from './vercel-CGDmz9Bt.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Request authentication and CORS for the report handler.
|
package/dist/server.js
CHANGED
|
@@ -874,9 +874,11 @@ function createMailchimpClient(apiKey, listId) {
|
|
|
874
874
|
const month = range.start.slice(0, 7);
|
|
875
875
|
const history = await get(
|
|
876
876
|
`/lists/${listId}/growth-history/${month}`,
|
|
877
|
-
{ fields: "existing,imports,optins" }
|
|
877
|
+
{ fields: "subscribed,existing,imports,optins" }
|
|
878
878
|
);
|
|
879
|
-
|
|
879
|
+
const legacy = [history.existing, history.imports, history.optins];
|
|
880
|
+
const legacyTotal = legacy.some((v) => typeof v === "number") ? legacy.reduce((sum, v) => sum + count(v), 0) : null;
|
|
881
|
+
membersAtStart = typeof history.subscribed === "number" ? history.subscribed : legacyTotal;
|
|
880
882
|
} catch {
|
|
881
883
|
membersAtStart = null;
|
|
882
884
|
}
|
|
@@ -887,8 +889,17 @@ function createMailchimpClient(apiKey, listId) {
|
|
|
887
889
|
const reports = await get("/reports", {
|
|
888
890
|
// Bounded by the range on both sides, so a panel showing one week does not pick up a
|
|
889
891
|
// campaign from last year and attribute its clicks to this window.
|
|
890
|
-
|
|
891
|
-
|
|
892
|
+
/*
|
|
893
|
+
* Bounded in the PROPERTY timezone, like the orders are.
|
|
894
|
+
*
|
|
895
|
+
* These were bare UTC instants while `zonedDayStartUtc`/`zonedDayEndUtc` exist and
|
|
896
|
+
* orders.ts spends a paragraph on why that asks for a different window than the one GA4
|
|
897
|
+
* was asked for. For a US-Pacific property a campaign sent at 5pm local on the last day
|
|
898
|
+
* of the range fell outside this window while the GA4 sessions it produced fell inside —
|
|
899
|
+
* quietly shrinking the denominator of an estimate whose note calls it known exactly.
|
|
900
|
+
*/
|
|
901
|
+
since_send_time: zonedDayStartUtc(range.start, range.timezone ?? "UTC"),
|
|
902
|
+
before_send_time: zonedDayEndUtc(range.end, range.timezone ?? "UTC"),
|
|
892
903
|
/*
|
|
893
904
|
* The documented maximum, and `total_items` so truncation can be SEEN.
|
|
894
905
|
*
|
|
@@ -1198,14 +1209,14 @@ function fromPageviews(ga4Pageviews, vercelPageviews) {
|
|
|
1198
1209
|
note: "GA4 pageviews against Vercel\u2019s. Read it as a ceiling: Vercel\u2019s own counter is a script too \u2014 blocked by fewer lists than GA4, but blocked \u2014 so real traffic is higher than both and GA4\u2019s true share is this or less."
|
|
1199
1210
|
};
|
|
1200
1211
|
}
|
|
1201
|
-
function fromEmail(
|
|
1212
|
+
function fromEmail(ga4Users, mailchimpClicks) {
|
|
1202
1213
|
if (mailchimpClicks < MIN_DENOMINATOR) return null;
|
|
1203
1214
|
return {
|
|
1204
1215
|
basis: "email",
|
|
1205
|
-
rate:
|
|
1206
|
-
observed:
|
|
1216
|
+
rate: ga4Users / mailchimpClicks,
|
|
1217
|
+
observed: ga4Users,
|
|
1207
1218
|
actual: mailchimpClicks,
|
|
1208
|
-
note: "GA4
|
|
1219
|
+
note: "Distinct people GA4 saw arriving from email, against the distinct subscribers Mailchimp says clicked. Both sides count people, so the comparison holds \u2014 but it measures tagging as much as capture."
|
|
1209
1220
|
};
|
|
1210
1221
|
}
|
|
1211
1222
|
function samplingInterval(estimate) {
|
|
@@ -1235,13 +1246,16 @@ function describeDisagreement(estimates) {
|
|
|
1235
1246
|
if (orders && pageviews && Math.abs(orders.rate - pageviews.rate) > DISAGREEMENT_THRESHOLD) {
|
|
1236
1247
|
return orders.rate > pageviews.rate ? "GA4 is capturing purchases far better than pageviews. That points at the pageview comparison rather than at a measurement failure \u2014 Vercel counts crawlers and prefetches GA4 never sees. Conversion figures here are more trustworthy than traffic figures." : "GA4 is capturing pageviews but missing purchases. That is not consent or ad-blocking, which would cost both equally \u2014 look at the purchase event itself: whether it fires on a page some buyers never reach, or is blocked at checkout.";
|
|
1237
1248
|
}
|
|
1249
|
+
if (email && pageviews && Math.abs(email.rate - pageviews.rate) > DISAGREEMENT_THRESHOLD) {
|
|
1250
|
+
return email.rate < pageviews.rate ? "Far fewer people are arriving from your mailing list than GA4\u2019s general capture would predict. That points at the campaign links rather than at measurement \u2014 check they carry UTM tags, and that no redirect is stripping them." : "Email traffic is over-represented against GA4\u2019s general capture rate, which usually means campaign links are tagged while ordinary traffic is not \u2014 so email looks larger than it is.";
|
|
1251
|
+
}
|
|
1238
1252
|
if (email && orders && Math.abs(email.rate - orders.rate) > DISAGREEMENT_THRESHOLD) {
|
|
1239
1253
|
return email.rate < orders.rate ? "Traffic from email is arriving less often than GA4\u2019s general capture rate would predict. That is an attribution problem rather than a measurement one \u2014 check the campaign links for missing UTMs, or a redirect stripping them." : "Email traffic is over-represented against GA4\u2019s general capture rate, which usually means campaign links are tagged while ordinary traffic is not, so email looks larger than it is.";
|
|
1240
1254
|
}
|
|
1241
1255
|
return null;
|
|
1242
1256
|
}
|
|
1243
1257
|
function grossUp(observed, model, admissible = ["orders", "email", "pageviews"]) {
|
|
1244
|
-
const usable = model.estimates.filter((e) => admissible.includes(e.basis));
|
|
1258
|
+
const usable = model.estimates.filter((e) => admissible.includes(e.basis) && e.rate > 0 && e.rate < 1 && Number.isFinite(e.rate));
|
|
1245
1259
|
if (usable.length === 0) return null;
|
|
1246
1260
|
const rate = usable[0].rate;
|
|
1247
1261
|
const rates = usable.map((e) => e.rate);
|
|
@@ -1367,7 +1381,11 @@ async function measurementHealth(input) {
|
|
|
1367
1381
|
* what GA4 does not do.
|
|
1368
1382
|
*/
|
|
1369
1383
|
keepEmptyRows: true,
|
|
1370
|
-
|
|
1384
|
+
// A custom range may be 730 days, and this asked for 400 — so the tail simply did not
|
|
1385
|
+
// come back, drawing as absent on the chart whose job is dating when a gap opened.
|
|
1386
|
+
// Vercel has a whole mechanism for exactly this; GA4's own rowCount was parsed and
|
|
1387
|
+
// never checked here.
|
|
1388
|
+
limit: 800
|
|
1371
1389
|
},
|
|
1372
1390
|
{
|
|
1373
1391
|
// The same-event numerator for the capture model: GA4's purchase count against
|
|
@@ -1381,13 +1399,22 @@ async function measurementHealth(input) {
|
|
|
1381
1399
|
// Sessions GA4 attributes to email. The denominator side of this comes from
|
|
1382
1400
|
// Mailchimp's unique subscriber clicks, so the two must describe the same
|
|
1383
1401
|
// people — which they only do where campaign links carry a UTM.
|
|
1384
|
-
|
|
1402
|
+
// USERS, not sessions. Mailchimp counts distinct subscribers who clicked; one of them
|
|
1403
|
+
// arriving on two days is one click and two sessions, so dividing sessions by clicks
|
|
1404
|
+
// was not a capture rate at all — it was not even bounded by 1, on an estimate whose
|
|
1405
|
+
// own note calls it "a cohort whose true size is known exactly".
|
|
1406
|
+
metrics: [{ name: "totalUsers" }],
|
|
1385
1407
|
dateRanges: [{ startDate: range.start, endDate: range.end }],
|
|
1386
1408
|
dimensionFilter: {
|
|
1387
1409
|
filter: { fieldName: "sessionMedium", stringFilter: { matchType: "EXACT", value: "email" } }
|
|
1388
1410
|
}
|
|
1389
1411
|
}
|
|
1390
1412
|
]);
|
|
1413
|
+
if (daily2 && daily2.rowCount > daily2.rows.length) {
|
|
1414
|
+
input.notices?.push(
|
|
1415
|
+
`Google Analytics returned ${daily2.rows.length} of ${daily2.rowCount} days for this range, so the day-by-day comparison is incomplete. Choose a shorter range.`
|
|
1416
|
+
);
|
|
1417
|
+
}
|
|
1391
1418
|
if (daily2) {
|
|
1392
1419
|
for (const row of daily2.rows) {
|
|
1393
1420
|
const raw = row.dimensions[0];
|
|
@@ -1522,7 +1549,7 @@ async function measurementHealth(input) {
|
|
|
1522
1549
|
try {
|
|
1523
1550
|
const [list, sent] = await Promise.all([
|
|
1524
1551
|
mailchimp.audience({ start: range.start, end: range.end }),
|
|
1525
|
-
mailchimp.campaigns({ start: range.start, end: range.end })
|
|
1552
|
+
mailchimp.campaigns({ start: range.start, end: range.end, timezone: range.timezone })
|
|
1526
1553
|
]);
|
|
1527
1554
|
audience = ok(list.members);
|
|
1528
1555
|
audienceGrowth = list.membersAtStart === null ? unavailable("not_applicable", "Mailchimp reports list growth by calendar month, so this range has no start figure") : ok(list.members - list.membersAtStart);
|
|
@@ -1584,7 +1611,10 @@ async function measurementHealth(input) {
|
|
|
1584
1611
|
revenue: revenueByDate ? revenueByDate[date] ?? 0 : null
|
|
1585
1612
|
}));
|
|
1586
1613
|
const timelineEvents = campaigns.filter((campaign) => campaign.sentAt).map((campaign) => ({
|
|
1587
|
-
|
|
1614
|
+
// The day the send happened where the reader lives, matching the order stems it is meant
|
|
1615
|
+
// to be compared against. The UTC day put an evening send on the following day — the exact
|
|
1616
|
+
// misalignment orders.ts was zoned to prevent.
|
|
1617
|
+
date: zonedDay(campaign.sentAt, range.timezone) ?? campaign.sentAt.slice(0, 10),
|
|
1588
1618
|
label: campaign.title,
|
|
1589
1619
|
detail: `${formatInt(campaign.sent)} sent, ${formatInt(campaign.clicks)} clicked`
|
|
1590
1620
|
}));
|
|
@@ -2397,8 +2427,11 @@ async function runDiagnostics(input) {
|
|
|
2397
2427
|
checks.push({
|
|
2398
2428
|
id: "vercel",
|
|
2399
2429
|
label: "Vercel Web Analytics reachable",
|
|
2400
|
-
|
|
2401
|
-
|
|
2430
|
+
// A response carrying no total is not a healthy source. This reported `pass` and
|
|
2431
|
+
// rendered "null pageviews in the last 7 days" — a check that says nothing is wrong
|
|
2432
|
+
// while printing the word null is worse than no check.
|
|
2433
|
+
status: result.total === null ? "warn" : "pass",
|
|
2434
|
+
detail: result.total === null ? "Vercel answered but returned no pageview total, so its figures cannot be trusted for this range." : `${result.total} pageviews in the last 7 days.`
|
|
2402
2435
|
});
|
|
2403
2436
|
} catch (e) {
|
|
2404
2437
|
checks.push({
|