@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.
@@ -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
- if (event.clientContext) {
48
- ctx.userAgent = event.clientContext.userAgent;
49
- ctx.locale = event.clientContext.locale;
50
- ctx.page = {
51
- path: event.clientContext.path,
52
- referrer: event.clientContext.referer
53
- };
54
- if (event.clientContext.screen) {
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: event.clientContext.screen.width,
57
- height: event.clientContext.screen.height,
58
- innerWidth: event.clientContext.screen.innerWidth,
59
- innerHeight: event.clientContext.screen.innerHeight,
60
- density: event.clientContext.screen.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: event.serverContext.path,
70
- host: event.serverContext.host,
71
- method: event.serverContext.method,
72
- search: event.serverContext.search,
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,
@@ -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, "pageViewMode" | "excludeApiCalls" | "eventEndpoint" | "isApiPath" | "backends">> & 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(config: NextlyticsConfig): ConfigValidationResult;
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 };
@@ -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(config) {
45
- const warnings = [];
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) {
@@ -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
- declare function createHandlers(config: NextlyticsConfigWithDefaults, dispatchEvent: (event: NextlyticsEvent, ctx: RequestContext) => DispatchResult, updateEvent: (eventId: string, patch: Partial<NextlyticsEvent>, ctx: RequestContext) => Promise<void>): AppRouteHandlers;
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
- var import_server_component_context = require("./server-component-context");
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 (req) => {
43
- const pageRenderId = req.headers.get(import_server_component_context.headers.pageRenderId);
44
- if (!pageRenderId) {
45
- return Response.json({ error: "Missing page render ID" }, { status: 400 });
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';
@@ -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
 
@@ -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 backends = config.backends || [];
30
- return backends.map((backend) => typeof backend === "function" ? backend(ctx) : backend).filter((b) => b !== null);
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
- let scripts = [];
69
- if (config.pageViewMode !== "client-init") {
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
- serverContext,
95
- isApiPath,
96
- userContext,
97
- anonId
98
- );
99
- const { clientActions, completion } = dispatchEvent(pageViewEvent, ctx);
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 serverContext = (0, import_uitils.createServerContext)(request);
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
- if (clientContext?.path) {
155
- serverContext.path = clientContext.path;
156
- }
157
- if (config.pageViewMode === "client-init") {
158
- const event = {
159
- eventId: pageRenderId,
160
- type: "pageView",
161
- collectedAt: (/* @__PURE__ */ new Date()).toISOString(),
162
- anonymousUserId,
163
- serverContext,
164
- clientContext,
165
- userContext,
166
- properties: {}
167
- };
168
- const { clientActions, completion } = dispatchEvent(event, ctx);
169
- const actions = await clientActions;
170
- (0, import_server.after)(() => completion);
171
- const scripts = actions.items.filter((i) => i.type === "script-template");
172
- return Response.json({ ok: true, scripts: scripts.length > 0 ? scripts : void 0 });
173
- } else {
174
- (0, import_server.after)(() => updateEvent(pageRenderId, { clientContext, userContext, anonymousUserId }, ctx));
175
- return Response.json({ ok: true });
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
- if (clientContext?.path) {
180
- serverContext.path = clientContext.path;
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 resolveBackends(config, ctx) {
37
- const backends = config.backends || [];
38
- return backends.map((backend) => {
39
- if (typeof backend === "function") {
40
- return backend(ctx);
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
- return backend;
43
- }).filter((b) => b !== null);
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 backends = resolveBackends(config, ctx);
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 backends.map((backend) => {
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 backends = resolveBackends(config, ctx).filter((backend) => backend.supportsUpdates);
139
+ const resolved = resolveBackends(config, ctx, "immediate").filter(
140
+ ({ backend }) => backend.supportsUpdates
141
+ );
135
142
  const results = await Promise.all(
136
- backends.map(async (backend) => {
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 && backends.length > 0) {
149
- const nameWidth = Math.max(...backends.map((b) => b.name.length));
150
- console.log(`[Nextlytics] updateEvent ${eventId} (${backends.length} backends)`);
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)(config, dispatchEventInternal, updateEventInternal);
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?: (NextlyticsBackend | NextlyticsBackendFactory)[];
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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nextlytics/core",
3
- "version": "0.2.2",
3
+ "version": "0.3.0-canary.70",
4
4
  "description": "Analytics library for Next.js",
5
5
  "license": "MIT",
6
6
  "repository": {