@nextlytics/core 0.4.1-canary.103 → 0.4.1-canary.104

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,12 +1,14 @@
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
5
 
6
6
  type DispatchEvent = (event: NextlyticsEvent, ctx: RequestContext, policyFilter?: PageViewDelivery | "client-actions") => DispatchResult;
7
7
  type UpdateEvent = (eventId: string, patch: Partial<NextlyticsEvent>, ctx: RequestContext) => Promise<void>;
8
+ /** Collect the client-side templates from the configured backends. */
9
+ type CollectTemplates = (ctx: RequestContext) => Record<string, JavascriptTemplate>;
8
10
  declare function getUserContext(config: NextlyticsConfigWithDefaults, ctx: RequestContext): Promise<UserContext | undefined>;
9
11
  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>;
12
+ declare function handleEventPost(request: NextRequest, config: NextlyticsConfigWithDefaults, dispatchEvent: DispatchEvent, updateEvent: UpdateEvent, collectTemplates: CollectTemplates): Promise<Response>;
11
13
 
12
- export { type DispatchEvent, type UpdateEvent, getEventProps, getUserContext, handleEventPost };
14
+ 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) {
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,9 +1,9 @@
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
6
 
7
- declare function createNextlyticsMiddleware(config: NextlyticsConfigWithDefaults, dispatchEvent: DispatchEvent, updateEvent: UpdateEvent): NextMiddleware;
7
+ declare function createNextlyticsMiddleware(config: NextlyticsConfigWithDefaults, dispatchEvent: DispatchEvent, updateEvent: UpdateEvent, collectTemplates: CollectTemplates): NextMiddleware;
8
8
 
9
9
  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
  }
@@ -6,7 +6,7 @@ import 'next/dist/server/web/spec-extension/cookies';
6
6
  import 'next/server';
7
7
 
8
8
  type PagesRouterContext = {
9
- req: {
9
+ req?: {
10
10
  headers: Record<string, string | string[] | undefined>;
11
11
  cookies?: Record<string, string>;
12
12
  };
@@ -14,6 +14,11 @@ type PagesRouterContext = {
14
14
  /**
15
15
  * Get Nextlytics props for Pages Router _app.tsx.
16
16
  * Reads context from headers set by middleware.
17
+ *
18
+ * `_app`'s getInitialProps re-runs on every client-side navigation, where there
19
+ * is no `req`. Return an empty context in that case rather than throwing — the
20
+ * client already has its scripts and templates from the initial render and the
21
+ * /api/event round-trip.
17
22
  */
18
23
  declare function getNextlyticsProps(ctx: PagesRouterContext): NextlyticsContext;
19
24
 
@@ -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
  }
@@ -9,6 +9,9 @@ declare const headerNames: {
9
9
  readonly isSoftNavigation: "x-nl-is-soft-nav";
10
10
  readonly active: "x-nl-active";
11
11
  readonly scripts: "x-nl-scripts";
12
+ /** Comma-separated template ids the client already holds, so the server only
13
+ * returns the ones it's missing (see api-handler / client templates merge). */
14
+ readonly knownTemplates: "x-nl-known-templates";
12
15
  };
13
16
  declare const LAST_PAGE_RENDER_ID_COOKIE = "last-page-render-id";
14
17
  /** 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.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
  }
package/dist/types.d.ts CHANGED
@@ -266,6 +266,9 @@ type ClientRequest = {
266
266
  type ClientRequestResult = {
267
267
  ok: boolean;
268
268
  items?: ClientActionItem[];
269
+ /** Client-side templates from the backends, so Pages Router clients (which
270
+ * can't read them from config) can compile the script insertions. */
271
+ templates?: Record<string, JavascriptTemplate>;
269
272
  };
270
273
  /** Return value from Nextlytics() */
271
274
  type NextlyticsResult = {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextlytics/core",
3
- "version": "0.4.1-canary.103",
3
+ "version": "0.4.1-canary.104",
4
4
  "description": "Analytics library for Next.js",
5
5
  "license": "MIT",
6
6
  "repository": {