@liiift-studio/sanity-visitor-insights 0.29.0 → 0.30.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 +68 -9
- package/dist/index.d.ts +68 -9
- package/dist/index.js +169 -57
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +166 -57
- package/dist/index.mjs.map +1 -1
- package/dist/{ranges-D1A9JY7h.d.mts → ranges-B3BWyG3q.d.mts} +12 -4
- package/dist/{ranges-D1A9JY7h.d.ts → ranges-B3BWyG3q.d.ts} +12 -4
- package/dist/server.d.mts +2 -2
- package/dist/server.d.ts +2 -2
- package/dist/server.js +6 -5
- package/dist/server.js.map +1 -1
- package/dist/server.mjs +6 -5
- package/dist/server.mjs.map +1 -1
- package/package.json +1 -1
- package/src/core/capture.ts +13 -5
- package/src/index.ts +1 -0
- package/src/server/reports/acquisition.ts +22 -8
- package/src/server/reports/measurementHealth.ts +2 -1
- package/src/studio/CrossSourceTimeline.tsx +126 -65
- package/src/studio/VisitorInsightsTool.tsx +39 -4
- package/src/studio/panels.test.tsx +156 -6
- package/src/studio/panels.tsx +54 -6
- package/src/studio/urlState.ts +118 -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-B3BWyG3q.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-B3BWyG3q.mjs';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Data hook for the Studio panels.
|
|
@@ -100,6 +100,62 @@ interface VisitorInsightsToolComponentProps extends Partial<VisitorInsightsToolP
|
|
|
100
100
|
}
|
|
101
101
|
declare function VisitorInsightsTool(props: VisitorInsightsToolComponentProps): React.ReactElement;
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* The tool's view, in the URL — so a finding can be sent to someone.
|
|
105
|
+
*
|
|
106
|
+
* Every piece of state here was component-local: tab, range, and the custom window. A Studio
|
|
107
|
+
* reload, a browser back, or an accidental navigation discarded the whole investigation, and
|
|
108
|
+
* "Acquisition, 24 August to 7 September" — the exact view in which someone notices the thing worth
|
|
109
|
+
* noticing — could not be handed to a colleague or returned to by the person who found it. There is
|
|
110
|
+
* a "Copy as CSV" in this tool precisely because sharing a finding is the real task; the view
|
|
111
|
+
* itself was the one thing that could not be shared.
|
|
112
|
+
*
|
|
113
|
+
* The HASH, not the path or the query. Sanity's own router owns the path, and writing to it would
|
|
114
|
+
* fight the Studio for control of navigation; the hash is unclaimed, survives a reload, is carried
|
|
115
|
+
* by a copied link, and is ignored by every server. The value is namespaced because other tools in
|
|
116
|
+
* the same Studio may want the same trick.
|
|
117
|
+
*
|
|
118
|
+
* Everything here is a pure string transformation, so it is testable without a DOM — which matters,
|
|
119
|
+
* because the alternative is a feature whose only proof is clicking around a deployed Studio.
|
|
120
|
+
*/
|
|
121
|
+
/** The part of the tool's state worth putting in a link. */
|
|
122
|
+
interface ViewState {
|
|
123
|
+
/** Tab id, e.g. `overview`. */
|
|
124
|
+
tab?: string;
|
|
125
|
+
/** Range key, e.g. `week` or `custom`. */
|
|
126
|
+
range?: string;
|
|
127
|
+
/** Custom window start, ISO date. Only meaningful when `range` is `custom`. */
|
|
128
|
+
from?: string;
|
|
129
|
+
/** Custom window end, ISO date. */
|
|
130
|
+
to?: string;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Serialise a view to the hash fragment it should occupy.
|
|
134
|
+
*
|
|
135
|
+
* Returns an empty string for an empty view, so a default view leaves the URL clean rather than
|
|
136
|
+
* decorating it with state nobody set.
|
|
137
|
+
*
|
|
138
|
+
* @param view - the state to encode
|
|
139
|
+
*/
|
|
140
|
+
declare function encodeView(view: ViewState): string;
|
|
141
|
+
/**
|
|
142
|
+
* Read a view out of a hash fragment.
|
|
143
|
+
*
|
|
144
|
+
* Every field is validated and an unrecognised one is dropped rather than defaulted, so a truncated
|
|
145
|
+
* or hand-edited link degrades to whatever it could parse instead of failing or, worse, silently
|
|
146
|
+
* requesting a window nobody chose.
|
|
147
|
+
*
|
|
148
|
+
* @param hash - `window.location.hash`, with or without its leading `#`
|
|
149
|
+
*/
|
|
150
|
+
declare function decodeView(hash: string): ViewState;
|
|
151
|
+
/**
|
|
152
|
+
* Replace our fragment in a hash, preserving anyone else's.
|
|
153
|
+
*
|
|
154
|
+
* @param hash - the current hash
|
|
155
|
+
* @param view - the state to write
|
|
156
|
+
*/
|
|
157
|
+
declare function mergeIntoHash(hash: string, view: ViewState): string;
|
|
158
|
+
|
|
103
159
|
/**
|
|
104
160
|
* Shared renderers for metric values and comparison bars.
|
|
105
161
|
*
|
|
@@ -426,6 +482,15 @@ interface Series {
|
|
|
426
482
|
* event, on a date, of a size. Days with nothing draw nothing, which is the truth.
|
|
427
483
|
*/
|
|
428
484
|
mark?: 'line' | 'events';
|
|
485
|
+
/**
|
|
486
|
+
* A fixed y domain, instead of scaling the row to its own peak.
|
|
487
|
+
*
|
|
488
|
+
* Small multiples normally scale each row to itself, which is right for quantities whose
|
|
489
|
+
* absolute size is not comparable between rows. It is wrong for a proportion: a coverage row
|
|
490
|
+
* auto-scaled to its own maximum would redraw 100% at whatever the best day happened to be, so
|
|
491
|
+
* a site running at a flat 20% would show a full-height line and read as healthy.
|
|
492
|
+
*/
|
|
493
|
+
domain?: [number, number];
|
|
429
494
|
points: SeriesPoint[];
|
|
430
495
|
/**
|
|
431
496
|
* What a lossier source saw of the same thing.
|
|
@@ -465,12 +530,6 @@ interface CrossSourceTimelineProps {
|
|
|
465
530
|
*/
|
|
466
531
|
onBrush?: (start: string, end: string) => void;
|
|
467
532
|
}
|
|
468
|
-
/**
|
|
469
|
-
* The cross-source timeline.
|
|
470
|
-
*
|
|
471
|
-
* Renders nothing rather than an empty frame when there is not enough to plot — two points cannot
|
|
472
|
-
* show a shape, and an axis with one dot on it invites a reading it cannot support.
|
|
473
|
-
*/
|
|
474
533
|
declare function CrossSourceTimeline({ series, markers, currency, onBrush }: CrossSourceTimelineProps): React.ReactElement | null;
|
|
475
534
|
|
|
476
535
|
/**
|
|
@@ -579,4 +638,4 @@ interface VisitorInsightsPluginOptions {
|
|
|
579
638
|
*/
|
|
580
639
|
declare const visitorInsights: sanity.Plugin<VisitorInsightsPluginOptions>;
|
|
581
640
|
|
|
582
|
-
export { AcquisitionData, AcquisitionPanel, ChartData, type ChartDataProps, ComparisonBar, CrossSourceTimeline, type CrossSourceTimelineProps, DataHealthPanel, Delta, type DeltaProps, DiagnosticReport, DiagnosticsPanel, FunnelChart, JourneyData, JourneyPanel, MeasurementHealthData, DataHealthPanel as MeasurementHealthPanel, MetricFigure, MetricValue, NoticeList, OverviewPanel, type ProportionBar, ProportionChart, type ProportionChartProps, RangeKey, ReportEnvelope, ReportName, type ReportState, type Series, type SeriesPoint, type SortColumn, SortableTable, type SortableTableProps, type TimelineMarker, TrendChart, TypefaceInterestData, TypefaceInterestPanel, type UseReportOptions, type VisitorInsightsPluginOptions, VisitorInsightsTool, type VisitorInsightsToolProps, visitorInsights as default, formatCount, formatMoney, formatPercent, knownShortfall, useReport, visitorInsights };
|
|
641
|
+
export { AcquisitionData, AcquisitionPanel, ChartData, type ChartDataProps, ComparisonBar, CrossSourceTimeline, type CrossSourceTimelineProps, DataHealthPanel, Delta, type DeltaProps, DiagnosticReport, DiagnosticsPanel, FunnelChart, JourneyData, JourneyPanel, MeasurementHealthData, DataHealthPanel as MeasurementHealthPanel, MetricFigure, MetricValue, NoticeList, OverviewPanel, type ProportionBar, ProportionChart, type ProportionChartProps, RangeKey, ReportEnvelope, ReportName, type ReportState, type Series, type SeriesPoint, type SortColumn, SortableTable, type SortableTableProps, type TimelineMarker, TrendChart, TypefaceInterestData, TypefaceInterestPanel, type UseReportOptions, type ViewState, type VisitorInsightsPluginOptions, VisitorInsightsTool, type VisitorInsightsToolProps, decodeView, visitorInsights as default, encodeView, formatCount, formatMoney, formatPercent, knownShortfall, mergeIntoHash, useReport, visitorInsights };
|
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-B3BWyG3q.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-B3BWyG3q.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Data hook for the Studio panels.
|
|
@@ -100,6 +100,62 @@ interface VisitorInsightsToolComponentProps extends Partial<VisitorInsightsToolP
|
|
|
100
100
|
}
|
|
101
101
|
declare function VisitorInsightsTool(props: VisitorInsightsToolComponentProps): React.ReactElement;
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* The tool's view, in the URL — so a finding can be sent to someone.
|
|
105
|
+
*
|
|
106
|
+
* Every piece of state here was component-local: tab, range, and the custom window. A Studio
|
|
107
|
+
* reload, a browser back, or an accidental navigation discarded the whole investigation, and
|
|
108
|
+
* "Acquisition, 24 August to 7 September" — the exact view in which someone notices the thing worth
|
|
109
|
+
* noticing — could not be handed to a colleague or returned to by the person who found it. There is
|
|
110
|
+
* a "Copy as CSV" in this tool precisely because sharing a finding is the real task; the view
|
|
111
|
+
* itself was the one thing that could not be shared.
|
|
112
|
+
*
|
|
113
|
+
* The HASH, not the path or the query. Sanity's own router owns the path, and writing to it would
|
|
114
|
+
* fight the Studio for control of navigation; the hash is unclaimed, survives a reload, is carried
|
|
115
|
+
* by a copied link, and is ignored by every server. The value is namespaced because other tools in
|
|
116
|
+
* the same Studio may want the same trick.
|
|
117
|
+
*
|
|
118
|
+
* Everything here is a pure string transformation, so it is testable without a DOM — which matters,
|
|
119
|
+
* because the alternative is a feature whose only proof is clicking around a deployed Studio.
|
|
120
|
+
*/
|
|
121
|
+
/** The part of the tool's state worth putting in a link. */
|
|
122
|
+
interface ViewState {
|
|
123
|
+
/** Tab id, e.g. `overview`. */
|
|
124
|
+
tab?: string;
|
|
125
|
+
/** Range key, e.g. `week` or `custom`. */
|
|
126
|
+
range?: string;
|
|
127
|
+
/** Custom window start, ISO date. Only meaningful when `range` is `custom`. */
|
|
128
|
+
from?: string;
|
|
129
|
+
/** Custom window end, ISO date. */
|
|
130
|
+
to?: string;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Serialise a view to the hash fragment it should occupy.
|
|
134
|
+
*
|
|
135
|
+
* Returns an empty string for an empty view, so a default view leaves the URL clean rather than
|
|
136
|
+
* decorating it with state nobody set.
|
|
137
|
+
*
|
|
138
|
+
* @param view - the state to encode
|
|
139
|
+
*/
|
|
140
|
+
declare function encodeView(view: ViewState): string;
|
|
141
|
+
/**
|
|
142
|
+
* Read a view out of a hash fragment.
|
|
143
|
+
*
|
|
144
|
+
* Every field is validated and an unrecognised one is dropped rather than defaulted, so a truncated
|
|
145
|
+
* or hand-edited link degrades to whatever it could parse instead of failing or, worse, silently
|
|
146
|
+
* requesting a window nobody chose.
|
|
147
|
+
*
|
|
148
|
+
* @param hash - `window.location.hash`, with or without its leading `#`
|
|
149
|
+
*/
|
|
150
|
+
declare function decodeView(hash: string): ViewState;
|
|
151
|
+
/**
|
|
152
|
+
* Replace our fragment in a hash, preserving anyone else's.
|
|
153
|
+
*
|
|
154
|
+
* @param hash - the current hash
|
|
155
|
+
* @param view - the state to write
|
|
156
|
+
*/
|
|
157
|
+
declare function mergeIntoHash(hash: string, view: ViewState): string;
|
|
158
|
+
|
|
103
159
|
/**
|
|
104
160
|
* Shared renderers for metric values and comparison bars.
|
|
105
161
|
*
|
|
@@ -426,6 +482,15 @@ interface Series {
|
|
|
426
482
|
* event, on a date, of a size. Days with nothing draw nothing, which is the truth.
|
|
427
483
|
*/
|
|
428
484
|
mark?: 'line' | 'events';
|
|
485
|
+
/**
|
|
486
|
+
* A fixed y domain, instead of scaling the row to its own peak.
|
|
487
|
+
*
|
|
488
|
+
* Small multiples normally scale each row to itself, which is right for quantities whose
|
|
489
|
+
* absolute size is not comparable between rows. It is wrong for a proportion: a coverage row
|
|
490
|
+
* auto-scaled to its own maximum would redraw 100% at whatever the best day happened to be, so
|
|
491
|
+
* a site running at a flat 20% would show a full-height line and read as healthy.
|
|
492
|
+
*/
|
|
493
|
+
domain?: [number, number];
|
|
429
494
|
points: SeriesPoint[];
|
|
430
495
|
/**
|
|
431
496
|
* What a lossier source saw of the same thing.
|
|
@@ -465,12 +530,6 @@ interface CrossSourceTimelineProps {
|
|
|
465
530
|
*/
|
|
466
531
|
onBrush?: (start: string, end: string) => void;
|
|
467
532
|
}
|
|
468
|
-
/**
|
|
469
|
-
* The cross-source timeline.
|
|
470
|
-
*
|
|
471
|
-
* Renders nothing rather than an empty frame when there is not enough to plot — two points cannot
|
|
472
|
-
* show a shape, and an axis with one dot on it invites a reading it cannot support.
|
|
473
|
-
*/
|
|
474
533
|
declare function CrossSourceTimeline({ series, markers, currency, onBrush }: CrossSourceTimelineProps): React.ReactElement | null;
|
|
475
534
|
|
|
476
535
|
/**
|
|
@@ -579,4 +638,4 @@ interface VisitorInsightsPluginOptions {
|
|
|
579
638
|
*/
|
|
580
639
|
declare const visitorInsights: sanity.Plugin<VisitorInsightsPluginOptions>;
|
|
581
640
|
|
|
582
|
-
export { AcquisitionData, AcquisitionPanel, ChartData, type ChartDataProps, ComparisonBar, CrossSourceTimeline, type CrossSourceTimelineProps, DataHealthPanel, Delta, type DeltaProps, DiagnosticReport, DiagnosticsPanel, FunnelChart, JourneyData, JourneyPanel, MeasurementHealthData, DataHealthPanel as MeasurementHealthPanel, MetricFigure, MetricValue, NoticeList, OverviewPanel, type ProportionBar, ProportionChart, type ProportionChartProps, RangeKey, ReportEnvelope, ReportName, type ReportState, type Series, type SeriesPoint, type SortColumn, SortableTable, type SortableTableProps, type TimelineMarker, TrendChart, TypefaceInterestData, TypefaceInterestPanel, type UseReportOptions, type VisitorInsightsPluginOptions, VisitorInsightsTool, type VisitorInsightsToolProps, visitorInsights as default, formatCount, formatMoney, formatPercent, knownShortfall, useReport, visitorInsights };
|
|
641
|
+
export { AcquisitionData, AcquisitionPanel, ChartData, type ChartDataProps, ComparisonBar, CrossSourceTimeline, type CrossSourceTimelineProps, DataHealthPanel, Delta, type DeltaProps, DiagnosticReport, DiagnosticsPanel, FunnelChart, JourneyData, JourneyPanel, MeasurementHealthData, DataHealthPanel as MeasurementHealthPanel, MetricFigure, MetricValue, NoticeList, OverviewPanel, type ProportionBar, ProportionChart, type ProportionChartProps, RangeKey, ReportEnvelope, ReportName, type ReportState, type Series, type SeriesPoint, type SortColumn, SortableTable, type SortableTableProps, type TimelineMarker, TrendChart, TypefaceInterestData, TypefaceInterestPanel, type UseReportOptions, type ViewState, type VisitorInsightsPluginOptions, VisitorInsightsTool, type VisitorInsightsToolProps, decodeView, visitorInsights as default, encodeView, formatCount, formatMoney, formatPercent, knownShortfall, mergeIntoHash, useReport, visitorInsights };
|
package/dist/index.js
CHANGED
|
@@ -51,13 +51,16 @@ __export(index_exports, {
|
|
|
51
51
|
TypefaceInterestPanel: () => TypefaceInterestPanel,
|
|
52
52
|
VisitorInsightsTool: () => VisitorInsightsTool,
|
|
53
53
|
coverageForRange: () => coverageForRange,
|
|
54
|
+
decodeView: () => decodeView,
|
|
54
55
|
default: () => index_default,
|
|
56
|
+
encodeView: () => encodeView,
|
|
55
57
|
estimated: () => estimated,
|
|
56
58
|
formatCount: () => formatCount,
|
|
57
59
|
formatMoney: () => formatMoney,
|
|
58
60
|
formatPercent: () => formatPercent,
|
|
59
61
|
isReportName: () => isReportName,
|
|
60
62
|
knownShortfall: () => knownShortfall,
|
|
63
|
+
mergeIntoHash: () => mergeIntoHash,
|
|
61
64
|
ok: () => ok,
|
|
62
65
|
partial: () => partial,
|
|
63
66
|
previousRange: () => previousRange,
|
|
@@ -167,6 +170,50 @@ function useReport({ apiBaseUrl, report, range, custom, enabled = true }) {
|
|
|
167
170
|
return { state, reload };
|
|
168
171
|
}
|
|
169
172
|
|
|
173
|
+
// src/studio/urlState.ts
|
|
174
|
+
var KEY = "insights";
|
|
175
|
+
var ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
176
|
+
var IDENTIFIER = /^[a-z][a-z0-9-]{0,31}$/;
|
|
177
|
+
function encodeView(view) {
|
|
178
|
+
const parts = [];
|
|
179
|
+
if (view.tab && IDENTIFIER.test(view.tab)) parts.push(`tab:${view.tab}`);
|
|
180
|
+
if (view.range && IDENTIFIER.test(view.range)) parts.push(`range:${view.range}`);
|
|
181
|
+
if (view.range === "custom") {
|
|
182
|
+
if (view.from && ISO_DATE.test(view.from)) parts.push(`from:${view.from}`);
|
|
183
|
+
if (view.to && ISO_DATE.test(view.to)) parts.push(`to:${view.to}`);
|
|
184
|
+
}
|
|
185
|
+
return parts.length > 0 ? `${KEY}=${parts.join(";")}` : "";
|
|
186
|
+
}
|
|
187
|
+
function decodeView(hash) {
|
|
188
|
+
const raw = hash.startsWith("#") ? hash.slice(1) : hash;
|
|
189
|
+
const mine = raw.split("&").find((part) => part.startsWith(`${KEY}=`));
|
|
190
|
+
if (!mine) return {};
|
|
191
|
+
const view = {};
|
|
192
|
+
for (const pair of mine.slice(KEY.length + 1).split(";")) {
|
|
193
|
+
const separator = pair.indexOf(":");
|
|
194
|
+
if (separator < 1) continue;
|
|
195
|
+
const name = pair.slice(0, separator);
|
|
196
|
+
const value = pair.slice(separator + 1);
|
|
197
|
+
if (name === "tab" && IDENTIFIER.test(value)) view.tab = value;
|
|
198
|
+
else if (name === "range" && IDENTIFIER.test(value)) view.range = value;
|
|
199
|
+
else if (name === "from" && ISO_DATE.test(value)) view.from = value;
|
|
200
|
+
else if (name === "to" && ISO_DATE.test(value)) view.to = value;
|
|
201
|
+
}
|
|
202
|
+
if (view.range === "custom" && !(view.from && view.to)) {
|
|
203
|
+
delete view.from;
|
|
204
|
+
delete view.to;
|
|
205
|
+
delete view.range;
|
|
206
|
+
}
|
|
207
|
+
return view;
|
|
208
|
+
}
|
|
209
|
+
function mergeIntoHash(hash, view) {
|
|
210
|
+
const raw = hash.startsWith("#") ? hash.slice(1) : hash;
|
|
211
|
+
const others = raw.split("&").filter((part) => part.length > 0 && !part.startsWith(`${KEY}=`));
|
|
212
|
+
const mine = encodeView(view);
|
|
213
|
+
const all = mine ? [...others, mine] : others;
|
|
214
|
+
return all.length > 0 ? `#${all.join("&")}` : "";
|
|
215
|
+
}
|
|
216
|
+
|
|
170
217
|
// src/core/ranges.ts
|
|
171
218
|
function formatInTimeZone(date, timeZone) {
|
|
172
219
|
const parts = new Intl.DateTimeFormat("en-CA", {
|
|
@@ -1092,44 +1139,57 @@ function findCoverageIncident(days, dates) {
|
|
|
1092
1139
|
const measured = days.filter((d) => d !== null);
|
|
1093
1140
|
if (measured.length < 21) return null;
|
|
1094
1141
|
const sorted = [...measured].sort((a, b) => a - b);
|
|
1095
|
-
const normal = sorted[Math.floor(sorted.length
|
|
1142
|
+
const normal = sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.75))];
|
|
1096
1143
|
if (normal <= 0.1) return null;
|
|
1097
1144
|
const threshold = normal * 0.5;
|
|
1098
1145
|
const MIN_RUN = 3;
|
|
1099
|
-
|
|
1146
|
+
const MAX_GAP = 2;
|
|
1147
|
+
const runs = [];
|
|
1100
1148
|
let runStart = null;
|
|
1149
|
+
let gap = 0;
|
|
1101
1150
|
for (let i = 0; i <= days.length; i++) {
|
|
1102
|
-
const value = days[i];
|
|
1103
|
-
const low =
|
|
1104
|
-
|
|
1105
|
-
if (
|
|
1106
|
-
|
|
1107
|
-
|
|
1151
|
+
const value = i < days.length ? days[i] : void 0;
|
|
1152
|
+
const low = value !== null && value !== void 0 && value < threshold;
|
|
1153
|
+
const unmeasured = i < days.length && (value === null || value === void 0);
|
|
1154
|
+
if (low) {
|
|
1155
|
+
if (runStart === null) runStart = i;
|
|
1156
|
+
gap = 0;
|
|
1157
|
+
} else if (unmeasured && runStart !== null && gap < MAX_GAP) {
|
|
1158
|
+
gap += 1;
|
|
1159
|
+
} else if (runStart !== null) {
|
|
1160
|
+
runs.push({ start: runStart, end: i - gap });
|
|
1108
1161
|
runStart = null;
|
|
1162
|
+
gap = 0;
|
|
1109
1163
|
}
|
|
1110
1164
|
}
|
|
1111
|
-
|
|
1112
|
-
const during = days.slice(best.start, best.end).filter((d) => d !== null);
|
|
1113
|
-
const before = days.slice(0, best.start).filter((d) => d !== null);
|
|
1114
|
-
if (before.length < 3 || during.length === 0) return null;
|
|
1165
|
+
runs.sort((a, b) => b.end - b.start - (a.end - a.start));
|
|
1115
1166
|
const mean = (values) => values.reduce((sum, v) => sum + v, 0) / values.length;
|
|
1116
|
-
const
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1167
|
+
for (const run of runs) {
|
|
1168
|
+
if (run.end - run.start < MIN_RUN) continue;
|
|
1169
|
+
const during = days.slice(run.start, run.end).filter((d) => d !== null);
|
|
1170
|
+
const before = days.slice(0, run.start).filter((d) => d !== null);
|
|
1171
|
+
if (before.length < 3 || during.length === 0) continue;
|
|
1172
|
+
const onset = dates[run.start];
|
|
1173
|
+
if (!onset) continue;
|
|
1174
|
+
const beforeMean = mean(before);
|
|
1175
|
+
const after = days.slice(run.end).filter((d) => d !== null);
|
|
1176
|
+
const recovered = after.length >= MIN_RUN && mean(after) >= beforeMean * 0.8;
|
|
1177
|
+
return {
|
|
1178
|
+
onset,
|
|
1179
|
+
days: run.end - run.start,
|
|
1180
|
+
before: beforeMean,
|
|
1181
|
+
during: mean(during),
|
|
1182
|
+
ongoing: !recovered
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
return null;
|
|
1127
1186
|
}
|
|
1128
1187
|
function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
1129
1188
|
const [hoverIndex, setHoverIndex] = (0, import_react3.useState)(null);
|
|
1130
1189
|
const [pinned, setPinned] = (0, import_react3.useState)(false);
|
|
1131
1190
|
const [brushAnchor, setBrushAnchor] = (0, import_react3.useState)(null);
|
|
1132
1191
|
const [brushed, setBrushed] = (0, import_react3.useState)(null);
|
|
1192
|
+
const instanceId = import_react3.default.useId().replace(/[^a-zA-Z0-9]/g, "");
|
|
1133
1193
|
const [steppedByKeyboard, setSteppedByKeyboard] = (0, import_react3.useState)(false);
|
|
1134
1194
|
const [tooShort, setTooShort] = (0, import_react3.useState)(false);
|
|
1135
1195
|
const [measured, setMeasured] = (0, import_react3.useState)(WIDTH);
|
|
@@ -1337,13 +1397,34 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1337
1397
|
}
|
|
1338
1398
|
},
|
|
1339
1399
|
children: [
|
|
1400
|
+
tickIndexes.map((index) => {
|
|
1401
|
+
const date = dates[index];
|
|
1402
|
+
if (!date) return null;
|
|
1403
|
+
const at = x(/* @__PURE__ */ new Date(`${date}T00:00:00Z`));
|
|
1404
|
+
if (!Number.isFinite(at)) return null;
|
|
1405
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1406
|
+
"line",
|
|
1407
|
+
{
|
|
1408
|
+
x1: at,
|
|
1409
|
+
x2: at,
|
|
1410
|
+
y1: TOP_PAD,
|
|
1411
|
+
y2: TOP_PAD + series.length * ROW_HEIGHT - 18,
|
|
1412
|
+
stroke: "currentColor",
|
|
1413
|
+
strokeWidth: 1,
|
|
1414
|
+
opacity: 0.08
|
|
1415
|
+
},
|
|
1416
|
+
`grid-${date}`
|
|
1417
|
+
);
|
|
1418
|
+
}),
|
|
1340
1419
|
series.map((row, rowIndex) => {
|
|
1341
1420
|
const top = TOP_PAD + rowIndex * ROW_HEIGHT;
|
|
1342
1421
|
const bottom = top + ROW_HEIGHT - 18;
|
|
1343
1422
|
const plotTop = top + LABEL_STRIP;
|
|
1344
1423
|
const values = row.points.map((p) => p.value).filter((v) => v !== null);
|
|
1345
1424
|
const peak = (0, import_d3_array.max)(values) ?? 0;
|
|
1346
|
-
const
|
|
1425
|
+
const lowest = Math.min(0, ...values);
|
|
1426
|
+
const y = row.domain ? (0, import_d3_scale.scaleLinear)().domain(row.domain).range([bottom, plotTop]) : (0, import_d3_scale.scaleLinear)().domain([lowest, peak || 1]).nice().range([bottom, plotTop]);
|
|
1427
|
+
const zeroY = y(0);
|
|
1347
1428
|
const at = (p) => x(/* @__PURE__ */ new Date(`${p.date}T00:00:00Z`));
|
|
1348
1429
|
const defined = (p) => p.value !== null;
|
|
1349
1430
|
const lineGen = (0, import_d3_shape.line)().defined(defined).x(at).y((p) => y(p.value)).curve(import_d3_shape.curveMonotoneX);
|
|
@@ -1357,10 +1438,10 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1357
1438
|
const zeroes = row.mark === "events" && pitch >= 6 ? row.points.filter((p) => p.value === 0) : [];
|
|
1358
1439
|
const gap = row.shortfall ? gapGen(row.points) ?? "" : "";
|
|
1359
1440
|
const shortfallLine = row.shortfall && revealed ? lineGen(shortfallPoints) ?? "" : "";
|
|
1360
|
-
const clipId =
|
|
1441
|
+
const clipId = `${instanceId}-row-${rowIndex}`;
|
|
1361
1442
|
return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("g", { children: [
|
|
1362
1443
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("clipPath", { id: clipId, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("rect", { x: GUTTER, y: plotTop, width: plotWidth, height: Math.max(1, bottom - plotTop) }) }) }),
|
|
1363
|
-
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: GUTTER, x2: GUTTER + plotWidth, y1:
|
|
1444
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("line", { x1: GUTTER, x2: GUTTER + plotWidth, y1: zeroY, y2: zeroY, stroke: "currentColor", strokeWidth: 1, opacity: 0.45 }),
|
|
1364
1445
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)("text", { x: GUTTER + 4, y: top + 11, fontSize: AXIS_TYPE, fill: "currentColor", opacity: 0.85, fontWeight: 500, children: row.label }),
|
|
1365
1446
|
/* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("text", { x: GUTTER + plotWidth, y: top + 11, textAnchor: "end", fontSize: AXIS_TYPE, fill: "currentColor", opacity: 0.72, children: [
|
|
1366
1447
|
row.source,
|
|
@@ -1378,7 +1459,7 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1378
1459
|
"rect",
|
|
1379
1460
|
{
|
|
1380
1461
|
x: Math.max(GUTTER, Math.min(GUTTER + plotWidth - stemWidth, cx - stemWidth / 2)),
|
|
1381
|
-
y:
|
|
1462
|
+
y: zeroY - 3,
|
|
1382
1463
|
width: stemWidth,
|
|
1383
1464
|
height: 3,
|
|
1384
1465
|
fill: "currentColor",
|
|
@@ -1395,9 +1476,9 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1395
1476
|
"rect",
|
|
1396
1477
|
{
|
|
1397
1478
|
x: Math.max(GUTTER, Math.min(GUTTER + plotWidth - stemWidth, cx - stemWidth / 2)),
|
|
1398
|
-
y: Math.min(headY,
|
|
1479
|
+
y: Math.min(headY, zeroY),
|
|
1399
1480
|
width: stemWidth,
|
|
1400
|
-
height: Math.max(1.5, Math.abs(
|
|
1481
|
+
height: Math.max(1.5, Math.abs(zeroY - headY)),
|
|
1401
1482
|
fill: "currentColor",
|
|
1402
1483
|
opacity: 0.75
|
|
1403
1484
|
},
|
|
@@ -1474,25 +1555,6 @@ function CrossSourceTimeline({ series, markers = [], currency, onBrush }) {
|
|
|
1474
1555
|
opacity: 0.8
|
|
1475
1556
|
}
|
|
1476
1557
|
),
|
|
1477
|
-
tickIndexes.map((index) => {
|
|
1478
|
-
const date = dates[index];
|
|
1479
|
-
if (!date) return null;
|
|
1480
|
-
const at = x(/* @__PURE__ */ new Date(`${date}T00:00:00Z`));
|
|
1481
|
-
if (!Number.isFinite(at)) return null;
|
|
1482
|
-
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
1483
|
-
"line",
|
|
1484
|
-
{
|
|
1485
|
-
x1: at,
|
|
1486
|
-
x2: at,
|
|
1487
|
-
y1: TOP_PAD,
|
|
1488
|
-
y2: height - AXIS_HEIGHT,
|
|
1489
|
-
stroke: "currentColor",
|
|
1490
|
-
strokeWidth: 1,
|
|
1491
|
-
opacity: 0.08
|
|
1492
|
-
},
|
|
1493
|
-
`grid-${date}`
|
|
1494
|
-
);
|
|
1495
|
-
}),
|
|
1496
1558
|
tickIndexes.map((index, position) => {
|
|
1497
1559
|
const date = dates[index];
|
|
1498
1560
|
if (!date) return null;
|
|
@@ -1673,7 +1735,7 @@ function OverviewPanel({ data, previous, onBrush }) {
|
|
|
1673
1735
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: metricOr(data.vercelPageviews, OLDER_ROUTE), label: "Pageviews" }),
|
|
1674
1736
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(Delta, { current: metricSortValue(data.vercelPageviews), previous: metricSortValue(previous?.vercelPageviews) })
|
|
1675
1737
|
] }),
|
|
1676
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: "Pageviews,
|
|
1738
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: "Pageviews, from Vercel\u2019s own counter." })
|
|
1677
1739
|
] }) }),
|
|
1678
1740
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Card, { padding: 3, radius: 2, tone: "transparent", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
|
|
1679
1741
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "Mailing list, total" }),
|
|
@@ -1723,6 +1785,37 @@ function OverviewPanel({ data, previous, onBrush }) {
|
|
|
1723
1785
|
points: (data.crossSource ?? []).map((d) => ({ date: d.date, value: d.ga4Pageviews }))
|
|
1724
1786
|
}
|
|
1725
1787
|
},
|
|
1788
|
+
/*
|
|
1789
|
+
* Coverage as its own row, on a fixed 0–100% axis.
|
|
1790
|
+
*
|
|
1791
|
+
* The shaded band under the traffic line is an ABSOLUTE quantity — Vercel
|
|
1792
|
+
* minus GA4 — so under a flat 20% coverage it is 0.8 times the traffic curve
|
|
1793
|
+
* and tracks it exactly. Every busy day therefore shows a wider alarm than
|
|
1794
|
+
* every quiet day with no change whatever in the instrument, and the one
|
|
1795
|
+
* distinction this tool exists to draw — a standing shortfall against a dated
|
|
1796
|
+
* collapse — is the one the encoding could not show. It had to be asserted in
|
|
1797
|
+
* prose by a detector instead, which made that sentence the reader's only
|
|
1798
|
+
* witness to its own subject.
|
|
1799
|
+
*
|
|
1800
|
+
* As a proportion it draws itself: a standing shortfall is a flat line, a
|
|
1801
|
+
* collapse is a step with a date under it, a gradual decay is a ramp that
|
|
1802
|
+
* plainly is not an event, and a partial recovery is a line that visibly did
|
|
1803
|
+
* not return. The reader can check the sentence rather than trust it.
|
|
1804
|
+
*/
|
|
1805
|
+
...(data.crossSource ?? []).some((d) => d.vercelPageviews !== null && d.ga4Pageviews !== null) ? [{
|
|
1806
|
+
key: "coverage",
|
|
1807
|
+
label: "GA4 coverage",
|
|
1808
|
+
source: "GA4",
|
|
1809
|
+
complete: true,
|
|
1810
|
+
unit: "percent",
|
|
1811
|
+
// Fixed, not scaled to its own best day — otherwise a site at a flat 20%
|
|
1812
|
+
// draws a full-height line and reads as healthy.
|
|
1813
|
+
domain: [0, 1],
|
|
1814
|
+
points: (data.crossSource ?? []).map((d) => ({
|
|
1815
|
+
date: d.date,
|
|
1816
|
+
value: d.vercelPageviews !== null && d.ga4Pageviews !== null && d.vercelPageviews > 0 ? Math.min(1, d.ga4Pageviews / d.vercelPageviews) : null
|
|
1817
|
+
}))
|
|
1818
|
+
}] : [],
|
|
1726
1819
|
...data.crossSource?.some((d) => d.revenue !== null) ? [{
|
|
1727
1820
|
key: "revenue",
|
|
1728
1821
|
label: "Revenue",
|
|
@@ -1887,8 +1980,9 @@ function Verdict({ data, previous }) {
|
|
|
1887
1980
|
return;
|
|
1888
1981
|
}
|
|
1889
1982
|
const change = (a - b) / Math.abs(b);
|
|
1890
|
-
if (Math.abs(change) < 0.05)
|
|
1891
|
-
|
|
1983
|
+
if (Math.abs(change) < 0.05) {
|
|
1984
|
+
parts.push(Math.abs(change) < 5e-3 ? `${label} flat` : `${label} little changed (${change > 0 ? "+" : "\u2212"}${formatPercent(Math.abs(change), 0)})`);
|
|
1985
|
+
} else parts.push(`${label} ${change > 0 ? "up" : "down"} ${formatPercent(Math.abs(change), 0)}`);
|
|
1892
1986
|
};
|
|
1893
1987
|
say("Revenue", data.revenue, previous?.revenue, "money");
|
|
1894
1988
|
say("Traffic", data.vercelPageviews, previous?.vercelPageviews, "count");
|
|
@@ -1922,7 +2016,7 @@ function DataHealthPanel({ data, diagnostics }) {
|
|
|
1922
2016
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
|
|
1923
2017
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Heading, { size: 1, style: sectionHeading, children: "Pageviews, source against source" }),
|
|
1924
2018
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 1, muted: true, children: "The same unit on both sides. Vercel is cookieless and ungated; GA4 is consent-gated and blockable, so GA4 seeing fewer is expected." }),
|
|
1925
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ComparisonBar, { label: "Vercel pageviews", metric: data.vercelPageviews, max: pageviewMax, outOf: "
|
|
2019
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ComparisonBar, { label: "Vercel pageviews", metric: data.vercelPageviews, max: pageviewMax, outOf: "Blocked less than GA4, but not immune to it." }),
|
|
1926
2020
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(ComparisonBar, { label: "GA4 pageviews", metric: data.ga4Pageviews, max: pageviewMax, outOf: "A subset of the bar above, not a rival measurement." })
|
|
1927
2021
|
] }),
|
|
1928
2022
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Card, { padding: 3, radius: 2, tone: "transparent", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 2, children: [
|
|
@@ -1967,7 +2061,7 @@ function DataHealthPanel({ data, diagnostics }) {
|
|
|
1967
2061
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Card, { padding: 3, radius: 2, tone: "transparent", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
|
|
1968
2062
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "Vercel visitors" }),
|
|
1969
2063
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(MetricFigure, { metric: metricOr(data.vercelVisitors, OLDER_ROUTE), label: "Vercel visitors" }),
|
|
1970
|
-
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: "
|
|
2064
|
+
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Text, { size: 0, muted: true, children: "Vercel\u2019s own counter. Blocked far less often than Google Analytics, but not never \u2014 so read it as a floor on your real traffic." })
|
|
1971
2065
|
] }) }),
|
|
1972
2066
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Card, { padding: 3, radius: 2, tone: "transparent", border: true, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_sanity_ui_compat3.Stack, { space: 3, children: [
|
|
1973
2067
|
/* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_sanity_ui_compat3.Label, { size: 1, muted: true, children: "GA4 sessions" }),
|
|
@@ -2866,11 +2960,26 @@ var PanelBoundary = class extends import_react4.default.Component {
|
|
|
2866
2960
|
function VisitorInsightsTool(props) {
|
|
2867
2961
|
const apiBaseUrl = props.tool?.options?.apiBaseUrl ?? props.apiBaseUrl ?? "";
|
|
2868
2962
|
const siteLabel = props.tool?.options?.siteLabel ?? props.siteLabel ?? "";
|
|
2869
|
-
const [
|
|
2870
|
-
const
|
|
2871
|
-
const
|
|
2963
|
+
const [linked] = (0, import_react4.useState)(() => typeof window === "undefined" ? {} : decodeView(window.location.hash));
|
|
2964
|
+
const linkedRange = RANGES.some((r) => r.key === linked.range) ? linked.range : null;
|
|
2965
|
+
const linkedTab = PANELS.some((p) => p.id === linked.tab) ? linked.tab : null;
|
|
2966
|
+
const [range, setRange] = (0, import_react4.useState)(linkedRange ?? "week");
|
|
2967
|
+
const [rangeBeforeBrush, setRangeBeforeBrush] = (0, import_react4.useState)(linkedRange ?? "week");
|
|
2968
|
+
const [custom, setCustom] = (0, import_react4.useState)(() => linked.from && linked.to ? { start: linked.from, end: linked.to } : { start: isoDaysAgo(30), end: isoDaysAgo(0) });
|
|
2872
2969
|
const atPresent = custom.end >= isoDaysAgo(0);
|
|
2873
|
-
const [activePanel, setActivePanel] = (0, import_react4.useState)("overview");
|
|
2970
|
+
const [activePanel, setActivePanel] = (0, import_react4.useState)(linkedTab ?? "overview");
|
|
2971
|
+
import_react4.default.useEffect(() => {
|
|
2972
|
+
if (typeof window === "undefined") return;
|
|
2973
|
+
const next = mergeIntoHash(window.location.hash, {
|
|
2974
|
+
tab: activePanel,
|
|
2975
|
+
range,
|
|
2976
|
+
from: custom.start,
|
|
2977
|
+
to: custom.end
|
|
2978
|
+
});
|
|
2979
|
+
if (next !== window.location.hash) {
|
|
2980
|
+
window.history.replaceState(null, "", `${window.location.pathname}${window.location.search}${next}`);
|
|
2981
|
+
}
|
|
2982
|
+
}, [activePanel, range, custom.start, custom.end]);
|
|
2874
2983
|
const active = PANELS.find((p) => p.id === activePanel) ?? PANELS[0];
|
|
2875
2984
|
return (
|
|
2876
2985
|
// Named for assistive technology even though the name is not printed: a Studio user with
|
|
@@ -3176,12 +3285,15 @@ var index_default = visitorInsights;
|
|
|
3176
3285
|
TypefaceInterestPanel,
|
|
3177
3286
|
VisitorInsightsTool,
|
|
3178
3287
|
coverageForRange,
|
|
3288
|
+
decodeView,
|
|
3289
|
+
encodeView,
|
|
3179
3290
|
estimated,
|
|
3180
3291
|
formatCount,
|
|
3181
3292
|
formatMoney,
|
|
3182
3293
|
formatPercent,
|
|
3183
3294
|
isReportName,
|
|
3184
3295
|
knownShortfall,
|
|
3296
|
+
mergeIntoHash,
|
|
3185
3297
|
ok,
|
|
3186
3298
|
partial,
|
|
3187
3299
|
previousRange,
|