@nextlytics/core 0.3.0-canary.70 → 0.3.0-canary.80

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/client.js CHANGED
@@ -25,13 +25,15 @@ __export(client_exports, {
25
25
  module.exports = __toCommonJS(client_exports);
26
26
  var import_jsx_runtime = require("react/jsx-runtime");
27
27
  var import_react = require("react");
28
+ var import_client_utils = require("./client-utils");
28
29
  var import_server_component_context = require("./server-component-context");
29
30
  var import_template = require("./template");
31
+ var import_stable_hash = require("./stable-hash");
30
32
  const templateFunctions = {
31
33
  q: (v) => JSON.stringify(v ?? null),
32
- json: (v) => JSON.stringify(v ?? null)
34
+ json: (v) => JSON.stringify(v ?? null),
35
+ stableHash: (v) => (0, import_stable_hash.stableHash)(v)
33
36
  };
34
- const compiledCache = {};
35
37
  const NextlyticsContext = (0, import_react.createContext)(null);
36
38
  function createClientContext() {
37
39
  const isBrowser = typeof window !== "undefined";
@@ -55,94 +57,165 @@ function createClientContext() {
55
57
  locale: isBrowser ? navigator.language : void 0
56
58
  };
57
59
  }
58
- function getCompiledTemplate(templateId, itemIndex, item) {
59
- const cacheKey = `${templateId}:${itemIndex}`;
60
- if (!compiledCache[cacheKey]) {
61
- compiledCache[cacheKey] = {
62
- src: item.src ? (0, import_template.compile)(item.src) : void 0,
63
- body: item.body ? (0, import_template.compile)(item.body) : void 0
64
- };
65
- }
66
- return compiledCache[cacheKey];
67
- }
68
- function executeTemplatedScripts(scripts, templates) {
69
- if (!scripts || typeof window === "undefined") return;
60
+ function deduplicateScripts(scripts, templates) {
61
+ const result = [];
62
+ const firstSeenByDeps = /* @__PURE__ */ new Set();
70
63
  for (const script of scripts) {
71
- if (script.type !== "script-template") {
72
- console.warn(`[Nextlytics] unsupported script type ${script.type} `);
64
+ if (script.type !== "script-template") continue;
65
+ const template = templates[script.templateId];
66
+ if (!template) continue;
67
+ if (!template.deps) {
68
+ result.push(script);
73
69
  continue;
74
70
  }
71
+ const paramsRecord = script.params || {};
72
+ const deps = compileTemplateDeps(template, paramsRecord);
73
+ const depsKey = `${script.templateId}\0${deps.join("\0")}`;
74
+ if (firstSeenByDeps.has(depsKey)) continue;
75
+ firstSeenByDeps.add(depsKey);
76
+ result.push(script);
77
+ }
78
+ return result;
79
+ }
80
+ function compileScripts(scripts, templates) {
81
+ const result = [];
82
+ for (const [scriptIndex, script] of scripts.entries()) {
83
+ if (script.type !== "script-template") continue;
75
84
  const template = templates[script.templateId];
76
85
  if (!template) {
77
- console.warn(`[Nextlytics] Missing template: ${script.templateId}`);
86
+ console.warn(`[Nextlytics] Template "${script.templateId}" not found`);
78
87
  continue;
79
88
  }
80
- const params = script.params;
81
- for (let i = 0; i < template.items.length; i++) {
82
- const item = template.items[i];
83
- const compiled = getCompiledTemplate(script.templateId, i, item);
84
- const src = compiled.src ? (0, import_template.apply)(compiled.src, params, templateFunctions) : void 0;
85
- if (item.singleton && src && document.querySelector(`script[src="${src}"]`)) {
89
+ const paramsRecord = script.params || {};
90
+ const deps = compileTemplateDeps(template, paramsRecord);
91
+ let itemIndex = 0;
92
+ for (const item of template.items) {
93
+ const keyPrefix = `${script.templateId}:${scriptIndex}:${item.src ? "ext" : "body"}:${itemIndex}`;
94
+ if (item.src) {
95
+ const compiledSrc = (0, import_template.compile)(item.src);
96
+ const src = (0, import_template.apply)(compiledSrc, paramsRecord, templateFunctions);
97
+ result.push({
98
+ key: keyPrefix,
99
+ src,
100
+ async: item.async,
101
+ deps
102
+ });
103
+ itemIndex++;
86
104
  continue;
87
105
  }
88
- const el = document.createElement("script");
89
- if (src) {
90
- el.src = src;
91
- }
92
- if (compiled.body) {
93
- el.textContent = (0, import_template.apply)(compiled.body, params, templateFunctions);
94
- }
95
- if (item.async) {
96
- el.async = true;
106
+ if (!item.body) {
107
+ itemIndex++;
108
+ continue;
97
109
  }
98
- document.head.appendChild(el);
110
+ const bodyText = Array.isArray(item.body) ? item.body.join("\n") : item.body;
111
+ const compiled = (0, import_template.compile)(bodyText);
112
+ const body = (0, import_template.apply)(compiled, paramsRecord, templateFunctions);
113
+ result.push({ key: keyPrefix, body, deps });
114
+ itemIndex++;
99
115
  }
100
116
  }
117
+ return result;
118
+ }
119
+ function compileTemplateDeps(template, paramsRecord) {
120
+ if (!template.deps) return [];
121
+ const rawDeps = Array.isArray(template.deps) ? template.deps : [template.deps];
122
+ return rawDeps.map((dep) => (0, import_template.apply)((0, import_template.compile)(dep), paramsRecord, templateFunctions));
101
123
  }
102
- async function sendEvent(requestId, type, payload) {
124
+ function NextlyticsScripts({
125
+ initialScripts
126
+ }) {
127
+ const context = (0, import_react.useContext)(NextlyticsContext);
128
+ if (!context) {
129
+ throw new Error("NextlyticsScripts should be called within NextlyticsContext");
130
+ }
131
+ const { scriptsRef, subscribersRef, templates } = context;
132
+ const [, forceUpdate] = (0, import_react.useReducer)((x) => x + 1, 0);
133
+ (0, import_react.useEffect)(() => {
134
+ subscribersRef.current.add(forceUpdate);
135
+ return () => {
136
+ subscribersRef.current.delete(forceUpdate);
137
+ };
138
+ }, [subscribersRef]);
139
+ const allScripts = [...initialScripts, ...scriptsRef.current];
140
+ const dedupedScripts = (0, import_react.useMemo)(
141
+ () => deduplicateScripts(allScripts, templates),
142
+ [allScripts, templates]
143
+ );
144
+ const compiled = (0, import_react.useMemo)(
145
+ () => compileScripts(dedupedScripts, templates),
146
+ [dedupedScripts, templates]
147
+ );
148
+ (0, import_client_utils.debug)("Rendering scripts", {
149
+ initialCount: initialScripts.length,
150
+ dynamicCount: scriptsRef.current.length,
151
+ totalCount: allScripts.length,
152
+ compiledCount: compiled.length
153
+ });
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
+ }
156
+ async function sendEventToServer(requestId, request, { signal, isSoftNavigation } = {}) {
103
157
  try {
158
+ const headers = {
159
+ "Content-Type": "application/json",
160
+ [import_server_component_context.headerNames.pageRenderId]: requestId
161
+ };
162
+ if (isSoftNavigation) {
163
+ headers[import_server_component_context.headerNames.isSoftNavigation] = "1";
164
+ }
104
165
  const response = await fetch("/api/event", {
105
166
  method: "POST",
106
- headers: {
107
- "Content-Type": "application/json",
108
- [import_server_component_context.headers.pageRenderId]: requestId
109
- },
110
- body: JSON.stringify({ type, payload })
167
+ headers,
168
+ body: JSON.stringify(request),
169
+ signal
111
170
  });
112
171
  if (response.status === 404) {
113
172
  console.error(
114
- "[Nextlytics] In order for NextlyticsClient to work, you must mount nextlyticsRouteHandler to /api/event"
173
+ "[Nextlytics] In order for NextlyticsClient to work, you must install nextlytics middleware"
115
174
  );
116
175
  return { ok: false };
117
176
  }
118
177
  const data = await response.json().catch(() => ({ ok: response.ok }));
119
- return { ok: data.ok ?? response.ok, scripts: data.scripts };
178
+ return { ok: data.ok ?? response.ok, items: data.items };
120
179
  } catch (error) {
180
+ if (error instanceof Error && error.name === "AbortError") {
181
+ return { ok: false };
182
+ }
121
183
  console.error("[Nextlytics] Failed to send event:", error);
122
184
  return { ok: false };
123
185
  }
124
186
  }
125
- const initializedRequestIds = /* @__PURE__ */ new Set();
126
187
  function NextlyticsClient(props) {
127
- const requestId = props.ctx?.requestId ?? props.requestId ?? "";
128
- const scripts = props.ctx?.scripts ?? props.scripts;
129
- const templates = props.ctx?.templates ?? props.templates ?? {};
130
- (0, import_react.useEffect)(() => {
131
- if (initializedRequestIds.has(requestId)) return;
132
- initializedRequestIds.add(requestId);
133
- if (scripts && Object.keys(templates).length > 0) {
134
- executeTemplatedScripts(scripts, templates);
135
- }
188
+ const { requestId, scripts: initialScripts = [], templates = {} } = props.ctx;
189
+ const scriptsRef = (0, import_react.useRef)([]);
190
+ const subscribersRef = (0, import_react.useRef)(/* @__PURE__ */ new Set());
191
+ const addScripts = (0, import_react.useCallback)((newScripts) => {
192
+ (0, import_client_utils.debug)("Adding scripts", {
193
+ newCount: newScripts.length,
194
+ templateIds: newScripts.map((s) => s.templateId)
195
+ });
196
+ scriptsRef.current = [...scriptsRef.current, ...newScripts];
197
+ subscribersRef.current.forEach((cb) => cb());
198
+ }, []);
199
+ const contextValue = (0, import_react.useMemo)(
200
+ () => ({ requestId, templates, addScripts, scriptsRef, subscribersRef }),
201
+ [requestId, templates, addScripts]
202
+ );
203
+ (0, import_client_utils.useNavigation)(requestId, ({ softNavigation, signal }) => {
204
+ (0, import_client_utils.debug)("Sending page-view", { requestId, softNavigation });
136
205
  const clientContext = createClientContext();
137
- sendEvent(requestId, "client-init", clientContext).then(
138
- ({ scripts: responseScripts }) => {
139
- if (responseScripts) {
140
- executeTemplatedScripts(responseScripts, templates);
141
- }
142
- }
143
- );
144
- }, [requestId, scripts, templates]);
145
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NextlyticsContext.Provider, { value: { requestId, templates }, children: props.children });
206
+ sendEventToServer(
207
+ requestId,
208
+ { type: "page-view", clientContext, softNavigation: softNavigation || void 0 },
209
+ { signal, isSoftNavigation: softNavigation }
210
+ ).then(({ items }) => {
211
+ (0, import_client_utils.debug)("page-view response", { scriptsCount: items?.length ?? 0 });
212
+ if (items?.length) addScripts(items);
213
+ });
214
+ });
215
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(NextlyticsContext.Provider, { value: contextValue, children: [
216
+ props.children,
217
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(NextlyticsScripts, { initialScripts })
218
+ ] });
146
219
  }
147
220
  function useNextlytics() {
148
221
  const context = (0, import_react.useContext)(NextlyticsContext);
@@ -151,23 +224,24 @@ function useNextlytics() {
151
224
  "[Nextlytics] useNextlytics() must be used within a component wrapped by <NextlyticsServer>. Add <NextlyticsServer> at the top of your layout.tsx file."
152
225
  );
153
226
  }
154
- const { requestId, templates } = context;
155
- const send = (0, import_react.useCallback)(
227
+ const { requestId, addScripts } = context;
228
+ const sendEvent = (0, import_react.useCallback)(
156
229
  async (eventName, opts) => {
157
- const result = await sendEvent(requestId, "client-event", {
230
+ const result = await sendEventToServer(requestId, {
231
+ type: "custom-event",
158
232
  name: eventName,
159
233
  props: opts?.props,
160
234
  collectedAt: (/* @__PURE__ */ new Date()).toISOString(),
161
235
  clientContext: createClientContext()
162
236
  });
163
- if (result.scripts) {
164
- executeTemplatedScripts(result.scripts, templates);
237
+ if (result.items && result.items.length > 0) {
238
+ addScripts(result.items);
165
239
  }
166
240
  return { ok: result.ok };
167
241
  },
168
- [requestId, templates]
242
+ [requestId, addScripts]
169
243
  );
170
- return { sendEvent: send };
244
+ return { sendEvent };
171
245
  }
172
246
  // Annotate the CommonJS export names for ESM import in node:
173
247
  0 && (module.exports = {
@@ -28,7 +28,7 @@ function withDefaults(config) {
28
28
  ...config,
29
29
  excludeApiCalls: config.excludeApiCalls ?? false,
30
30
  eventEndpoint: config.eventEndpoint ?? "/api/event",
31
- isApiPath: config.isApiPath ?? (() => false),
31
+ isApiPath: config.isApiPath ?? ((str) => str.startsWith("/api")),
32
32
  backends: config.backends ?? [],
33
33
  anonymousUsers: {
34
34
  gdprMode: config.anonymousUsers?.gdprMode ?? true,
package/dist/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- export { Nextlytics, NextlyticsServer } from './server.js';
2
- export { NextlyticsClient, NextlyticsContext, useNextlytics } from './client.js';
1
+ export { Nextlytics } from './server.js';
3
2
  export { getNextlyticsProps } from './pages-router.js';
3
+ export { NextlyticsClient, NextlyticsContext, useNextlytics } from './client.js';
4
4
  export { loggingBackend } from './backends/logging.js';
5
- export { AnonymousUserResult, BackendConfigEntry, BackendWithConfig, ClientContext, IngestPolicy, NextlyticsBackend, NextlyticsBackendFactory, NextlyticsConfig, NextlyticsEvent, NextlyticsPlugin, NextlyticsPluginFactory, NextlyticsResult, NextlyticsServerSide, RequestContext, ServerEventContext, UserContext } from './types.js';
5
+ export { AnonymousUserResult, BackendConfigEntry, BackendWithConfig, ClientAction, ClientContext, ClientRequest, JavascriptTemplate, NextlyticsBackend, NextlyticsBackendFactory, NextlyticsClientContext, NextlyticsConfig, NextlyticsEvent, NextlyticsPlugin, NextlyticsPluginFactory, NextlyticsResult, NextlyticsServerSide, PageViewDelivery, PagesRouterContext, RequestContext, ServerEventContext, UserContext } from './types.js';
6
6
  import 'react/jsx-runtime';
7
7
  import 'react';
8
8
  import 'next/dist/server/web/spec-extension/cookies';
package/dist/index.js CHANGED
@@ -20,21 +20,19 @@ var index_exports = {};
20
20
  __export(index_exports, {
21
21
  Nextlytics: () => import_server.Nextlytics,
22
22
  NextlyticsClient: () => import_client.NextlyticsClient,
23
- NextlyticsServer: () => import_server.NextlyticsServer,
24
23
  getNextlyticsProps: () => import_pages_router.getNextlyticsProps,
25
24
  loggingBackend: () => import_logging.loggingBackend,
26
25
  useNextlytics: () => import_client.useNextlytics
27
26
  });
28
27
  module.exports = __toCommonJS(index_exports);
29
28
  var import_server = require("./server");
30
- var import_client = require("./client");
31
29
  var import_pages_router = require("./pages-router");
30
+ var import_client = require("./client");
32
31
  var import_logging = require("./backends/logging");
33
32
  // Annotate the CommonJS export names for ESM import in node:
34
33
  0 && (module.exports = {
35
34
  Nextlytics,
36
35
  NextlyticsClient,
37
- NextlyticsServer,
38
36
  getNextlyticsProps,
39
37
  loggingBackend,
40
38
  useNextlytics
@@ -1,10 +1,9 @@
1
1
  import { NextMiddleware } from 'next/server';
2
- import { NextlyticsEvent, RequestContext, IngestPolicy, DispatchResult } from './types.js';
3
2
  import { NextlyticsConfigWithDefaults } from './config-helpers.js';
3
+ import { DispatchEvent, UpdateEvent } from './api-handler.js';
4
+ import './types.js';
4
5
  import 'next/dist/server/web/spec-extension/cookies';
5
6
 
6
- type DispatchEvent = (event: NextlyticsEvent, ctx: RequestContext, policyFilter?: IngestPolicy) => DispatchResult;
7
- type UpdateEvent = (eventId: string, patch: Partial<NextlyticsEvent>, ctx: RequestContext) => Promise<void>;
8
7
  declare function createNextlyticsMiddleware(config: NextlyticsConfigWithDefaults, dispatchEvent: DispatchEvent, updateEvent: UpdateEvent): NextMiddleware;
9
8
 
10
9
  export { createNextlyticsMiddleware };
@@ -25,29 +25,7 @@ var import_server = require("next/server");
25
25
  var import_server_component_context = require("./server-component-context");
26
26
  var import_uitils = require("./uitils");
27
27
  var import_anonymous_user = require("./anonymous-user");
28
- function isBackendWithConfig(entry) {
29
- return typeof entry === "object" && entry !== null && "backend" in entry;
30
- }
31
- function resolveBackends(config, ctx) {
32
- const entries = config.backends || [];
33
- return entries.map((entry) => {
34
- if (isBackendWithConfig(entry)) {
35
- const backend2 = typeof entry.backend === "function" ? entry.backend(ctx) : entry.backend;
36
- return backend2 ? { backend: backend2, ingestPolicy: entry.ingestPolicy ?? "immediate" } : null;
37
- }
38
- const backend = typeof entry === "function" ? entry(ctx) : entry;
39
- return backend ? { backend, ingestPolicy: "immediate" } : null;
40
- }).filter((b) => b !== null);
41
- }
42
- function collectTemplates(backends) {
43
- const templates = {};
44
- for (const { backend } of backends) {
45
- if (backend.getClientSideTemplates) {
46
- Object.assign(templates, backend.getClientSideTemplates());
47
- }
48
- }
49
- return templates;
50
- }
28
+ var import_api_handler = require("./api-handler");
51
29
  function createRequestContext(request) {
52
30
  return {
53
31
  headers: request.headers,
@@ -59,29 +37,57 @@ function createNextlyticsMiddleware(config, dispatchEvent, updateEvent) {
59
37
  return async (request) => {
60
38
  const pathname = request.nextUrl.pathname;
61
39
  const reqInfo = (0, import_uitils.getRequestInfo)(request);
40
+ const middlewareDebug = config.debug || process.env.NEXTLYTICS_MIDDLEWARE_DEBUG === "true";
41
+ if (middlewareDebug) {
42
+ const headers = request.headers;
43
+ const debugHeaders = {};
44
+ headers.forEach((value, key) => {
45
+ debugHeaders[key] = value;
46
+ });
47
+ console.log("[Nextlytics][middleware]", {
48
+ url: request.url,
49
+ pathname,
50
+ search: request.nextUrl.search,
51
+ method: request.method,
52
+ nextVersion: (0, import_uitils.getNextVersion)(),
53
+ destination: request.destination,
54
+ referrer: request.referrer,
55
+ mode: request.mode,
56
+ cache: request.cache,
57
+ redirect: request.redirect,
58
+ integrity: request.integrity,
59
+ isPrefetch: reqInfo.isPrefetch,
60
+ isRsc: reqInfo.isRsc,
61
+ isPageNavigation: reqInfo.isPageNavigation,
62
+ isStaticFile: reqInfo.isStaticFile,
63
+ isNextjsInternal: reqInfo.isNextjsInternal,
64
+ headers: debugHeaders
65
+ });
66
+ }
62
67
  if (pathname === eventEndpoint) {
63
68
  if (request.method === "POST") {
64
- return handleEventPost(request, config, dispatchEvent, updateEvent);
69
+ return (0, import_api_handler.handleEventPost)(request, config, dispatchEvent, updateEvent);
65
70
  }
66
71
  return Response.json({ error: "Method not allowed" }, { status: 405 });
67
72
  }
68
73
  if (reqInfo.isNextjsInternal || reqInfo.isPrefetch || reqInfo.isStaticFile) {
69
74
  return import_server.NextResponse.next();
70
75
  }
76
+ if (!reqInfo.isPageNavigation && !config.isApiPath(pathname)) {
77
+ return import_server.NextResponse.next();
78
+ }
71
79
  const pageRenderId = (0, import_uitils.generateId)();
72
80
  const serverContext = (0, import_uitils.createServerContext)(request);
73
81
  const response = import_server.NextResponse.next();
74
82
  const ctx = createRequestContext(request);
83
+ response.cookies.set(import_server_component_context.LAST_PAGE_RENDER_ID_COOKIE, pageRenderId, { path: "/" });
75
84
  const { anonId } = await (0, import_anonymous_user.resolveAnonymousUser)({ ctx, serverContext, config, response });
76
- const backends = resolveBackends(config, ctx);
77
- const templates = collectTemplates(backends);
78
85
  if (config.excludePaths?.(pathname)) {
79
86
  (0, import_server_component_context.serializeServerComponentContext)(response, {
80
87
  pageRenderId,
81
88
  pathname: request.nextUrl.pathname,
82
89
  search: request.nextUrl.search,
83
- scripts: [],
84
- templates
90
+ scripts: []
85
91
  });
86
92
  return response;
87
93
  }
@@ -91,12 +97,11 @@ function createNextlyticsMiddleware(config, dispatchEvent, updateEvent) {
91
97
  pageRenderId,
92
98
  pathname: request.nextUrl.pathname,
93
99
  search: request.nextUrl.search,
94
- scripts: [],
95
- templates
100
+ scripts: []
96
101
  });
97
102
  return response;
98
103
  }
99
- const userContext = await getUserContext(config, ctx);
104
+ const userContext = await (0, import_api_handler.getUserContext)(config, ctx);
100
105
  const pageViewEvent = createPageViewEvent(
101
106
  pageRenderId,
102
107
  serverContext,
@@ -104,7 +109,7 @@ function createNextlyticsMiddleware(config, dispatchEvent, updateEvent) {
104
109
  userContext,
105
110
  anonId
106
111
  );
107
- const { clientActions, completion } = dispatchEvent(pageViewEvent, ctx, "immediate");
112
+ const { clientActions, completion } = dispatchEvent(pageViewEvent, ctx, "on-request");
108
113
  const actions = await clientActions;
109
114
  const scripts = actions.items.filter(
110
115
  (i) => i.type === "script-template"
@@ -114,8 +119,7 @@ function createNextlyticsMiddleware(config, dispatchEvent, updateEvent) {
114
119
  pageRenderId,
115
120
  pathname: request.nextUrl.pathname,
116
121
  search: request.nextUrl.search,
117
- scripts,
118
- templates
122
+ scripts
119
123
  });
120
124
  return response;
121
125
  };
@@ -123,6 +127,7 @@ function createNextlyticsMiddleware(config, dispatchEvent, updateEvent) {
123
127
  function createPageViewEvent(pageRenderId, serverContext, isApiPath, userContext, anonymousUserId) {
124
128
  const eventType = isApiPath ? "apiCall" : "pageView";
125
129
  return {
130
+ origin: "server",
126
131
  collectedAt: serverContext.collectedAt.toISOString(),
127
132
  eventId: pageRenderId,
128
133
  type: eventType,
@@ -132,99 +137,6 @@ function createPageViewEvent(pageRenderId, serverContext, isApiPath, userContext
132
137
  properties: {}
133
138
  };
134
139
  }
135
- async function getUserContext(config, ctx) {
136
- if (!config.callbacks.getUser) return void 0;
137
- try {
138
- return await config.callbacks.getUser(ctx) || void 0;
139
- } catch {
140
- return void 0;
141
- }
142
- }
143
- function reconstructServerContext(apiCallContext, clientInit) {
144
- const searchParams = {};
145
- if (clientInit.search) {
146
- const params = new URLSearchParams(clientInit.search);
147
- params.forEach((value, key) => {
148
- if (!searchParams[key]) searchParams[key] = [];
149
- searchParams[key].push(value);
150
- });
151
- }
152
- return {
153
- ...apiCallContext,
154
- // Override with client-provided values
155
- host: clientInit.host || apiCallContext.host,
156
- path: clientInit.path || apiCallContext.path,
157
- search: Object.keys(searchParams).length > 0 ? searchParams : apiCallContext.search,
158
- method: "GET"
159
- // Page loads are always GET
160
- };
161
- }
162
- async function handleEventPost(request, config, dispatchEvent, updateEvent) {
163
- const pageRenderId = request.headers.get(import_server_component_context.headers.pageRenderId);
164
- if (!pageRenderId) {
165
- return Response.json({ error: "Missing page render ID" }, { status: 400 });
166
- }
167
- let body;
168
- try {
169
- body = await request.json();
170
- } catch {
171
- return Response.json({ error: "Invalid JSON" }, { status: 400 });
172
- }
173
- const { type, payload } = body;
174
- const ctx = createRequestContext(request);
175
- const apiCallServerContext = (0, import_uitils.createServerContext)(request);
176
- const userContext = await getUserContext(config, ctx);
177
- if (type === "client-init") {
178
- const clientContext = payload;
179
- const serverContext = reconstructServerContext(apiCallServerContext, clientContext);
180
- const { anonId: anonymousUserId } = await (0, import_anonymous_user.resolveAnonymousUser)({
181
- ctx,
182
- serverContext,
183
- config
184
- });
185
- const event = {
186
- eventId: pageRenderId,
187
- type: "pageView",
188
- collectedAt: (/* @__PURE__ */ new Date()).toISOString(),
189
- anonymousUserId,
190
- serverContext,
191
- clientContext,
192
- userContext,
193
- properties: {}
194
- };
195
- const { clientActions, completion } = dispatchEvent(event, ctx, "on-client-event");
196
- const actions = await clientActions;
197
- (0, import_server.after)(() => completion);
198
- (0, import_server.after)(() => updateEvent(pageRenderId, { clientContext, userContext, anonymousUserId }, ctx));
199
- const scripts = actions.items.filter((i) => i.type === "script-template");
200
- return Response.json({ ok: true, scripts: scripts.length > 0 ? scripts : void 0 });
201
- } else if (type === "client-event") {
202
- const clientContext = payload.clientContext || void 0;
203
- const serverContext = clientContext ? reconstructServerContext(apiCallServerContext, clientContext) : apiCallServerContext;
204
- const { anonId: anonymousUserId } = await (0, import_anonymous_user.resolveAnonymousUser)({
205
- ctx,
206
- serverContext,
207
- config
208
- });
209
- const event = {
210
- eventId: (0, import_uitils.generateId)(),
211
- parentEventId: pageRenderId,
212
- type: payload.name || type,
213
- collectedAt: payload.collectedAt || (/* @__PURE__ */ new Date()).toISOString(),
214
- anonymousUserId,
215
- serverContext,
216
- clientContext,
217
- userContext,
218
- properties: payload.props || {}
219
- };
220
- const { clientActions, completion } = dispatchEvent(event, ctx);
221
- const actions = await clientActions;
222
- (0, import_server.after)(() => completion);
223
- const scripts = actions.items.filter((i) => i.type === "script-template");
224
- return Response.json({ ok: true, scripts: scripts.length > 0 ? scripts : void 0 });
225
- }
226
- return Response.json({ ok: true });
227
- }
228
140
  // Annotate the CommonJS export names for ESM import in node:
229
141
  0 && (module.exports = {
230
142
  createNextlyticsMiddleware
@@ -5,41 +5,16 @@ import './types.js';
5
5
  import 'next/dist/server/web/spec-extension/cookies';
6
6
  import 'next/server';
7
7
 
8
- type ContextWithHeaders = {
8
+ type PagesRouterContext = {
9
9
  req: {
10
10
  headers: Record<string, string | string[] | undefined>;
11
+ cookies?: Record<string, string>;
11
12
  };
12
13
  };
13
14
  /**
14
- * Extract Nextlytics context from Pages Router context (getServerSideProps or getInitialProps).
15
- * Use this in _app.tsx with getInitialProps to pass context to NextlyticsClient.
16
- *
17
- * @example
18
- * ```tsx
19
- * // pages/_app.tsx
20
- * import type { AppContext, AppProps } from 'next/app'
21
- * import { NextlyticsClient, getNextlyticsProps, type NextlyticsContext } from '@nextlytics/core'
22
- *
23
- * type MyAppProps = AppProps & { nextlyticsCtx: NextlyticsContext }
24
- *
25
- * function MyApp({ Component, pageProps, nextlyticsCtx }: MyAppProps) {
26
- * return (
27
- * <NextlyticsClient ctx={nextlyticsCtx}>
28
- * <Component {...pageProps} />
29
- * </NextlyticsClient>
30
- * )
31
- * }
32
- *
33
- * MyApp.getInitialProps = async (appContext: AppContext) => {
34
- * return {
35
- * pageProps: appContext.Component.getInitialProps
36
- * ? await appContext.Component.getInitialProps(appContext.ctx)
37
- * : {},
38
- * nextlyticsCtx: getNextlyticsProps(appContext.ctx),
39
- * }
40
- * }
41
- * ```
15
+ * Get Nextlytics props for Pages Router _app.tsx.
16
+ * Reads context from headers set by middleware.
42
17
  */
43
- declare function getNextlyticsProps(ctx: ContextWithHeaders): NextlyticsContext;
18
+ declare function getNextlyticsProps(ctx: PagesRouterContext): NextlyticsContext;
44
19
 
45
- export { getNextlyticsProps };
20
+ export { type PagesRouterContext, getNextlyticsProps };
@@ -35,8 +35,7 @@ function getNextlyticsProps(ctx) {
35
35
  }
36
36
  return {
37
37
  requestId: context.pageRenderId,
38
- scripts: context.scripts,
39
- templates: context.templates
38
+ scripts: context.scripts
40
39
  };
41
40
  }
42
41
  // Annotate the CommonJS export names for ESM import in node:
@@ -1,14 +1,15 @@
1
1
  import { NextResponse } from 'next/server';
2
- import { TemplatizedScriptInsertion, JavascriptTemplate } from './types.js';
2
+ import { TemplatizedScriptInsertion } from './types.js';
3
3
  import 'next/dist/server/web/spec-extension/cookies';
4
4
 
5
- declare const headerKeys: {
6
- readonly pathname: "x-nc-pathname";
7
- readonly search: "x-nc-search";
8
- readonly pageRenderId: "x-nc-page-render-id";
9
- readonly scripts: "x-nc-scripts";
10
- readonly templates: "x-nc-templates";
5
+ declare const headerNames: {
6
+ readonly pathname: "x-nl-pathname";
7
+ readonly search: "x-nl-search";
8
+ readonly pageRenderId: "x-nl-page-render-id";
9
+ readonly isSoftNavigation: "x-nl-is-soft-nav";
10
+ readonly scripts: "x-nl-scripts";
11
11
  };
12
+ declare const LAST_PAGE_RENDER_ID_COOKIE = "last-page-render-id";
12
13
  /** Context passed from middleware to server components via headers */
13
14
  type ServerComponentContext = {
14
15
  /** Unique page render ID (event ID) */
@@ -17,14 +18,12 @@ type ServerComponentContext = {
17
18
  pathname: string;
18
19
  /** Query string */
19
20
  search: string;
20
- /** Script actions to execute on client */
21
+ /** Script actions to execute on client (params only, templates come from config) */
21
22
  scripts: TemplatizedScriptInsertion<unknown>[];
22
- /** Template definitions for scripts */
23
- templates: Record<string, JavascriptTemplate>;
24
23
  };
25
24
  /** Serialize context to response headers (called in middleware) */
26
25
  declare function serializeServerComponentContext(response: NextResponse, ctx: ServerComponentContext): void;
27
26
  /** Restore context from request headers (called in server components) */
28
27
  declare function restoreServerComponentContext(headersList: Headers): ServerComponentContext | null;
29
28
 
30
- export { type ServerComponentContext, headerKeys as headers, restoreServerComponentContext, serializeServerComponentContext };
29
+ export { LAST_PAGE_RENDER_ID_COOKIE, type ServerComponentContext, headerNames, restoreServerComponentContext, serializeServerComponentContext };