@nuxtseo/protocol 0.1.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/analytics-codecs/analytics-row.d.ts +265 -0
- package/dist/analytics-codecs/analytics-row.js +175 -0
- package/dist/analytics-codecs/analytics-web-vitals.d.ts +57 -0
- package/dist/analytics-codecs/analytics-web-vitals.js +76 -0
- package/dist/analytics-codecs/capabilities.d.ts +35 -0
- package/dist/analytics-codecs/capabilities.js +76 -0
- package/dist/analytics-codecs/index.d.ts +3 -0
- package/dist/analytics-codecs/index.js +3 -0
- package/dist/cli-pairing.d.ts +67 -0
- package/dist/cli-pairing.js +52 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/v1/account.d.ts +715 -0
- package/dist/v1/account.js +123 -0
- package/dist/v1/actions.d.ts +2591 -0
- package/dist/v1/actions.js +460 -0
- package/dist/v1/analytics.d.ts +6145 -0
- package/dist/v1/analytics.js +296 -0
- package/dist/v1/audit.d.ts +4053 -0
- package/dist/v1/audit.js +609 -0
- package/dist/v1/backlinks.d.ts +1002 -0
- package/dist/v1/backlinks.js +239 -0
- package/dist/v1/content.d.ts +2745 -0
- package/dist/v1/content.js +326 -0
- package/dist/v1/core.d.ts +301 -0
- package/dist/v1/core.js +333 -0
- package/dist/v1/documents.d.ts +4 -0
- package/dist/v1/documents.js +216 -0
- package/dist/v1/gsc.d.ts +5172 -0
- package/dist/v1/gsc.js +583 -0
- package/dist/v1/index.d.ts +49 -0
- package/dist/v1/index.js +52 -0
- package/dist/v1/pages.d.ts +1404 -0
- package/dist/v1/pages.js +287 -0
- package/dist/v1/performance.d.ts +1379 -0
- package/dist/v1/performance.js +198 -0
- package/dist/v1/research.d.ts +1173 -0
- package/dist/v1/research.js +174 -0
- package/dist/v1/sitemaps.d.ts +1506 -0
- package/dist/v1/sitemaps.js +199 -0
- package/dist/v1/sites.d.ts +591 -0
- package/dist/v1/sites.js +101 -0
- package/generated/openapi.v1.json +1 -0
- package/package.json +131 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const analyticsProviderSchema: z.ZodEnum<{
|
|
3
|
+
cloudflare: "cloudflare";
|
|
4
|
+
ga4: "ga4";
|
|
5
|
+
plausible: "plausible";
|
|
6
|
+
posthog: "posthog";
|
|
7
|
+
umami: "umami";
|
|
8
|
+
}>;
|
|
9
|
+
export type AnalyticsProvider = z.infer<typeof analyticsProviderSchema>;
|
|
10
|
+
/**
|
|
11
|
+
* Dimensions every provider can be asked to group by. `source`/`medium`/
|
|
12
|
+
* `campaign` are the UTM-style traffic-attribution triple — a provider with no
|
|
13
|
+
* analog for one of them (Cloudflare RUM has only a referrer host) resolves it
|
|
14
|
+
* to an empty string rather than erroring, mirroring how metrics resolve to 0.
|
|
15
|
+
* Provider-native dimensions with no cross-provider analog go through the
|
|
16
|
+
* per-provider escape hatch on the query, not this enum.
|
|
17
|
+
*/
|
|
18
|
+
export declare const analyticsDimensionSchema: z.ZodEnum<{
|
|
19
|
+
browser: "browser";
|
|
20
|
+
campaign: "campaign";
|
|
21
|
+
city: "city";
|
|
22
|
+
country: "country";
|
|
23
|
+
date: "date";
|
|
24
|
+
deviceCategory: "deviceCategory";
|
|
25
|
+
hostname: "hostname";
|
|
26
|
+
medium: "medium";
|
|
27
|
+
operatingSystem: "operatingSystem";
|
|
28
|
+
page: "page";
|
|
29
|
+
region: "region";
|
|
30
|
+
source: "source";
|
|
31
|
+
}>;
|
|
32
|
+
export type AnalyticsDimension = z.infer<typeof analyticsDimensionSchema>;
|
|
33
|
+
/**
|
|
34
|
+
* Metrics every provider maps onto. Cloudflare Web Analytics has no concept of
|
|
35
|
+
* conversions/keyEvents; those resolve to 0 rather than erroring.
|
|
36
|
+
*/
|
|
37
|
+
export declare const analyticsMetricSchema: z.ZodEnum<{
|
|
38
|
+
avgSessionDuration: "avgSessionDuration";
|
|
39
|
+
bounceRate: "bounceRate";
|
|
40
|
+
conversions: "conversions";
|
|
41
|
+
engagementRate: "engagementRate";
|
|
42
|
+
keyEvents: "keyEvents";
|
|
43
|
+
pageviews: "pageviews";
|
|
44
|
+
sessions: "sessions";
|
|
45
|
+
users: "users";
|
|
46
|
+
}>;
|
|
47
|
+
export type AnalyticsMetric = z.infer<typeof analyticsMetricSchema>;
|
|
48
|
+
/**
|
|
49
|
+
* Cloudflare-only RUM dimensions with no cross-provider analog. Kept out of the
|
|
50
|
+
* generic `analyticsDimensionSchema` (they would be dead weight on GA4); reached
|
|
51
|
+
* through the opt-in `cloudflare` escape hatch on the query and the row. The enum
|
|
52
|
+
* values are the raw Cloudflare RUM field names.
|
|
53
|
+
*/
|
|
54
|
+
export declare const cloudflareDimensionSchema: z.ZodEnum<{
|
|
55
|
+
bot: "bot";
|
|
56
|
+
deliveryType: "deliveryType";
|
|
57
|
+
navigationType: "navigationType";
|
|
58
|
+
}>;
|
|
59
|
+
export type CloudflareDimension = z.infer<typeof cloudflareDimensionSchema>;
|
|
60
|
+
/** Query-side hatch — opt a provider into raw native dimensions/metrics. */
|
|
61
|
+
export declare const cloudflareQueryHatchSchema: z.ZodObject<{
|
|
62
|
+
dimensions: z.ZodArray<z.ZodEnum<{
|
|
63
|
+
bot: "bot";
|
|
64
|
+
deliveryType: "deliveryType";
|
|
65
|
+
navigationType: "navigationType";
|
|
66
|
+
}>>;
|
|
67
|
+
}, z.core.$strip>;
|
|
68
|
+
export type CloudflareQueryHatch = z.infer<typeof cloudflareQueryHatchSchema>;
|
|
69
|
+
export declare const ga4QueryHatchSchema: z.ZodObject<{
|
|
70
|
+
dimensions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
71
|
+
metrics: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
72
|
+
}, z.core.$strip>;
|
|
73
|
+
export type Ga4QueryHatch = z.infer<typeof ga4QueryHatchSchema>;
|
|
74
|
+
export declare const posthogQueryHatchSchema: z.ZodObject<{
|
|
75
|
+
dimensions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
76
|
+
}, z.core.$strip>;
|
|
77
|
+
export type PosthogQueryHatch = z.infer<typeof posthogQueryHatchSchema>;
|
|
78
|
+
/** Row-side hatch — raw native values grouped onto one result row. */
|
|
79
|
+
export declare const cloudflareRowHatchSchema: z.ZodRecord<z.ZodEnum<{
|
|
80
|
+
bot: "bot";
|
|
81
|
+
deliveryType: "deliveryType";
|
|
82
|
+
navigationType: "navigationType";
|
|
83
|
+
}> & z.core.$partial, z.ZodString>;
|
|
84
|
+
export type CloudflareRowHatch = z.infer<typeof cloudflareRowHatchSchema>;
|
|
85
|
+
export declare const ga4RowHatchSchema: z.ZodObject<{
|
|
86
|
+
dimensions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
87
|
+
metrics: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
88
|
+
}, z.core.$strip>;
|
|
89
|
+
export type Ga4RowHatch = z.infer<typeof ga4RowHatchSchema>;
|
|
90
|
+
export declare const posthogRowHatchSchema: z.ZodRecord<z.ZodString, z.ZodString>;
|
|
91
|
+
export type PosthogRowHatch = z.infer<typeof posthogRowHatchSchema>;
|
|
92
|
+
/**
|
|
93
|
+
* One grouped result: dimension values keyed by name, metric values keyed by
|
|
94
|
+
* name. Partial records — a row only carries what its query asked for.
|
|
95
|
+
*/
|
|
96
|
+
export declare const analyticsRowSchema: z.ZodObject<{
|
|
97
|
+
dimensions: z.ZodRecord<z.ZodEnum<{
|
|
98
|
+
browser: "browser";
|
|
99
|
+
campaign: "campaign";
|
|
100
|
+
city: "city";
|
|
101
|
+
country: "country";
|
|
102
|
+
date: "date";
|
|
103
|
+
deviceCategory: "deviceCategory";
|
|
104
|
+
hostname: "hostname";
|
|
105
|
+
medium: "medium";
|
|
106
|
+
operatingSystem: "operatingSystem";
|
|
107
|
+
page: "page";
|
|
108
|
+
region: "region";
|
|
109
|
+
source: "source";
|
|
110
|
+
}> & z.core.$partial, z.ZodString>;
|
|
111
|
+
metrics: z.ZodRecord<z.ZodEnum<{
|
|
112
|
+
avgSessionDuration: "avgSessionDuration";
|
|
113
|
+
bounceRate: "bounceRate";
|
|
114
|
+
conversions: "conversions";
|
|
115
|
+
engagementRate: "engagementRate";
|
|
116
|
+
keyEvents: "keyEvents";
|
|
117
|
+
pageviews: "pageviews";
|
|
118
|
+
sessions: "sessions";
|
|
119
|
+
users: "users";
|
|
120
|
+
}> & z.core.$partial, z.ZodNumber>;
|
|
121
|
+
cloudflare: z.ZodOptional<z.ZodRecord<z.ZodEnum<{
|
|
122
|
+
bot: "bot";
|
|
123
|
+
deliveryType: "deliveryType";
|
|
124
|
+
navigationType: "navigationType";
|
|
125
|
+
}> & z.core.$partial, z.ZodString>>;
|
|
126
|
+
ga4: z.ZodOptional<z.ZodObject<{
|
|
127
|
+
dimensions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
128
|
+
metrics: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
129
|
+
}, z.core.$strip>>;
|
|
130
|
+
posthog: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
131
|
+
}, z.core.$strip>;
|
|
132
|
+
export type AnalyticsRow = z.infer<typeof analyticsRowSchema>;
|
|
133
|
+
/**
|
|
134
|
+
* Cross-provider filter operators. Kept to the set every provider can serve so
|
|
135
|
+
* a portable filter degrades predictably:
|
|
136
|
+
* - `is` / `isNot` — exact-match (and its negation).
|
|
137
|
+
* - `in` — match any of a value list.
|
|
138
|
+
* - `contains` — substring match.
|
|
139
|
+
* Regex / OR-groups are intentionally absent: Cloudflare RUM can't express them,
|
|
140
|
+
* so they would not be portable. Reach for the per-provider escape hatch instead.
|
|
141
|
+
*/
|
|
142
|
+
export declare const analyticsFilterOpSchema: z.ZodEnum<{
|
|
143
|
+
contains: "contains";
|
|
144
|
+
in: "in";
|
|
145
|
+
is: "is";
|
|
146
|
+
isNot: "isNot";
|
|
147
|
+
}>;
|
|
148
|
+
export type AnalyticsFilterOp = z.infer<typeof analyticsFilterOpSchema>;
|
|
149
|
+
/**
|
|
150
|
+
* One portable filter predicate over a generic dimension. Translated per
|
|
151
|
+
* provider (GA4 `dimensionFilter`, Cloudflare GraphQL filter keys, PostHog HogQL
|
|
152
|
+
* `WHERE`). A provider with no field for `dimension` drops the predicate rather
|
|
153
|
+
* than erroring — same graceful-degrade contract as dimensions resolving to `''`
|
|
154
|
+
* (e.g. Cloudflare RUM has no `medium`/`campaign`, so those filters are no-ops
|
|
155
|
+
* there). `value` is a single string for `is`/`isNot`/`contains` and a list for
|
|
156
|
+
* `in` (a bare string is accepted and treated as a one-element list).
|
|
157
|
+
*/
|
|
158
|
+
export declare const analyticsFilterSchema: z.ZodObject<{
|
|
159
|
+
dimension: z.ZodEnum<{
|
|
160
|
+
browser: "browser";
|
|
161
|
+
campaign: "campaign";
|
|
162
|
+
city: "city";
|
|
163
|
+
country: "country";
|
|
164
|
+
date: "date";
|
|
165
|
+
deviceCategory: "deviceCategory";
|
|
166
|
+
hostname: "hostname";
|
|
167
|
+
medium: "medium";
|
|
168
|
+
operatingSystem: "operatingSystem";
|
|
169
|
+
page: "page";
|
|
170
|
+
region: "region";
|
|
171
|
+
source: "source";
|
|
172
|
+
}>;
|
|
173
|
+
op: z.ZodEnum<{
|
|
174
|
+
contains: "contains";
|
|
175
|
+
in: "in";
|
|
176
|
+
is: "is";
|
|
177
|
+
isNot: "isNot";
|
|
178
|
+
}>;
|
|
179
|
+
value: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>;
|
|
180
|
+
caseInsensitive: z.ZodDefault<z.ZodBoolean>;
|
|
181
|
+
}, z.core.$strip>;
|
|
182
|
+
export type AnalyticsFilter = z.input<typeof analyticsFilterSchema>;
|
|
183
|
+
/**
|
|
184
|
+
* A provider-agnostic query. The provider client owns translation into GA4's
|
|
185
|
+
* `runReport` body or Cloudflare's GraphQL document.
|
|
186
|
+
*/
|
|
187
|
+
export declare const analyticsQuerySchema: z.ZodObject<{
|
|
188
|
+
provider: z.ZodEnum<{
|
|
189
|
+
cloudflare: "cloudflare";
|
|
190
|
+
ga4: "ga4";
|
|
191
|
+
plausible: "plausible";
|
|
192
|
+
posthog: "posthog";
|
|
193
|
+
umami: "umami";
|
|
194
|
+
}>;
|
|
195
|
+
propertyId: z.ZodString;
|
|
196
|
+
dimensions: z.ZodArray<z.ZodEnum<{
|
|
197
|
+
browser: "browser";
|
|
198
|
+
campaign: "campaign";
|
|
199
|
+
city: "city";
|
|
200
|
+
country: "country";
|
|
201
|
+
date: "date";
|
|
202
|
+
deviceCategory: "deviceCategory";
|
|
203
|
+
hostname: "hostname";
|
|
204
|
+
medium: "medium";
|
|
205
|
+
operatingSystem: "operatingSystem";
|
|
206
|
+
page: "page";
|
|
207
|
+
region: "region";
|
|
208
|
+
source: "source";
|
|
209
|
+
}>>;
|
|
210
|
+
metrics: z.ZodArray<z.ZodEnum<{
|
|
211
|
+
avgSessionDuration: "avgSessionDuration";
|
|
212
|
+
bounceRate: "bounceRate";
|
|
213
|
+
conversions: "conversions";
|
|
214
|
+
engagementRate: "engagementRate";
|
|
215
|
+
keyEvents: "keyEvents";
|
|
216
|
+
pageviews: "pageviews";
|
|
217
|
+
sessions: "sessions";
|
|
218
|
+
users: "users";
|
|
219
|
+
}>>;
|
|
220
|
+
dateRange: z.ZodObject<{
|
|
221
|
+
from: z.ZodString;
|
|
222
|
+
to: z.ZodString;
|
|
223
|
+
}, z.core.$strip>;
|
|
224
|
+
limit: z.ZodOptional<z.ZodNumber>;
|
|
225
|
+
excludeBots: z.ZodDefault<z.ZodBoolean>;
|
|
226
|
+
filters: z.ZodDefault<z.ZodArray<z.ZodObject<{
|
|
227
|
+
dimension: z.ZodEnum<{
|
|
228
|
+
browser: "browser";
|
|
229
|
+
campaign: "campaign";
|
|
230
|
+
city: "city";
|
|
231
|
+
country: "country";
|
|
232
|
+
date: "date";
|
|
233
|
+
deviceCategory: "deviceCategory";
|
|
234
|
+
hostname: "hostname";
|
|
235
|
+
medium: "medium";
|
|
236
|
+
operatingSystem: "operatingSystem";
|
|
237
|
+
page: "page";
|
|
238
|
+
region: "region";
|
|
239
|
+
source: "source";
|
|
240
|
+
}>;
|
|
241
|
+
op: z.ZodEnum<{
|
|
242
|
+
contains: "contains";
|
|
243
|
+
in: "in";
|
|
244
|
+
is: "is";
|
|
245
|
+
isNot: "isNot";
|
|
246
|
+
}>;
|
|
247
|
+
value: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodString>]>;
|
|
248
|
+
caseInsensitive: z.ZodDefault<z.ZodBoolean>;
|
|
249
|
+
}, z.core.$strip>>>;
|
|
250
|
+
cloudflare: z.ZodOptional<z.ZodObject<{
|
|
251
|
+
dimensions: z.ZodArray<z.ZodEnum<{
|
|
252
|
+
bot: "bot";
|
|
253
|
+
deliveryType: "deliveryType";
|
|
254
|
+
navigationType: "navigationType";
|
|
255
|
+
}>>;
|
|
256
|
+
}, z.core.$strip>>;
|
|
257
|
+
ga4: z.ZodOptional<z.ZodObject<{
|
|
258
|
+
dimensions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
259
|
+
metrics: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
260
|
+
}, z.core.$strip>>;
|
|
261
|
+
posthog: z.ZodOptional<z.ZodObject<{
|
|
262
|
+
dimensions: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
263
|
+
}, z.core.$strip>>;
|
|
264
|
+
}, z.core.$strip>;
|
|
265
|
+
export type AnalyticsQuery = z.input<typeof analyticsQuerySchema>;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// The normalized, provider-agnostic data shape for Web Analytics. GA4 and
|
|
3
|
+
// Cloudflare provider clients reshape their raw API responses onto `AnalyticsRow`.
|
|
4
|
+
// Generic — carries no Pro or nuxtseo.com concepts. See ADR-0032 §2.
|
|
5
|
+
export const analyticsProviderSchema = z.enum(['ga4', 'cloudflare', 'posthog', 'umami', 'plausible']);
|
|
6
|
+
/**
|
|
7
|
+
* Dimensions every provider can be asked to group by. `source`/`medium`/
|
|
8
|
+
* `campaign` are the UTM-style traffic-attribution triple — a provider with no
|
|
9
|
+
* analog for one of them (Cloudflare RUM has only a referrer host) resolves it
|
|
10
|
+
* to an empty string rather than erroring, mirroring how metrics resolve to 0.
|
|
11
|
+
* Provider-native dimensions with no cross-provider analog go through the
|
|
12
|
+
* per-provider escape hatch on the query, not this enum.
|
|
13
|
+
*/
|
|
14
|
+
export const analyticsDimensionSchema = z.enum([
|
|
15
|
+
'date',
|
|
16
|
+
'page',
|
|
17
|
+
'hostname',
|
|
18
|
+
'source',
|
|
19
|
+
'medium',
|
|
20
|
+
'campaign',
|
|
21
|
+
'country',
|
|
22
|
+
'region',
|
|
23
|
+
'city',
|
|
24
|
+
'deviceCategory',
|
|
25
|
+
'browser',
|
|
26
|
+
'operatingSystem',
|
|
27
|
+
]);
|
|
28
|
+
/**
|
|
29
|
+
* Metrics every provider maps onto. Cloudflare Web Analytics has no concept of
|
|
30
|
+
* conversions/keyEvents; those resolve to 0 rather than erroring.
|
|
31
|
+
*/
|
|
32
|
+
export const analyticsMetricSchema = z.enum([
|
|
33
|
+
'sessions',
|
|
34
|
+
'users',
|
|
35
|
+
'pageviews',
|
|
36
|
+
'conversions',
|
|
37
|
+
'keyEvents',
|
|
38
|
+
'engagementRate',
|
|
39
|
+
'bounceRate',
|
|
40
|
+
'avgSessionDuration',
|
|
41
|
+
]);
|
|
42
|
+
/**
|
|
43
|
+
* Cloudflare-only RUM dimensions with no cross-provider analog. Kept out of the
|
|
44
|
+
* generic `analyticsDimensionSchema` (they would be dead weight on GA4); reached
|
|
45
|
+
* through the opt-in `cloudflare` escape hatch on the query and the row. The enum
|
|
46
|
+
* values are the raw Cloudflare RUM field names.
|
|
47
|
+
*/
|
|
48
|
+
export const cloudflareDimensionSchema = z.enum([
|
|
49
|
+
'navigationType',
|
|
50
|
+
'deliveryType',
|
|
51
|
+
'bot',
|
|
52
|
+
]);
|
|
53
|
+
// --- Provider escape hatches -----------------------------------------------
|
|
54
|
+
//
|
|
55
|
+
// Each provider may be opted into raw provider-native fields that have no
|
|
56
|
+
// cross-provider analog. Field names are validated, never bare strings:
|
|
57
|
+
// - Cloudflare RUM dimensions are a closed enum (`cloudflareDimensionSchema`).
|
|
58
|
+
// - GA4 Data API names are word chars plus `:` — the `:` admits custom
|
|
59
|
+
// `customEvent:*` / `customUser:*` dimensions; the set is otherwise open.
|
|
60
|
+
// - PostHog hatch values are HogQL `$pageview` property expressions, dotted
|
|
61
|
+
// `$`-identifiers. The regex doubles as a HogQL injection guard, since the
|
|
62
|
+
// expression is interpolated into the generated SQL.
|
|
63
|
+
const ga4FieldNameSchema = z.string().regex(/^[\w:]+$/, 'invalid GA4 field name');
|
|
64
|
+
const posthogPropertyExprSchema = z.string().regex(/^[\w$.]+$/, 'invalid PostHog property expression');
|
|
65
|
+
/** Query-side hatch — opt a provider into raw native dimensions/metrics. */
|
|
66
|
+
export const cloudflareQueryHatchSchema = z.object({
|
|
67
|
+
dimensions: z.array(cloudflareDimensionSchema),
|
|
68
|
+
});
|
|
69
|
+
export const ga4QueryHatchSchema = z.object({
|
|
70
|
+
dimensions: z.array(ga4FieldNameSchema).optional(),
|
|
71
|
+
metrics: z.array(ga4FieldNameSchema).optional(),
|
|
72
|
+
});
|
|
73
|
+
export const posthogQueryHatchSchema = z.object({
|
|
74
|
+
dimensions: z.array(posthogPropertyExprSchema).optional(),
|
|
75
|
+
});
|
|
76
|
+
/** Row-side hatch — raw native values grouped onto one result row. */
|
|
77
|
+
export const cloudflareRowHatchSchema = z.partialRecord(cloudflareDimensionSchema, z.string());
|
|
78
|
+
export const ga4RowHatchSchema = z.object({
|
|
79
|
+
dimensions: z.record(ga4FieldNameSchema, z.string()).optional(),
|
|
80
|
+
metrics: z.record(ga4FieldNameSchema, z.number()).optional(),
|
|
81
|
+
});
|
|
82
|
+
export const posthogRowHatchSchema = z.record(posthogPropertyExprSchema, z.string());
|
|
83
|
+
/**
|
|
84
|
+
* One grouped result: dimension values keyed by name, metric values keyed by
|
|
85
|
+
* name. Partial records — a row only carries what its query asked for.
|
|
86
|
+
*/
|
|
87
|
+
export const analyticsRowSchema = z.object({
|
|
88
|
+
dimensions: z.partialRecord(analyticsDimensionSchema, z.string()),
|
|
89
|
+
metrics: z.partialRecord(analyticsMetricSchema, z.number()),
|
|
90
|
+
/**
|
|
91
|
+
* Raw provider-native values — present only when the query opted into a
|
|
92
|
+
* provider escape hatch (`query.cloudflare` / `query.ga4` / `query.posthog`),
|
|
93
|
+
* and only on rows from that provider. Keyed by the raw provider field name.
|
|
94
|
+
* Cloudflare's keys are a closed enum; GA4's and PostHog's are open string
|
|
95
|
+
* records because those providers expose unbounded (incl. custom) fields.
|
|
96
|
+
*/
|
|
97
|
+
cloudflare: cloudflareRowHatchSchema.optional(),
|
|
98
|
+
ga4: ga4RowHatchSchema.optional(),
|
|
99
|
+
posthog: posthogRowHatchSchema.optional(),
|
|
100
|
+
});
|
|
101
|
+
/**
|
|
102
|
+
* Cross-provider filter operators. Kept to the set every provider can serve so
|
|
103
|
+
* a portable filter degrades predictably:
|
|
104
|
+
* - `is` / `isNot` — exact-match (and its negation).
|
|
105
|
+
* - `in` — match any of a value list.
|
|
106
|
+
* - `contains` — substring match.
|
|
107
|
+
* Regex / OR-groups are intentionally absent: Cloudflare RUM can't express them,
|
|
108
|
+
* so they would not be portable. Reach for the per-provider escape hatch instead.
|
|
109
|
+
*/
|
|
110
|
+
export const analyticsFilterOpSchema = z.enum(['is', 'isNot', 'in', 'contains']);
|
|
111
|
+
/**
|
|
112
|
+
* One portable filter predicate over a generic dimension. Translated per
|
|
113
|
+
* provider (GA4 `dimensionFilter`, Cloudflare GraphQL filter keys, PostHog HogQL
|
|
114
|
+
* `WHERE`). A provider with no field for `dimension` drops the predicate rather
|
|
115
|
+
* than erroring — same graceful-degrade contract as dimensions resolving to `''`
|
|
116
|
+
* (e.g. Cloudflare RUM has no `medium`/`campaign`, so those filters are no-ops
|
|
117
|
+
* there). `value` is a single string for `is`/`isNot`/`contains` and a list for
|
|
118
|
+
* `in` (a bare string is accepted and treated as a one-element list).
|
|
119
|
+
*/
|
|
120
|
+
export const analyticsFilterSchema = z.object({
|
|
121
|
+
dimension: analyticsDimensionSchema,
|
|
122
|
+
op: analyticsFilterOpSchema,
|
|
123
|
+
value: z.union([z.string(), z.array(z.string())]),
|
|
124
|
+
/**
|
|
125
|
+
* Match case-insensitively. Defaults true so cross-provider dimension values
|
|
126
|
+
* with inconsistent casing (e.g. PostHog `$device_type` is `Desktop`, GA4
|
|
127
|
+
* `deviceCategory` is `desktop`) match the same filter. Cloudflare RUM has no
|
|
128
|
+
* case-insensitive operator, so it is best-effort there.
|
|
129
|
+
*/
|
|
130
|
+
caseInsensitive: z.boolean().default(true),
|
|
131
|
+
});
|
|
132
|
+
/**
|
|
133
|
+
* A provider-agnostic query. The provider client owns translation into GA4's
|
|
134
|
+
* `runReport` body or Cloudflare's GraphQL document.
|
|
135
|
+
*/
|
|
136
|
+
export const analyticsQuerySchema = z.object({
|
|
137
|
+
provider: analyticsProviderSchema,
|
|
138
|
+
/** GA4 property id or Cloudflare Web Analytics site tag. */
|
|
139
|
+
propertyId: z.string().min(1),
|
|
140
|
+
dimensions: z.array(analyticsDimensionSchema),
|
|
141
|
+
metrics: z.array(analyticsMetricSchema).min(1),
|
|
142
|
+
dateRange: z.object({
|
|
143
|
+
from: z.string(), // ISO yyyy-mm-dd
|
|
144
|
+
to: z.string(),
|
|
145
|
+
}),
|
|
146
|
+
limit: z.number().int().positive().optional(),
|
|
147
|
+
/**
|
|
148
|
+
* Exclude automated traffic. Default true. Cloudflare applies a `bot: 0`
|
|
149
|
+
* filter; GA4 already excludes known bots from every report, so it is a
|
|
150
|
+
* no-op there.
|
|
151
|
+
*/
|
|
152
|
+
excludeBots: z.boolean().default(true),
|
|
153
|
+
/**
|
|
154
|
+
* Portable dimension filters, ANDed together and applied on top of the date
|
|
155
|
+
* window + `excludeBots`. Each provider translates what it can and silently
|
|
156
|
+
* drops predicates over dimensions it has no field for (see
|
|
157
|
+
* `analyticsFilterSchema`). Empty by default (no filtering).
|
|
158
|
+
*/
|
|
159
|
+
filters: z.array(analyticsFilterSchema).default([]),
|
|
160
|
+
/**
|
|
161
|
+
* Provider-specific escape hatches. Each opts the matching provider into raw
|
|
162
|
+
* provider-native fields with no cross-provider analog; the grouped values
|
|
163
|
+
* come back on the matching key of `AnalyticsRow`. A provider ignores hatches
|
|
164
|
+
* addressed to other providers.
|
|
165
|
+
*
|
|
166
|
+
* - `cloudflare` — Cloudflare-only RUM dimensions (closed enum).
|
|
167
|
+
* - `ga4` — raw GA4 Data API dimension/metric names, including custom
|
|
168
|
+
* `customEvent:*` / `customUser:*` dimensions.
|
|
169
|
+
* - `posthog` — raw HogQL `$pageview` property expressions (e.g.
|
|
170
|
+
* `properties.$browser_version`); validated as dotted identifiers.
|
|
171
|
+
*/
|
|
172
|
+
cloudflare: cloudflareQueryHatchSchema.optional(),
|
|
173
|
+
ga4: ga4QueryHatchSchema.optional(),
|
|
174
|
+
posthog: posthogQueryHatchSchema.optional(),
|
|
175
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export declare const analyticsWebVitalsRowSchema: z.ZodObject<{
|
|
3
|
+
date: z.ZodString;
|
|
4
|
+
lcpP75: z.ZodNullable<z.ZodNumber>;
|
|
5
|
+
inpP75: z.ZodNullable<z.ZodNumber>;
|
|
6
|
+
clsP75: z.ZodNullable<z.ZodNumber>;
|
|
7
|
+
lcpGood: z.ZodNumber;
|
|
8
|
+
lcpNeedsImprovement: z.ZodNumber;
|
|
9
|
+
lcpPoor: z.ZodNumber;
|
|
10
|
+
inpGood: z.ZodNumber;
|
|
11
|
+
inpNeedsImprovement: z.ZodNumber;
|
|
12
|
+
inpPoor: z.ZodNumber;
|
|
13
|
+
clsGood: z.ZodNumber;
|
|
14
|
+
clsNeedsImprovement: z.ZodNumber;
|
|
15
|
+
clsPoor: z.ZodNumber;
|
|
16
|
+
}, z.core.$strip>;
|
|
17
|
+
export type AnalyticsWebVitalsRow = z.infer<typeof analyticsWebVitalsRowSchema>;
|
|
18
|
+
/** The three Core Web Vitals a RUM dataset attributes to a DOM element. */
|
|
19
|
+
export declare const webVitalsMetricSchema: z.ZodEnum<{
|
|
20
|
+
cls: "cls";
|
|
21
|
+
inp: "inp";
|
|
22
|
+
lcp: "lcp";
|
|
23
|
+
}>;
|
|
24
|
+
export type WebVitalsMetric = z.infer<typeof webVitalsMetricSchema>;
|
|
25
|
+
export declare const analyticsWebVitalsElementSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
26
|
+
route: z.ZodString;
|
|
27
|
+
selector: z.ZodString;
|
|
28
|
+
p75: z.ZodNullable<z.ZodNumber>;
|
|
29
|
+
count: z.ZodNumber;
|
|
30
|
+
poorFraction: z.ZodNullable<z.ZodNumber>;
|
|
31
|
+
metric: z.ZodLiteral<"lcp">;
|
|
32
|
+
resource: z.ZodNullable<z.ZodString>;
|
|
33
|
+
fetchPriority: z.ZodNullable<z.ZodString>;
|
|
34
|
+
initiatorType: z.ZodNullable<z.ZodString>;
|
|
35
|
+
phases: z.ZodObject<{
|
|
36
|
+
timeToFirstByte: z.ZodNullable<z.ZodNumber>;
|
|
37
|
+
resourceLoadDelay: z.ZodNullable<z.ZodNumber>;
|
|
38
|
+
resourceLoadTime: z.ZodNullable<z.ZodNumber>;
|
|
39
|
+
elementRenderDelay: z.ZodNullable<z.ZodNumber>;
|
|
40
|
+
}, z.core.$strip>;
|
|
41
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
42
|
+
route: z.ZodString;
|
|
43
|
+
selector: z.ZodString;
|
|
44
|
+
p75: z.ZodNullable<z.ZodNumber>;
|
|
45
|
+
count: z.ZodNumber;
|
|
46
|
+
poorFraction: z.ZodNullable<z.ZodNumber>;
|
|
47
|
+
metric: z.ZodLiteral<"inp">;
|
|
48
|
+
actionName: z.ZodNullable<z.ZodString>;
|
|
49
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
50
|
+
route: z.ZodString;
|
|
51
|
+
selector: z.ZodString;
|
|
52
|
+
p75: z.ZodNullable<z.ZodNumber>;
|
|
53
|
+
count: z.ZodNumber;
|
|
54
|
+
poorFraction: z.ZodNullable<z.ZodNumber>;
|
|
55
|
+
metric: z.ZodLiteral<"cls">;
|
|
56
|
+
}, z.core.$strip>], "metric">;
|
|
57
|
+
export type AnalyticsWebVitalsElement = z.infer<typeof analyticsWebVitalsElementSchema>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
// Field-data Core Web Vitals from a provider's RUM dataset (real users), one row
|
|
3
|
+
// per day. Distinct from the traffic metrics in `analytics-row` — web vitals are
|
|
4
|
+
// p75 quantiles + good/needs-improvement/poor distribution counts, not summable
|
|
5
|
+
// counts. Only providers with a RUM web-vitals dataset emit these (Cloudflare).
|
|
6
|
+
// The shape mirrors the Pro CrUX `CwvHistoryRow` so a consumer can drop it into
|
|
7
|
+
// the existing Core Web Vitals surface as a fallback when CrUX has no data.
|
|
8
|
+
export const analyticsWebVitalsRowSchema = z.object({
|
|
9
|
+
/** ISO `yyyy-mm-dd`. */
|
|
10
|
+
date: z.string(),
|
|
11
|
+
/** p75 — the headline CrUX-parity figure. Null when the metric has no samples. */
|
|
12
|
+
lcpP75: z.number().nullable(),
|
|
13
|
+
inpP75: z.number().nullable(),
|
|
14
|
+
clsP75: z.number().nullable(),
|
|
15
|
+
// Provider observations behind the good/needs-improvement/poor histogram.
|
|
16
|
+
// Adaptive-provider estimates are normalized to their effective sample size.
|
|
17
|
+
lcpGood: z.number(),
|
|
18
|
+
lcpNeedsImprovement: z.number(),
|
|
19
|
+
lcpPoor: z.number(),
|
|
20
|
+
inpGood: z.number(),
|
|
21
|
+
inpNeedsImprovement: z.number(),
|
|
22
|
+
inpPoor: z.number(),
|
|
23
|
+
clsGood: z.number(),
|
|
24
|
+
clsNeedsImprovement: z.number(),
|
|
25
|
+
clsPoor: z.number(),
|
|
26
|
+
});
|
|
27
|
+
/** The three Core Web Vitals a RUM dataset attributes to a DOM element. */
|
|
28
|
+
export const webVitalsMetricSchema = z.enum(['lcp', 'inp', 'cls']);
|
|
29
|
+
// Per-element field-CWV attribution — one DOM element behind a metric, from the
|
|
30
|
+
// provider's RUM dataset, discriminated by `metric`. This is the "what to fix"
|
|
31
|
+
// payload: `selector` is the element's DOM path, grep-able in the user's own
|
|
32
|
+
// source. LCP additionally carries the resource + fetch-priority + phase split
|
|
33
|
+
// (so a text-LCP vs image-LCP fix can be told apart); INP carries the interaction
|
|
34
|
+
// action; CLS carries only the shifted element. `count` is a provider-sampled
|
|
35
|
+
// estimate (Cloudflare adaptive sampling), never exact. Rank by severity and
|
|
36
|
+
// label it as estimated.
|
|
37
|
+
const elementBase = {
|
|
38
|
+
/** Page path where the element was observed (`requestPath`). */
|
|
39
|
+
route: z.string(),
|
|
40
|
+
/** DOM selector of the element — grep-able in the user's source. */
|
|
41
|
+
selector: z.string(),
|
|
42
|
+
/** p75: ms for lcp/inp, unitless for cls. Null when the group has no samples. */
|
|
43
|
+
p75: z.number().nullable(),
|
|
44
|
+
/** Sampled estimate of affected page views, not an exact count. */
|
|
45
|
+
count: z.number(),
|
|
46
|
+
/** poor / (good + ni + poor) for this element, or null when no distribution. */
|
|
47
|
+
poorFraction: z.number().nullable(),
|
|
48
|
+
};
|
|
49
|
+
export const analyticsWebVitalsElementSchema = z.discriminatedUnion('metric', [
|
|
50
|
+
z.object({
|
|
51
|
+
metric: z.literal('lcp'),
|
|
52
|
+
...elementBase,
|
|
53
|
+
/** Host+path of the LCP resource. Null = text LCP (heading/paragraph). */
|
|
54
|
+
resource: z.string().nullable(),
|
|
55
|
+
/** `fetchpriority` the browser saw on the LCP element. Null when n/a. */
|
|
56
|
+
fetchPriority: z.string().nullable(),
|
|
57
|
+
initiatorType: z.string().nullable(),
|
|
58
|
+
/** The four LCP sub-phases (ms averages) — which one dominates names the fix. */
|
|
59
|
+
phases: z.object({
|
|
60
|
+
timeToFirstByte: z.number().nullable(),
|
|
61
|
+
resourceLoadDelay: z.number().nullable(),
|
|
62
|
+
resourceLoadTime: z.number().nullable(),
|
|
63
|
+
elementRenderDelay: z.number().nullable(),
|
|
64
|
+
}),
|
|
65
|
+
}),
|
|
66
|
+
z.object({
|
|
67
|
+
metric: z.literal('inp'),
|
|
68
|
+
...elementBase,
|
|
69
|
+
/** Interaction type behind the delay (pointer/keyboard/…). */
|
|
70
|
+
actionName: z.string().nullable(),
|
|
71
|
+
}),
|
|
72
|
+
z.object({
|
|
73
|
+
metric: z.literal('cls'),
|
|
74
|
+
...elementBase,
|
|
75
|
+
}),
|
|
76
|
+
]);
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AnalyticsDimension, AnalyticsMetric, AnalyticsProvider } from './analytics-row.js';
|
|
2
|
+
export interface AnalyticsCapabilities {
|
|
3
|
+
/** Aggregate (dimensionless) metrics that carry real data; others resolve to 0. */
|
|
4
|
+
aggregateMetrics: AnalyticsMetric[];
|
|
5
|
+
/**
|
|
6
|
+
* Count metrics with a real per-day series. A subset of `{sessions, users,
|
|
7
|
+
* pageviews}` — drives the chartable series. Cloudflare's `users` equals
|
|
8
|
+
* `sessions` (drop the duplicate); Umami has no per-day visitors series.
|
|
9
|
+
*/
|
|
10
|
+
seriesMetrics: AnalyticsMetric[];
|
|
11
|
+
/** Dimensions with a real breakdown; others return an empty list. */
|
|
12
|
+
breakdownDimensions: AnalyticsDimension[];
|
|
13
|
+
/**
|
|
14
|
+
* Count metrics populated PER breakdown row. Umami's `/metrics` returns a
|
|
15
|
+
* visitor count only (`['users']`); everything else returns the full
|
|
16
|
+
* `{sessions, users, pageviews}`. Drives breakdown ranking + visible columns.
|
|
17
|
+
*/
|
|
18
|
+
breakdownMetrics: AnalyticsMetric[];
|
|
19
|
+
/** The session-quality metric the provider reports, or null (neither). */
|
|
20
|
+
quality: 'engagementRate' | 'bounceRate' | null;
|
|
21
|
+
/** Field-data Core Web Vitals (RUM) dataset available (`queryWebVitals`). */
|
|
22
|
+
webVitals: boolean;
|
|
23
|
+
/** Key events / conversions available. */
|
|
24
|
+
keyEvents: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare const ANALYTICS_CAPABILITIES: Record<AnalyticsProvider, AnalyticsCapabilities>;
|
|
27
|
+
export declare function getAnalyticsCapabilities(provider: string | null | undefined): AnalyticsCapabilities | null;
|
|
28
|
+
/**
|
|
29
|
+
* The metric to rank/show a breakdown by. Returns `preferred` when the provider
|
|
30
|
+
* populates it per row (the common case: pages by pageviews, else sessions),
|
|
31
|
+
* otherwise the provider's single available breakdown metric — Umami serves a
|
|
32
|
+
* visitor count only, so every breakdown there ranks by `users`. An unknown
|
|
33
|
+
* provider keeps `preferred`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function resolveBreakdownMetric(provider: string | null | undefined, preferred: AnalyticsMetric): AnalyticsMetric;
|