@nextlytics/core 0.4.1 → 0.4.2
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/api-handler.d.ts +5 -3
- package/dist/api-handler.js +24 -5
- package/dist/client.js +56 -16
- package/dist/middleware.d.ts +2 -2
- package/dist/middleware.js +2 -2
- package/dist/pages-router.d.ts +6 -1
- package/dist/pages-router.js +5 -1
- package/dist/server-component-context.d.ts +3 -0
- package/dist/server-component-context.js +4 -1
- package/dist/server.js +9 -2
- package/dist/types.d.ts +3 -0
- package/package.json +1 -1
package/dist/api-handler.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
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
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 };
|
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/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/middleware.d.ts
CHANGED
|
@@ -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 };
|
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
|
@@ -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
|
|
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
|
}
|
|
@@ -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)(
|
|
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
|
}
|
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 = {
|