keystone-design-bootstrap 1.0.96 → 1.0.98

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,11 +1,12 @@
1
1
  'use client';
2
2
 
3
- import { captureCustomEvent } from './captureEvent';
3
+ import { logs } from '@opentelemetry/api-logs';
4
+ import { OTLPLogExporter } from '@opentelemetry/exporter-logs-otlp-http';
5
+ import { resourceFromAttributes } from '@opentelemetry/resources';
6
+ import { BatchLogRecordProcessor, LoggerProvider } from '@opentelemetry/sdk-logs';
4
7
 
5
8
  const BUILD_CONSOLE_DISABLED = process.env.NEXT_PUBLIC_LOGGING_DISABLED === '1';
6
9
  const BUILD_POSTHOG_DISABLED = process.env.NEXT_PUBLIC_POSTHOG_LOGGING_DISABLED === '1';
7
- const POSTHOG_LOG_EVENT = 'keystone_client_log';
8
-
9
10
  interface LoggingWindow extends Window {
10
11
  __loggingDisabled?: boolean;
11
12
  __posthogLoggingDisabled?: boolean;
@@ -14,10 +15,23 @@ interface LoggingWindow extends Window {
14
15
  export type LogLevel = 'info' | 'warn' | 'error';
15
16
 
16
17
  export type LogOptions = {
17
- /** Send this log payload to PostHog (`keystone_client_log`). */
18
+ /** Send this log payload to PostHog Logs product. */
18
19
  shipToPostHog?: boolean;
19
20
  };
20
21
 
22
+ type OTelInitOptions = {
23
+ apiKey: string;
24
+ apiHost?: string;
25
+ serviceName?: string;
26
+ environment?: string;
27
+ accountId?: number;
28
+ accountName?: string;
29
+ };
30
+ let otelProvider: LoggerProvider | null = null;
31
+ let otelLogger: ReturnType<typeof logs.getLogger> | null = null;
32
+ let otelInitFingerprint = '';
33
+ const browserConsole = globalThis?.['console'];
34
+
21
35
  /**
22
36
  * Channel → accent colour. Unregistered channels fall through to gray.
23
37
  */
@@ -43,6 +57,81 @@ export const CHANNEL_COLORS: Record<string, string> = {
43
57
  'mobile-value-props-pin': '#4fafa0',
44
58
  };
45
59
 
60
+ function flattenAttributes(input: Record<string, unknown>, prefix = ''): Record<string, string | number | boolean> {
61
+ const output: Record<string, string | number | boolean> = {};
62
+
63
+ for (const [key, value] of Object.entries(input)) {
64
+ const nextKey = prefix ? `${prefix}.${key}` : key;
65
+ if (value === null || value === undefined) continue;
66
+
67
+ if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
68
+ output[nextKey] = value;
69
+ continue;
70
+ }
71
+
72
+ if (Array.isArray(value)) {
73
+ output[nextKey] = JSON.stringify(value);
74
+ continue;
75
+ }
76
+
77
+ if (typeof value === 'object') {
78
+ Object.assign(output, flattenAttributes(value as Record<string, unknown>, nextKey));
79
+ continue;
80
+ }
81
+
82
+ output[nextKey] = String(value);
83
+ }
84
+
85
+ return output;
86
+ }
87
+
88
+ function stripTrailingSlash(host: string): string {
89
+ return host.replace(/\/+$/, '');
90
+ }
91
+
92
+ export function initializePostHogLogging(options: OTelInitOptions): void {
93
+ if (typeof window === 'undefined') return;
94
+ if (!options.apiKey?.trim()) return;
95
+
96
+ const apiHost = stripTrailingSlash(options.apiHost?.trim() || 'https://us.i.posthog.com');
97
+ const serviceName = options.serviceName?.trim() || 'keystone-customer-site';
98
+ const environment = options.environment?.trim() || 'production';
99
+ const fingerprint = `${apiHost}|${options.apiKey}|${serviceName}|${environment}|${options.accountId ?? ''}|${options.accountName ?? ''}`;
100
+ if (otelLogger && otelInitFingerprint === fingerprint) return;
101
+
102
+ if (otelProvider) {
103
+ // Cleanly replace logger provider when config changes.
104
+ void otelProvider.shutdown().catch(() => {});
105
+ otelProvider = null;
106
+ otelLogger = null;
107
+ }
108
+
109
+ const baseAttributes: Record<string, string | number | boolean> = {
110
+ 'service.name': serviceName,
111
+ environment,
112
+ site_domain: window.location.hostname,
113
+ };
114
+ if (options.accountId !== undefined) baseAttributes.account_id = options.accountId;
115
+ if (options.accountName) baseAttributes.account_name = options.accountName;
116
+
117
+ const exporter = new OTLPLogExporter({
118
+ url: `${apiHost}/i/v1/logs?token=${encodeURIComponent(options.apiKey)}`,
119
+ headers: {
120
+ // Keep request "simple" to avoid CORS preflight in browser.
121
+ 'Content-Type': 'text/plain',
122
+ },
123
+ });
124
+
125
+ otelProvider = new LoggerProvider({
126
+ resource: resourceFromAttributes(baseAttributes),
127
+ processors: [new BatchLogRecordProcessor(exporter)],
128
+ });
129
+
130
+ logs.setGlobalLoggerProvider(otelProvider);
131
+ otelLogger = logs.getLogger(serviceName);
132
+ otelInitFingerprint = fingerprint;
133
+ }
134
+
46
135
  function isConsoleEnabled(): boolean {
47
136
  if (BUILD_CONSOLE_DISABLED) return false;
48
137
  if (typeof window === 'undefined') return true;
@@ -61,7 +150,12 @@ function formatDetail(detail: Record<string, unknown>): string {
61
150
  .join(' ');
62
151
  }
63
152
 
64
- function emitConsole(level: LogLevel, channel: string, event: string, detail: Record<string, unknown>): void {
153
+ function emitConsole(
154
+ level: LogLevel,
155
+ channel: string,
156
+ event: string,
157
+ detail: Record<string, unknown>,
158
+ ): void {
65
159
  const color = CHANNEL_COLORS[channel] ?? '#aaa';
66
160
  const detailStr = formatDetail(detail);
67
161
  const message = `%c[${channel}] %c${event}%c ${detailStr}`;
@@ -74,40 +168,38 @@ function emitConsole(level: LogLevel, channel: string, event: string, detail: Re
74
168
  const detailStyle = 'color:#999; font-weight:normal';
75
169
 
76
170
  if (level === 'error') {
77
- console.error(message, channelStyle, eventStyle, detailStyle);
171
+ browserConsole?.error?.(message, channelStyle, eventStyle, detailStyle);
78
172
  return;
79
173
  }
80
174
  if (level === 'warn') {
81
- console.warn(message, channelStyle, eventStyle, detailStyle);
175
+ browserConsole?.warn?.(message, channelStyle, eventStyle, detailStyle);
82
176
  return;
83
177
  }
84
- console.log(message, channelStyle, eventStyle, detailStyle);
178
+ browserConsole?.log?.(message, channelStyle, eventStyle, detailStyle);
85
179
  }
86
180
 
87
- function maybeShipToPostHog(
88
- level: LogLevel,
89
- channel: string,
90
- event: string,
91
- detail: Record<string, unknown>,
92
- options?: LogOptions,
93
- ): void {
94
- const shouldShip = options?.shipToPostHog ?? level !== 'info';
95
- if (!shouldShip || !isPostHogShippingEnabled()) return;
96
-
97
- captureCustomEvent(POSTHOG_LOG_EVENT, {
98
- level,
99
- channel,
100
- event,
101
- detail,
181
+ function emitToPostHog(level: LogLevel, channel: string, event: string, detail: Record<string, unknown>): void {
182
+ if (!otelLogger) return;
183
+
184
+ const severityText = level === 'warn' ? 'WARNING' : level === 'error' ? 'ERROR' : 'INFO';
185
+ const attributes = flattenAttributes({ channel, ...detail });
186
+
187
+ otelLogger.emit({
188
+ severityText,
189
+ body: event,
190
+ attributes,
102
191
  });
103
192
  }
104
193
 
105
194
  function emit(level: LogLevel, channel: string, event: string, detail: Record<string, unknown>, options?: LogOptions): void {
195
+ const shouldShip = (options?.shipToPostHog ?? level !== 'info') && isPostHogShippingEnabled();
106
196
  // Keep warn/error visible even when regular logs are muted.
107
197
  if (level !== 'info' || isConsoleEnabled()) {
108
198
  emitConsole(level, channel, event, detail);
109
199
  }
110
- maybeShipToPostHog(level, channel, event, detail, options);
200
+ if (shouldShip) {
201
+ emitToPostHog(level, channel, event, detail);
202
+ }
111
203
  }
112
204
 
113
205
  export function log(channel: string, event: string, detail: Record<string, unknown> = {}, options?: LogOptions): void {