@nextlytics/core 0.4.1 → 0.4.2-canary.106
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/anonymous-user.d.ts +1 -0
- package/dist/api-handler.d.ts +6 -3
- package/dist/api-handler.js +24 -5
- package/dist/backends/clickhouse.d.ts +1 -0
- package/dist/backends/ga.d.ts +1 -0
- package/dist/backends/gtm.d.ts +1 -0
- package/dist/backends/lib/db.d.ts +1 -0
- package/dist/backends/logging.d.ts +1 -0
- package/dist/backends/neon.d.ts +1 -0
- package/dist/backends/postgrest.d.ts +1 -0
- package/dist/backends/posthog.d.ts +1 -0
- package/dist/backends/segment.d.ts +1 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.js +56 -16
- package/dist/config-helpers.d.ts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/middleware.d.ts +3 -2
- package/dist/middleware.js +2 -2
- package/dist/pages-router.d.ts +7 -1
- package/dist/pages-router.js +5 -1
- package/dist/plugins/vercel-geo.d.ts +1 -0
- package/dist/server-component-context.d.ts +4 -0
- package/dist/server-component-context.js +4 -1
- package/dist/server.d.ts +1 -0
- package/dist/server.js +80 -33
- package/dist/types.d.ts +15 -4
- package/dist/uitils.d.ts +1 -0
- package/package.json +1 -1
package/dist/anonymous-user.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
2
|
import { RequestContext, ServerEventContext, NextlyticsConfig, AnonymousUserResult } from './types.js';
|
|
3
3
|
import 'next/dist/server/web/spec-extension/cookies';
|
|
4
|
+
import 'next';
|
|
4
5
|
|
|
5
6
|
type ResolveAnonymousUserParams = {
|
|
6
7
|
ctx: RequestContext;
|
package/dist/api-handler.d.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
import { NextRequest } from 'next/server';
|
|
2
|
-
import {
|
|
2
|
+
import { RequestContext, JavascriptTemplate, NextlyticsEvent, PageViewDelivery, DispatchResult, UserContext } from './types.js';
|
|
3
3
|
import { NextlyticsConfigWithDefaults } from './config-helpers.js';
|
|
4
4
|
import 'next/dist/server/web/spec-extension/cookies';
|
|
5
|
+
import 'next';
|
|
5
6
|
|
|
6
7
|
type DispatchEvent = (event: NextlyticsEvent, ctx: RequestContext, policyFilter?: PageViewDelivery | "client-actions") => DispatchResult;
|
|
7
8
|
type UpdateEvent = (eventId: string, patch: Partial<NextlyticsEvent>, ctx: RequestContext) => Promise<void>;
|
|
9
|
+
/** Collect the client-side templates from the configured backends. */
|
|
10
|
+
type CollectTemplates = (ctx: RequestContext) => Record<string, JavascriptTemplate>;
|
|
8
11
|
declare function getUserContext(config: NextlyticsConfigWithDefaults, ctx: RequestContext): Promise<UserContext | undefined>;
|
|
9
12
|
declare function getEventProps(config: NextlyticsConfigWithDefaults, ctx: RequestContext, userContext?: UserContext): Promise<Record<string, unknown> | undefined>;
|
|
10
|
-
declare function handleEventPost(request: NextRequest, config: NextlyticsConfigWithDefaults, dispatchEvent: DispatchEvent, updateEvent: UpdateEvent): Promise<Response>;
|
|
13
|
+
declare function handleEventPost(request: NextRequest, config: NextlyticsConfigWithDefaults, dispatchEvent: DispatchEvent, updateEvent: UpdateEvent, collectTemplates: CollectTemplates): Promise<Response>;
|
|
11
14
|
|
|
12
|
-
export { type DispatchEvent, type UpdateEvent, getEventProps, getUserContext, handleEventPost };
|
|
15
|
+
export { type CollectTemplates, type DispatchEvent, type UpdateEvent, getEventProps, getUserContext, handleEventPost };
|
package/dist/api-handler.js
CHANGED
|
@@ -27,6 +27,14 @@ var import_server = require("next/server");
|
|
|
27
27
|
var import_server_component_context = require("./server-component-context");
|
|
28
28
|
var import_uitils = require("./uitils");
|
|
29
29
|
var import_anonymous_user = require("./anonymous-user");
|
|
30
|
+
function newTemplatesFor(hctx) {
|
|
31
|
+
const all = hctx.collectTemplates(hctx.ctx);
|
|
32
|
+
const missing = {};
|
|
33
|
+
for (const [id, template] of Object.entries(all)) {
|
|
34
|
+
if (!hctx.knownTemplateIds.has(id)) missing[id] = template;
|
|
35
|
+
}
|
|
36
|
+
return Object.keys(missing).length > 0 ? missing : void 0;
|
|
37
|
+
}
|
|
30
38
|
function createRequestContext(request) {
|
|
31
39
|
return {
|
|
32
40
|
headers: request.headers,
|
|
@@ -110,13 +118,14 @@ async function handleClientInit(request, hctx) {
|
|
|
110
118
|
(0, import_server.after)(() => completion2);
|
|
111
119
|
return Response.json({
|
|
112
120
|
ok: true,
|
|
113
|
-
items: filterScripts(actions)
|
|
121
|
+
items: filterScripts(actions),
|
|
122
|
+
templates: newTemplatesFor(hctx)
|
|
114
123
|
});
|
|
115
124
|
}
|
|
116
125
|
const { completion } = dispatchEvent(event, ctx, "client-actions");
|
|
117
126
|
(0, import_server.after)(() => completion);
|
|
118
127
|
(0, import_server.after)(() => updateEvent(pageRenderId, { clientContext, userContext, anonymousUserId }, ctx));
|
|
119
|
-
return Response.json({ ok: true });
|
|
128
|
+
return Response.json({ ok: true, templates: newTemplatesFor(hctx) });
|
|
120
129
|
}
|
|
121
130
|
async function handleClientEvent(request, hctx) {
|
|
122
131
|
const { pageRenderId, ctx, apiCallServerContext, userContext, config, dispatchEvent } = hctx;
|
|
@@ -144,9 +153,13 @@ async function handleClientEvent(request, hctx) {
|
|
|
144
153
|
const { clientActions, completion } = dispatchEvent(event, ctx);
|
|
145
154
|
const actions = await clientActions;
|
|
146
155
|
(0, import_server.after)(() => completion);
|
|
147
|
-
return Response.json({
|
|
156
|
+
return Response.json({
|
|
157
|
+
ok: true,
|
|
158
|
+
items: filterScripts(actions),
|
|
159
|
+
templates: newTemplatesFor(hctx)
|
|
160
|
+
});
|
|
148
161
|
}
|
|
149
|
-
async function handleEventPost(request, config, dispatchEvent, updateEvent) {
|
|
162
|
+
async function handleEventPost(request, config, dispatchEvent, updateEvent, collectTemplates) {
|
|
150
163
|
const softNavHeader = request.headers.get(import_server_component_context.headerNames.isSoftNavigation);
|
|
151
164
|
const isSoftNavigation = softNavHeader === "1";
|
|
152
165
|
const pageRenderIdHeader = request.headers.get(import_server_component_context.headerNames.pageRenderId);
|
|
@@ -162,6 +175,10 @@ async function handleEventPost(request, config, dispatchEvent, updateEvent) {
|
|
|
162
175
|
const ctx = createRequestContext(request);
|
|
163
176
|
const apiCallServerContext = (0, import_uitils.createServerContext)(request);
|
|
164
177
|
const userContext = await getUserContext(config, ctx);
|
|
178
|
+
const knownTemplatesHeader = request.headers.get(import_server_component_context.headerNames.knownTemplates);
|
|
179
|
+
const knownTemplateIds = new Set(
|
|
180
|
+
knownTemplatesHeader ? knownTemplatesHeader.split(",").map((id) => id.trim()).filter(Boolean) : []
|
|
181
|
+
);
|
|
165
182
|
const cookiePageRenderId = request.cookies.get(import_server_component_context.LAST_PAGE_RENDER_ID_COOKIE)?.value;
|
|
166
183
|
const pageRenderId = isSoftNavigation ? cookiePageRenderId ?? (0, import_uitils.generateId)() : pageRenderIdHeader;
|
|
167
184
|
if (isSoftNavigation && !cookiePageRenderId && config.debug) {
|
|
@@ -177,7 +194,9 @@ async function handleEventPost(request, config, dispatchEvent, updateEvent) {
|
|
|
177
194
|
userContext,
|
|
178
195
|
config,
|
|
179
196
|
dispatchEvent,
|
|
180
|
-
updateEvent
|
|
197
|
+
updateEvent,
|
|
198
|
+
collectTemplates,
|
|
199
|
+
knownTemplateIds
|
|
181
200
|
};
|
|
182
201
|
const bodyType = body.type;
|
|
183
202
|
switch (bodyType) {
|
package/dist/backends/ga.d.ts
CHANGED
package/dist/backends/gtm.d.ts
CHANGED
package/dist/backends/neon.d.ts
CHANGED
package/dist/client.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { ReactNode } from 'react';
|
|
|
3
3
|
import { TemplatizedScriptInsertion, JavascriptTemplate } from './types.js';
|
|
4
4
|
import 'next/dist/server/web/spec-extension/cookies';
|
|
5
5
|
import 'next/server';
|
|
6
|
+
import 'next';
|
|
6
7
|
|
|
7
8
|
/** Context object for Pages Router integration */
|
|
8
9
|
type NextlyticsContext = {
|
package/dist/client.js
CHANGED
|
@@ -153,7 +153,11 @@ function NextlyticsScripts({
|
|
|
153
153
|
});
|
|
154
154
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children: compiled.map(({ key, ...props }) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_client_utils.InjectScript, { ...props }, key)) });
|
|
155
155
|
}
|
|
156
|
-
async function sendEventToServer(requestId, request, {
|
|
156
|
+
async function sendEventToServer(requestId, request, {
|
|
157
|
+
signal,
|
|
158
|
+
isSoftNavigation,
|
|
159
|
+
knownTemplateIds
|
|
160
|
+
} = {}) {
|
|
157
161
|
try {
|
|
158
162
|
const headers = {
|
|
159
163
|
"Content-Type": "application/json",
|
|
@@ -162,6 +166,9 @@ async function sendEventToServer(requestId, request, { signal, isSoftNavigation
|
|
|
162
166
|
if (isSoftNavigation) {
|
|
163
167
|
headers[import_server_component_context.headerNames.isSoftNavigation] = "1";
|
|
164
168
|
}
|
|
169
|
+
if (knownTemplateIds?.length) {
|
|
170
|
+
headers[import_server_component_context.headerNames.knownTemplates] = knownTemplateIds.join(",");
|
|
171
|
+
}
|
|
165
172
|
const response = await fetch("/api/event", {
|
|
166
173
|
method: "POST",
|
|
167
174
|
headers,
|
|
@@ -175,7 +182,7 @@ async function sendEventToServer(requestId, request, { signal, isSoftNavigation
|
|
|
175
182
|
return { ok: false };
|
|
176
183
|
}
|
|
177
184
|
const data = await response.json().catch(() => ({ ok: response.ok }));
|
|
178
|
-
return { ok: data.ok ?? response.ok, items: data.items };
|
|
185
|
+
return { ok: data.ok ?? response.ok, items: data.items, templates: data.templates };
|
|
179
186
|
} catch (error) {
|
|
180
187
|
if (error instanceof Error && error.name === "AbortError") {
|
|
181
188
|
return { ok: false };
|
|
@@ -185,9 +192,28 @@ async function sendEventToServer(requestId, request, { signal, isSoftNavigation
|
|
|
185
192
|
}
|
|
186
193
|
}
|
|
187
194
|
function NextlyticsClient(props) {
|
|
188
|
-
const { requestId, scripts: initialScripts = []
|
|
195
|
+
const { requestId, scripts: initialScripts = [] } = props.ctx;
|
|
189
196
|
const scriptsRef = (0, import_react.useRef)([]);
|
|
190
197
|
const subscribersRef = (0, import_react.useRef)(/* @__PURE__ */ new Set());
|
|
198
|
+
const [templates, setTemplates] = (0, import_react.useState)(
|
|
199
|
+
() => props.ctx.templates ?? {}
|
|
200
|
+
);
|
|
201
|
+
const mergeTemplates = (0, import_react.useCallback)((incoming) => {
|
|
202
|
+
if (!incoming) return;
|
|
203
|
+
const keys = Object.keys(incoming);
|
|
204
|
+
if (keys.length === 0) return;
|
|
205
|
+
setTemplates((prev) => {
|
|
206
|
+
const hasNew = keys.some((k) => prev[k] !== incoming[k]);
|
|
207
|
+
return hasNew ? { ...prev, ...incoming } : prev;
|
|
208
|
+
});
|
|
209
|
+
}, []);
|
|
210
|
+
(0, import_react.useEffect)(() => {
|
|
211
|
+
mergeTemplates(props.ctx.templates);
|
|
212
|
+
}, [props.ctx.templates, mergeTemplates]);
|
|
213
|
+
const knownTemplateIdsRef = (0, import_react.useRef)(Object.keys(props.ctx.templates ?? {}));
|
|
214
|
+
(0, import_react.useEffect)(() => {
|
|
215
|
+
knownTemplateIdsRef.current = Object.keys(templates);
|
|
216
|
+
}, [templates]);
|
|
191
217
|
const addScripts = (0, import_react.useCallback)((newScripts) => {
|
|
192
218
|
(0, import_client_utils.debug)("Adding scripts", {
|
|
193
219
|
newCount: newScripts.length,
|
|
@@ -197,8 +223,16 @@ function NextlyticsClient(props) {
|
|
|
197
223
|
subscribersRef.current.forEach((cb) => cb());
|
|
198
224
|
}, []);
|
|
199
225
|
const contextValue = (0, import_react.useMemo)(
|
|
200
|
-
() => ({
|
|
201
|
-
|
|
226
|
+
() => ({
|
|
227
|
+
requestId,
|
|
228
|
+
templates,
|
|
229
|
+
addScripts,
|
|
230
|
+
scriptsRef,
|
|
231
|
+
subscribersRef,
|
|
232
|
+
mergeTemplates,
|
|
233
|
+
knownTemplateIdsRef
|
|
234
|
+
}),
|
|
235
|
+
[requestId, templates, addScripts, mergeTemplates]
|
|
202
236
|
);
|
|
203
237
|
(0, import_client_utils.useNavigation)(requestId, ({ softNavigation, signal }) => {
|
|
204
238
|
(0, import_client_utils.debug)("Sending page-view", { requestId, softNavigation });
|
|
@@ -206,9 +240,10 @@ function NextlyticsClient(props) {
|
|
|
206
240
|
sendEventToServer(
|
|
207
241
|
requestId,
|
|
208
242
|
{ type: "page-view", clientContext, softNavigation: softNavigation || void 0 },
|
|
209
|
-
{ signal, isSoftNavigation: softNavigation }
|
|
210
|
-
).then(({ items }) => {
|
|
243
|
+
{ signal, isSoftNavigation: softNavigation, knownTemplateIds: knownTemplateIdsRef.current }
|
|
244
|
+
).then(({ items, templates: responseTemplates }) => {
|
|
211
245
|
(0, import_client_utils.debug)("page-view response", { scriptsCount: items?.length ?? 0 });
|
|
246
|
+
mergeTemplates(responseTemplates);
|
|
212
247
|
if (items?.length) addScripts(items);
|
|
213
248
|
});
|
|
214
249
|
});
|
|
@@ -224,22 +259,27 @@ function useNextlytics() {
|
|
|
224
259
|
"[Nextlytics] useNextlytics() must be used within a component wrapped by <NextlyticsServer>. Add <NextlyticsServer> at the top of your layout.tsx file."
|
|
225
260
|
);
|
|
226
261
|
}
|
|
227
|
-
const { requestId, addScripts } = context;
|
|
262
|
+
const { requestId, addScripts, mergeTemplates, knownTemplateIdsRef } = context;
|
|
228
263
|
const sendEvent = (0, import_react.useCallback)(
|
|
229
264
|
async (eventName, opts) => {
|
|
230
|
-
const result = await sendEventToServer(
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
265
|
+
const result = await sendEventToServer(
|
|
266
|
+
requestId,
|
|
267
|
+
{
|
|
268
|
+
type: "custom-event",
|
|
269
|
+
name: eventName,
|
|
270
|
+
props: opts?.props,
|
|
271
|
+
collectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
272
|
+
clientContext: createClientContext()
|
|
273
|
+
},
|
|
274
|
+
{ knownTemplateIds: knownTemplateIdsRef.current }
|
|
275
|
+
);
|
|
276
|
+
mergeTemplates(result.templates);
|
|
237
277
|
if (result.items && result.items.length > 0) {
|
|
238
278
|
addScripts(result.items);
|
|
239
279
|
}
|
|
240
280
|
return { ok: result.ok };
|
|
241
281
|
},
|
|
242
|
-
[requestId, addScripts]
|
|
282
|
+
[requestId, addScripts, mergeTemplates, knownTemplateIdsRef]
|
|
243
283
|
);
|
|
244
284
|
return { sendEvent };
|
|
245
285
|
}
|
package/dist/config-helpers.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextlyticsConfig } from './types.js';
|
|
2
2
|
import 'next/dist/server/web/spec-extension/cookies';
|
|
3
3
|
import 'next/server';
|
|
4
|
+
import 'next';
|
|
4
5
|
|
|
5
6
|
type NextlyticsConfigWithDefaults = Required<Pick<NextlyticsConfig, "excludeApiCalls" | "eventEndpoint" | "isApiPath" | "backends">> & NextlyticsConfig & {
|
|
6
7
|
anonymousUsers: Required<NonNullable<NextlyticsConfig["anonymousUsers"]>>;
|
package/dist/index.d.ts
CHANGED
package/dist/middleware.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { NextMiddleware } from 'next/server';
|
|
2
2
|
import { NextlyticsConfigWithDefaults } from './config-helpers.js';
|
|
3
|
-
import { DispatchEvent, UpdateEvent } from './api-handler.js';
|
|
3
|
+
import { DispatchEvent, UpdateEvent, CollectTemplates } from './api-handler.js';
|
|
4
4
|
import './types.js';
|
|
5
5
|
import 'next/dist/server/web/spec-extension/cookies';
|
|
6
|
+
import 'next';
|
|
6
7
|
|
|
7
|
-
declare function createNextlyticsMiddleware(config: NextlyticsConfigWithDefaults, dispatchEvent: DispatchEvent, updateEvent: UpdateEvent): NextMiddleware;
|
|
8
|
+
declare function createNextlyticsMiddleware(config: NextlyticsConfigWithDefaults, dispatchEvent: DispatchEvent, updateEvent: UpdateEvent, collectTemplates: CollectTemplates): NextMiddleware;
|
|
8
9
|
|
|
9
10
|
export { createNextlyticsMiddleware };
|
package/dist/middleware.js
CHANGED
|
@@ -33,7 +33,7 @@ function createRequestContext(request) {
|
|
|
33
33
|
path: request.nextUrl.pathname
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
-
function createNextlyticsMiddleware(config, dispatchEvent, updateEvent) {
|
|
36
|
+
function createNextlyticsMiddleware(config, dispatchEvent, updateEvent, collectTemplates) {
|
|
37
37
|
const { eventEndpoint } = config;
|
|
38
38
|
return async (request) => {
|
|
39
39
|
const pathname = request.nextUrl.pathname;
|
|
@@ -67,7 +67,7 @@ function createNextlyticsMiddleware(config, dispatchEvent, updateEvent) {
|
|
|
67
67
|
}
|
|
68
68
|
if (pathname === eventEndpoint) {
|
|
69
69
|
if (request.method === "POST") {
|
|
70
|
-
return (0, import_api_handler.handleEventPost)(request, config, dispatchEvent, updateEvent);
|
|
70
|
+
return (0, import_api_handler.handleEventPost)(request, config, dispatchEvent, updateEvent, collectTemplates);
|
|
71
71
|
}
|
|
72
72
|
return Response.json({ error: "Method not allowed" }, { status: 405 });
|
|
73
73
|
}
|
package/dist/pages-router.d.ts
CHANGED
|
@@ -4,9 +4,10 @@ import 'react';
|
|
|
4
4
|
import './types.js';
|
|
5
5
|
import 'next/dist/server/web/spec-extension/cookies';
|
|
6
6
|
import 'next/server';
|
|
7
|
+
import 'next';
|
|
7
8
|
|
|
8
9
|
type PagesRouterContext = {
|
|
9
|
-
req
|
|
10
|
+
req?: {
|
|
10
11
|
headers: Record<string, string | string[] | undefined>;
|
|
11
12
|
cookies?: Record<string, string>;
|
|
12
13
|
};
|
|
@@ -14,6 +15,11 @@ type PagesRouterContext = {
|
|
|
14
15
|
/**
|
|
15
16
|
* Get Nextlytics props for Pages Router _app.tsx.
|
|
16
17
|
* Reads context from headers set by middleware.
|
|
18
|
+
*
|
|
19
|
+
* `_app`'s getInitialProps re-runs on every client-side navigation, where there
|
|
20
|
+
* is no `req`. Return an empty context in that case rather than throwing — the
|
|
21
|
+
* client already has its scripts and templates from the initial render and the
|
|
22
|
+
* /api/event round-trip.
|
|
17
23
|
*/
|
|
18
24
|
declare function getNextlyticsProps(ctx: PagesRouterContext): NextlyticsContext;
|
|
19
25
|
|
package/dist/pages-router.js
CHANGED
|
@@ -23,8 +23,12 @@ __export(pages_router_exports, {
|
|
|
23
23
|
module.exports = __toCommonJS(pages_router_exports);
|
|
24
24
|
var import_server_component_context = require("./server-component-context");
|
|
25
25
|
function getNextlyticsProps(ctx) {
|
|
26
|
+
const reqHeaders = ctx?.req?.headers;
|
|
27
|
+
if (!reqHeaders) {
|
|
28
|
+
return { requestId: "" };
|
|
29
|
+
}
|
|
26
30
|
const headersList = new Headers();
|
|
27
|
-
for (const [key, value] of Object.entries(
|
|
31
|
+
for (const [key, value] of Object.entries(reqHeaders)) {
|
|
28
32
|
if (value) {
|
|
29
33
|
headersList.set(key, Array.isArray(value) ? value[0] : value);
|
|
30
34
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextResponse } from 'next/server';
|
|
2
2
|
import { TemplatizedScriptInsertion } from './types.js';
|
|
3
3
|
import 'next/dist/server/web/spec-extension/cookies';
|
|
4
|
+
import 'next';
|
|
4
5
|
|
|
5
6
|
declare const headerNames: {
|
|
6
7
|
readonly pathname: "x-nl-pathname";
|
|
@@ -9,6 +10,9 @@ declare const headerNames: {
|
|
|
9
10
|
readonly isSoftNavigation: "x-nl-is-soft-nav";
|
|
10
11
|
readonly active: "x-nl-active";
|
|
11
12
|
readonly scripts: "x-nl-scripts";
|
|
13
|
+
/** Comma-separated template ids the client already holds, so the server only
|
|
14
|
+
* returns the ones it's missing (see api-handler / client templates merge). */
|
|
15
|
+
readonly knownTemplates: "x-nl-known-templates";
|
|
12
16
|
};
|
|
13
17
|
declare const LAST_PAGE_RENDER_ID_COOKIE = "last-page-render-id";
|
|
14
18
|
/** Context passed from middleware to server components via headers */
|
|
@@ -31,7 +31,10 @@ const headerNames = {
|
|
|
31
31
|
pageRenderId: `${HEADER_PREFIX}page-render-id`,
|
|
32
32
|
isSoftNavigation: `${HEADER_PREFIX}is-soft-nav`,
|
|
33
33
|
active: `${HEADER_PREFIX}active`,
|
|
34
|
-
scripts: `${HEADER_PREFIX}scripts
|
|
34
|
+
scripts: `${HEADER_PREFIX}scripts`,
|
|
35
|
+
/** Comma-separated template ids the client already holds, so the server only
|
|
36
|
+
* returns the ones it's missing (see api-handler / client templates merge). */
|
|
37
|
+
knownTemplates: `${HEADER_PREFIX}known-templates`
|
|
35
38
|
};
|
|
36
39
|
const LAST_PAGE_RENDER_ID_COOKIE = "last-page-render-id";
|
|
37
40
|
function serializeServerComponentContext(response, ctx) {
|
package/dist/server.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextlyticsConfig, NextlyticsResult, RequestContext } from './types.js';
|
|
2
2
|
import 'next/dist/server/web/spec-extension/cookies';
|
|
3
3
|
import 'next/server';
|
|
4
|
+
import 'next';
|
|
4
5
|
|
|
5
6
|
declare function createRequestContext(): Promise<RequestContext>;
|
|
6
7
|
declare function Nextlytics(userConfig: NextlyticsConfig): NextlyticsResult;
|
package/dist/server.js
CHANGED
|
@@ -174,13 +174,20 @@ function Nextlytics(userConfig) {
|
|
|
174
174
|
const ctx = await createRequestContext();
|
|
175
175
|
return updateEventInternal(eventId, patch, ctx);
|
|
176
176
|
};
|
|
177
|
-
const middleware = (0, import_middleware.createNextlyticsMiddleware)(
|
|
177
|
+
const middleware = (0, import_middleware.createNextlyticsMiddleware)(
|
|
178
|
+
config,
|
|
179
|
+
dispatchEventInternal,
|
|
180
|
+
updateEventInternal,
|
|
181
|
+
(ctx) => collectTemplates(config, ctx)
|
|
182
|
+
);
|
|
178
183
|
async function Server({ children }) {
|
|
179
184
|
const headersList = await (0, import_headers.headers)();
|
|
180
185
|
const ctx = (0, import_server_component_context.restoreServerComponentContext)(headersList);
|
|
181
186
|
if (!ctx) {
|
|
182
187
|
if (!headersList.get(import_server_component_context.headerNames.active)) {
|
|
183
|
-
console.warn(
|
|
188
|
+
console.warn(
|
|
189
|
+
"[Nextlytics] nextlyticsMiddleware should be added in order for Server to work"
|
|
190
|
+
);
|
|
184
191
|
}
|
|
185
192
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_jsx_runtime.Fragment, { children });
|
|
186
193
|
}
|
|
@@ -188,15 +195,14 @@ function Nextlytics(userConfig) {
|
|
|
188
195
|
const templates = collectTemplates(config, requestCtx);
|
|
189
196
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_client.NextlyticsClient, { ctx: { requestId: ctx.pageRenderId, scripts: ctx.scripts, templates }, children });
|
|
190
197
|
}
|
|
191
|
-
const analytics = async () => {
|
|
192
|
-
const
|
|
193
|
-
const
|
|
194
|
-
const
|
|
195
|
-
const serverContext = createServerContextFromHeaders(headersList);
|
|
198
|
+
const analytics = async (req) => {
|
|
199
|
+
const source = req ? normalizeRequest(req) : await normalizeFromNextHeaders();
|
|
200
|
+
const pageRenderId = source.headers.get(import_server_component_context.headerNames.pageRenderId) || source.cookies.get(import_server_component_context.LAST_PAGE_RENDER_ID_COOKIE)?.value || void 0;
|
|
201
|
+
const serverContext = buildServerContext(source);
|
|
196
202
|
const ctx = {
|
|
197
|
-
headers:
|
|
198
|
-
cookies:
|
|
199
|
-
path:
|
|
203
|
+
headers: source.headers,
|
|
204
|
+
cookies: source.cookies,
|
|
205
|
+
path: source.path
|
|
200
206
|
};
|
|
201
207
|
const { anonId: anonymousUserId } = await (0, import_anonymous_user.resolveAnonymousUser)({ ctx, serverContext, config });
|
|
202
208
|
let userContext;
|
|
@@ -209,14 +215,14 @@ function Nextlytics(userConfig) {
|
|
|
209
215
|
const propsFromCallback = await (0, import_api_handler.getEventProps)(config, ctx, userContext);
|
|
210
216
|
return {
|
|
211
217
|
sendEvent: async (eventName, opts) => {
|
|
212
|
-
if (!pageRenderId) {
|
|
218
|
+
if (!pageRenderId && !req) {
|
|
213
219
|
console.error("[Nextlytics] analytics() requires nextlyticsMiddleware");
|
|
214
220
|
return { ok: false };
|
|
215
221
|
}
|
|
216
222
|
const event = {
|
|
217
223
|
origin: "server",
|
|
218
224
|
eventId: (0, import_uitils.generateId)(),
|
|
219
|
-
parentEventId: pageRenderId,
|
|
225
|
+
...pageRenderId ? { parentEventId: pageRenderId } : {},
|
|
220
226
|
type: eventName,
|
|
221
227
|
collectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
222
228
|
anonymousUserId,
|
|
@@ -237,32 +243,73 @@ function Nextlytics(userConfig) {
|
|
|
237
243
|
NextlyticsServer: Server
|
|
238
244
|
};
|
|
239
245
|
}
|
|
240
|
-
function
|
|
246
|
+
function searchToRecord(params) {
|
|
247
|
+
const out = {};
|
|
248
|
+
params.forEach((value, key) => {
|
|
249
|
+
(out[key] ?? (out[key] = [])).push(value);
|
|
250
|
+
});
|
|
251
|
+
return out;
|
|
252
|
+
}
|
|
253
|
+
function isNextApiRequest(req) {
|
|
254
|
+
return typeof req.headers?.get !== "function";
|
|
255
|
+
}
|
|
256
|
+
async function normalizeFromNextHeaders() {
|
|
257
|
+
const [_cookies, _headers] = await Promise.all([(0, import_headers.cookies)(), (0, import_headers.headers)()]);
|
|
258
|
+
return {
|
|
259
|
+
headers: _headers,
|
|
260
|
+
cookies: _cookies,
|
|
261
|
+
path: _headers.get(import_server_component_context.headerNames.pathname) || "",
|
|
262
|
+
search: searchToRecord(new URLSearchParams(_headers.get(import_server_component_context.headerNames.search) || "")),
|
|
263
|
+
method: "GET"
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function normalizeRequest(req) {
|
|
267
|
+
if (!isNextApiRequest(req)) {
|
|
268
|
+
return {
|
|
269
|
+
headers: req.headers,
|
|
270
|
+
cookies: req.cookies,
|
|
271
|
+
path: req.nextUrl.pathname,
|
|
272
|
+
search: searchToRecord(req.nextUrl.searchParams),
|
|
273
|
+
method: req.method
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
const headersList = new Headers();
|
|
277
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
278
|
+
if (value !== void 0) {
|
|
279
|
+
headersList.set(key, Array.isArray(value) ? value.join(", ") : value);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
const cookieMap = req.cookies || {};
|
|
283
|
+
const cookieStore = {
|
|
284
|
+
get: (name) => {
|
|
285
|
+
const value = cookieMap[name];
|
|
286
|
+
return value === void 0 ? void 0 : { name, value };
|
|
287
|
+
},
|
|
288
|
+
getAll: () => Object.entries(cookieMap).map(([name, value]) => ({ name, value })),
|
|
289
|
+
has: (name) => name in cookieMap
|
|
290
|
+
};
|
|
291
|
+
const url = new URL(req.url || "/", `http://${headersList.get("host") || "localhost"}`);
|
|
292
|
+
return {
|
|
293
|
+
headers: headersList,
|
|
294
|
+
cookies: cookieStore,
|
|
295
|
+
path: url.pathname,
|
|
296
|
+
search: searchToRecord(url.searchParams),
|
|
297
|
+
method: req.method || "GET"
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
function buildServerContext(source) {
|
|
241
301
|
const rawHeaders = {};
|
|
242
|
-
|
|
302
|
+
source.headers.forEach((value, key) => {
|
|
243
303
|
rawHeaders[key] = value;
|
|
244
304
|
});
|
|
245
|
-
const requestHeaders = (0, import_headers2.removeSensitiveHeaders)(rawHeaders);
|
|
246
|
-
const pathname = headersList.get(import_server_component_context.headerNames.pathname) || "";
|
|
247
|
-
const search = headersList.get(import_server_component_context.headerNames.search) || "";
|
|
248
|
-
const searchParams = {};
|
|
249
|
-
if (search) {
|
|
250
|
-
const params = new URLSearchParams(search);
|
|
251
|
-
params.forEach((value, key) => {
|
|
252
|
-
if (!searchParams[key]) {
|
|
253
|
-
searchParams[key] = [];
|
|
254
|
-
}
|
|
255
|
-
searchParams[key].push(value);
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
305
|
return {
|
|
259
306
|
collectedAt: /* @__PURE__ */ new Date(),
|
|
260
|
-
host:
|
|
261
|
-
method:
|
|
262
|
-
path:
|
|
263
|
-
search:
|
|
264
|
-
ip:
|
|
265
|
-
requestHeaders,
|
|
307
|
+
host: source.headers.get("host") || "",
|
|
308
|
+
method: source.method,
|
|
309
|
+
path: source.path,
|
|
310
|
+
search: source.search,
|
|
311
|
+
ip: source.headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "",
|
|
312
|
+
requestHeaders: (0, import_headers2.removeSensitiveHeaders)(rawHeaders),
|
|
266
313
|
responseHeaders: {}
|
|
267
314
|
};
|
|
268
315
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { RequestCookies } from 'next/dist/server/web/spec-extension/cookies';
|
|
2
|
-
import { NextMiddleware } from 'next/server';
|
|
2
|
+
import { NextRequest, NextMiddleware } from 'next/server';
|
|
3
|
+
import { NextApiRequest } from 'next';
|
|
3
4
|
|
|
4
5
|
/** Server-side request context collected in middleware */
|
|
5
6
|
interface ServerEventContext {
|
|
@@ -227,7 +228,7 @@ type NextlyticsBackend = {
|
|
|
227
228
|
type NextlyticsBackendFactory = (ctx: RequestContext) => NextlyticsBackend;
|
|
228
229
|
/** Server-side analytics API */
|
|
229
230
|
type NextlyticsServerSide = {
|
|
230
|
-
/** Send custom event from server component
|
|
231
|
+
/** Send custom event from a server component, server action, or API route */
|
|
231
232
|
sendEvent: (eventName: string, opts?: {
|
|
232
233
|
props?: Record<string, unknown>;
|
|
233
234
|
}) => Promise<{
|
|
@@ -266,11 +267,21 @@ type ClientRequest = {
|
|
|
266
267
|
type ClientRequestResult = {
|
|
267
268
|
ok: boolean;
|
|
268
269
|
items?: ClientActionItem[];
|
|
270
|
+
/** Client-side templates from the backends, so Pages Router clients (which
|
|
271
|
+
* can't read them from config) can compile the script insertions. */
|
|
272
|
+
templates?: Record<string, JavascriptTemplate>;
|
|
269
273
|
};
|
|
270
274
|
/** Return value from Nextlytics() */
|
|
271
275
|
type NextlyticsResult = {
|
|
272
|
-
/** Get server-side analytics API
|
|
273
|
-
|
|
276
|
+
/** Get server-side analytics API.
|
|
277
|
+
*
|
|
278
|
+
* App Router (server components, server actions, route handlers): call with no
|
|
279
|
+
* argument — context is read from `next/headers`.
|
|
280
|
+
*
|
|
281
|
+
* Pages Router API routes: `next/headers` is unavailable there, so pass the
|
|
282
|
+
* request (`analytics(req)`). A NextRequest (App Router route handler) is also
|
|
283
|
+
* accepted. */
|
|
284
|
+
analytics: (req?: NextRequest | NextApiRequest) => Promise<NextlyticsServerSide>;
|
|
274
285
|
/** Middleware to intercept requests */
|
|
275
286
|
middleware: NextMiddleware;
|
|
276
287
|
/** Manually dispatch event (returns two-phase result) */
|
package/dist/uitils.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextRequest } from 'next/server';
|
|
2
2
|
import { ServerEventContext } from './types.js';
|
|
3
3
|
import 'next/dist/server/web/spec-extension/cookies';
|
|
4
|
+
import 'next';
|
|
4
5
|
|
|
5
6
|
/** Returns the full installed Next.js version string (e.g. "16.1.6"), or undefined. */
|
|
6
7
|
declare function getNextVersion(): string | undefined;
|