@nextlytics/core 0.2.2-canary.68 → 0.2.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.
@@ -37,25 +37,6 @@ 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
- }
59
40
  function buildContext(event) {
60
41
  const ctx = {
61
42
  ip: event.serverContext.ip
@@ -63,45 +44,32 @@ function segmentBackend(config) {
63
44
  if (event.userContext?.traits) {
64
45
  ctx.traits = event.userContext.traits;
65
46
  }
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) {
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) {
81
55
  ctx.screen = {
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
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
87
61
  };
88
62
  }
89
63
  }
90
64
  return ctx;
91
65
  }
92
66
  function buildProperties(event) {
93
- const cc = event.clientContext;
94
- const sc = event.serverContext;
95
67
  return {
96
68
  parentEventId: event.parentEventId,
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,
69
+ path: event.serverContext.path,
70
+ host: event.serverContext.host,
71
+ method: event.serverContext.method,
72
+ search: event.serverContext.search,
105
73
  ...event.properties
106
74
  };
107
75
  }
package/dist/client.js CHANGED
@@ -39,11 +39,6 @@ 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,
47
42
  screen: {
48
43
  width: isBrowser ? window.screen.width : void 0,
49
44
  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, "excludeApiCalls" | "eventEndpoint" | "isApiPath" | "backends">> & NextlyticsConfig & {
5
+ type NextlyticsConfigWithDefaults = Required<Pick<NextlyticsConfig, "pageViewMode" | "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,6 +26,7 @@ module.exports = __toCommonJS(config_helpers_exports);
26
26
  function withDefaults(config) {
27
27
  return {
28
28
  ...config,
29
+ pageViewMode: config.pageViewMode ?? "server",
29
30
  excludeApiCalls: config.excludeApiCalls ?? false,
30
31
  eventEndpoint: config.eventEndpoint ?? "/api/event",
31
32
  isApiPath: config.isApiPath ?? (() => false),
@@ -40,8 +41,22 @@ function withDefaults(config) {
40
41
  }
41
42
  };
42
43
  }
43
- function validateConfig(_config) {
44
- return { valid: true, warnings: [] };
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
+ };
45
60
  }
46
61
  function logConfigWarnings(result) {
47
62
  for (const warning of result.warnings) {
@@ -1,12 +1,9 @@
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';
2
5
 
3
6
  type AppRouteHandlers = Record<"GET" | "POST", (req: NextRequest) => Promise<Response>>;
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;
7
+ declare function createHandlers(config: NextlyticsConfigWithDefaults, dispatchEvent: (event: NextlyticsEvent, ctx: RequestContext) => DispatchResult, updateEvent: (eventId: string, patch: Partial<NextlyticsEvent>, ctx: RequestContext) => Promise<void>): AppRouteHandlers;
11
8
 
12
9
  export { createHandlers };
package/dist/handlers.js CHANGED
@@ -21,16 +21,99 @@ __export(handlers_exports, {
21
21
  createHandlers: () => createHandlers
22
22
  });
23
23
  module.exports = __toCommonJS(handlers_exports);
24
- function createHandlers() {
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) {
25
38
  return {
26
39
  GET: async () => {
27
40
  return Response.json({ status: "ok" });
28
41
  },
29
- POST: async () => {
30
- return Response.json(
31
- { error: "Middleware not configured. Events are handled by nextlyticsMiddleware." },
32
- { status: 500 }
33
- );
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 });
34
117
  }
35
118
  };
36
119
  }
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, BackendConfigEntry, BackendWithConfig, ClientContext, IngestPolicy, NextlyticsBackend, NextlyticsBackendFactory, NextlyticsConfig, NextlyticsEvent, NextlyticsPlugin, NextlyticsPluginFactory, NextlyticsResult, NextlyticsServerSide, RequestContext, ServerEventContext, UserContext } from './types.js';
5
+ export { AnonymousUserResult, ClientContext, 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, IngestPolicy, DispatchResult } from './types.js';
2
+ import { NextlyticsEvent, RequestContext, 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, policyFilter?: IngestPolicy) => DispatchResult;
6
+ type DispatchEvent = (event: NextlyticsEvent, ctx: RequestContext) => 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,23 +25,13 @@ 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
- }
31
28
  function resolveBackends(config, ctx) {
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);
29
+ const backends = config.backends || [];
30
+ return backends.map((backend) => typeof backend === "function" ? backend(ctx) : backend).filter((b) => b !== null);
41
31
  }
