@superblocksteam/sdk 2.0.150-next.0 → 2.0.150-next.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.
- package/.turbo/turbo-build.log +1 -1
- package/dist/cli-replacement/dev.d.mts.map +1 -1
- package/dist/cli-replacement/dev.mjs +111 -8
- package/dist/cli-replacement/dev.mjs.map +1 -1
- package/dist/cli-replacement/install-packages.npm-registry.test.mjs +153 -0
- package/dist/cli-replacement/install-packages.npm-registry.test.mjs.map +1 -1
- package/dist/cli-replacement/npm-install-summary.d.mts +30 -0
- package/dist/cli-replacement/npm-install-summary.d.mts.map +1 -0
- package/dist/cli-replacement/npm-install-summary.mjs +67 -0
- package/dist/cli-replacement/npm-install-summary.mjs.map +1 -0
- package/dist/cli-replacement/npm-install-summary.test.d.mts +2 -0
- package/dist/cli-replacement/npm-install-summary.test.d.mts.map +1 -0
- package/dist/cli-replacement/npm-install-summary.test.mjs +70 -0
- package/dist/cli-replacement/npm-install-summary.test.mjs.map +1 -0
- package/dist/cli-replacement/npm-install-timing.d.mts +16 -0
- package/dist/cli-replacement/npm-install-timing.d.mts.map +1 -0
- package/dist/cli-replacement/npm-install-timing.mjs +104 -0
- package/dist/cli-replacement/npm-install-timing.mjs.map +1 -0
- package/dist/cli-replacement/npm-install-timing.test.d.mts +2 -0
- package/dist/cli-replacement/npm-install-timing.test.d.mts.map +1 -0
- package/dist/cli-replacement/npm-install-timing.test.mjs +151 -0
- package/dist/cli-replacement/npm-install-timing.test.mjs.map +1 -0
- package/dist/telemetry/logging.d.ts +7 -0
- package/dist/telemetry/logging.d.ts.map +1 -1
- package/dist/telemetry/logging.js +13 -0
- package/dist/telemetry/logging.js.map +1 -1
- package/dist/telemetry/logging.test.js +40 -0
- package/dist/telemetry/logging.test.js.map +1 -1
- package/package.json +6 -6
- package/src/cli-replacement/dev.mts +128 -10
- package/src/cli-replacement/install-packages.npm-registry.test.mts +235 -0
- package/src/cli-replacement/npm-install-summary.mts +82 -0
- package/src/cli-replacement/npm-install-summary.test.mts +94 -0
- package/src/cli-replacement/npm-install-timing.mts +121 -0
- package/src/cli-replacement/npm-install-timing.test.mts +199 -0
- package/src/telemetry/logging.test.ts +55 -0
- package/src/telemetry/logging.ts +21 -0
- package/tsconfig.tsbuildinfo +1 -1
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import nodeFs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
6
|
+
|
|
7
|
+
import { consumeNpmInstallTimingAttributes } from "./npm-install-timing.mjs";
|
|
8
|
+
|
|
9
|
+
let logsDir: string;
|
|
10
|
+
|
|
11
|
+
beforeEach(async () => {
|
|
12
|
+
logsDir = await nodeFs.mkdtemp(path.join(os.tmpdir(), "npm-timing-"));
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(async () => {
|
|
16
|
+
await nodeFs.rm(logsDir, { force: true, recursive: true });
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/** Write a timing file using npm's own naming (`<log id>-timing.json`). */
|
|
20
|
+
async function writeTimingFile(
|
|
21
|
+
logId: string,
|
|
22
|
+
contents: unknown,
|
|
23
|
+
): Promise<string> {
|
|
24
|
+
const file = path.join(logsDir, `${logId}-timing.json`);
|
|
25
|
+
await nodeFs.writeFile(
|
|
26
|
+
file,
|
|
27
|
+
typeof contents === "string" ? contents : JSON.stringify(contents),
|
|
28
|
+
);
|
|
29
|
+
return file;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function listTimingFiles(): Promise<string[]> {
|
|
33
|
+
const entries = await nodeFs.readdir(logsDir);
|
|
34
|
+
return entries.filter((name) => name.endsWith("-timing.json"));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
describe("consumeNpmInstallTimingAttributes", () => {
|
|
38
|
+
it("converts the whitelisted timers from milliseconds to seconds", async () => {
|
|
39
|
+
await writeTimingFile("2026-08-07T12_00_00_000Z", {
|
|
40
|
+
metadata: { version: "11.16.0" },
|
|
41
|
+
timers: {
|
|
42
|
+
idealTree: 230,
|
|
43
|
+
reify: 24500,
|
|
44
|
+
"reify:build": 90,
|
|
45
|
+
"reify:loadTrees": 400,
|
|
46
|
+
"reify:unpack": 23200,
|
|
47
|
+
},
|
|
48
|
+
unfinishedTimers: {},
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
expect(await consumeNpmInstallTimingAttributes(logsDir)).toEqual({
|
|
52
|
+
"superblocks.npm.install.build_seconds": 0.09,
|
|
53
|
+
"superblocks.npm.install.ideal_tree_seconds": 0.23,
|
|
54
|
+
"superblocks.npm.install.load_trees_seconds": 0.4,
|
|
55
|
+
"superblocks.npm.install.reify_seconds": 24.5,
|
|
56
|
+
"superblocks.npm.install.unpack_seconds": 23.2,
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// npm records one `reifyNode:node_modules/<package>` timer per package it
|
|
61
|
+
// touched. Emitting those would be unbounded attribute cardinality and would
|
|
62
|
+
// put dependency names into telemetry, so only the fixed whitelist is ever
|
|
63
|
+
// emitted.
|
|
64
|
+
it("never emits a per-package reifyNode timer", async () => {
|
|
65
|
+
await writeTimingFile("2026-08-07T12_00_00_000Z", {
|
|
66
|
+
timers: {
|
|
67
|
+
idealTree: 100,
|
|
68
|
+
"reifyNode:node_modules/@dnd-kit/core": 12,
|
|
69
|
+
"reifyNode:node_modules/lodash": 34,
|
|
70
|
+
"reifyNode:node_modules/react": 56,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
const attributes = await consumeNpmInstallTimingAttributes(logsDir);
|
|
75
|
+
|
|
76
|
+
expect(attributes).toEqual({
|
|
77
|
+
"superblocks.npm.install.ideal_tree_seconds": 0.1,
|
|
78
|
+
});
|
|
79
|
+
expect(
|
|
80
|
+
Object.keys(attributes ?? {}).some((key) => key.includes("reifyNode")),
|
|
81
|
+
).toBe(false);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// Which phases npm records depends on what the install did, so a missing
|
|
85
|
+
// timer is normal rather than an error.
|
|
86
|
+
it("omits timers npm did not record", async () => {
|
|
87
|
+
await writeTimingFile("2026-08-07T12_00_00_000Z", {
|
|
88
|
+
timers: { reify: 500 },
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
expect(await consumeNpmInstallTimingAttributes(logsDir)).toEqual({
|
|
92
|
+
"superblocks.npm.install.reify_seconds": 0.5,
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// "npm unpacked nothing" is the signal that separates a metadata-only
|
|
97
|
+
// install from one that wrote tens of thousands of files, so a recorded zero
|
|
98
|
+
// is real data and must not be dropped as if it were missing.
|
|
99
|
+
it("emits a recorded zero rather than dropping it", async () => {
|
|
100
|
+
await writeTimingFile("2026-08-07T12_00_00_000Z", {
|
|
101
|
+
timers: { "reify:unpack": 0 },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
expect(await consumeNpmInstallTimingAttributes(logsDir)).toEqual({
|
|
105
|
+
"superblocks.npm.install.unpack_seconds": 0,
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
// npm's log ids are fixed-width ISO timestamps, so the newest file is the
|
|
110
|
+
// last one alphabetically.
|
|
111
|
+
it("reads the newest file when older installs left files behind", async () => {
|
|
112
|
+
await writeTimingFile("2026-08-07T11_00_00_000Z", {
|
|
113
|
+
timers: { reify: 1000 },
|
|
114
|
+
});
|
|
115
|
+
await writeTimingFile("2026-08-07T12_00_00_000Z", {
|
|
116
|
+
timers: { reify: 2000 },
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(await consumeNpmInstallTimingAttributes(logsDir)).toEqual({
|
|
120
|
+
"superblocks.npm.install.reify_seconds": 2,
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
// npm's `logs-max` pruning only covers its debug logs, so timing files would
|
|
125
|
+
// pile up on the app volume forever if nothing removed them.
|
|
126
|
+
it("removes the timing files it read so they cannot accumulate", async () => {
|
|
127
|
+
await writeTimingFile("2026-08-07T11_00_00_000Z", {
|
|
128
|
+
timers: { reify: 1000 },
|
|
129
|
+
});
|
|
130
|
+
await writeTimingFile("2026-08-07T12_00_00_000Z", {
|
|
131
|
+
timers: { reify: 2000 },
|
|
132
|
+
});
|
|
133
|
+
const debugLog = path.join(logsDir, "2026-08-07T12_00_00_000Z-debug-0.log");
|
|
134
|
+
await nodeFs.writeFile(debugLog, "npm debug output");
|
|
135
|
+
|
|
136
|
+
await consumeNpmInstallTimingAttributes(logsDir);
|
|
137
|
+
|
|
138
|
+
expect(await listTimingFiles()).toEqual([]);
|
|
139
|
+
// npm's debug log is a separate diagnostic that must survive.
|
|
140
|
+
await expect(nodeFs.readFile(debugLog, "utf8")).resolves.toBe(
|
|
141
|
+
"npm debug output",
|
|
142
|
+
);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("returns null when the logs dir has no timing file", async () => {
|
|
146
|
+
await nodeFs.writeFile(
|
|
147
|
+
path.join(logsDir, "2026-08-07T12_00_00_000Z-debug-0.log"),
|
|
148
|
+
"npm debug output",
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
expect(await consumeNpmInstallTimingAttributes(logsDir)).toBeNull();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("returns null when the logs dir does not exist", async () => {
|
|
155
|
+
expect(
|
|
156
|
+
await consumeNpmInstallTimingAttributes(path.join(logsDir, "missing")),
|
|
157
|
+
).toBeNull();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("returns null for a file that is not JSON rather than throwing", async () => {
|
|
161
|
+
await writeTimingFile("2026-08-07T12_00_00_000Z", "not json at all");
|
|
162
|
+
|
|
163
|
+
expect(await consumeNpmInstallTimingAttributes(logsDir)).toBeNull();
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("returns null when the file carries no timers object", async () => {
|
|
167
|
+
await writeTimingFile("2026-08-07T12_00_00_000Z", { metadata: {} });
|
|
168
|
+
|
|
169
|
+
expect(await consumeNpmInstallTimingAttributes(logsDir)).toBeNull();
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("returns null when none of the whitelisted timers are present", async () => {
|
|
173
|
+
await writeTimingFile("2026-08-07T12_00_00_000Z", {
|
|
174
|
+
timers: { "npm:load": 12, "reifyNode:node_modules/lodash": 34 },
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
expect(await consumeNpmInstallTimingAttributes(logsDir)).toBeNull();
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("ignores timer values that are not finite numbers", async () => {
|
|
181
|
+
await writeTimingFile("2026-08-07T12_00_00_000Z", {
|
|
182
|
+
timers: { idealTree: "230", reify: null, "reify:unpack": 23200 },
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
expect(await consumeNpmInstallTimingAttributes(logsDir)).toEqual({
|
|
186
|
+
"superblocks.npm.install.unpack_seconds": 23.2,
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("clamps a negative duration so dashboards cannot be skewed", async () => {
|
|
191
|
+
await writeTimingFile("2026-08-07T12_00_00_000Z", {
|
|
192
|
+
timers: { reify: -500 },
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
expect(await consumeNpmInstallTimingAttributes(logsDir)).toEqual({
|
|
196
|
+
"superblocks.npm.install.reify_seconds": 0,
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
});
|
|
@@ -156,6 +156,61 @@ describe("telemetry logger error sanitization", () => {
|
|
|
156
156
|
});
|
|
157
157
|
});
|
|
158
158
|
|
|
159
|
+
describe("telemetry logger infoStructured", () => {
|
|
160
|
+
beforeEach(() => {
|
|
161
|
+
vi.clearAllMocks();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("emits an INFO record carrying structured attributes (not flattened into the body)", () => {
|
|
165
|
+
const logger = getLogger();
|
|
166
|
+
|
|
167
|
+
logger.infoStructured?.("Package installation completed successfully", {
|
|
168
|
+
"superblocks.npm.install.added": 8,
|
|
169
|
+
"superblocks.npm.install.duration_seconds": 32.5,
|
|
170
|
+
"superblocks.npm.install.outcome": "success",
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
expect(emitMock).toHaveBeenCalledWith(
|
|
174
|
+
expect.objectContaining({
|
|
175
|
+
severityText: "INFO",
|
|
176
|
+
body: "sanitized-message:Package installation completed successfully",
|
|
177
|
+
attributes: {
|
|
178
|
+
"superblocks.npm.install.added": 8,
|
|
179
|
+
"superblocks.npm.install.duration_seconds": 32.5,
|
|
180
|
+
"superblocks.npm.install.outcome": "sanitized-object:success",
|
|
181
|
+
},
|
|
182
|
+
}),
|
|
183
|
+
);
|
|
184
|
+
// Winston gets the body only (structured attributes go to OTel, not also
|
|
185
|
+
// to stdout log shippers as a duplicate).
|
|
186
|
+
expect(winstonLoggerMock.info).toHaveBeenCalledWith(
|
|
187
|
+
"sanitized-message:Package installation completed successfully",
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
it("strips secret-named attribute keys before emitting", () => {
|
|
192
|
+
const logger = getLogger();
|
|
193
|
+
|
|
194
|
+
logger.infoStructured?.("install done", {
|
|
195
|
+
registry: "https://npm.example.com",
|
|
196
|
+
token: "[REDACTED:token]",
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
expect(emitMock).toHaveBeenCalledWith(
|
|
200
|
+
expect.objectContaining({
|
|
201
|
+
attributes: expect.objectContaining({
|
|
202
|
+
registry: "sanitized-object:https://npm.example.com",
|
|
203
|
+
}),
|
|
204
|
+
}),
|
|
205
|
+
);
|
|
206
|
+
expect(emitMock).not.toHaveBeenCalledWith(
|
|
207
|
+
expect.objectContaining({
|
|
208
|
+
attributes: expect.objectContaining({ token: expect.anything() }),
|
|
209
|
+
}),
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
|
|
159
214
|
describe("telemetry logger warnStructured", () => {
|
|
160
215
|
beforeEach(() => {
|
|
161
216
|
vi.clearAllMocks();
|
package/src/telemetry/logging.ts
CHANGED
|
@@ -79,6 +79,13 @@ export interface Logger {
|
|
|
79
79
|
debug: (...messages: unknown[]) => void;
|
|
80
80
|
info: (...messages: unknown[]) => void;
|
|
81
81
|
warn: (...messages: unknown[]) => void;
|
|
82
|
+
/**
|
|
83
|
+
* Emit an INFO-level log whose structured attributes are exported as OTel
|
|
84
|
+
* log attributes (Datadog facets) instead of being flattened into the
|
|
85
|
+
* message body. Plain `info()` joins every argument into one body string, so
|
|
86
|
+
* attributes passed to it are never queryable.
|
|
87
|
+
*/
|
|
88
|
+
infoStructured?: (message: string, attributes: LogAttributes) => void;
|
|
82
89
|
/**
|
|
83
90
|
* Emit a WARN-level log whose structured attributes are exported as OTel log
|
|
84
91
|
* attributes (Datadog facets) instead of being flattened into the message
|
|
@@ -120,6 +127,18 @@ const logger: Logger = Object.freeze({
|
|
|
120
127
|
});
|
|
121
128
|
winstonLogger.warn(body);
|
|
122
129
|
},
|
|
130
|
+
infoStructured: (message: string, attributes: LogAttributes) => {
|
|
131
|
+
const body = sanitizeLogMessage(message);
|
|
132
|
+
// Same sanitize-then-emit contract as warnStructured below.
|
|
133
|
+
const safeAttributes = sanitizeLogObject(attributes);
|
|
134
|
+
getTracedLogger().emit({
|
|
135
|
+
severityNumber: SeverityNumber.INFO,
|
|
136
|
+
severityText: "INFO",
|
|
137
|
+
body,
|
|
138
|
+
attributes: safeAttributes,
|
|
139
|
+
});
|
|
140
|
+
winstonLogger.info(body);
|
|
141
|
+
},
|
|
123
142
|
warnStructured: (message: string, attributes: LogAttributes) => {
|
|
124
143
|
const body = sanitizeLogMessage(message);
|
|
125
144
|
// sanitizeLogObject strips secret-named keys and redacts secret values,
|
|
@@ -174,6 +193,8 @@ export function getLogger(
|
|
|
174
193
|
// above, it forwards raw args to the supplied function and does NOT
|
|
175
194
|
// sanitize or emit OTel attributes. Production callers use the default
|
|
176
195
|
// logger (and the vite-plugin wrapLogger) which sanitize before emitting.
|
|
196
|
+
infoStructured: (message: string, attributes: LogAttributes) =>
|
|
197
|
+
loggerOverride(message, attributes),
|
|
177
198
|
warnStructured: (message: string, attributes: LogAttributes) =>
|
|
178
199
|
loggerOverride(message, attributes),
|
|
179
200
|
error: loggerOverride as (message: string, meta?: ErrorMeta) => void,
|