@superblocksteam/sdk 2.0.114 → 2.0.115-next.1

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.
@@ -0,0 +1,213 @@
1
+ /**
2
+ * Helpers to classify Vite / esbuild stderr and emit logs that are easy to facet in Datadog
3
+ * (stable tokens: sdk_dev_server_vite_error, category=module_resolve, unresolved_specifier=...).
4
+ */
5
+
6
+ /** Maximum characters of raw Vite/esbuild output stored on the error meta stack field. */
7
+ export const VITE_ERROR_RAW_LOG_MAX_CHARS = 12_000;
8
+
9
+ export type ViteBuildErrorDiagnostic =
10
+ | {
11
+ category: "module_resolve";
12
+ /** Package specifier esbuild could not resolve, when detected. */
13
+ unresolvedSpecifier: string;
14
+ }
15
+ | {
16
+ category: "optimize_deps" | "unknown";
17
+ unresolvedSpecifier?: undefined;
18
+ };
19
+
20
+ type BuildViteBuildErrorLogDeps = {
21
+ diagnose: typeof diagnoseViteBuildLogMessage;
22
+ formatSummary: typeof formatViteBuildErrorLogSummary;
23
+ errorKind: typeof viteErrorKindForDiagnostic;
24
+ truncate: typeof truncateViteRawLog;
25
+ };
26
+
27
+ type BuildViteBuildErrorLogParams = {
28
+ rawMessage: string;
29
+ sdkVersion: string;
30
+ viteRootBasename: string;
31
+ };
32
+
33
+ function quoteFacetValue(value: string): string {
34
+ if (/^[A-Za-z0-9@._:/-]+$/.test(value)) {
35
+ return value;
36
+ }
37
+ return JSON.stringify(value);
38
+ }
39
+
40
+ /**
41
+ * Best-effort parse of Vite/esbuild multi-line error text (optimizeDeps, build, etc.).
42
+ */
43
+ export function diagnoseViteBuildLogMessage(
44
+ msg: string,
45
+ ): ViteBuildErrorDiagnostic {
46
+ const resolveMatch = msg.match(/Could not resolve ['"]([^'"]+)['"]/);
47
+ if (resolveMatch?.[1]) {
48
+ return {
49
+ category: "module_resolve",
50
+ unresolvedSpecifier: resolveMatch[1],
51
+ };
52
+ }
53
+ if (/\boptimizeDeps\b|pre-bundl/i.test(msg)) {
54
+ return { category: "optimize_deps" };
55
+ }
56
+ return { category: "unknown" };
57
+ }
58
+
59
+ export function formatViteDevServerStartedLog(params: {
60
+ sdkVersion: string;
61
+ viteRootBasename: string;
62
+ }): string {
63
+ return `sdk_dev_server_vite_ready sdk_version=${quoteFacetValue(params.sdkVersion)} vite_root=${quoteFacetValue(params.viteRootBasename)}`;
64
+ }
65
+
66
+ export function formatViteBuildErrorLogSummary(params: {
67
+ diagnostic: ViteBuildErrorDiagnostic;
68
+ sdkVersion: string;
69
+ viteRootBasename: string;
70
+ }): string {
71
+ const { diagnostic, sdkVersion, viteRootBasename } = params;
72
+ const parts = [
73
+ "sdk_dev_server_vite_error",
74
+ `sdk_version=${quoteFacetValue(sdkVersion)}`,
75
+ `vite_root=${quoteFacetValue(viteRootBasename)}`,
76
+ `category=${diagnostic.category}`,
77
+ ];
78
+ if (diagnostic.unresolvedSpecifier) {
79
+ parts.push(
80
+ `unresolved_specifier=${quoteFacetValue(diagnostic.unresolvedSpecifier)}`,
81
+ );
82
+ }
83
+ return parts.join(" ");
84
+ }
85
+
86
+ export function viteErrorKindForDiagnostic(
87
+ diagnostic: ViteBuildErrorDiagnostic,
88
+ ): string {
89
+ switch (diagnostic.category) {
90
+ case "module_resolve":
91
+ return "ViteModuleResolveError";
92
+ case "optimize_deps":
93
+ return "ViteOptimizeDepsError";
94
+ case "unknown":
95
+ return "ViteBuildError";
96
+ }
97
+
98
+ throw new Error(
99
+ `Unhandled Vite build diagnostic category: ${String((diagnostic as { category?: unknown }).category)}`,
100
+ );
101
+ }
102
+
103
+ export function truncateViteRawLog(msg: string): string {
104
+ if (msg.length <= VITE_ERROR_RAW_LOG_MAX_CHARS) {
105
+ return msg;
106
+ }
107
+ return `${msg.slice(0, VITE_ERROR_RAW_LOG_MAX_CHARS)}\n...(truncated)`;
108
+ }
109
+
110
+ function defaultBuildSummary(params: BuildViteBuildErrorLogParams): string {
111
+ return formatViteBuildErrorLogSummary({
112
+ diagnostic: { category: "unknown" },
113
+ sdkVersion: params.sdkVersion,
114
+ viteRootBasename: params.viteRootBasename,
115
+ });
116
+ }
117
+
118
+ function diagnosticsFailureMeta(error: unknown, rawMessage: string) {
119
+ return {
120
+ error: {
121
+ kind: "ViteDiagnosticsError",
122
+ message: error instanceof Error ? error.message : "unknown",
123
+ stack: truncateViteRawLog(rawMessage),
124
+ },
125
+ };
126
+ }
127
+
128
+ export function buildViteBuildErrorLog(
129
+ params: BuildViteBuildErrorLogParams,
130
+ overrides?: Partial<BuildViteBuildErrorLogDeps>,
131
+ ): {
132
+ error: {
133
+ kind: string;
134
+ message: string;
135
+ stack: string;
136
+ };
137
+ message: string;
138
+ } {
139
+ const deps: BuildViteBuildErrorLogDeps = {
140
+ diagnose: diagnoseViteBuildLogMessage,
141
+ errorKind: viteErrorKindForDiagnostic,
142
+ formatSummary: formatViteBuildErrorLogSummary,
143
+ truncate: truncateViteRawLog,
144
+ ...overrides,
145
+ };
146
+
147
+ try {
148
+ const diagnostic = deps.diagnose(params.rawMessage);
149
+ return {
150
+ error: {
151
+ kind: deps.errorKind(diagnostic),
152
+ message:
153
+ diagnostic.category === "module_resolve"
154
+ ? `Could not resolve: ${diagnostic.unresolvedSpecifier}`
155
+ : "Vite or esbuild reported an error",
156
+ stack: deps.truncate(params.rawMessage),
157
+ },
158
+ message: deps.formatSummary({
159
+ diagnostic,
160
+ sdkVersion: params.sdkVersion,
161
+ viteRootBasename: params.viteRootBasename,
162
+ }),
163
+ };
164
+ } catch {
165
+ // Keep the final fallback independent from injected helpers so a helper bug
166
+ // cannot prevent us from returning some structured error payload at all.
167
+ return {
168
+ error: {
169
+ kind: "ViteBuildError",
170
+ message: "Vite or esbuild reported an error",
171
+ stack: truncateViteRawLog(params.rawMessage),
172
+ },
173
+ message: defaultBuildSummary(params),
174
+ };
175
+ }
176
+ }
177
+
178
+ export function logViteBuildError(
179
+ logger: {
180
+ error: (
181
+ message: string,
182
+ meta?: {
183
+ error: {
184
+ kind: string;
185
+ message: string;
186
+ stack?: string;
187
+ };
188
+ },
189
+ ) => void;
190
+ },
191
+ params: BuildViteBuildErrorLogParams,
192
+ overrides?: {
193
+ build?: typeof buildViteBuildErrorLog;
194
+ },
195
+ ): void {
196
+ const build = overrides?.build ?? buildViteBuildErrorLog;
197
+
198
+ try {
199
+ const viteErrorLog = build(params);
200
+ logger.error(viteErrorLog.message, {
201
+ error: viteErrorLog.error,
202
+ });
203
+ } catch (error) {
204
+ try {
205
+ logger.error(
206
+ params.rawMessage,
207
+ diagnosticsFailureMeta(error, params.rawMessage),
208
+ );
209
+ } catch {
210
+ // Prevent exceptions from propagating into Vite's logger internals.
211
+ }
212
+ }
213
+ }
@@ -0,0 +1,142 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+
3
+ const {
4
+ emitMock,
5
+ winstonErrorMock,
6
+ winstonLoggerMock,
7
+ sanitizeLogMessageMock,
8
+ sanitizeLogErrorMock,
9
+ } = vi.hoisted(() => {
10
+ const emitMock = vi.fn();
11
+ const winstonErrorMock = vi.fn();
12
+ const winstonLoggerMock = {
13
+ debug: vi.fn(),
14
+ info: vi.fn(),
15
+ warn: vi.fn(),
16
+ error: winstonErrorMock,
17
+ };
18
+
19
+ const sanitizeLogMessageMock = vi.fn(
20
+ (message: string) => `sanitized-message:${message}`,
21
+ );
22
+ const sanitizeLogErrorMock = vi.fn(
23
+ (error: { kind: string; message: string; stack?: string }) => ({
24
+ ...error,
25
+ message: `sanitized-error:${error.message}`,
26
+ stack: error.stack ? `sanitized-error:${error.stack}` : undefined,
27
+ }),
28
+ );
29
+
30
+ return {
31
+ emitMock,
32
+ winstonErrorMock,
33
+ winstonLoggerMock,
34
+ sanitizeLogMessageMock,
35
+ sanitizeLogErrorMock,
36
+ };
37
+ });
38
+
39
+ vi.mock("@superblocksteam/telemetry", () => ({
40
+ sanitizeLogMessage: sanitizeLogMessageMock,
41
+ sanitizeLogError: sanitizeLogErrorMock,
42
+ }));
43
+
44
+ vi.mock("./index.js", () => ({
45
+ getLogger: vi.fn(() => ({
46
+ emit: emitMock,
47
+ })),
48
+ }));
49
+
50
+ vi.mock("winston", () => ({
51
+ createLogger: vi.fn(() => winstonLoggerMock),
52
+ format: {
53
+ combine: vi.fn(() => ({})),
54
+ timestamp: vi.fn(() => ({})),
55
+ json: vi.fn(() => ({})),
56
+ printf: vi.fn(() => ({})),
57
+ colorize: vi.fn(() => ({})),
58
+ },
59
+ transports: {
60
+ File: class {
61
+ constructor() {}
62
+ },
63
+ Console: class {
64
+ constructor() {}
65
+ },
66
+ },
67
+ }));
68
+
69
+ import { getLogger } from "./logging.js";
70
+
71
+ describe("telemetry logger error sanitization", () => {
72
+ beforeEach(() => {
73
+ vi.clearAllMocks();
74
+ });
75
+
76
+ it.each(["debug", "info", "warn"] as const)(
77
+ "sanitizes %s-level messages before emit and winston",
78
+ (level) => {
79
+ const logger = getLogger();
80
+ logger[level]("api_key=secret");
81
+ expect(sanitizeLogMessageMock).toHaveBeenCalledWith("api_key=secret");
82
+ },
83
+ );
84
+
85
+ it("sanitizes error meta for traced emit and winston output", () => {
86
+ const logger = getLogger();
87
+ const meta = {
88
+ error: {
89
+ kind: "RuntimeError",
90
+ message: "token=super-secret-token",
91
+ stack: "Error: token=super-secret-token",
92
+ },
93
+ };
94
+
95
+ logger.error("api_key=super-secret-key", meta);
96
+
97
+ expect(sanitizeLogMessageMock).toHaveBeenCalledWith(
98
+ "api_key=super-secret-key",
99
+ );
100
+ expect(sanitizeLogErrorMock).toHaveBeenCalledWith(meta.error);
101
+ expect(emitMock).toHaveBeenCalledWith(
102
+ expect.objectContaining({
103
+ body: "sanitized-message:api_key=super-secret-key",
104
+ attributes: {
105
+ error: {
106
+ kind: "RuntimeError",
107
+ message: "sanitized-error:token=super-secret-token",
108
+ stack: "sanitized-error:Error: token=super-secret-token",
109
+ },
110
+ },
111
+ }),
112
+ );
113
+ expect(winstonErrorMock).toHaveBeenCalledWith(
114
+ "sanitized-message:api_key=super-secret-key",
115
+ {
116
+ error: {
117
+ kind: "RuntimeError",
118
+ message: "sanitized-error:token=super-secret-token",
119
+ stack: "sanitized-error:Error: token=super-secret-token",
120
+ },
121
+ },
122
+ );
123
+ });
124
+
125
+ it("does not sanitize or emit error attributes when meta is missing", () => {
126
+ const logger = getLogger();
127
+
128
+ logger.error("plain failure");
129
+
130
+ expect(sanitizeLogMessageMock).toHaveBeenCalledWith("plain failure");
131
+ expect(sanitizeLogErrorMock).not.toHaveBeenCalled();
132
+ expect(emitMock).toHaveBeenCalledWith(
133
+ expect.objectContaining({
134
+ body: "sanitized-message:plain failure",
135
+ attributes: undefined,
136
+ }),
137
+ );
138
+ expect(winstonErrorMock).toHaveBeenCalledWith(
139
+ "sanitized-message:plain failure",
140
+ );
141
+ });
142
+ });
@@ -3,20 +3,32 @@ import type { AnyValueMap } from "@opentelemetry/api-logs";
3
3
  import { createLogger, format, transports } from "winston";
