@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.
@@ -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;
@@ -1,12 +1,15 @@
1
1
  import { NextRequest } from 'next/server';
2
- import { NextlyticsEvent, RequestContext, PageViewDelivery, DispatchResult, UserContext } from './types.js';
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 };
@@ -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({ ok: true, items: filterScripts(actions) });
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) {
@@ -1,6 +1,7 @@
1
1
  import { NextlyticsBackend } 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
  /**
6
7
  * ClickHouse backend for Nextlytics
@@ -1,6 +1,7 @@
1
1
  import { NextlyticsBackendFactory } 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 GoogleAnalyticsBackendOptions = {
6
7
  /** GA4 Measurement ID (e.g. "G-XXXXXXXXXX") */
@@ -1,6 +1,7 @@
1
1
  import { NextlyticsBackend } 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 GoogleTagManagerBackendOptions = {
6
7
  /** GTM Container ID (e.g. "GTM-XXXXXXX") */
@@ -1,6 +1,7 @@
1
1
  import { NextlyticsEvent, ClientContext, ServerEventContext } 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 const tableColumns: readonly [{
6
7
  readonly name: "timestamp";
@@ -1,6 +1,7 @@
1
1
  import { NextlyticsBackend } 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 loggingBackend(): NextlyticsBackend;
6
7
 
@@ -1,6 +1,7 @@
1
1
  import { NextlyticsBackend } 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 NeonBackendConfig = {
6
7
  databaseUrl: string;
@@ -2,6 +2,7 @@ import { NextlyticsBackend } from '../types.js';
2
2
  export { AnalyticsEventRow, generatePgCreateTableSQL } from './lib/db.js';
3
3
  import 'next/dist/server/web/spec-extension/cookies';
4
4
  import 'next/server';
5
+ import 'next';
5
6
 
6
7
  /**
7
8
  * PostgREST backend for Nextlytics
@@ -1,6 +1,7 @@
1
1
  import { NextlyticsBackend } 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 PosthogBackendOptions = {
6
7
  /** PostHog project API key */
@@ -1,6 +1,7 @@
1
1
  import { NextlyticsBackend } 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
  /**
6
7
  * Segment backend for Nextlytics
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, { signal, isSoftNavigation } = {}) {
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 = [], templates = {} } = props.ctx;
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
- () => ({ requestId, templates, addScripts, scriptsRef, subscribersRef }),
201
- [requestId, templates, addScripts]
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(requestId, {
231
- type: "custom-event",
232
- name: eventName,
233
- props: opts?.props,
234
- collectedAt: (/* @__PURE__ */ new Date()).toISOString(),
235
- clientContext: createClientContext()
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
  }
@@ -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
@@ -8,3 +8,4 @@ import 'react/jsx-runtime';
8
8
  import 'react';
9
9
  import 'next/dist/server/web/spec-extension/cookies';
10
10
  import 'next/server';
11
+ import 'next';
@@ -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 };
@@ -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
  }
@@ -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
 
@@ -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(ctx.req.headers)) {
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 { NextlyticsPlugin } 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 VercelGeoPluginOptions = {
6
7
  /** Property name to store geo data under. Default: "geo" */
@@ -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)(config, dispatchEventInternal, updateEventInternal);
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("[Nextlytics] nextlyticsMiddleware should be added in order for Server to work");
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 headersList = await (0, import_headers.headers)();
193
- const cookieStore = await (0, import_headers.cookies)();
194
- const pageRenderId = headersList.get(import_server_component_context.headerNames.pageRenderId);
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: headersList,
198
- cookies: cookieStore,
199
- path: headersList.get(import_server_component_context.headerNames.pathname) || ""
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 createServerContextFromHeaders(headersList) {
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
- headersList.forEach((value, key) => {
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: headersList.get("host") || "",
261
- method: "GET",
262
- path: pathname,
263
- search: searchParams,
264
- ip: headersList.get("x-forwarded-for")?.split(",")[0]?.trim() || "",
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/action */
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
- analytics: () => Promise<NextlyticsServerSide>;
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextlytics/core",
3
- "version": "0.4.1",
3
+ "version": "0.4.2-canary.106",
4
4
  "description": "Analytics library for Next.js",
5
5
  "license": "MIT",
6
6
  "repository": {