@nextlytics/core 0.2.2 → 0.3.0-canary.70
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/backends/segment.js +49 -17
- package/dist/client.js +5 -0
- package/dist/config-helpers.d.ts +2 -2
- package/dist/config-helpers.js +2 -17
- package/dist/handlers.d.ts +7 -4
- package/dist/handlers.js +6 -89
- package/dist/index.d.ts +1 -1
- package/dist/middleware.d.ts +2 -2
- package/dist/middleware.js +94 -67
- package/dist/server.js +24 -17
- package/dist/types.d.ts +26 -8
- package/package.json +1 -1
package/dist/backends/segment.js
CHANGED
|
@@ -37,6 +37,25 @@ function segmentBackend(config) {
|
|
|
37
37
|
throw new Error(`Segment error ${res.status}: ${text}`);
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
+
function buildUrl(event) {
|
|
41
|
+
if (event.clientContext?.url) return event.clientContext.url;
|
|
42
|
+
const { host: host2, path, search } = event.serverContext;
|
|
43
|
+
const protocol = host2.includes("localhost") || host2.match(/^[\d.:]+$/) ? "http" : "https";
|
|
44
|
+
const searchStr = Object.entries(search).flatMap(([k, vals]) => vals.map((v) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)).join("&");
|
|
45
|
+
return `${protocol}://${host2}${path}${searchStr ? `?${searchStr}` : ""}`;
|
|
46
|
+
}
|
|
47
|
+
function getSearchString(event) {
|
|
48
|
+
if (event.clientContext?.search) return event.clientContext.search;
|
|
49
|
+
return Object.entries(event.serverContext.search).flatMap(([k, vals]) => vals.map((v) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)).join("&");
|
|
50
|
+
}
|
|
51
|
+
function getReferringDomain(referer) {
|
|
52
|
+
if (!referer) return void 0;
|
|
53
|
+
try {
|
|
54
|
+
return new URL(referer).hostname;
|
|
55
|
+
} catch {
|
|
56
|
+
return void 0;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
40
59
|
function buildContext(event) {
|
|
41
60
|
const ctx = {
|
|
42
61
|
ip: event.serverContext.ip
|
|
@@ -44,32 +63,45 @@ function segmentBackend(config) {
|
|
|
44
63
|
if (event.userContext?.traits) {
|
|
45
64
|
ctx.traits = event.userContext.traits;
|
|
46
65
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
66
|
+
const cc = event.clientContext;
|
|
67
|
+
const sc = event.serverContext;
|
|
68
|
+
ctx.page = {
|
|
69
|
+
path: cc?.path ?? sc.path,
|
|
70
|
+
referrer: cc?.referer,
|
|
71
|
+
referring_domain: getReferringDomain(cc?.referer),
|
|
72
|
+
host: cc?.host ?? sc.host,
|
|
73
|
+
search: getSearchString(event),
|
|
74
|
+
title: cc?.title,
|
|
75
|
+
url: buildUrl(event)
|
|
76
|
+
};
|
|
77
|
+
if (cc) {
|
|
78
|
+
ctx.userAgent = cc.userAgent;
|
|
79
|
+
ctx.locale = cc.locale;
|
|
80
|
+
if (cc.screen) {
|
|
55
81
|
ctx.screen = {
|
|
56
|
-
width:
|
|
57
|
-
height:
|
|
58
|
-
innerWidth:
|
|
59
|
-
innerHeight:
|
|
60
|
-
density:
|
|
82
|
+
width: cc.screen.width,
|
|
83
|
+
height: cc.screen.height,
|
|
84
|
+
innerWidth: cc.screen.innerWidth,
|
|
85
|
+
innerHeight: cc.screen.innerHeight,
|
|
86
|
+
density: cc.screen.density
|
|
61
87
|
};
|
|
62
88
|
}
|
|
63
89
|
}
|
|
64
90
|
return ctx;
|
|
65
91
|
}
|
|
66
92
|
function buildProperties(event) {
|
|
93
|
+
const cc = event.clientContext;
|
|
94
|
+
const sc = event.serverContext;
|
|
67
95
|
return {
|
|
68
96
|
parentEventId: event.parentEventId,
|
|
69
|
-
path:
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
97
|
+
path: cc?.path ?? sc.path,
|
|
98
|
+
url: buildUrl(event),
|
|
99
|
+
search: getSearchString(event),
|
|
100
|
+
hash: cc?.hash,
|
|
101
|
+
title: cc?.title,
|
|
102
|
+
referrer: cc?.referer,
|
|
103
|
+
width: cc?.screen?.innerWidth,
|
|
104
|
+
height: cc?.screen?.innerHeight,
|
|
73
105
|
...event.properties
|
|
74
106
|
};
|
|
75
107
|
}
|
package/dist/client.js
CHANGED
|
@@ -39,6 +39,11 @@ function createClientContext() {
|
|
|
39
39
|
collectedAt: /* @__PURE__ */ new Date(),
|
|
40
40
|
referer: isBrowser ? document.referrer || void 0 : void 0,
|
|
41
41
|
path: isBrowser ? window.location.pathname : void 0,
|
|
42
|
+
url: isBrowser ? window.location.href : void 0,
|
|
43
|
+
host: isBrowser ? window.location.host : void 0,
|
|
44
|
+
search: isBrowser ? window.location.search : void 0,
|
|
45
|
+
hash: isBrowser ? window.location.hash : void 0,
|
|
46
|
+
title: isBrowser ? document.title : void 0,
|
|
42
47
|
screen: {
|
|
43
48
|
width: isBrowser ? window.screen.width : void 0,
|
|
44
49
|
height: isBrowser ? window.screen.height : void 0,
|
package/dist/config-helpers.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { NextlyticsConfig } from './types.js';
|
|
|
2
2
|
import 'next/dist/server/web/spec-extension/cookies';
|
|
3
3
|
import 'next/server';
|
|
4
4
|
|
|
5
|
-
type NextlyticsConfigWithDefaults = Required<Pick<NextlyticsConfig, "
|
|
5
|
+
type NextlyticsConfigWithDefaults = Required<Pick<NextlyticsConfig, "excludeApiCalls" | "eventEndpoint" | "isApiPath" | "backends">> & NextlyticsConfig & {
|
|
6
6
|
anonymousUsers: Required<NonNullable<NextlyticsConfig["anonymousUsers"]>>;
|
|
7
7
|
};
|
|
8
8
|
declare function withDefaults(config: NextlyticsConfig): NextlyticsConfigWithDefaults;
|
|
@@ -10,7 +10,7 @@ interface ConfigValidationResult {
|
|
|
10
10
|
valid: boolean;
|
|
11
11
|
warnings: string[];
|
|
12
12
|
}
|
|
13
|
-
declare function validateConfig(
|
|
13
|
+
declare function validateConfig(_config: NextlyticsConfig): ConfigValidationResult;
|
|
14
14
|
declare function logConfigWarnings(result: ConfigValidationResult): void;
|
|
15
15
|
|
|
16
16
|
export { type ConfigValidationResult, type NextlyticsConfigWithDefaults, logConfigWarnings, validateConfig, withDefaults };
|
package/dist/config-helpers.js
CHANGED
|
@@ -26,7 +26,6 @@ module.exports = __toCommonJS(config_helpers_exports);
|
|
|
26
26
|
function withDefaults(config) {
|
|
27
27
|
return {
|
|
28
28
|
...config,
|
|
29
|
-
pageViewMode: config.pageViewMode ?? "server",
|
|
30
29
|
excludeApiCalls: config.excludeApiCalls ?? false,
|
|
31
30
|
eventEndpoint: config.eventEndpoint ?? "/api/event",
|
|
32
31
|
isApiPath: config.isApiPath ?? (() => false),
|
|
@@ -41,22 +40,8 @@ function withDefaults(config) {
|
|
|
41
40
|
}
|
|
42
41
|
};
|
|
43
42
|
}
|
|
44
|
-
function validateConfig(
|
|
45
|
-
|
|
46
|
-
if (config.pageViewMode === "client-init" && config.backends?.length) {
|
|
47
|
-
const staticBackends = config.backends.filter((b) => typeof b !== "function");
|
|
48
|
-
const backendsWithoutUpdates = staticBackends.filter((b) => !b.supportsUpdates);
|
|
49
|
-
if (backendsWithoutUpdates.length > 0) {
|
|
50
|
-
const backendNames = backendsWithoutUpdates.map((b) => `"${b.name}"`).join(", ");
|
|
51
|
-
warnings.push(
|
|
52
|
-
`[Nextlytics] pageViewMode="client-init" requires backends that support updates. These don't: ${backendNames}`
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return {
|
|
57
|
-
valid: warnings.length === 0,
|
|
58
|
-
warnings
|
|
59
|
-
};
|
|
43
|
+
function validateConfig(_config) {
|
|
44
|
+
return { valid: true, warnings: [] };
|
|
60
45
|
}
|
|
61
46
|
function logConfigWarnings(result) {
|
|
62
47
|
for (const warning of result.warnings) {
|
package/dist/handlers.d.ts
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { NextRequest } from 'next/server';
|
|
2
|
-
import { NextlyticsEvent, RequestContext, DispatchResult } from './types.js';
|
|
3
|
-
import { NextlyticsConfigWithDefaults } from './config-helpers.js';
|
|
4
|
-
import 'next/dist/server/web/spec-extension/cookies';
|
|
5
2
|
|
|
6
3
|
type AppRouteHandlers = Record<"GET" | "POST", (req: NextRequest) => Promise<Response>>;
|
|
7
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Route handlers for /api/event (deprecated - middleware handles this now)
|
|
6
|
+
*
|
|
7
|
+
* Kept for backward compatibility. If you have mounted these handlers at /api/event,
|
|
8
|
+
* the middleware will intercept the request first, so these won't be called.
|
|
9
|
+
*/
|
|
10
|
+
declare function createHandlers(): AppRouteHandlers;
|
|
8
11
|
|
|
9
12
|
export { createHandlers };
|
package/dist/handlers.js
CHANGED
|
@@ -21,99 +21,16 @@ __export(handlers_exports, {
|
|
|
21
21
|
createHandlers: () => createHandlers
|
|
22
22
|
});
|
|
23
23
|
module.exports = __toCommonJS(handlers_exports);
|
|
24
|
-
|
|
25
|
-
var import_uitils = require("./uitils");
|
|
26
|
-
var import_anonymous_user = require("./anonymous-user");
|
|
27
|
-
function createRequestContext(request) {
|
|
28
|
-
return {
|
|
29
|
-
headers: request.headers,
|
|
30
|
-
cookies: request.cookies
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
async function getUserContext(config, ctx) {
|
|
34
|
-
if (!config.callbacks.getUser) return void 0;
|
|
35
|
-
return await config.callbacks.getUser(ctx) || void 0;
|
|
36
|
-
}
|
|
37
|
-
function createHandlers(config, dispatchEvent, updateEvent) {
|
|
24
|
+
function createHandlers() {
|
|
38
25
|
return {
|
|
39
26
|
GET: async () => {
|
|
40
27
|
return Response.json({ status: "ok" });
|
|
41
28
|
},
|
|
42
|
-
POST: async (
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
let body;
|
|
48
|
-
try {
|
|
49
|
-
body = await req.json();
|
|
50
|
-
} catch {
|
|
51
|
-
return Response.json({ error: "Invalid JSON" }, { status: 400 });
|
|
52
|
-
}
|
|
53
|
-
const { type, payload } = body;
|
|
54
|
-
const ctx = createRequestContext(req);
|
|
55
|
-
if (type === "client-init") {
|
|
56
|
-
const clientContext = payload;
|
|
57
|
-
const serverContext = (0, import_uitils.createServerContext)(req);
|
|
58
|
-
if (clientContext?.path) {
|
|
59
|
-
serverContext.path = clientContext.path;
|
|
60
|
-
}
|
|
61
|
-
const userContext = await getUserContext(config, ctx);
|
|
62
|
-
const { anonId: anonymousUserId } = await (0, import_anonymous_user.resolveAnonymousUser)({
|
|
63
|
-
ctx,
|
|
64
|
-
serverContext,
|
|
65
|
-
config
|
|
66
|
-
});
|
|
67
|
-
if (config.pageViewMode === "client-init") {
|
|
68
|
-
const event = {
|
|
69
|
-
eventId: pageRenderId,
|
|
70
|
-
type: "pageView",
|
|
71
|
-
collectedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
72
|
-
anonymousUserId,
|
|
73
|
-
serverContext,
|
|
74
|
-
clientContext,
|
|
75
|
-
userContext,
|
|
76
|
-
properties: {}
|
|
77
|
-
};
|
|
78
|
-
const { clientActions, completion } = dispatchEvent(event, ctx);
|
|
79
|
-
const actions = await clientActions;
|
|
80
|
-
completion.catch((err) => console.warn("[Nextlytics] Dispatch completion error:", err));
|
|
81
|
-
const scripts = actions.items.filter((i) => i.type === "script-template");
|
|
82
|
-
return Response.json({ ok: true, scripts: scripts.length > 0 ? scripts : void 0 });
|
|
83
|
-
} else {
|
|
84
|
-
await updateEvent(pageRenderId, { clientContext, userContext, anonymousUserId }, ctx);
|
|
85
|
-
return Response.json({ ok: true });
|
|
86
|
-
}
|
|
87
|
-
} else if (type === "client-event") {
|
|
88
|
-
const clientContext = payload.clientContext || void 0;
|
|
89
|
-
const serverContext = (0, import_uitils.createServerContext)(req);
|
|
90
|
-
if (clientContext?.path) {
|
|
91
|
-
serverContext.path = clientContext.path;
|
|
92
|
-
}
|
|
93
|
-
const userContext = await getUserContext(config, ctx);
|
|
94
|
-
const { anonId: anonymousUserId } = await (0, import_anonymous_user.resolveAnonymousUser)({
|
|
95
|
-
ctx,
|
|
96
|
-
serverContext,
|
|
97
|
-
config
|
|
98
|
-
});
|
|
99
|
-
const event = {
|
|
100
|
-
eventId: (0, import_uitils.generateId)(),
|
|
101
|
-
parentEventId: pageRenderId,
|
|
102
|
-
type: payload.name || type,
|
|
103
|
-
collectedAt: payload.collectedAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
104
|
-
anonymousUserId,
|
|
105
|
-
serverContext,
|
|
106
|
-
clientContext,
|
|
107
|
-
userContext,
|
|
108
|
-
properties: payload.props || {}
|
|
109
|
-
};
|
|
110
|
-
const { clientActions, completion } = dispatchEvent(event, ctx);
|
|
111
|
-
const actions = await clientActions;
|
|
112
|
-
completion.catch((err) => console.warn("[Nextlytics] Dispatch completion error:", err));
|
|
113
|
-
const scripts = actions.items.filter((i) => i.type === "script-template");
|
|
114
|
-
return Response.json({ ok: true, scripts: scripts.length > 0 ? scripts : void 0 });
|
|
115
|
-
}
|
|
116
|
-
return Response.json({ ok: true });
|
|
29
|
+
POST: async () => {
|
|
30
|
+
return Response.json(
|
|
31
|
+
{ error: "Middleware not configured. Events are handled by nextlyticsMiddleware." },
|
|
32
|
+
{ status: 500 }
|
|
33
|
+
);
|
|
117
34
|
}
|
|
118
35
|
};
|
|
119
36
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { Nextlytics, NextlyticsServer } from './server.js';
|
|
|
2
2
|
export { NextlyticsClient, NextlyticsContext, useNextlytics } from './client.js';
|
|
3
3
|
export { getNextlyticsProps } from './pages-router.js';
|
|
4
4
|
export { loggingBackend } from './backends/logging.js';
|
|
5
|
-
export { AnonymousUserResult, ClientContext, NextlyticsBackend, NextlyticsBackendFactory, NextlyticsConfig, NextlyticsEvent, NextlyticsPlugin, NextlyticsPluginFactory, NextlyticsResult, NextlyticsServerSide, RequestContext, ServerEventContext, UserContext } from './types.js';
|
|
5
|
+
export { AnonymousUserResult, BackendConfigEntry, BackendWithConfig, ClientContext, IngestPolicy, NextlyticsBackend, NextlyticsBackendFactory, NextlyticsConfig, NextlyticsEvent, NextlyticsPlugin, NextlyticsPluginFactory, NextlyticsResult, NextlyticsServerSide, 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/middleware.d.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { NextMiddleware } from 'next/server';
|
|
2
|
-
import { NextlyticsEvent, RequestContext, DispatchResult } from './types.js';
|
|
2
|
+
import { NextlyticsEvent, RequestContext, IngestPolicy, DispatchResult } from './types.js';
|
|
3
3
|
import { NextlyticsConfigWithDefaults } from './config-helpers.js';
|
|
4
4
|
import 'next/dist/server/web/spec-extension/cookies';
|
|
5
5
|
|
|
6
|
-
type DispatchEvent = (event: NextlyticsEvent, ctx: RequestContext) => DispatchResult;
|
|
6
|
+
type DispatchEvent = (event: NextlyticsEvent, ctx: RequestContext, policyFilter?: IngestPolicy) => DispatchResult;
|
|
7
7
|
type UpdateEvent = (eventId: string, patch: Partial<NextlyticsEvent>, ctx: RequestContext) => Promise<void>;
|
|
8
8
|
declare function createNextlyticsMiddleware(config: NextlyticsConfigWithDefaults, dispatchEvent: DispatchEvent, updateEvent: UpdateEvent): NextMiddleware;
|
|
9
9
|
|
package/dist/middleware.js
CHANGED
|
@@ -25,13 +25,23 @@ 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
|
+
}
|
|
28
31
|
function resolveBackends(config, ctx) {
|
|
29
|
-
const
|
|
30
|
-
return
|
|
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);
|
|
31
41
|
}
|
|
32
42
|
function collectTemplates(backends) {
|
|
33
43
|
const templates = {};
|
|
34
|
-
for (const backend of backends) {
|
|
44
|
+
for (const { backend } of backends) {
|
|
35
45
|
if (backend.getClientSideTemplates) {
|
|
36
46
|
Object.assign(templates, backend.getClientSideTemplates());
|
|
37
47
|
}
|
|
@@ -65,44 +75,41 @@ function createNextlyticsMiddleware(config, dispatchEvent, updateEvent) {
|
|
|
65
75
|
const { anonId } = await (0, import_anonymous_user.resolveAnonymousUser)({ ctx, serverContext, config, response });
|
|
66
76
|
const backends = resolveBackends(config, ctx);
|
|
67
77
|
const templates = collectTemplates(backends);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (config.excludePaths?.(pathname)) {
|
|
71
|
-
(0, import_server_component_context.serializeServerComponentContext)(response, {
|
|
72
|
-
pageRenderId,
|
|
73
|
-
pathname: request.nextUrl.pathname,
|
|
74
|
-
search: request.nextUrl.search,
|
|
75
|
-
scripts,
|
|
76
|
-
templates
|
|
77
|
-
});
|
|
78
|
-
return response;
|
|
79
|
-
}
|
|
80
|
-
const isApiPath = config.isApiPath(pathname);
|
|
81
|
-
if (isApiPath && config.excludeApiCalls) {
|
|
82
|
-
(0, import_server_component_context.serializeServerComponentContext)(response, {
|
|
83
|
-
pageRenderId,
|
|
84
|
-
pathname: request.nextUrl.pathname,
|
|
85
|
-
search: request.nextUrl.search,
|
|
86
|
-
scripts,
|
|
87
|
-
templates
|
|
88
|
-
});
|
|
89
|
-
return response;
|
|
90
|
-
}
|
|
91
|
-
const userContext = await getUserContext(config, ctx);
|
|
92
|
-
const pageViewEvent = createPageViewEvent(
|
|
78
|
+
if (config.excludePaths?.(pathname)) {
|
|
79
|
+
(0, import_server_component_context.serializeServerComponentContext)(response, {
|
|
93
80
|
pageRenderId,
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
);
|
|
99
|
-
|
|
100
|
-
const actions = await clientActions;
|
|
101
|
-
scripts = actions.items.filter(
|
|
102
|
-
(i) => i.type === "script-template"
|
|
103
|
-
);
|
|
104
|
-
(0, import_server.after)(() => completion);
|
|
81
|
+
pathname: request.nextUrl.pathname,
|
|
82
|
+
search: request.nextUrl.search,
|
|
83
|
+
scripts: [],
|
|
84
|
+
templates
|
|
85
|
+
});
|
|
86
|
+
return response;
|
|
105
87
|
}
|
|
88
|
+
const isApiPath = config.isApiPath(pathname);
|
|
89
|
+
if (isApiPath && config.excludeApiCalls) {
|
|
90
|
+
(0, import_server_component_context.serializeServerComponentContext)(response, {
|
|
91
|
+
pageRenderId,
|
|
92
|
+
pathname: request.nextUrl.pathname,
|
|
93
|
+
search: request.nextUrl.search,
|
|
94
|
+
scripts: [],
|
|
95
|
+
templates
|
|
96
|
+
});
|
|
97
|
+
return response;
|
|
98
|
+
}
|
|
99
|
+
const userContext = await getUserContext(config, ctx);
|
|
100
|
+
const pageViewEvent = createPageViewEvent(
|
|
101
|
+
pageRenderId,
|
|
102
|
+
serverContext,
|
|
103
|
+
isApiPath,
|
|
104
|
+
userContext,
|
|
105
|
+
anonId
|
|
106
|
+
);
|
|
107
|
+
const { clientActions, completion } = dispatchEvent(pageViewEvent, ctx, "immediate");
|
|
108
|
+
const actions = await clientActions;
|
|
109
|
+
const scripts = actions.items.filter(
|
|
110
|
+
(i) => i.type === "script-template"
|
|
111
|
+
);
|
|
112
|
+
(0, import_server.after)(() => completion);
|
|
106
113
|
(0, import_server_component_context.serializeServerComponentContext)(response, {
|
|
107
114
|
pageRenderId,
|
|
108
115
|
pathname: request.nextUrl.pathname,
|
|
@@ -133,6 +140,25 @@ async function getUserContext(config, ctx) {
|
|
|
133
140
|
return void 0;
|
|
134
141
|
}
|
|
135
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
|
+
}
|
|
136
162
|
async function handleEventPost(request, config, dispatchEvent, updateEvent) {
|
|
137
163
|
const pageRenderId = request.headers.get(import_server_component_context.headers.pageRenderId);
|
|
138
164
|
if (!pageRenderId) {
|
|
@@ -146,39 +172,40 @@ async function handleEventPost(request, config, dispatchEvent, updateEvent) {
|
|
|
146
172
|
}
|
|
147
173
|
const { type, payload } = body;
|
|
148
174
|
const ctx = createRequestContext(request);
|
|
149
|
-
const
|
|
175
|
+
const apiCallServerContext = (0, import_uitils.createServerContext)(request);
|
|
150
176
|
const userContext = await getUserContext(config, ctx);
|
|
151
|
-
const { anonId: anonymousUserId } = await (0, import_anonymous_user.resolveAnonymousUser)({ ctx, serverContext, config });
|
|
152
177
|
if (type === "client-init") {
|
|
153
178
|
const clientContext = payload;
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
}
|
|
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 });
|
|
177
201
|
} else if (type === "client-event") {
|
|
178
202
|
const clientContext = payload.clientContext || void 0;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
+
});
|
|
182
209
|
const event = {
|
|
183
210
|
eventId: (0, import_uitils.generateId)(),
|
|
184
211
|
parentEventId: pageRenderId,
|
package/dist/server.js
CHANGED
|
@@ -33,14 +33,19 @@ var import_handlers = require("./handlers");
|
|
|
33
33
|
var import_config_helpers = require("./config-helpers");
|
|
34
34
|
var import_middleware = require("./middleware");
|
|
35
35
|
var import_uitils = require("./uitils");
|
|
36
|
-
function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
function isBackendWithConfig(entry) {
|
|
37
|
+
return typeof entry === "object" && entry !== null && "backend" in entry;
|
|
38
|
+
}
|
|
39
|
+
function resolveBackends(config, ctx, policyFilter) {
|
|
40
|
+
const entries = config.backends || [];
|
|
41
|
+
return entries.map((entry) => {
|
|
42
|
+
if (isBackendWithConfig(entry)) {
|
|
43
|
+
const backend2 = typeof entry.backend === "function" ? entry.backend(ctx) : entry.backend;
|
|
44
|
+
return backend2 ? { backend: backend2, ingestPolicy: entry.ingestPolicy ?? "immediate" } : null;
|
|
41
45
|
}
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
const backend = typeof entry === "function" ? entry(ctx) : entry;
|
|
47
|
+
return backend ? { backend, ingestPolicy: "immediate" } : null;
|
|
48
|
+
}).filter((b) => b !== null).filter((b) => !policyFilter || b.ingestPolicy === policyFilter);
|
|
44
49
|
}
|
|
45
50
|
function resolvePlugins(config, ctx) {
|
|
46
51
|
const plugins = config.plugins || [];
|
|
@@ -85,9 +90,9 @@ function Nextlytics(userConfig) {
|
|
|
85
90
|
const config = (0, import_config_helpers.withDefaults)(userConfig);
|
|
86
91
|
const validationResult = (0, import_config_helpers.validateConfig)(config);
|
|
87
92
|
(0, import_config_helpers.logConfigWarnings)(validationResult);
|
|
88
|
-
const dispatchEventInternal = (event, ctx) => {
|
|
93
|
+
const dispatchEventInternal = (event, ctx, policyFilter) => {
|
|
89
94
|
const plugins = resolvePlugins(config, ctx);
|
|
90
|
-
const
|
|
95
|
+
const resolved = resolveBackends(config, ctx, policyFilter);
|
|
91
96
|
const pluginsDone = (async () => {
|
|
92
97
|
for (const plugin of plugins) {
|
|
93
98
|
try {
|
|
@@ -98,7 +103,7 @@ function Nextlytics(userConfig) {
|
|
|
98
103
|
}
|
|
99
104
|
})();
|
|
100
105
|
const backendResults = pluginsDone.then(() => {
|
|
101
|
-
return
|
|
106
|
+
return resolved.map(({ backend }) => {
|
|
102
107
|
const start = Date.now();
|
|
103
108
|
const promise = backend.onEvent(event).then((result) => ({ ok: true, ms: Date.now() - start, result })).catch((err) => {
|
|
104
109
|
console.error(`[Nextlytics] Backend "${backend.name}" failed on onEvent:`, err);
|
|
@@ -116,7 +121,7 @@ function Nextlytics(userConfig) {
|
|
|
116
121
|
const completion = backendResults.then(async (results) => {
|
|
117
122
|
const settled = await Promise.all(results.map((r) => r.promise));
|
|
118
123
|
if (config.debug) {
|
|
119
|
-
const nameWidth = Math.max(...results.map((r) => r.backend.name.length));
|
|
124
|
+
const nameWidth = Math.max(...results.map((r) => r.backend.name.length), 1);
|
|
120
125
|
console.log(
|
|
121
126
|
`[Nextlytics] dispatchEvent ${event.type} ${event.eventId} (${results.length} backends)`
|
|
122
127
|
);
|
|
@@ -131,9 +136,11 @@ function Nextlytics(userConfig) {
|
|
|
131
136
|
return { clientActions, completion };
|
|
132
137
|
};
|
|
133
138
|
const updateEventInternal = async (eventId, patch, ctx) => {
|
|
134
|
-
const
|
|
139
|
+
const resolved = resolveBackends(config, ctx, "immediate").filter(
|
|
140
|
+
({ backend }) => backend.supportsUpdates
|
|
141
|
+
);
|
|
135
142
|
const results = await Promise.all(
|
|
136
|
-
|
|
143
|
+
resolved.map(async ({ backend }) => {
|
|
137
144
|
const start = Date.now();
|
|
138
145
|
try {
|
|
139
146
|
await backend.updateEvent(eventId, patch);
|
|
@@ -145,9 +152,9 @@ function Nextlytics(userConfig) {
|
|
|
145
152
|
}
|
|
146
153
|
})
|
|
147
154
|
);
|
|
148
|
-
if (config.debug &&
|
|
149
|
-
const nameWidth = Math.max(...
|
|
150
|
-
console.log(`[Nextlytics] updateEvent ${eventId} (${
|
|
155
|
+
if (config.debug && resolved.length > 0) {
|
|
156
|
+
const nameWidth = Math.max(...resolved.map(({ backend }) => backend.name.length));
|
|
157
|
+
console.log(`[Nextlytics] updateEvent ${eventId} (${resolved.length} backends)`);
|
|
151
158
|
results.forEach((r) => {
|
|
152
159
|
const status = r.ok ? "ok" : "fail";
|
|
153
160
|
console.log(` ${r.backend.name.padEnd(nameWidth)} ${status.padEnd(4)} ${r.ms}ms`);
|
|
@@ -163,7 +170,7 @@ function Nextlytics(userConfig) {
|
|
|
163
170
|
return updateEventInternal(eventId, patch, ctx);
|
|
164
171
|
};
|
|
165
172
|
const middleware = (0, import_middleware.createNextlyticsMiddleware)(config, dispatchEventInternal, updateEventInternal);
|
|
166
|
-
const handlers = (0, import_handlers.createHandlers)(
|
|
173
|
+
const handlers = (0, import_handlers.createHandlers)();
|
|
167
174
|
const analytics = async () => {
|
|
168
175
|
const headersList = await (0, import_headers.headers)();
|
|
169
176
|
const cookieStore = await (0, import_headers.cookies)();
|
package/dist/types.d.ts
CHANGED
|
@@ -28,6 +28,16 @@ interface ClientContext {
|
|
|
28
28
|
referer?: string;
|
|
29
29
|
/** window.location.pathname - may differ from server path in SPAs */
|
|
30
30
|
path?: string;
|
|
31
|
+
/** window.location.href */
|
|
32
|
+
url?: string;
|
|
33
|
+
/** window.location.host */
|
|
34
|
+
host?: string;
|
|
35
|
+
/** window.location.search (as string, e.g. "?foo=bar") */
|
|
36
|
+
search?: string;
|
|
37
|
+
/** window.location.hash */
|
|
38
|
+
hash?: string;
|
|
39
|
+
/** document.title */
|
|
40
|
+
title?: string;
|
|
31
41
|
/** Screen and viewport dimensions */
|
|
32
42
|
screen: {
|
|
33
43
|
/** screen.width */
|
|
@@ -99,6 +109,20 @@ type NextlyticsPlugin = {
|
|
|
99
109
|
};
|
|
100
110
|
/** Factory to create plugin per-request (for request-scoped plugins) */
|
|
101
111
|
type NextlyticsPluginFactory = (ctx: RequestContext) => NextlyticsPlugin;
|
|
112
|
+
/** When to ingest events for a backend */
|
|
113
|
+
type IngestPolicy =
|
|
114
|
+
/** Dispatch immediately in middleware (default) - faster but no client context */
|
|
115
|
+
"immediate"
|
|
116
|
+
/** Dispatch when client-init is received - has full client context (title, screen, etc) */
|
|
117
|
+
| "on-client-event";
|
|
118
|
+
/** Backend with configuration options */
|
|
119
|
+
type BackendWithConfig = {
|
|
120
|
+
backend: NextlyticsBackend | NextlyticsBackendFactory;
|
|
121
|
+
/** When to send events. Default: "immediate" */
|
|
122
|
+
ingestPolicy?: IngestPolicy;
|
|
123
|
+
};
|
|
124
|
+
/** Backend config entry - either a backend directly or with config */
|
|
125
|
+
type BackendConfigEntry = NextlyticsBackend | NextlyticsBackendFactory | BackendWithConfig;
|
|
102
126
|
type NextlyticsConfig = {
|
|
103
127
|
/** Enable debug logging (shows backend stats for each event) */
|
|
104
128
|
debug?: boolean;
|
|
@@ -114,12 +138,6 @@ type NextlyticsConfig = {
|
|
|
114
138
|
/** Cookie max age in seconds (default: 2 years) */
|
|
115
139
|
cookieMaxAge?: number;
|
|
116
140
|
};
|
|
117
|
-
/**
|
|
118
|
-
* When to record pageView:
|
|
119
|
-
* - "server": in middleware (default, more reliable)
|
|
120
|
-
* - "client-init": when JS loads (has client context) - NOT SUPPORTED CURRENTLU
|
|
121
|
-
*/
|
|
122
|
-
pageViewMode?: "server" | "client-init";
|
|
123
141
|
/** Skip tracking for API routes */
|
|
124
142
|
excludeApiCalls?: boolean;
|
|
125
143
|
/** Skip tracking for specific paths */
|
|
@@ -138,7 +156,7 @@ type NextlyticsConfig = {
|
|
|
138
156
|
}) => Promise<AnonymousUserResult>;
|
|
139
157
|
};
|
|
140
158
|
/** Analytics backends to send events to */
|
|
141
|
-
backends?:
|
|
159
|
+
backends?: BackendConfigEntry[];
|
|
142
160
|
plugins?: (NextlyticsPlugin | NextlyticsPluginFactory)[];
|
|
143
161
|
};
|
|
144
162
|
type ClientAction = {
|
|
@@ -213,4 +231,4 @@ type NextlyticsResult = {
|
|
|
213
231
|
updateEvent: (eventId: string, patch: Partial<NextlyticsEvent>) => Promise<void>;
|
|
214
232
|
};
|
|
215
233
|
|
|
216
|
-
export type { AnonymousUserResult, ClientAction, ClientActionItem, ClientContext, DispatchResult, JavascriptTemplate, NextlyticsBackend, NextlyticsBackendFactory, NextlyticsConfig, NextlyticsEvent, NextlyticsPlugin, NextlyticsPluginFactory, NextlyticsResult, NextlyticsServerSide, RequestContext, ScriptElement, ServerEventContext, TemplatizedScriptInsertion, UserContext };
|
|
234
|
+
export type { AnonymousUserResult, BackendConfigEntry, BackendWithConfig, ClientAction, ClientActionItem, ClientContext, DispatchResult, IngestPolicy, JavascriptTemplate, NextlyticsBackend, NextlyticsBackendFactory, NextlyticsConfig, NextlyticsEvent, NextlyticsPlugin, NextlyticsPluginFactory, NextlyticsResult, NextlyticsServerSide, RequestContext, ScriptElement, ServerEventContext, TemplatizedScriptInsertion, UserContext };
|