@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.
Files changed (38) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/cli-replacement/dev.d.mts.map +1 -1
  3. package/dist/cli-replacement/dev.mjs +111 -8
  4. package/dist/cli-replacement/dev.mjs.map +1 -1
  5. package/dist/cli-replacement/install-packages.npm-registry.test.mjs +153 -0
  6. package/dist/cli-replacement/install-packages.npm-registry.test.mjs.map +1 -1
  7. package/dist/cli-replacement/npm-install-summary.d.mts +30 -0
  8. package/dist/cli-replacement/npm-install-summary.d.mts.map +1 -0
  9. package/dist/cli-replacement/npm-install-summary.mjs +67 -0
  10. package/dist/cli-replacement/npm-install-summary.mjs.map +1 -0
  11. package/dist/cli-replacement/npm-install-summary.test.d.mts +2 -0
  12. package/dist/cli-replacement/npm-install-summary.test.d.mts.map +1 -0
  13. package/dist/cli-replacement/npm-install-summary.test.mjs +70 -0
  14. package/dist/cli-replacement/npm-install-summary.test.mjs.map +1 -0
  15. package/dist/cli-replacement/npm-install-timing.d.mts +16 -0
  16. package/dist/cli-replacement/npm-install-timing.d.mts.map +1 -0
  17. package/dist/cli-replacement/npm-install-timing.mjs +104 -0
  18. package/dist/cli-replacement/npm-install-timing.mjs.map +1 -0
  19. package/dist/cli-replacement/npm-install-timing.test.d.mts +2 -0
  20. package/dist/cli-replacement/npm-install-timing.test.d.mts.map +1 -0
  21. package/dist/cli-replacement/npm-install-timing.test.mjs +151 -0
  22. package/dist/cli-replacement/npm-install-timing.test.mjs.map +1 -0
  23. package/dist/telemetry/logging.d.ts +7 -0
  24. package/dist/telemetry/logging.d.ts.map +1 -1
  25. package/dist/telemetry/logging.js +13 -0
  26. package/dist/telemetry/logging.js.map +1 -1
  27. package/dist/telemetry/logging.test.js +40 -0
  28. package/dist/telemetry/logging.test.js.map +1 -1
  29. package/package.json +6 -6
  30. package/src/cli-replacement/dev.mts +128 -10
  31. package/src/cli-replacement/install-packages.npm-registry.test.mts +235 -0
  32. package/src/cli-replacement/npm-install-summary.mts +82 -0
  33. package/src/cli-replacement/npm-install-summary.test.mts +94 -0
  34. package/src/cli-replacement/npm-install-timing.mts +121 -0
  35. package/src/cli-replacement/npm-install-timing.test.mts +199 -0
  36. package/src/telemetry/logging.test.ts +55 -0
  37. package/src/telemetry/logging.ts +21 -0
  38. 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();
@@ -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,