@carlonicora/nextjs-jsonapi 3.24.2 → 3.25.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/{AssistantMessageInterface-1Kbz8RVs.d.ts → AssistantMessageInterface-CubxuBIJ.d.ts} +1 -1
- package/dist/{AssistantMessageInterface-CoxJyuFM.d.mts → AssistantMessageInterface-sHjI2zPN.d.mts} +1 -1
- package/dist/{BlockNoteEditor-65HR6ULV.mjs → BlockNoteEditor-46O3JC4Z.mjs} +5 -5
- package/dist/{BlockNoteEditor-35D4R7HJ.js → BlockNoteEditor-HKS6AXDA.js} +20 -20
- package/dist/{BlockNoteEditor-35D4R7HJ.js.map → BlockNoteEditor-HKS6AXDA.js.map} +1 -1
- package/dist/{BlockNoteViewer-QDXDVYEM.js → BlockNoteViewer-7BFNTN4E.js} +4 -4
- package/dist/{BlockNoteViewer-QDXDVYEM.js.map → BlockNoteViewer-7BFNTN4E.js.map} +1 -1
- package/dist/{BlockNoteViewer-KDKGUIG5.mjs → BlockNoteViewer-KRTAFTUX.mjs} +3 -3
- package/dist/{HandbookSectionInterface-OePKPbqN.d.ts → HandbookSectionInterface-Ck1wZJ81.d.ts} +1 -1
- package/dist/{HandbookSectionInterface-BYsmnPEz.d.mts → HandbookSectionInterface-DgYH--Ik.d.mts} +1 -1
- package/dist/JsonApiRequest-TE5WD3WS.js +26 -0
- package/dist/{JsonApiRequest-G625QAH5.js.map → JsonApiRequest-TE5WD3WS.js.map} +1 -1
- package/dist/{JsonApiRequest-RRMNV7YE.mjs → JsonApiRequest-UHOWNI3O.mjs} +2 -2
- package/dist/analytics.modules-CfczU12P.d.ts +25 -0
- package/dist/analytics.modules-pzGwMASI.d.mts +25 -0
- package/dist/{auth.interface-BC1jp_ni.d.ts → auth.interface-BzI4onI_.d.ts} +1 -1
- package/dist/{auth.interface-dR2-6gPM.d.mts → auth.interface-Df4vXbNz.d.mts} +1 -1
- package/dist/billing/index.js +402 -402
- package/dist/billing/index.mjs +4 -4
- package/dist/{chunk-YXRVPEEJ.js → chunk-26MQIJNB.js} +1 -1
- package/dist/{chunk-YXRVPEEJ.js.map → chunk-26MQIJNB.js.map} +1 -1
- package/dist/{chunk-T76WLDE5.mjs → chunk-37DMFVLN.mjs} +1 -1
- package/dist/chunk-4Z2UONN2.mjs +110 -0
- package/dist/chunk-4Z2UONN2.mjs.map +1 -0
- package/dist/{chunk-3AR4KA5D.js → chunk-4Z45NYPR.js} +13 -13
- package/dist/{chunk-3AR4KA5D.js.map → chunk-4Z45NYPR.js.map} +1 -1
- package/dist/chunk-7B4E42R5.mjs +280 -0
- package/dist/chunk-7B4E42R5.mjs.map +1 -0
- package/dist/{chunk-U7QVISQS.js → chunk-CAKOFJZD.js} +450 -40
- package/dist/chunk-CAKOFJZD.js.map +1 -0
- package/dist/{chunk-IBVKPLHT.mjs → chunk-E3I4MT53.mjs} +422 -12
- package/dist/chunk-E3I4MT53.mjs.map +1 -0
- package/dist/chunk-GHFMKH3M.js +280 -0
- package/dist/chunk-GHFMKH3M.js.map +1 -0
- package/dist/chunk-KLV6RLDE.js +110 -0
- package/dist/chunk-KLV6RLDE.js.map +1 -0
- package/dist/{chunk-ZJI5ZZW5.mjs → chunk-Q6J2ICKO.mjs} +3 -3
- package/dist/{chunk-4O7R2Z2O.mjs → chunk-XDYILJ2S.mjs} +1749 -1737
- package/dist/{chunk-4O7R2Z2O.mjs.map → chunk-XDYILJ2S.mjs.map} +1 -1
- package/dist/{chunk-2XCUAHQT.js → chunk-Y3OKEQGL.js} +1126 -1114
- package/dist/chunk-Y3OKEQGL.js.map +1 -0
- package/dist/client/index.d.mts +6 -5
- package/dist/client/index.d.ts +6 -5
- package/dist/client/index.js +5 -5
- package/dist/client/index.js.map +1 -1
- package/dist/client/index.mjs +4 -4
- package/dist/components/index.d.mts +17 -7
- package/dist/components/index.d.ts +17 -7
- package/dist/components/index.js +5 -5
- package/dist/components/index.mjs +4 -4
- package/dist/{config-B6UB7j69.d.ts → config-3N1LSrcC.d.ts} +1 -1
- package/dist/{config-Cd5xTEfZ.d.ts → config-AVtuvfM1.d.ts} +1 -1
- package/dist/{config-n-ZpPmOL.d.mts → config-BSKTYDoh.d.mts} +1 -1
- package/dist/{config-YyhFHxkU.d.mts → config-BmufyEj6.d.mts} +1 -1
- package/dist/contexts/index.d.mts +5 -4
- package/dist/contexts/index.d.ts +5 -4
- package/dist/contexts/index.js +5 -5
- package/dist/contexts/index.mjs +4 -4
- package/dist/core/index.d.mts +31 -14
- package/dist/core/index.d.ts +31 -14
- package/dist/core/index.js +9 -5
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +8 -4
- package/dist/features/analytics/index.css +30 -0
- package/dist/features/analytics/index.css.map +1 -0
- package/dist/features/analytics/index.d.mts +508 -0
- package/dist/features/analytics/index.d.ts +508 -0
- package/dist/features/analytics/index.js +1166 -0
- package/dist/features/analytics/index.js.map +1 -0
- package/dist/features/analytics/index.mjs +1166 -0
- package/dist/features/analytics/index.mjs.map +1 -0
- package/dist/features/analytics/server.d.mts +28 -0
- package/dist/features/analytics/server.d.ts +28 -0
- package/dist/features/analytics/server.js +41 -0
- package/dist/features/analytics/server.js.map +1 -0
- package/dist/features/analytics/server.mjs +41 -0
- package/dist/features/analytics/server.mjs.map +1 -0
- package/dist/features/help/index.d.mts +2 -2
- package/dist/features/help/index.d.ts +2 -2
- package/dist/features/help/index.js +38 -38
- package/dist/features/help/index.mjs +4 -4
- package/dist/features/tokenusage/index.d.mts +2 -2
- package/dist/features/tokenusage/index.d.ts +2 -2
- package/dist/features/tokenusage/index.js +139 -221
- package/dist/features/tokenusage/index.js.map +1 -1
- package/dist/features/tokenusage/index.mjs +26 -108
- package/dist/features/tokenusage/index.mjs.map +1 -1
- package/dist/index.d.mts +11 -9
- package/dist/index.d.ts +11 -9
- package/dist/index.js +10 -6
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +9 -5
- package/dist/notification.interface-BNCvnVCW.d.ts +34 -0
- package/dist/notification.interface-P_pNfzZQ.d.mts +34 -0
- package/dist/{s3.service-Bqr-fQmi.d.ts → s3.service-CbUa6a-8.d.ts} +3 -2
- package/dist/{s3.service-Buiy1ncj.d.mts → s3.service-DAp8FbuG.d.mts} +3 -2
- package/dist/server/index.d.mts +5 -4
- package/dist/server/index.d.ts +5 -4
- package/dist/server/index.js +39 -279
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +20 -260
- package/dist/server/index.mjs.map +1 -1
- package/dist/{types-bsm8Fp55.d.ts → types-Cn153-mH.d.ts} +1 -1
- package/dist/{types-Bq_UA8Lg.d.mts → types-D0uLYV2P.d.mts} +1 -1
- package/dist/{usePageUrlGenerator-J_ECv-oQ.d.ts → usePageUrlGenerator-B-3FRKBD.d.ts} +1 -1
- package/dist/{usePageUrlGenerator-uKBjAKqC.d.mts → usePageUrlGenerator-CCJ_cdQy.d.mts} +1 -1
- package/dist/{useSocket-BcH7l7B8.d.mts → useSocket-B9gTbuCj.d.mts} +1 -1
- package/dist/{useSocket-DCZd_eJk.d.ts → useSocket-BHVIyigI.d.ts} +1 -1
- package/dist/{notification.interface-MnRt1Tjm.d.mts → user.interface-CSaGWFbg.d.mts} +1 -31
- package/dist/{notification.interface-CjzV_KII.d.ts → user.interface-CrmwJLYW.d.ts} +1 -31
- package/package.json +11 -1
- package/src/client/index.ts +2 -0
- package/src/core/index.ts +4 -0
- package/src/core/registry/ModuleRegistry.ts +7 -0
- package/src/features/__tests__/i18n-keys.contract.spec.ts +1 -0
- package/src/features/analytics/__tests__/server-entry.spec.tsx +64 -0
- package/src/features/analytics/analytics.modules.ts +30 -0
- package/src/features/analytics/components/AnalyticsAdminContainer.tsx +97 -0
- package/src/features/analytics/components/AnalyticsAdminFilterBar.tsx +117 -0
- package/src/features/analytics/components/AnalyticsAdminPageContainer.tsx +25 -0
- package/src/features/analytics/components/AnalyticsAdminTiles.tsx +191 -0
- package/src/features/analytics/components/AnalyticsBreakdownTable.tsx +104 -0
- package/src/features/analytics/components/AnalyticsJourneyContainer.tsx +246 -0
- package/src/features/analytics/components/AnalyticsJourneyPageContainer.tsx +14 -0
- package/src/features/analytics/components/AnalyticsSessionsTable.tsx +107 -0
- package/src/features/analytics/components/AnalyticsTimelineChart.tsx +175 -0
- package/src/features/analytics/components/AnalyticsTracker.tsx +112 -0
- package/src/features/analytics/components/__tests__/AnalyticsAdminTiles.spec.tsx +39 -0
- package/src/features/analytics/components/__tests__/AnalyticsBreakdownTable.spec.tsx +39 -0
- package/src/features/analytics/components/__tests__/AnalyticsJourneyContainer.spec.tsx +124 -0
- package/src/features/analytics/components/__tests__/AnalyticsPageContainers.spec.tsx +117 -0
- package/src/features/analytics/components/__tests__/AnalyticsSessionsTable.spec.tsx +121 -0
- package/src/features/analytics/components/__tests__/AnalyticsTimelineChart.spec.tsx +35 -0
- package/src/features/analytics/components/__tests__/AnalyticsTracker.spec.tsx +248 -0
- package/src/features/analytics/components/server/AnalyticsAdminPage.tsx +28 -0
- package/src/features/analytics/components/server/AnalyticsJourneyPage.tsx +25 -0
- package/src/features/analytics/contexts/AnalyticsAdminContext.tsx +273 -0
- package/src/features/analytics/contexts/__tests__/AnalyticsAdminContext.spec.tsx +140 -0
- package/src/features/analytics/data/AnalyticsAdminService.ts +91 -0
- package/src/features/analytics/data/AnalyticsService.ts +23 -0
- package/src/features/analytics/data/__tests__/AnalyticsAdminService.spec.ts +117 -0
- package/src/features/analytics/data/__tests__/AnalyticsService.spec.ts +56 -0
- package/src/features/analytics/data/__tests__/models.spec.ts +300 -0
- package/src/features/analytics/data/analytics-breakdown.interface.ts +9 -0
- package/src/features/analytics/data/analytics-breakdown.ts +65 -0
- package/src/features/analytics/data/analytics-event.interface.ts +7 -0
- package/src/features/analytics/data/analytics-event.ts +63 -0
- package/src/features/analytics/data/analytics-page-view.interface.ts +9 -0
- package/src/features/analytics/data/analytics-page-view.ts +57 -0
- package/src/features/analytics/data/analytics-session.interface.ts +23 -0
- package/src/features/analytics/data/analytics-session.ts +144 -0
- package/src/features/analytics/data/analytics-summary.interface.ts +11 -0
- package/src/features/analytics/data/analytics-summary.ts +79 -0
- package/src/features/analytics/data/analytics-timeline.interface.ts +10 -0
- package/src/features/analytics/data/analytics-timeline.ts +67 -0
- package/src/features/analytics/data/analytics.types.ts +97 -0
- package/src/features/analytics/data/index.ts +15 -0
- package/src/features/analytics/i18n-keys.ts +76 -0
- package/src/features/analytics/index.ts +31 -0
- package/src/features/analytics/lib/__tests__/utm.spec.ts +20 -0
- package/src/features/analytics/lib/__tests__/visitorCookie.spec.ts +63 -0
- package/src/features/analytics/lib/utm.ts +29 -0
- package/src/features/analytics/lib/visitorCookie.ts +32 -0
- package/src/features/analytics/server-entry.ts +8 -0
- package/src/features/waitlist/components/lists/WaitlistList.tsx +68 -121
- package/src/features/waitlist/data/WaitlistService.ts +2 -2
- package/src/features/waitlist/data/index.ts +1 -0
- package/src/features/waitlist/data/waitlist.fields.ts +8 -0
- package/src/features/waitlist/hooks/useWaitlistTableStructure.tsx +69 -30
- package/dist/JsonApiRequest-G625QAH5.js +0 -26
- package/dist/chunk-2XCUAHQT.js.map +0 -1
- package/dist/chunk-IBVKPLHT.mjs.map +0 -1
- package/dist/chunk-U7QVISQS.js.map +0 -1
- /package/dist/{BlockNoteEditor-65HR6ULV.mjs.map → BlockNoteEditor-46O3JC4Z.mjs.map} +0 -0
- /package/dist/{BlockNoteViewer-KDKGUIG5.mjs.map → BlockNoteViewer-KRTAFTUX.mjs.map} +0 -0
- /package/dist/{JsonApiRequest-RRMNV7YE.mjs.map → JsonApiRequest-UHOWNI3O.mjs.map} +0 -0
- /package/dist/{chunk-T76WLDE5.mjs.map → chunk-37DMFVLN.mjs.map} +0 -0
- /package/dist/{chunk-ZJI5ZZW5.mjs.map → chunk-Q6J2ICKO.mjs.map} +0 -0
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../core";
|
|
2
|
+
import { UserInterface } from "../../user/data/user.interface";
|
|
3
|
+
import { AnalyticsSessionInterface } from "./analytics-session.interface";
|
|
4
|
+
import { AnalyticsSessionInput } from "./analytics.types";
|
|
5
|
+
|
|
6
|
+
/** One visit: a run of page views from one visitor with no 30-minute gap. */
|
|
7
|
+
export class AnalyticsSession extends AbstractApiData implements AnalyticsSessionInterface {
|
|
8
|
+
private _visitorId: string = "";
|
|
9
|
+
private _consented: boolean = false;
|
|
10
|
+
private _startedAt: Date = new Date(0);
|
|
11
|
+
private _lastSeenAt: Date = new Date(0);
|
|
12
|
+
private _pageViews: number = 0;
|
|
13
|
+
private _section: string = "";
|
|
14
|
+
private _landingRoute: string = "";
|
|
15
|
+
private _referrerHost?: string;
|
|
16
|
+
private _utmSource?: string;
|
|
17
|
+
private _utmMedium?: string;
|
|
18
|
+
private _utmCampaign?: string;
|
|
19
|
+
private _utmTerm?: string;
|
|
20
|
+
private _utmContent?: string;
|
|
21
|
+
private _deviceType: string = "";
|
|
22
|
+
private _user?: UserInterface;
|
|
23
|
+
|
|
24
|
+
get visitorId(): string {
|
|
25
|
+
return this._visitorId;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get consented(): boolean {
|
|
29
|
+
return this._consented;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
get startedAt(): Date {
|
|
33
|
+
return this._startedAt;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
get lastSeenAt(): Date {
|
|
37
|
+
return this._lastSeenAt;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get pageViews(): number {
|
|
41
|
+
return this._pageViews;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get section(): string {
|
|
45
|
+
return this._section;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get landingRoute(): string {
|
|
49
|
+
return this._landingRoute;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
get referrerHost(): string | undefined {
|
|
53
|
+
return this._referrerHost;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
get utmSource(): string | undefined {
|
|
57
|
+
return this._utmSource;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
get utmMedium(): string | undefined {
|
|
61
|
+
return this._utmMedium;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
get utmCampaign(): string | undefined {
|
|
65
|
+
return this._utmCampaign;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
get utmTerm(): string | undefined {
|
|
69
|
+
return this._utmTerm;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
get utmContent(): string | undefined {
|
|
73
|
+
return this._utmContent;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get deviceType(): string {
|
|
77
|
+
return this._deviceType;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
get user(): UserInterface | undefined {
|
|
81
|
+
return this._user;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
rehydrate(data: JsonApiHydratedDataInterface): this {
|
|
85
|
+
super.rehydrate(data);
|
|
86
|
+
|
|
87
|
+
const attrs = data.jsonApi.attributes;
|
|
88
|
+
this._visitorId = attrs.visitorId ?? "";
|
|
89
|
+
this._consented = attrs.consented ?? false;
|
|
90
|
+
// Wire format is an ISO 8601 instant; the interface promises a Date.
|
|
91
|
+
this._startedAt = attrs.startedAt ? new Date(attrs.startedAt) : new Date(0);
|
|
92
|
+
this._lastSeenAt = attrs.lastSeenAt ? new Date(attrs.lastSeenAt) : new Date(0);
|
|
93
|
+
this._pageViews = attrs.pageViews ?? 0;
|
|
94
|
+
this._section = attrs.section ?? "";
|
|
95
|
+
this._landingRoute = attrs.landingRoute ?? "";
|
|
96
|
+
this._referrerHost = attrs.referrerHost ?? undefined;
|
|
97
|
+
this._utmSource = attrs.utmSource ?? undefined;
|
|
98
|
+
this._utmMedium = attrs.utmMedium ?? undefined;
|
|
99
|
+
this._utmCampaign = attrs.utmCampaign ?? undefined;
|
|
100
|
+
this._utmTerm = attrs.utmTerm ?? undefined;
|
|
101
|
+
this._utmContent = attrs.utmContent ?? undefined;
|
|
102
|
+
this._deviceType = attrs.deviceType ?? "";
|
|
103
|
+
|
|
104
|
+
this._user = this._readIncluded(data, "user", Modules.User) as UserInterface | undefined;
|
|
105
|
+
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
createJsonApi(data: AnalyticsSessionInput) {
|
|
110
|
+
const response: any = {
|
|
111
|
+
data: {
|
|
112
|
+
type: Modules.AnalyticsSession.name,
|
|
113
|
+
id: data.id,
|
|
114
|
+
attributes: {},
|
|
115
|
+
relationships: {},
|
|
116
|
+
},
|
|
117
|
+
included: [],
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
if (data.visitorId !== undefined) response.data.attributes.visitorId = data.visitorId;
|
|
121
|
+
if (data.consented !== undefined) response.data.attributes.consented = data.consented;
|
|
122
|
+
// Both are type: "datetime" on the backend descriptor: ISO 8601 instants.
|
|
123
|
+
if (data.startedAt !== undefined) response.data.attributes.startedAt = data.startedAt.toISOString();
|
|
124
|
+
if (data.lastSeenAt !== undefined) response.data.attributes.lastSeenAt = data.lastSeenAt.toISOString();
|
|
125
|
+
if (data.pageViews !== undefined) response.data.attributes.pageViews = data.pageViews;
|
|
126
|
+
if (data.section !== undefined) response.data.attributes.section = data.section;
|
|
127
|
+
if (data.landingRoute !== undefined) response.data.attributes.landingRoute = data.landingRoute;
|
|
128
|
+
if (data.referrerHost !== undefined) response.data.attributes.referrerHost = data.referrerHost;
|
|
129
|
+
if (data.utmSource !== undefined) response.data.attributes.utmSource = data.utmSource;
|
|
130
|
+
if (data.utmMedium !== undefined) response.data.attributes.utmMedium = data.utmMedium;
|
|
131
|
+
if (data.utmCampaign !== undefined) response.data.attributes.utmCampaign = data.utmCampaign;
|
|
132
|
+
if (data.utmTerm !== undefined) response.data.attributes.utmTerm = data.utmTerm;
|
|
133
|
+
if (data.utmContent !== undefined) response.data.attributes.utmContent = data.utmContent;
|
|
134
|
+
if (data.deviceType !== undefined) response.data.attributes.deviceType = data.deviceType;
|
|
135
|
+
|
|
136
|
+
if (data.userId) {
|
|
137
|
+
response.data.relationships.user = {
|
|
138
|
+
data: { type: Modules.User.name, id: data.userId },
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return response;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { ApiDataInterface } from "../../../core";
|
|
2
|
+
|
|
3
|
+
export interface AnalyticsSummaryInterface extends ApiDataInterface {
|
|
4
|
+
get section(): string;
|
|
5
|
+
get window(): string;
|
|
6
|
+
get visitors(): number;
|
|
7
|
+
get sessions(): number;
|
|
8
|
+
get pageViews(): number;
|
|
9
|
+
get pagesPerSession(): number;
|
|
10
|
+
get consentShare(): number;
|
|
11
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { AbstractApiData, JsonApiHydratedDataInterface, Modules } from "../../../core";
|
|
2
|
+
import { AnalyticsSummaryInterface } from "./analytics-summary.interface";
|
|
3
|
+
import { AnalyticsSummaryInput } from "./analytics.types";
|
|
4
|
+
|
|
5
|
+
/** One (section, window) row of the headline tiles: current or previous period. */
|
|
6
|
+
export class AnalyticsSummary extends AbstractApiData implements AnalyticsSummaryInterface {
|
|
7
|
+
private _section: string = "";
|
|
8
|
+
private _window: string = "";
|
|
9
|
+
private _visitors: number = 0;
|
|
10
|
+
private _sessions: number = 0;
|
|
11
|
+
private _pageViews: number = 0;
|
|
12
|
+
private _pagesPerSession: number = 0;
|
|
13
|
+
private _consentShare: number = 0;
|
|
14
|
+
|
|
15
|
+
get section(): string {
|
|
16
|
+
return this._section;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get window(): string {
|
|
20
|
+
return this._window;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get visitors(): number {
|
|
24
|
+
return this._visitors;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get sessions(): number {
|
|
28
|
+
return this._sessions;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
get pageViews(): number {
|
|
32
|
+
return this._pageViews;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get pagesPerSession(): number {
|
|
36
|
+
return this._pagesPerSession;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get consentShare(): number {
|
|
40
|
+
return this._consentShare;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
rehydrate(data: JsonApiHydratedDataInterface): this {
|
|
44
|
+
super.rehydrate(data);
|
|
45
|
+
|
|
46
|
+
const attrs = data.jsonApi.attributes;
|
|
47
|
+
this._section = attrs.section ?? "";
|
|
48
|
+
this._window = attrs.window ?? "";
|
|
49
|
+
this._visitors = attrs.visitors ?? 0;
|
|
50
|
+
this._sessions = attrs.sessions ?? 0;
|
|
51
|
+
this._pageViews = attrs.pageViews ?? 0;
|
|
52
|
+
this._pagesPerSession = attrs.pagesPerSession ?? 0;
|
|
53
|
+
this._consentShare = attrs.consentShare ?? 0;
|
|
54
|
+
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
createJsonApi(data: AnalyticsSummaryInput) {
|
|
59
|
+
const response: any = {
|
|
60
|
+
data: {
|
|
61
|
+
type: Modules.AnalyticsSummary.name,
|
|
62
|
+
id: data.id,
|
|
63
|
+
attributes: {},
|
|
64
|
+
relationships: {},
|
|
65
|
+
},
|
|
66
|
+
included: [],
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
if (data.section !== undefined) response.data.attributes.section = data.section;
|
|
70
|
+
if (data.window !== undefined) response.data.attributes.window = data.window;
|
|
71
|
+
if (data.visitors !== undefined) response.data.attributes.visitors = data.visitors;
|
|
72
|
+
if (data.sessions !== undefined) response.data.attributes.sessions = data.sessions;
|
|
73
|
+
if (data.pageViews !== undefined) response.data.attributes.pageViews = data.pageViews;
|
|
74
|
+
if (data.pagesPerSession !== undefined) response.data.attributes.pagesPerSession = data.pagesPerSession;
|
|
75
|
+
if (data.consentShare !== undefined) response.data.attributes.consentShare = data.consentShare;
|
|
76
|
+
|
|
77
|
+
return response;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { ApiDataInterface } from "../../../core";
|
|
2
|
+
|
|
3
|
+
export interface AnalyticsTimelineInterface extends ApiDataInterface {
|
|
4
|
+
/** Backend declares this `type: "date"`, so it is a Date in memory. */
|
|
5
|
+
get bucket(): Date;
|
|
6
|
+
get section(): string;
|
|
7
|
+
get visitors(): number;
|
|
8
|
+
get sessions(): number;
|
|
9
|
+
get pageViews(): number;
|
|
10
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { AbstractApiData, formatLocalDate, JsonApiHydratedDataInterface, Modules } from "../../../core";
|
|
2
|
+
import { AnalyticsTimelineInterface } from "./analytics-timeline.interface";
|
|
3
|
+
import { AnalyticsTimelineInput } from "./analytics.types";
|
|
4
|
+
|
|
5
|
+
/** One (bucket, section) point of the visitors-over-time chart. */
|
|
6
|
+
export class AnalyticsTimeline extends AbstractApiData implements AnalyticsTimelineInterface {
|
|
7
|
+
private _bucket: Date = new Date(0);
|
|
8
|
+
private _section: string = "";
|
|
9
|
+
private _visitors: number = 0;
|
|
10
|
+
private _sessions: number = 0;
|
|
11
|
+
private _pageViews: number = 0;
|
|
12
|
+
|
|
13
|
+
get bucket(): Date {
|
|
14
|
+
return this._bucket;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get section(): string {
|
|
18
|
+
return this._section;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
get visitors(): number {
|
|
22
|
+
return this._visitors;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
get sessions(): number {
|
|
26
|
+
return this._sessions;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
get pageViews(): number {
|
|
30
|
+
return this._pageViews;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
rehydrate(data: JsonApiHydratedDataInterface): this {
|
|
34
|
+
super.rehydrate(data);
|
|
35
|
+
|
|
36
|
+
const attrs = data.jsonApi.attributes;
|
|
37
|
+
// Wire format is "YYYY-MM-DD"; the interface promises a Date.
|
|
38
|
+
this._bucket = attrs.bucket ? new Date(attrs.bucket) : new Date(0);
|
|
39
|
+
this._section = attrs.section ?? "";
|
|
40
|
+
this._visitors = attrs.visitors ?? 0;
|
|
41
|
+
this._sessions = attrs.sessions ?? 0;
|
|
42
|
+
this._pageViews = attrs.pageViews ?? 0;
|
|
43
|
+
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
createJsonApi(data: AnalyticsTimelineInput) {
|
|
48
|
+
const response: any = {
|
|
49
|
+
data: {
|
|
50
|
+
type: Modules.AnalyticsTimeline.name,
|
|
51
|
+
id: data.id,
|
|
52
|
+
attributes: {},
|
|
53
|
+
relationships: {},
|
|
54
|
+
},
|
|
55
|
+
included: [],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// `bucket` is type: "date" on the backend descriptor: formatLocalDate, never the raw Date.
|
|
59
|
+
if (data.bucket !== undefined) response.data.attributes.bucket = formatLocalDate(data.bucket);
|
|
60
|
+
if (data.section !== undefined) response.data.attributes.section = data.section;
|
|
61
|
+
if (data.visitors !== undefined) response.data.attributes.visitors = data.visitors;
|
|
62
|
+
if (data.sessions !== undefined) response.data.attributes.sessions = data.sessions;
|
|
63
|
+
if (data.pageViews !== undefined) response.data.attributes.pageViews = data.pageViews;
|
|
64
|
+
|
|
65
|
+
return response;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// Each value list is the single source of its union type: components iterate
|
|
2
|
+
// the constant, and the type is derived from it so the two cannot drift.
|
|
3
|
+
|
|
4
|
+
export const ANALYTICS_SECTIONS = ["all", "public", "app"] as const;
|
|
5
|
+
export type AnalyticsSection = (typeof ANALYTICS_SECTIONS)[number];
|
|
6
|
+
|
|
7
|
+
export const ANALYTICS_GRANULARITIES = ["day", "week", "month"] as const;
|
|
8
|
+
export type AnalyticsGranularity = (typeof ANALYTICS_GRANULARITIES)[number];
|
|
9
|
+
|
|
10
|
+
export const ANALYTICS_DIMENSIONS = ["source", "medium", "campaign", "referrer", "route", "landing"] as const;
|
|
11
|
+
export type AnalyticsDimension = (typeof ANALYTICS_DIMENSIONS)[number];
|
|
12
|
+
|
|
13
|
+
export type AnalyticsFilters = {
|
|
14
|
+
/** ISO 8601 instant. */
|
|
15
|
+
from: string;
|
|
16
|
+
/** ISO 8601 instant. */
|
|
17
|
+
to: string;
|
|
18
|
+
section: AnalyticsSection;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/** One page view, as the tracker posts it to `analytics/events`. */
|
|
22
|
+
export type AnalyticsEventInput = {
|
|
23
|
+
id: string;
|
|
24
|
+
path: string;
|
|
25
|
+
section: "public" | "app";
|
|
26
|
+
referrer?: string;
|
|
27
|
+
utmSource?: string;
|
|
28
|
+
utmMedium?: string;
|
|
29
|
+
utmCampaign?: string;
|
|
30
|
+
utmTerm?: string;
|
|
31
|
+
utmContent?: string;
|
|
32
|
+
visitorId?: string;
|
|
33
|
+
screenWidth?: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
// Input types for the read-only models' createJsonApi(). `id` is always
|
|
37
|
+
// required — JSON:API resource objects are identified.
|
|
38
|
+
|
|
39
|
+
export type AnalyticsSummaryInput = {
|
|
40
|
+
id: string;
|
|
41
|
+
section: string;
|
|
42
|
+
window: string;
|
|
43
|
+
visitors: number;
|
|
44
|
+
sessions: number;
|
|
45
|
+
pageViews: number;
|
|
46
|
+
pagesPerSession: number;
|
|
47
|
+
consentShare: number;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type AnalyticsTimelineInput = {
|
|
51
|
+
id: string;
|
|
52
|
+
/** Backend field type is "date" — emitted via formatLocalDate, never raw. */
|
|
53
|
+
bucket: Date;
|
|
54
|
+
section: string;
|
|
55
|
+
visitors: number;
|
|
56
|
+
sessions: number;
|
|
57
|
+
pageViews: number;
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export type AnalyticsBreakdownInput = {
|
|
61
|
+
id: string;
|
|
62
|
+
dimension: string;
|
|
63
|
+
key: string;
|
|
64
|
+
visitors: number;
|
|
65
|
+
sessions: number;
|
|
66
|
+
pageViews: number;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export type AnalyticsSessionInput = {
|
|
70
|
+
id: string;
|
|
71
|
+
visitorId: string;
|
|
72
|
+
consented: boolean;
|
|
73
|
+
/** Backend field type is "datetime" — emitted via toISOString(). */
|
|
74
|
+
startedAt: Date;
|
|
75
|
+
/** Backend field type is "datetime" — emitted via toISOString(). */
|
|
76
|
+
lastSeenAt: Date;
|
|
77
|
+
pageViews: number;
|
|
78
|
+
section: string;
|
|
79
|
+
landingRoute: string;
|
|
80
|
+
referrerHost?: string;
|
|
81
|
+
utmSource?: string;
|
|
82
|
+
utmMedium?: string;
|
|
83
|
+
utmCampaign?: string;
|
|
84
|
+
utmTerm?: string;
|
|
85
|
+
utmContent?: string;
|
|
86
|
+
deviceType: string;
|
|
87
|
+
userId?: string;
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
export type AnalyticsPageViewInput = {
|
|
91
|
+
id: string;
|
|
92
|
+
/** Backend field type is "datetime" — emitted via toISOString(). */
|
|
93
|
+
createdAt: Date;
|
|
94
|
+
path: string;
|
|
95
|
+
route: string;
|
|
96
|
+
section: string;
|
|
97
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export * from "./analytics.types";
|
|
2
|
+
export { AnalyticsEvent } from "./analytics-event";
|
|
3
|
+
export type { AnalyticsEventInterface } from "./analytics-event.interface";
|
|
4
|
+
export { AnalyticsSummary } from "./analytics-summary";
|
|
5
|
+
export type { AnalyticsSummaryInterface } from "./analytics-summary.interface";
|
|
6
|
+
export { AnalyticsTimeline } from "./analytics-timeline";
|
|
7
|
+
export type { AnalyticsTimelineInterface } from "./analytics-timeline.interface";
|
|
8
|
+
export { AnalyticsBreakdown } from "./analytics-breakdown";
|
|
9
|
+
export type { AnalyticsBreakdownInterface } from "./analytics-breakdown.interface";
|
|
10
|
+
export { AnalyticsSession } from "./analytics-session";
|
|
11
|
+
export type { AnalyticsSessionInterface } from "./analytics-session.interface";
|
|
12
|
+
export { AnalyticsPageView } from "./analytics-page-view";
|
|
13
|
+
export type { AnalyticsPageViewInterface } from "./analytics-page-view.interface";
|
|
14
|
+
export { AnalyticsService } from "./AnalyticsService";
|
|
15
|
+
export { AnalyticsAdminService } from "./AnalyticsAdminService";
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fixed namespace of i18n keys the administrative analytics feature reads
|
|
3
|
+
* via useTranslations. Consuming apps must define each entry in their
|
|
4
|
+
* messages/<locale>.json — this list is the contract between the package and
|
|
5
|
+
* the app.
|
|
6
|
+
*
|
|
7
|
+
* The description key is read by the consuming app's page metadata only, not by
|
|
8
|
+
* a component in this package, which is why the contract spec lists it among
|
|
9
|
+
* the keys declared for app-rendered surfaces.
|
|
10
|
+
*/
|
|
11
|
+
export const ANALYTICS_ADMIN_I18N_KEYS = [
|
|
12
|
+
// Page
|
|
13
|
+
"analytics.admin.title",
|
|
14
|
+
"analytics.admin.description",
|
|
15
|
+
|
|
16
|
+
// KPI tiles
|
|
17
|
+
"analytics.admin.tiles.visitors",
|
|
18
|
+
"analytics.admin.tiles.sessions",
|
|
19
|
+
"analytics.admin.tiles.page_views",
|
|
20
|
+
"analytics.admin.tiles.pages_per_session",
|
|
21
|
+
"analytics.admin.tiles.consent_share",
|
|
22
|
+
"analytics.admin.tiles.vs_previous",
|
|
23
|
+
"analytics.admin.tiles.summarised_note",
|
|
24
|
+
|
|
25
|
+
// Filter bar
|
|
26
|
+
"analytics.admin.filter.section",
|
|
27
|
+
"analytics.admin.filter.section_all",
|
|
28
|
+
"analytics.admin.filter.section_public",
|
|
29
|
+
"analytics.admin.filter.section_app",
|
|
30
|
+
"analytics.admin.filter.granularity",
|
|
31
|
+
"analytics.admin.filter.granularity_day",
|
|
32
|
+
"analytics.admin.filter.granularity_week",
|
|
33
|
+
"analytics.admin.filter.granularity_month",
|
|
34
|
+
|
|
35
|
+
// Panel titles
|
|
36
|
+
"analytics.admin.panels.over_time",
|
|
37
|
+
"analytics.admin.panels.breakdown",
|
|
38
|
+
"analytics.admin.panels.sessions",
|
|
39
|
+
"analytics.admin.panels.journey",
|
|
40
|
+
"analytics.admin.panels.other_sessions",
|
|
41
|
+
|
|
42
|
+
// Breakdown tabs
|
|
43
|
+
"analytics.admin.dimensions.source",
|
|
44
|
+
"analytics.admin.dimensions.medium",
|
|
45
|
+
"analytics.admin.dimensions.campaign",
|
|
46
|
+
"analytics.admin.dimensions.referrer",
|
|
47
|
+
"analytics.admin.dimensions.route",
|
|
48
|
+
"analytics.admin.dimensions.landing",
|
|
49
|
+
|
|
50
|
+
// Table columns
|
|
51
|
+
"analytics.admin.columns.key",
|
|
52
|
+
"analytics.admin.columns.visitors",
|
|
53
|
+
"analytics.admin.columns.sessions",
|
|
54
|
+
"analytics.admin.columns.page_views",
|
|
55
|
+
"analytics.admin.columns.started",
|
|
56
|
+
"analytics.admin.columns.visitor",
|
|
57
|
+
"analytics.admin.columns.source",
|
|
58
|
+
"analytics.admin.columns.landing",
|
|
59
|
+
"analytics.admin.columns.pages",
|
|
60
|
+
"analytics.admin.columns.device",
|
|
61
|
+
"analytics.admin.columns.time",
|
|
62
|
+
"analytics.admin.columns.gap",
|
|
63
|
+
"analytics.admin.columns.path",
|
|
64
|
+
|
|
65
|
+
// Shared states and markers
|
|
66
|
+
"analytics.admin.anonymous",
|
|
67
|
+
"analytics.admin.consented",
|
|
68
|
+
"analytics.admin.not_consented",
|
|
69
|
+
"analytics.admin.load_more",
|
|
70
|
+
"analytics.admin.no_data",
|
|
71
|
+
"analytics.admin.other",
|
|
72
|
+
|
|
73
|
+
// Timeline chart series
|
|
74
|
+
"analytics.admin.series.visitors",
|
|
75
|
+
"analytics.admin.series.page_views",
|
|
76
|
+
] as const;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Client-only public barrel for the first-party analytics feature.
|
|
2
|
+
// Consumed via `@carlonicora/nextjs-jsonapi/analytics`. Tsup adds a top-level
|
|
3
|
+
// "use client" directive to the bundled output via clientEntries.
|
|
4
|
+
//
|
|
5
|
+
// Deliberately NOT re-exported from `src/index.ts` — keeping the feature off the
|
|
6
|
+
// main barrel is what keeps its chart (recharts) out of the main client bundle,
|
|
7
|
+
// exactly as the `help` and `tokenusage` features do.
|
|
8
|
+
//
|
|
9
|
+
// The feature is inert until the app mounts `AnalyticsTracker` and registers
|
|
10
|
+
// `analyticsModules` in its bootstrapper.
|
|
11
|
+
|
|
12
|
+
export { AnalyticsTracker } from "./components/AnalyticsTracker";
|
|
13
|
+
|
|
14
|
+
export { AnalyticsAdminProvider, useAnalyticsAdmin } from "./contexts/AnalyticsAdminContext";
|
|
15
|
+
export type { AnalyticsAdminContextType, AnalyticsAdminFilterState } from "./contexts/AnalyticsAdminContext";
|
|
16
|
+
|
|
17
|
+
export { AnalyticsAdminContainer } from "./components/AnalyticsAdminContainer";
|
|
18
|
+
export { AnalyticsAdminFilterBar } from "./components/AnalyticsAdminFilterBar";
|
|
19
|
+
export { AnalyticsAdminTiles } from "./components/AnalyticsAdminTiles";
|
|
20
|
+
export { AnalyticsTimelineChart } from "./components/AnalyticsTimelineChart";
|
|
21
|
+
export { AnalyticsBreakdownTable } from "./components/AnalyticsBreakdownTable";
|
|
22
|
+
export { AnalyticsSessionsTable } from "./components/AnalyticsSessionsTable";
|
|
23
|
+
export { AnalyticsJourneyContainer } from "./components/AnalyticsJourneyContainer";
|
|
24
|
+
export { AnalyticsAdminPageContainer } from "./components/AnalyticsAdminPageContainer";
|
|
25
|
+
export { AnalyticsJourneyPageContainer } from "./components/AnalyticsJourneyPageContainer";
|
|
26
|
+
|
|
27
|
+
export * from "./data";
|
|
28
|
+
|
|
29
|
+
export { analyticsModules } from "./analytics.modules";
|
|
30
|
+
|
|
31
|
+
export { ANALYTICS_ADMIN_I18N_KEYS } from "./i18n-keys";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { readUtm } from "../utm";
|
|
3
|
+
|
|
4
|
+
describe("readUtm", () => {
|
|
5
|
+
it("reads the utm parameters and ignores everything else", () => {
|
|
6
|
+
expect(readUtm("?utm_source=linkedin&utm_medium=social&utm_campaign=launch&x=1")).toEqual({
|
|
7
|
+
utmSource: "linkedin",
|
|
8
|
+
utmMedium: "social",
|
|
9
|
+
utmCampaign: "launch",
|
|
10
|
+
});
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("returns an empty object for an empty query string", () => {
|
|
14
|
+
expect(readUtm("")).toEqual({});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("truncates a value to 255 characters", () => {
|
|
18
|
+
expect(readUtm("?utm_term=" + "a".repeat(300)).utmTerm).toHaveLength(255);
|
|
19
|
+
});
|
|
20
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
// Records every setCookie call while still writing the real cookie to jsdom,
|
|
4
|
+
// so the options (SameSite, maxAge) and document.cookie can both be asserted.
|
|
5
|
+
const recorded = vi.hoisted(() => ({ calls: [] as unknown[][] }));
|
|
6
|
+
|
|
7
|
+
vi.mock("cookies-next", async () => {
|
|
8
|
+
const actual = await vi.importActual<typeof import("cookies-next")>("cookies-next");
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
setCookie: (...args: unknown[]) => {
|
|
12
|
+
recorded.calls.push(args);
|
|
13
|
+
return (actual.setCookie as (...a: unknown[]) => unknown)(...args);
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
import { ANALYTICS_VISITOR_COOKIE, clearVisitorCookie, ensureVisitorCookie, readVisitorCookie } from "../visitorCookie";
|
|
19
|
+
|
|
20
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
21
|
+
|
|
22
|
+
describe("visitorCookie", () => {
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
clearVisitorCookie();
|
|
25
|
+
recorded.calls = [];
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("names the cookie analytics_visitor", () => {
|
|
29
|
+
expect(ANALYTICS_VISITOR_COOKIE).toBe("analytics_visitor");
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("creates a UUID and writes it with SameSite=Lax and a ~395-day expiry", () => {
|
|
33
|
+
const id = ensureVisitorCookie();
|
|
34
|
+
|
|
35
|
+
expect(id).toMatch(UUID);
|
|
36
|
+
expect(document.cookie).toContain(`analytics_visitor=${id}`);
|
|
37
|
+
|
|
38
|
+
expect(recorded.calls).toHaveLength(1);
|
|
39
|
+
const [name, value, options] = recorded.calls[0] as [string, string, Record<string, unknown>];
|
|
40
|
+
expect(name).toBe("analytics_visitor");
|
|
41
|
+
expect(value).toBe(id);
|
|
42
|
+
expect(options.sameSite).toBe("lax");
|
|
43
|
+
expect(options.path).toBe("/");
|
|
44
|
+
expect(options.maxAge).toBe(395 * 24 * 3600);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("returns the same id on a second call without rewriting the cookie", () => {
|
|
48
|
+
const first = ensureVisitorCookie();
|
|
49
|
+
const second = ensureVisitorCookie();
|
|
50
|
+
|
|
51
|
+
expect(second).toBe(first);
|
|
52
|
+
expect(recorded.calls).toHaveLength(1);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("clears the cookie so readVisitorCookie returns undefined", () => {
|
|
56
|
+
ensureVisitorCookie();
|
|
57
|
+
expect(readVisitorCookie()).toBeDefined();
|
|
58
|
+
|
|
59
|
+
clearVisitorCookie();
|
|
60
|
+
|
|
61
|
+
expect(readVisitorCookie()).toBeUndefined();
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { AnalyticsEventInput } from "../data/analytics.types";
|
|
2
|
+
|
|
3
|
+
type UtmFields = Pick<AnalyticsEventInput, "utmSource" | "utmMedium" | "utmCampaign" | "utmTerm" | "utmContent">;
|
|
4
|
+
|
|
5
|
+
const MAX_LENGTH = 255;
|
|
6
|
+
|
|
7
|
+
const UTM_PARAMS: [string, keyof UtmFields][] = [
|
|
8
|
+
["utm_source", "utmSource"],
|
|
9
|
+
["utm_medium", "utmMedium"],
|
|
10
|
+
["utm_campaign", "utmCampaign"],
|
|
11
|
+
["utm_term", "utmTerm"],
|
|
12
|
+
["utm_content", "utmContent"],
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Reads the five utm_* parameters from a query string. Absent or empty values
|
|
17
|
+
* are left out; every value is cut to 255 characters.
|
|
18
|
+
*/
|
|
19
|
+
export function readUtm(search: string): UtmFields {
|
|
20
|
+
const params = new URLSearchParams(search);
|
|
21
|
+
const response: UtmFields = {};
|
|
22
|
+
|
|
23
|
+
for (const [param, field] of UTM_PARAMS) {
|
|
24
|
+
const value = params.get(param);
|
|
25
|
+
if (value) response[field] = value.slice(0, MAX_LENGTH);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return response;
|
|
29
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { deleteCookie, getCookie, setCookie } from "cookies-next";
|
|
2
|
+
import { v4 } from "uuid";
|
|
3
|
+
|
|
4
|
+
export const ANALYTICS_VISITOR_COOKIE = "analytics_visitor";
|
|
5
|
+
|
|
6
|
+
/** 395 days: the 13-month retention window. */
|
|
7
|
+
const MAX_AGE_SECONDS = 395 * 24 * 3600;
|
|
8
|
+
|
|
9
|
+
export function readVisitorCookie(): string | undefined {
|
|
10
|
+
const value = getCookie(ANALYTICS_VISITOR_COOKIE);
|
|
11
|
+
return typeof value === "string" && value !== "" ? value : undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Returns the existing visitor id, or creates and stores a new one. */
|
|
15
|
+
export function ensureVisitorCookie(): string {
|
|
16
|
+
const existing = readVisitorCookie();
|
|
17
|
+
if (existing) return existing;
|
|
18
|
+
|
|
19
|
+
const id = v4();
|
|
20
|
+
setCookie(ANALYTICS_VISITOR_COOKIE, id, {
|
|
21
|
+
maxAge: MAX_AGE_SECONDS,
|
|
22
|
+
sameSite: "lax",
|
|
23
|
+
path: "/",
|
|
24
|
+
secure: typeof location !== "undefined" && location.protocol === "https:",
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return id;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function clearVisitorCookie(): void {
|
|
31
|
+
deleteCookie(ANALYTICS_VISITOR_COOKIE, { path: "/" });
|
|
32
|
+
}
|