42
32
  function collectTemplates(backends) {
43
33
  const templates = {};
44
- for (const { backend } of backends) {
34
+ for (const backend of backends) {
45
35
  if (backend.getClientSideTemplates) {
46
36
  Object.assign(templates, backend.getClientSideTemplates());
47
37
  }
@@ -75,41 +65,44 @@ function createNextlyticsMiddleware(config, dispatchEvent, updateEvent) {
75
65
  const { anonId } = await (0, import_anonymous_user.resolveAnonymousUser)({ ctx, serverContext, config, response });
76
66
  const backends = resolveBackends(config, ctx);
77
67
  const templates = collectTemplates(backends);
78
- if (config.excludePaths?.(pathname)) {
79
- (0, import_server_component_context.serializeServerComponentContext)(response, {
80
- pageRenderId,
81
- pathname: request.nextUrl.pathname,
82
- search: request.nextUrl.search,
83
- scripts: [],
84
- templates
85
- });
86
- return response;
87
- }
88
- const isApiPath = config.isApiPath(pathname);
89
- if (isApiPath && config.excludeApiCalls) {
90
- (0, import_server_component_context.serializeServerComponentContext)(response, {
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(
91
93
  pageRenderId,
92
- pathname: request.nextUrl.pathname,
93
- search: request.nextUrl.search,
94
- scripts: [],
95
- templates
96
- });
97
- return response;
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);
98
105
  }
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);
113
106
  (0, import_server_component_context.serializeServerComponentContext)(response, {
114
107
  pageRenderId,
115
108
  pathname: request.nextUrl.pathname,
@@ -140,25 +133,6 @@ async function getUserContext(config, ctx) {
140
133
  return void 0;
141
134
  }
142
135
  }
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
- }
162
136
  async function handleEventPost(request, config, dispatchEvent, updateEvent) {
163
137
  const pageRenderId = request.headers.get(import_server_component_context.headers.pageRenderId);
164
138
  if (!pageRenderId) {
@@ -172,40 +146,39 @@ async function handleEventPost(request, config, dispatchEvent, updateEvent) {
172
146
  }
173
147
  const { type, payload } = body;
174
148
  const ctx = createRequestContext(request);
175
- const apiCallServerContext = (0, import_uitils.createServerContext)(request);
149
+ const serverContext = (0, import_uitils.createServerContext)(request);
176
150
  const userContext = await getUserContext(config, ctx);
151
+ const { anonId: anonymousUserId } = await (0, import_anonymous_user.resolveAnonymousUser)({ ctx, serverContext, config });
177
152
  if (type === "client-init") {
178
153
  const clientContext = payload;
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 });
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
+ }
201
177
  } else if (type === "client-event") {
202
178
  const clientContext = payload.clientContext || void 0;
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
- });
179
+ if (clientContext?.path) {
180
+ serverContext.path = clientContext.path;
181
+ }
209
182
  const event = {
210
183
  eventId: (0, import_uitils.generateId)(),
211
184
  parentEventId: pageRenderId,
package/dist/server.js CHANGED
@@ -33,19 +33,14 @@ 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 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;
36
+ function resolveBackends(config, ctx) {
37
+ const backends = config.backends || [];
38
+ return backends.map((backend) => {
39
+ if (typeof backend === "function") {
40
+ return backend(ctx);
45
41
  }
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);
42
+ return backend;
43
+ }).filter((b) => b !== null);
49
44
  }
50
45
  function resolvePlugins(config, ctx) {
51
46
  const plugins = config.plugins || [];
@@ -90,9 +85,9 @@ function Nextlytics(userConfig) {
90
85
  const config = (0, import_config_helpers.withDefaults)(userConfig);
91
86
  const validationResult = (0, import_config_helpers.validateConfig)(config);
92
87
  (0, import_config_helpers.logConfigWarnings)(validationResult);
93
- const dispatchEventInternal = (event, ctx, policyFilter) => {
88
+ const dispatchEventInternal = (event, ctx) => {
94
89
  const plugins = resolvePlugins(config, ctx);
95
- const resolved = resolveBackends(config, ctx, policyFilter);
90
+ const backends = resolveBackends(config, ctx);
96
91
  const pluginsDone = (async () => {
97
92
  for (const plugin of plugins) {
98
93
  try {
@@ -103,7 +98,7 @@ function Nextlytics(userConfig) {
103
98
  }
104
99
  })();
105
100
  const backendResults = pluginsDone.then(() => {
106
- return resolved.map(({ backend }) => {
101
+ return backends.map((backend) => {
107
102
  const start = Date.now();
108
103
  const promise = backend.onEvent(event).then((result) => ({ ok: true, ms: Date.now() - start, result })).catch((err) => {
109
104
  console.error(`[Nextlytics] Backend "${backend.name}" failed on onEvent:`, err);
@@ -121,7 +116,7 @@ function Nextlytics(userConfig) {
121
116
  const completion = backendResults.then(async (results) => {
122
117
  const settled = await Promise.all(results.map((r) => r.promise));
123
118
  if (config.debug) {
124
- const nameWidth = Math.max(...results.map((r) => r.backend.name.length), 1);
119
+ const nameWidth = Math.max(...results.map((r) => r.backend.name.length));
125
120
  console.log(
126
121
  `[Nextlytics] dispatchEvent ${event.type} ${event.eventId} (${results.length} backends)`
127
122
  );
@@ -136,11 +131,9 @@ function Nextlytics(userConfig) {
136
131
  return { clientActions, completion };
137
132
  };
138
133
  const updateEventInternal = async (eventId, patch, ctx) => {
139
- const resolved = resolveBackends(config, ctx, "immediate").filter(
140
- ({ backend }) => backend.supportsUpdates
141
- );
134
+ const backends = resolveBackends(config, ctx).filter((backend) => backend.supportsUpdates);
142
135
  const results = await Promise.all(
143
- resolved.map(async ({ backend }) => {
136
+ backends.map(async (backend) => {
144
137
  const start = Date.now();
145
138
  try {
146
139
  await backend.updateEvent(eventId, patch);
@@ -152,9 +145,9 @@ function Nextlytics(userConfig) {
152
145
  }
153
146
  })
154
147
  );
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)`);
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)`);
158
151
  results.forEach((r) => {
159
152
  const status = r.ok ? "ok" : "fail";
160
153
  console.log(` ${r.backend.name.padEnd(nameWidth)} ${status.padEnd(4)} ${r.ms}ms`);
@@ -170,7 +163,7 @@ function Nextlytics(userConfig) {
170
163
  return updateEventInternal(eventId, patch, ctx);
171
164
  };
172
165
  const middleware = (0, import_middleware.createNextlyticsMiddleware)(config, dispatchEventInternal, updateEventInternal);
173
- const handlers = (0, import_handlers.createHandlers)();
166
+ const handlers = (0, import_handlers.createHandlers)(config, dispatchEventInternal, updateEventInternal);
174
167
  const analytics = async () => {
175
168
  const headersList = await (0, import_headers.headers)();
176
169
  const cookieStore = await (0, import_headers.cookies)();
package/dist/types.d.ts CHANGED
@@ -28,16 +28,6 @@ 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;
41
31
  /** Screen and viewport dimensions */
42
32
  screen: {
43
33
  /** screen.width */
@@ -109,20 +99,6 @@ type NextlyticsPlugin = {
109
99
  };
110
100
  /** Factory to create plugin per-request (for request-scoped plugins) */
111
101
  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;
126
102
  type NextlyticsConfig = {
127
103
  /** Enable debug logging (shows backend stats for each event) */
128
104
  debug?: boolean;
@@ -138,6 +114,12 @@ type NextlyticsConfig = {
138
114
  /** Cookie max age in seconds (default: 2 years) */
139
115
  cookieMaxAge?: number;
140
116
  };
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";
141
123
  /** Skip tracking for API routes */
142
124
  excludeApiCalls?: boolean;
143
125
  /** Skip tracking for specific paths */
@@ -156,7 +138,7 @@ type NextlyticsConfig = {
156
138
  }) => Promise<AnonymousUserResult>;
157
139
  };
158
140
  /** Analytics backends to send events to */
159
- backends?: BackendConfigEntry[];
141
+ backends?: (NextlyticsBackend | NextlyticsBackendFactory)[];
160
142
  plugins?: (NextlyticsPlugin | NextlyticsPluginFactory)[];
161
143
  };
162
144
  type ClientAction = {
@@ -231,4 +213,4 @@ type NextlyticsResult = {
231
213
  updateEvent: (eventId: string, patch: Partial<NextlyticsEvent>) => Promise<void>;
232
214
  };
233
215
 
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 };
216
+ export type { AnonymousUserResult, ClientAction, ClientActionItem, ClientContext, DispatchResult, 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-canary.68",
3
+ "version": "0.2.2",
4
4
  "description": "Analytics library for Next.js",
5
5
  "license": "MIT",
6
6
  "repository": {