4
4
  import type winston from "winston";
5
5
 
6
+ import {
7
+ sanitizeLogError,
8
+ sanitizeLogMessage,
9
+ } from "@superblocksteam/telemetry";
10
+
6
11
  import { getLogger as getTracedLogger } from "./index.js";
7
12
  import { safeStringify } from "./safe-stringify.js";
8
13
 
9
14
  const activeTransports: winston.transport[] = [];
10
15
 
11
16
  if (process.env.SUPERBLOCKS_IS_CSB === "true") {
12
- // TODO(alex): Revisit this for cloud-prem if we want logger to write to stdout
17
+ // info to stdout for cluster log shippers / kubectl logs;
18
+ // debug stays in /tmp/dev-server.log for on-pod triage
13
19
  activeTransports.push(
14
20
  new transports.File({
15
- format: format.json(),
21
+ format: format.combine(format.timestamp(), format.json()),
16
22
  filename: `/tmp/dev-server.log`,
17
23
  level: "debug",
18
24
  }),
19
25
  );
26
+ activeTransports.push(
27
+ new transports.Console({
28
+ format: format.combine(format.timestamp(), format.json()),
29
+ level: process.env.SUPERBLOCKS_SDK_LOG_LEVEL ?? "info",
30
+ }),
31
+ );
20
32
  } else {
21
33
  // having no transport increases memory usage
22
34
  activeTransports.push(
@@ -43,6 +55,7 @@ const winstonLogger = createLogger({
43
55
  level: "debug",
44
56
  exitOnError: false,
45
57
  format: format.json(),
58
+ defaultMeta: { process: "child" },
46
59
  transports: activeTransports,
47
60
  });
48
61
 
@@ -67,7 +80,7 @@ function formatMessages(messages: unknown[]): string {
67
80
 
68
81
  const logger: Logger = Object.freeze({
69
82
  debug: (...messages: unknown[]) => {
70
- const body = formatMessages(messages);
83
+ const body = sanitizeLogMessage(formatMessages(messages));
71
84
  getTracedLogger().emit({
72
85
  severityNumber: SeverityNumber.DEBUG,
73
86
  severityText: "DEBUG",
@@ -76,7 +89,7 @@ const logger: Logger = Object.freeze({
76
89
  winstonLogger.debug(body);
77
90
  },
78
91
  info: (...messages: unknown[]) => {
79
- const body = formatMessages(messages);
92
+ const body = sanitizeLogMessage(formatMessages(messages));
80
93
  getTracedLogger().emit({
81
94
  severityNumber: SeverityNumber.INFO,
82
95
  severityText: "INFO",
@@ -85,7 +98,7 @@ const logger: Logger = Object.freeze({
85
98
  winstonLogger.info(body);
86
99
  },
87
100
  warn: (...messages: unknown[]) => {
88
- const body = formatMessages(messages);
101
+ const body = sanitizeLogMessage(formatMessages(messages));
89
102
  getTracedLogger().emit({
90
103
  severityNumber: SeverityNumber.WARN,
91
104
  severityText: "WARN",
@@ -94,16 +107,23 @@ const logger: Logger = Object.freeze({
94
107
  winstonLogger.warn(body);
95
108
  },
96
109
  error: (message: string, meta?: ErrorMeta) => {
110
+ const safeMessage = sanitizeLogMessage(message);
111
+ const safeError = meta?.error
112
+ ? (({ kind, message, stack }) => ({ kind, message, stack }))(
113
+ sanitizeLogError(meta.error) as ErrorMeta["error"],
114
+ )
115
+ : undefined;
116
+ const safeMeta = safeError ? { error: safeError } : undefined;
97
117
  getTracedLogger().emit({
98
118
  severityNumber: SeverityNumber.ERROR,
99
119
  severityText: "ERROR",
100
- body: message,
101
- attributes: meta as unknown as AnyValueMap | undefined,
120
+ body: safeMessage,
121
+ attributes: safeMeta as unknown as AnyValueMap | undefined,
102
122
  });
103
- if (meta) {
104
- winstonLogger.error(message, { error: meta.error });
123
+ if (safeError) {
124
+ winstonLogger.error(safeMessage, { error: safeError });
105
125
  } else {
106
- winstonLogger.error(message);
126
+ winstonLogger.error(safeMessage);
107
127
  }
108
128
  },
109
129
  });