@liiift-studio/sanity-visitor-insights 0.46.0 → 0.48.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 +17 -1
- package/dist/index.d.mts +44 -12
- package/dist/index.d.ts +44 -12
- package/dist/index.js +534 -284
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +534 -284
- package/dist/index.mjs.map +1 -1
- package/dist/{ranges-CWGvBi3C.d.mts → ranges-CeB8esIA.d.mts} +48 -2
- package/dist/{ranges-CWGvBi3C.d.ts → ranges-CeB8esIA.d.ts} +48 -2
- package/dist/server.d.mts +2 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js +62 -4
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +61 -4
- package/dist/server.mjs.map +1 -1
- package/package.json +1 -1
- package/src/core/core.test.ts +20 -0
- package/src/core/ranges.ts +37 -1
- package/src/reportData.ts +27 -0
- package/src/server/cache.test.ts +7 -0
- package/src/server/createHandler.ts +14 -3
- package/src/server/handler.test.ts +116 -0
- package/src/server/reports/measurementHealth.ts +78 -1
- package/src/server/reports/reports.test.ts +94 -1
- package/src/studio/CrossSourceTimeline.tsx +207 -12
- package/src/studio/VisitorInsightsTool.tsx +64 -3
- package/src/studio/palette.test.ts +118 -0
- package/src/studio/palette.ts +95 -0
- package/src/studio/panels.test.tsx +270 -32
- package/src/studio/panels.tsx +192 -14
- package/src/studio/useReport.ts +11 -4
- package/src/types.ts +2 -0
package/README.md
CHANGED
|
@@ -19,7 +19,23 @@ only as a conversion anchor and a count; no revenue, and no customer data.
|
|
|
19
19
|
| **Typeface interest** | Which families get viewed, tested and bought? |
|
|
20
20
|
| **Diagnostics** | What to fix before trusting any of the above? |
|
|
21
21
|
|
|
22
|
-
Each panel offers a week, quarter or year range
|
|
22
|
+
Each panel offers a week, quarter or year range, a custom window, and a choice of what the changes
|
|
23
|
+
are measured against — the preceding window, or the same window a year earlier. Type sales are
|
|
24
|
+
seasonal, so a quiet August against a quiet July often says less than August against last August.
|
|
25
|
+
Both cost the same: one comparison window either way.
|
|
26
|
+
|
|
27
|
+
Four figures are derived from the ones above rather than measured separately:
|
|
28
|
+
|
|
29
|
+
| Figure | How it is arrived at |
|
|
30
|
+
|---|---|
|
|
31
|
+
| **Average order value** | Revenue over the orders that *carry an amount*, not over every counted order. Marked partial, naming both counts, whenever those differ. |
|
|
32
|
+
| **Visitors per order** | Vercel visitors over orders. Vercel because it is server-side and complete; GA4 sessions would flatter the ratio by roughly the shortfall. |
|
|
33
|
+
| **Orders after a send** | What the order book records in the three days following each email, ending early if another send lands first, so no order is counted twice. |
|
|
34
|
+
| **Revenue per 1,000 sent** | The same window's revenue scaled to a thousand addresses — per send it is fractions of a cent and every campaign renders as the same zero. |
|
|
35
|
+
|
|
36
|
+
The last two are **what happened next, not what the send caused**. They are built from send times and
|
|
37
|
+
order times, both of which are exact, rather than from GA4's campaign attribution, which at a
|
|
38
|
+
foundry's volumes is withheld for being too thin to divide revenue by.
|
|
23
39
|
|
|
24
40
|
---
|
|
25
41
|
|
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-CeB8esIA.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-CeB8esIA.mjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Data hook for the Studio panels.
|
|
@@ -51,6 +51,8 @@ interface UseReportOptions {
|
|
|
51
51
|
apiBaseUrl: string;
|
|
52
52
|
report: ReportName;
|
|
53
53
|
range: RangeKey;
|
|
54
|
+
/** Which baseline the deltas are measured against. Defaults to the preceding window. */
|
|
55
|
+
compare?: 'previous-period' | 'same-period-last-year';
|
|
54
56
|
/**
|
|
55
57
|
* Whether to fetch at all. Defaults to true.
|
|
56
58
|
*
|
|
@@ -64,7 +66,7 @@ interface UseReportOptions {
|
|
|
64
66
|
*
|
|
65
67
|
* @returns the current state plus a `reload` for manual refresh
|
|
66
68
|
*/
|
|
67
|
-
declare function useReport<T>({ apiBaseUrl, report, range, custom, enabled }: UseReportOptions): {
|
|
69
|
+
declare function useReport<T>({ apiBaseUrl, report, range, custom, compare, enabled }: UseReportOptions): {
|
|
68
70
|
state: ReportState<T>;
|
|
69
71
|
reload: () => void;
|
|
70
72
|
};
|
|
@@ -401,6 +403,34 @@ interface SortableTableProps<Row> {
|
|
|
401
403
|
*/
|
|
402
404
|
declare function SortableTable<Row>({ caption, columns, rows, rowKey, initialSort, filterPlaceholder, filterOn, exportName, truncatedNote, onExclude, }: SortableTableProps<Row>): React.ReactElement;
|
|
403
405
|
|
|
406
|
+
/**
|
|
407
|
+
* The series palette, and the rules that keep it honest.
|
|
408
|
+
*
|
|
409
|
+
* Every chart in this package was drawn in `currentColor` at varying alpha, so five series on one
|
|
410
|
+
* chart were five greys separated by dash pattern alone. That is legible in a screenshot and not
|
|
411
|
+
* in use: a reader tracking co-movement between GA4 and Vercel had to keep which-line-is-which in
|
|
412
|
+
* their head while looking at the shape.
|
|
413
|
+
*
|
|
414
|
+
* Three constraints decided these values, and all three are enforced by tests rather than asserted
|
|
415
|
+
* here:
|
|
416
|
+
*
|
|
417
|
+
* 1. ONE SET FOR BOTH THEMES. The Studio ships light and dark and this component is not told which
|
|
418
|
+
* it is in. Rather than swap palettes at a breakpoint we cannot observe, every colour clears
|
|
419
|
+
* 3:1 — the WCAG floor for a graphical object — against BOTH a white card and Sanity's dark
|
|
420
|
+
* card. That is what pins them to mid-lightness.
|
|
421
|
+
*
|
|
422
|
+
* 2. HUE FAMILY MEANS SOURCE. The two GA4 series are deliberately neighbours in the warm range,
|
|
423
|
+
* because they are the same instrument measuring two things; Vercel, orders and revenue each
|
|
424
|
+
* get their own family. So the palette carries a fact rather than just distinguishing rows.
|
|
425
|
+
*
|
|
426
|
+
* 3. COLOUR IS NEVER THE ONLY CHANNEL. Completeness stays encoded as a dash pattern and every
|
|
427
|
+
* value stays readable in the tooltip. The two GA4 hues are close enough to converge for a
|
|
428
|
+
* deuteranope — which is acceptable precisely because dash and label still separate them, and
|
|
429
|
+
* unacceptable for any pair that does not have that second channel.
|
|
430
|
+
*/
|
|
431
|
+
/** One series colour. The key names what it measures, not what it looks like. */
|
|
432
|
+
type SeriesKey = 'vercel' | 'ga4Pageviews' | 'ga4Sessions' | 'orders' | 'revenue';
|
|
433
|
+
|
|
404
434
|
/**
|
|
405
435
|
* One time axis, every source stacked against it.
|
|
406
436
|
*
|
|
@@ -431,15 +461,6 @@ interface SeriesPoint {
|
|
|
431
461
|
}
|
|
432
462
|
/** How a value should be written out. */
|
|
433
463
|
type SeriesUnit = 'count' | 'money' | 'percent';
|
|
434
|
-
/**
|
|
435
|
-
* One row of the chart: one answer, with what a lossier source saw underneath it.
|
|
436
|
-
*
|
|
437
|
-
* A row shows a SINGLE line by default. Two peer lines make the reader reconcile before they get
|
|
438
|
-
* an answer, and at a glance a foundry owner wants "how much", not "here are two measurements that
|
|
439
|
-
* disagree". The disagreement is still drawn — as a filled region, and in full on hover — because
|
|
440
|
-
* hiding it entirely would switch off the alarm: the 24 August collapse was visible precisely
|
|
441
|
-
* because two lines came apart.
|
|
442
|
-
*/
|
|
443
464
|
interface Series {
|
|
444
465
|
key: string;
|
|
445
466
|
label: string;
|
|
@@ -447,6 +468,15 @@ interface Series {
|
|
|
447
468
|
source: 'GA4' | 'Vercel' | 'Sanity' | 'Mailchimp';
|
|
448
469
|
/** Whether the line's source sees everything. Drives the stroke and the wording. */
|
|
449
470
|
complete: boolean;
|
|
471
|
+
/**
|
|
472
|
+
* Which series colour this row draws in.
|
|
473
|
+
*
|
|
474
|
+
* Optional, and falling back to the row's `source`, so a caller that adds a row gets a colour
|
|
475
|
+
* consistent with every other row from the same upstream rather than an uncoloured one. Set it
|
|
476
|
+
* explicitly only where the source does not decide the meaning — coverage is computed FROM GA4
|
|
477
|
+
* and Vercel and belongs to neither.
|
|
478
|
+
*/
|
|
479
|
+
color?: SeriesKey;
|
|
450
480
|
unit: SeriesUnit;
|
|
451
481
|
/**
|
|
452
482
|
* How the row is drawn. Defaults to `line`.
|
|
@@ -492,6 +522,8 @@ interface Series {
|
|
|
492
522
|
*/
|
|
493
523
|
shortfall?: {
|
|
494
524
|
label: string;
|
|
525
|
+
/** The lossier source's colour. Falls back to its `source`, as the row's own does. */
|
|
526
|
+
color?: SeriesKey;
|
|
495
527
|
source: Series['source'];
|
|
496
528
|
points: SeriesPoint[];
|
|
497
529
|
};
|
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-CeB8esIA.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-CeB8esIA.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Data hook for the Studio panels.
|
|
@@ -51,6 +51,8 @@ interface UseReportOptions {
|
|
|
51
51
|
apiBaseUrl: string;
|
|
52
52
|
report: ReportName;
|
|
53
53
|
range: RangeKey;
|
|
54
|
+
/** Which baseline the deltas are measured against. Defaults to the preceding window. */
|
|
55
|
+
compare?: 'previous-period' | 'same-period-last-year';
|
|
54
56
|
/**
|
|
55
57
|
* Whether to fetch at all. Defaults to true.
|
|
56
58
|
*
|
|
@@ -64,7 +66,7 @@ interface UseReportOptions {
|
|
|
64
66
|
*
|
|
65
67
|
* @returns the current state plus a `reload` for manual refresh
|
|
66
68
|
*/
|
|
67
|
-
declare function useReport<T>({ apiBaseUrl, report, range, custom, enabled }: UseReportOptions): {
|
|
69
|
+
declare function useReport<T>({ apiBaseUrl, report, range, custom, compare, enabled }: UseReportOptions): {
|
|
68
70
|
state: ReportState<T>;
|
|
69
71
|
reload: () => void;
|
|
70
72
|
};
|
|
@@ -401,6 +403,34 @@ interface SortableTableProps<Row> {
|
|
|
401
403
|
*/
|
|
402
404
|
declare function SortableTable<Row>({ caption, columns, rows, rowKey, initialSort, filterPlaceholder, filterOn, exportName, truncatedNote, onExclude, }: SortableTableProps<Row>): React.ReactElement;
|
|
403
405
|
|
|
406
|
+
/**
|
|
407
|
+
* The series palette, and the rules that keep it honest.
|
|
408
|
+
*
|
|
409
|
+
* Every chart in this package was drawn in `currentColor` at varying alpha, so five series on one
|
|
410
|
+
* chart were five greys separated by dash pattern alone. That is legible in a screenshot and not
|
|
411
|
+
* in use: a reader tracking co-movement between GA4 and Vercel had to keep which-line-is-which in
|
|
412
|
+
* their head while looking at the shape.
|
|
413
|
+
*
|
|
414
|
+
* Three constraints decided these values, and all three are enforced by tests rather than asserted
|
|
415
|
+
* here:
|
|
416
|
+
*
|
|
417
|
+
* 1. ONE SET FOR BOTH THEMES. The Studio ships light and dark and this component is not told which
|
|
418
|
+
* it is in. Rather than swap palettes at a breakpoint we cannot observe, every colour clears
|
|
419
|
+
* 3:1 — the WCAG floor for a graphical object — against BOTH a white card and Sanity's dark
|
|
420
|
+
* card. That is what pins them to mid-lightness.
|
|
421
|
+
*
|
|
422
|
+
* 2. HUE FAMILY MEANS SOURCE. The two GA4 series are deliberately neighbours in the warm range,
|
|
423
|
+
* because they are the same instrument measuring two things; Vercel, orders and revenue each
|
|
424
|
+
* get their own family. So the palette carries a fact rather than just distinguishing rows.
|
|
425
|
+
*
|
|
426
|
+
* 3. COLOUR IS NEVER THE ONLY CHANNEL. Completeness stays encoded as a dash pattern and every
|
|
427
|
+
* value stays readable in the tooltip. The two GA4 hues are close enough to converge for a
|
|
428
|
+
* deuteranope — which is acceptable precisely because dash and label still separate them, and
|
|
429
|
+
* unacceptable for any pair that does not have that second channel.
|
|
430
|
+
*/
|
|
431
|
+
/** One series colour. The key names what it measures, not what it looks like. */
|
|
432
|
+
type SeriesKey = 'vercel' | 'ga4Pageviews' | 'ga4Sessions' | 'orders' | 'revenue';
|
|
433
|
+
|
|
404
434
|
/**
|
|
405
435
|
* One time axis, every source stacked against it.
|
|
406
436
|
*
|
|
@@ -431,15 +461,6 @@ interface SeriesPoint {
|
|
|
431
461
|
}
|
|
432
462
|
/** How a value should be written out. */
|
|
433
463
|
type SeriesUnit = 'count' | 'money' | 'percent';
|
|
434
|
-
/**
|
|
435
|
-
* One row of the chart: one answer, with what a lossier source saw underneath it.
|
|
436
|
-
*
|
|
437
|
-
* A row shows a SINGLE line by default. Two peer lines make the reader reconcile before they get
|
|
438
|
-
* an answer, and at a glance a foundry owner wants "how much", not "here are two measurements that
|
|
439
|
-
* disagree". The disagreement is still drawn — as a filled region, and in full on hover — because
|
|
440
|
-
* hiding it entirely would switch off the alarm: the 24 August collapse was visible precisely
|
|
441
|
-
* because two lines came apart.
|
|
442
|
-
*/
|
|
443
464
|
interface Series {
|
|
444
465
|
key: string;
|
|
445
466
|
label: string;
|
|
@@ -447,6 +468,15 @@ interface Series {
|
|
|
447
468
|
source: 'GA4' | 'Vercel' | 'Sanity' | 'Mailchimp';
|
|
448
469
|
/** Whether the line's source sees everything. Drives the stroke and the wording. */
|
|
449
470
|
complete: boolean;
|
|
471
|
+
/**
|
|
472
|
+
* Which series colour this row draws in.
|
|
473
|
+
*
|
|
474
|
+
* Optional, and falling back to the row's `source`, so a caller that adds a row gets a colour
|
|
475
|
+
* consistent with every other row from the same upstream rather than an uncoloured one. Set it
|
|
476
|
+
* explicitly only where the source does not decide the meaning — coverage is computed FROM GA4
|
|
477
|
+
* and Vercel and belongs to neither.
|
|
478
|
+
*/
|
|
479
|
+
color?: SeriesKey;
|
|
450
480
|
unit: SeriesUnit;
|
|
451
481
|
/**
|
|
452
482
|
* How the row is drawn. Defaults to `line`.
|
|
@@ -492,6 +522,8 @@ interface Series {
|
|
|
492
522
|
*/
|
|
493
523
|
shortfall?: {
|
|
494
524
|
label: string;
|
|
525
|
+
/** The lossier source's colour. Falls back to its `source`, as the row's own does. */
|
|
526
|
+
color?: SeriesKey;
|
|
495
527
|
source: Series['source'];
|
|
496
528
|
points: SeriesPoint[];
|
|
497
529
|
};
|