@liiift-studio/sanity-visitor-insights 0.14.0 → 0.16.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/index.js +98 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +97 -1
- package/dist/index.mjs.map +1 -1
- package/dist/{ranges-CEHxJQE6.d.mts → ranges-DvICo5H2.d.mts} +185 -2
- package/dist/{ranges-CEHxJQE6.d.ts → ranges-DvICo5H2.d.ts} +185 -2
- package/dist/server.d.mts +12 -4
- package/dist/server.d.ts +12 -4
- package/dist/server.js +317 -14
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +308 -14
- package/dist/server.mjs.map +1 -1
- package/dist/testing.d.mts +20 -2
- package/dist/testing.d.ts +20 -2
- package/dist/testing.js +14 -0
- package/dist/testing.js.map +1 -1
- package/dist/testing.mjs +13 -0
- package/dist/testing.mjs.map +1 -1
- package/dist/{vercel-CL2w9Jqs.d.mts → vercel-CU5PgGrJ.d.mts} +119 -1
- package/dist/{vercel-CL2w9Jqs.d.ts → vercel-CU5PgGrJ.d.ts} +119 -1
- package/package.json +1 -1
- package/src/core/capture.ts +206 -0
- package/src/core/core.test.ts +74 -0
- package/src/core/siteConfig.ts +24 -0
- package/src/index.ts +1 -0
- package/src/reportData.ts +45 -0
- package/src/server/createHandler.ts +30 -4
- package/src/server/mailchimp.ts +172 -0
- package/src/server/orders.ts +107 -0
- package/src/server/reports/measurementHealth.ts +110 -3
- package/src/server/reports/reports.test.ts +79 -0
- package/src/server.ts +6 -2
- package/src/studio/Figure.tsx +28 -0
- package/src/studio/VisitorInsightsTool.tsx +1 -0
- package/src/studio/panels.test.tsx +74 -0
- package/src/studio/panels.tsx +108 -0
- package/src/testing/fakes.ts +23 -0
- package/src/types.ts +22 -1
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, D as DiagnosticReport, J as JourneyData, c as MeasurementHealthData, T as TypefaceInterestData } from './ranges-
|
|
4
|
-
export { C as
|
|
3
|
+
import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, D as DiagnosticReport, J as JourneyData, c as MeasurementHealthData, T as TypefaceInterestData } from './ranges-DvICo5H2.mjs';
|
|
4
|
+
export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as DailyPoint, i as DateRange, j as DiagnosticCheck, E as EmailCampaign, k as EventCutover, l as JourneyOutcome, m as JourneyStep, L as LandingPage, P as PREEXISTING, n as REPORT_NAMES, o as ReportError, S as SiteAnalyticsConfig, p as SourceName, q as SourceRow, r as SourceStatus, s as TypefaceInterestRow, U as UnavailableReason, t as coverageForRange, u as estimated, v as isReportName, w as ok, x as partial, y as previousRange, z as resolveRange, B as unavailable, F as validateSiteConfig, G as valueOrNull } from './ranges-DvICo5H2.mjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The Visitor Insights Studio tool.
|
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, D as DiagnosticReport, J as JourneyData, c as MeasurementHealthData, T as TypefaceInterestData } from './ranges-
|
|
4
|
-
export { C as
|
|
3
|
+
import { R as ReportEnvelope, a as ReportName, b as RangeKey, M as MetricValue, A as AcquisitionData, D as DiagnosticReport, J as JourneyData, c as MeasurementHealthData, T as TypefaceInterestData } from './ranges-DvICo5H2.js';
|
|
4
|
+
export { C as CaptureBasis, d as CaptureEstimate, e as CaptureModel, f as CheckStatus, g as Coverage, h as DailyPoint, i as DateRange, j as DiagnosticCheck, E as EmailCampaign, k as EventCutover, l as JourneyOutcome, m as JourneyStep, L as LandingPage, P as PREEXISTING, n as REPORT_NAMES, o as ReportError, S as SiteAnalyticsConfig, p as SourceName, q as SourceRow, r as SourceStatus, s as TypefaceInterestRow, U as UnavailableReason, t as coverageForRange, u as estimated, v as isReportName, w as ok, x as partial, y as previousRange, z as resolveRange, B as unavailable, F as validateSiteConfig, G as valueOrNull } from './ranges-DvICo5H2.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* The Visitor Insights Studio tool.
|
package/dist/index.js
CHANGED
|
@@ -47,6 +47,7 @@ __export(index_exports, {
|
|
|
47
47
|
VisitorInsightsTool: () => VisitorInsightsTool,
|
|
48
48
|
coverageForRange: () => coverageForRange,
|
|
49
49
|
default: () => index_default,
|
|
50
|
+
estimated: () => estimated,
|
|
50
51
|
formatCount: () => formatCount,
|
|
51
52
|
formatMoney: () => formatMoney,
|
|
52
53
|
formatPercent: () => formatPercent,
|
|
@@ -213,6 +214,21 @@ function MetricFigure({ metric, label, size = 4, unit = "count" }) {
|
|
|
213
214
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Badge, { tone: "caution", fontSize: 0, children: "Partial" })
|
|
214
215
|
] });
|
|
215
216
|
}
|
|
217
|
+
if (metric.status === "estimated") {
|
|
218
|
+
const range = unit === "percent" ? `${metric.low.toFixed(1)}% to ${metric.high.toFixed(1)}%` : `${formatCount(metric.low)} to ${formatCount(metric.high)}`;
|
|
219
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Stack, { space: 2, children: [
|
|
220
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_sanity_ui_compat.Text, { size, "aria-label": `${label}: estimated ${formatted}, between ${range}. ${metric.basis}`, children: [
|
|
221
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { "aria-hidden": "true", children: "~" }),
|
|
222
|
+
formatted
|
|
223
|
+
] }),
|
|
224
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size: 0, muted: true, children: range }),
|
|
225
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Badge, { tone: "primary", fontSize: 0, children: "Estimated" })
|
|
226
|
+
] });
|
|
227
|
+
}
|
|
228
|
+
if (metric.status !== "ok") {
|
|
229
|
+
const unhandled = metric;
|
|
230
|
+
void unhandled;
|
|
231
|
+
}
|
|
216
232
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_sanity_ui_compat.Text, { size, "aria-label": `${label}: ${formatted}`, children: formatted });
|
|
217
233
|
}
|
|
218
234
|
function ComparisonBar({ label, metric, max, tone = "default" }) {
|
|
@@ -889,12 +905,71 @@ function MeasurementHealthPanel({ data, previous }) {
|
|
|
889
905
|
data.shortfallRatio !== null && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, weight: "semibold", children: data.shortfallRatio >= 0 ? `GA4 saw ${formatPercent(data.shortfallRatio, 1)} fewer pageviews than Vercel` : `GA4 saw ${formatPercent(-data.shortfallRatio, 1)} more pageviews than Vercel` }),
|
|
890
906
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: data.interpretation })
|
|
891
907
|
] }) }),
|
|
908
|
+
((data.capture?.estimates?.length ?? 0) > 0 || data.estimatedSessions?.status === "estimated") && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
|
|
909
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Heading, { size: 1, style: sectionHeading, children: "How much GA4 is seeing" }),
|
|
910
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: cardGrid, children: (data.capture?.estimates ?? []).map((estimate) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Card, { padding: 3, radius: 2, tone: "transparent", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
|
|
911
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Label, { size: 1, muted: true, children: estimate.basis === "orders" ? "Measured against orders" : estimate.basis === "email" ? "Measured against email clicks" : "Measured against Vercel" }),
|
|
912
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 4, children: formatPercent(Math.min(1, estimate.rate), 0) }),
|
|
913
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Text, { size: 0, muted: true, children: [
|
|
914
|
+
formatCount(estimate.observed),
|
|
915
|
+
" of ",
|
|
916
|
+
formatCount(estimate.actual),
|
|
917
|
+
". ",
|
|
918
|
+
estimate.note
|
|
919
|
+
] })
|
|
920
|
+
] }) }, estimate.basis)) }),
|
|
921
|
+
data.capture?.discrepancy && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Card, { padding: 3, radius: 2, tone: "caution", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 2, children: [
|
|
922
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, weight: "medium", children: "The sources disagree, and that is informative" }),
|
|
923
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: data.capture.discrepancy })
|
|
924
|
+
] }) }),
|
|
925
|
+
data.estimatedSessions && data.estimatedSessions.status === "estimated" && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Card, { padding: 3, radius: 2, tone: "transparent", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
|
|
926
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Label, { size: 1, muted: true, children: "Sessions, corrected for what GA4 misses" }),
|
|
927
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MetricFigure, { metric: data.estimatedSessions, label: "Estimated sessions" })
|
|
928
|
+
] }) })
|
|
929
|
+
] }),
|
|
892
930
|
(data.daily?.length ?? 0) >= 3 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
|
|
893
931
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Heading, { size: 1, style: sectionHeading, children: "Day by day" }),
|
|
894
932
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: "A gap that has always been there is consent and blocking. A gap that opens on one day is an incident." }),
|
|
895
933
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(TrendChart, { points: data.daily ?? [] }),
|
|
896
934
|
data.vercelDailyUnavailable && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 0, muted: true, children: "Vercel reports this range in weekly buckets, so only the GA4 line is plotted. Choose Week or Month to see both." })
|
|
897
935
|
] }),
|
|
936
|
+
(data.campaigns?.length ?? 0) > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
|
|
937
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Heading, { size: 1, style: sectionHeading, children: "Email campaigns" }),
|
|
938
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: "Clicks are distinct subscribers. Opens are inflated by Apple Mail Privacy Protection, which fetches images on the recipient\u2019s behalf \u2014 so sort on clicks, not opens." }),
|
|
939
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
940
|
+
SortableTable,
|
|
941
|
+
{
|
|
942
|
+
caption: "Email campaigns by clicks",
|
|
943
|
+
initialSort: "clicks",
|
|
944
|
+
rows: data.campaigns ?? [],
|
|
945
|
+
rowKey: (c) => `${c.sentAt}-${c.title}`,
|
|
946
|
+
filterOn: (c) => `${c.title} ${c.subject}`,
|
|
947
|
+
filterPlaceholder: "Filter campaigns",
|
|
948
|
+
exportName: "email-campaigns",
|
|
949
|
+
columns: [
|
|
950
|
+
{
|
|
951
|
+
key: "title",
|
|
952
|
+
label: "Campaign",
|
|
953
|
+
sortValue: (c) => c.title,
|
|
954
|
+
render: (c) => /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 1, children: [
|
|
955
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: c.title }),
|
|
956
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 0, muted: true, children: c.sentAt.slice(0, 10) })
|
|
957
|
+
] })
|
|
958
|
+
},
|
|
959
|
+
{ key: "sent", label: "Sent", numeric: true, sortValue: (c) => c.sent, render: (c) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: formatCount(c.sent) }) },
|
|
960
|
+
{ key: "opens", label: "Opens", numeric: true, sortValue: (c) => c.opens, render: (c) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: formatCount(c.opens) }) },
|
|
961
|
+
{ key: "clicks", label: "Clicks", numeric: true, sortValue: (c) => c.clicks, render: (c) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, children: formatCount(c.clicks) }) },
|
|
962
|
+
{
|
|
963
|
+
key: "unsub",
|
|
964
|
+
label: "Unsubscribed",
|
|
965
|
+
numeric: true,
|
|
966
|
+
sortValue: (c) => c.unsubscribed,
|
|
967
|
+
render: (c) => /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: formatCount(c.unsubscribed) })
|
|
968
|
+
}
|
|
969
|
+
]
|
|
970
|
+
}
|
|
971
|
+
)
|
|
972
|
+
] }),
|
|
898
973
|
Object.keys(data.orderStatuses ?? {}).length > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
|
|
899
974
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Heading, { size: 1, style: sectionHeading, children: "Order statuses in this range" }),
|
|
900
975
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: "What the orders actually say, before any filtering. Use these values to set which statuses count as a sale." }),
|
|
@@ -907,6 +982,20 @@ function MeasurementHealthPanel({ data, previous }) {
|
|
|
907
982
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Heading, { size: 1, style: sectionHeading, children: "Context" }),
|
|
908
983
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 1, muted: true, children: "Different units to the figures above, and to each other. Shown for scale, never differenced." }),
|
|
909
984
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: cardGrid, children: [
|
|
985
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Card, { padding: 3, radius: 2, tone: "transparent", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
|
|
986
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Label, { size: 1, muted: true, children: "Mailing list" }),
|
|
987
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: figureRow, children: [
|
|
988
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(MetricFigure, { metric: metricOr(data.audience, OLDER_ROUTE), label: "Mailing list members" }),
|
|
989
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
990
|
+
Delta,
|
|
991
|
+
{
|
|
992
|
+
current: metricSortValue(data.audience),
|
|
993
|
+
previous: metricSortValue(previous?.audience)
|
|
994
|
+
}
|
|
995
|
+
)
|
|
996
|
+
] }),
|
|
997
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Text, { size: 0, muted: true, children: "Mailchimp\u2019s own count. Not consent-gated or blockable, so it replaces the site\u2019s subscribe event rather than sitting beside it." })
|
|
998
|
+
] }) }),
|
|
910
999
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Card, { padding: 3, radius: 2, tone: "transparent", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_sanity_ui_compat2.Stack, { space: 3, children: [
|
|
911
1000
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_sanity_ui_compat2.Label, { size: 1, muted: true, children: "Revenue" }),
|
|
912
1001
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: figureRow, children: [
|
|
@@ -1448,7 +1537,8 @@ function RangeSelector({
|
|
|
1448
1537
|
var SOURCE_LABEL = {
|
|
1449
1538
|
ga4: "Google Analytics",
|
|
1450
1539
|
vercel: "Vercel",
|
|
1451
|
-
sanity: "Orders"
|
|
1540
|
+
sanity: "Orders",
|
|
1541
|
+
mailchimp: "Mailchimp"
|
|
1452
1542
|
};
|
|
1453
1543
|
function SourceStatusRow({ sources }) {
|
|
1454
1544
|
const degraded = Object.entries(sources).filter(([, s]) => s.status !== "ok");
|
|
@@ -1598,6 +1688,9 @@ function VisitorInsightsTool(props) {
|
|
|
1598
1688
|
}
|
|
1599
1689
|
|
|
1600
1690
|
// src/types.ts
|
|
1691
|
+
function estimated(value, low, high, basis) {
|
|
1692
|
+
return { status: "estimated", value, low, high, basis };
|
|
1693
|
+
}
|
|
1601
1694
|
function ok(value) {
|
|
1602
1695
|
return { status: "ok", value };
|
|
1603
1696
|
}
|
|
@@ -1686,6 +1779,9 @@ function validateSiteConfig(config) {
|
|
|
1686
1779
|
["orders.totalField", config.orders?.totalField],
|
|
1687
1780
|
["orders.typefacesField", config.orders?.typefacesField]
|
|
1688
1781
|
];
|
|
1782
|
+
for (const name of Object.keys(config.orders?.licenceFields ?? {})) {
|
|
1783
|
+
fieldSlots.push([`orders.licenceFields.${name}`, name]);
|
|
1784
|
+
}
|
|
1689
1785
|
for (const [field, value] of fieldSlots) {
|
|
1690
1786
|
if (!value) continue;
|
|
1691
1787
|
if (!FIELD_PATH.test(value)) {
|
|
@@ -1840,6 +1936,7 @@ var index_default = visitorInsights;
|
|
|
1840
1936
|
TypefaceInterestPanel,
|
|
1841
1937
|
VisitorInsightsTool,
|
|
1842
1938
|
coverageForRange,
|
|
1939
|
+
estimated,
|
|
1843
1940
|
formatCount,
|
|
1844
1941
|
formatMoney,
|
|
1845
1942
|
formatPercent,
|