midline-agent 0.5.0 → 0.6.0

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/CHANGELOG.md CHANGED
@@ -2,6 +2,11 @@
2
2
 
3
3
  All notable changes to `midline-agent`, compiled from git history. Dates reflect commit dates, newest first.
4
4
 
5
+ ## 2026-09-20
6
+ - feat: `midline-agent sourcemaps upload <dir> --release <name>` uploads a build's source maps to Midline
7
+ so stack traces show original code. Authenticates with a project server API key; browser keys are refused
8
+ (0.6.0)
9
+
5
10
  ## 2026-09-17
6
11
  - feat: `midline-agent/browser` captures pageviews — on load and on every SPA route change — as the
7
12
  basis for visitor counting and funnel views in the Midline dashboard (0.5.0)
@@ -1,2 +1,2 @@
1
1
  /** Kept in step with package.json by test/browser.test.js; a browser bundle can't read package.json. */
2
- export declare const BROWSER_SDK_VERSION = "0.5.0";
2
+ export declare const BROWSER_SDK_VERSION = "0.6.0";
@@ -2,4 +2,4 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.BROWSER_SDK_VERSION = void 0;
4
4
  /** Kept in step with package.json by test/browser.test.js; a browser bundle can't read package.json. */
5
- exports.BROWSER_SDK_VERSION = "0.5.0";
5
+ exports.BROWSER_SDK_VERSION = "0.6.0";
package/dist/cli.js CHANGED
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const agent_1 = require("./agent");
5
5
  const config_1 = require("./config");
6
6
  const proxy_1 = require("./proxy");
7
+ const sourcemaps_1 = require("./sourcemaps");
7
8
  const USAGE = `Usage: midline-agent proxy [--target <url>] [--port <n>] [--host <addr>]
8
9
 
9
10
  Forwards traffic to a destination API and reports every exchange to Midline.
@@ -17,6 +18,18 @@ Midline server:
17
18
  MIDLINE_ENDPOINT default https://api.usemidline.com
18
19
  MIDLINE_CUSTOM_CA extra CA for the Midline server only (PEM or file path)
19
20
  TARGET_API_CA extra CA for the destination only (PEM or file path)
21
+
22
+ Usage: midline-agent sourcemaps upload <dir> --release <name> [--url-prefix <prefix>] [--dry-run]
23
+
24
+ Uploads every .map file under <dir> so stack traces from that release show your
25
+ original code. Run it in CI after your build, with the same release name your SDK
26
+ reports.
27
+
28
+ --release Release the build belongs to (required)
29
+ --url-prefix What the built files are served under (default "~", any origin)
30
+ --endpoint Midline server (MIDLINE_ENDPOINT)
31
+ --api-key A project SERVER key, pk_ keys are refused (MIDLINE_API_KEY)
32
+ --dry-run List what would be uploaded
20
33
  `;
21
34
  function flag(args, name) {
22
35
  const index = args.indexOf(`--${name}`);
@@ -25,8 +38,30 @@ function flag(args, name) {
25
38
  const inline = args.find((arg) => arg.startsWith(`--${name}=`));
26
39
  return inline?.slice(name.length + 3);
27
40
  }
41
+ async function sourcemaps(args) {
42
+ const dir = args[2] && !args[2].startsWith("--") ? args[2] : undefined;
43
+ if (args[1] !== "upload" || !dir || args.includes("--help") || args.includes("-h")) {
44
+ process.stdout.write(USAGE);
45
+ process.exitCode = args.includes("--help") || args.includes("-h") ? 0 : 1;
46
+ return;
47
+ }
48
+ const result = await (0, sourcemaps_1.uploadSourceMaps)({
49
+ dir,
50
+ release: flag(args, "release") ?? "",
51
+ urlPrefix: flag(args, "url-prefix"),
52
+ endpoint: flag(args, "endpoint"),
53
+ apiKey: flag(args, "api-key"),
54
+ dryRun: args.includes("--dry-run"),
55
+ log: (line) => process.stdout.write(`midline sourcemaps: ${line}\n`),
56
+ });
57
+ process.stdout.write(`midline sourcemaps: ${result.uploaded.length} ${args.includes("--dry-run") ? "listed" : "uploaded"}, ${result.failed.length} failed\n`);
58
+ if (result.failed.length)
59
+ process.exitCode = 1;
60
+ }
28
61
  async function main() {
29
62
  const args = process.argv.slice(2);
63
+ if (args[0] === "sourcemaps")
64
+ return sourcemaps(args);
30
65
  if (args[0] !== "proxy" || args.includes("--help") || args.includes("-h")) {
31
66
  process.stdout.write(USAGE);
32
67
  process.exitCode = args[0] === "proxy" || args.length === 0 ? 0 : 1;
@@ -58,6 +93,6 @@ async function main() {
58
93
  process.on("SIGTERM", () => stop("SIGTERM"));
59
94
  }
60
95
  main().catch((err) => {
61
- process.stderr.write(`midline proxy: ${err instanceof config_1.ConfigError ? err.message : err?.stack ?? err}\n`);
96
+ process.stderr.write(`midline ${process.argv[2] === "sourcemaps" ? "sourcemaps" : "proxy"}: ${err instanceof config_1.ConfigError ? err.message : err?.stack ?? err}\n`);
62
97
  process.exit(1);
63
98
  });
@@ -1,2 +1,2 @@
1
1
  /** Kept in step with package.json by test/browser.test.js; a browser bundle can't read package.json. */
2
- export const BROWSER_SDK_VERSION = "0.5.0";
2
+ export const BROWSER_SDK_VERSION = "0.6.0";
@@ -0,0 +1,26 @@
1
+ export interface UploadOptions {
2
+ /** Directory containing the build output (searched recursively for `*.map`). */
3
+ dir: string;
4
+ /** The release the build belongs to: the same string passed as `release` to the SDK. */
5
+ release: string;
6
+ /** What the built files are served under. Default `~`, meaning "any origin, this path". */
7
+ urlPrefix?: string;
8
+ /** Midline server. Env fallback: `MIDLINE_ENDPOINT`. */
9
+ endpoint?: string;
10
+ /** A project's **server** API key. Env fallback: `MIDLINE_API_KEY`. Browser keys are refused by the server. */
11
+ apiKey?: string;
12
+ dryRun?: boolean;
13
+ log?: (line: string) => void;
14
+ }
15
+ export interface UploadResult {
16
+ uploaded: string[];
17
+ failed: Array<{
18
+ name: string;
19
+ error: string;
20
+ }>;
21
+ }
22
+ /** Every `*.map` under `dir`, without descending into node_modules. */
23
+ export declare function findMaps(dir: string, depth?: number): Promise<string[]>;
24
+ /** `dist/assets/app-3f2a.js.map` -> `~/assets/app-3f2a.js`: the built file the map describes, as the server will match it. */
25
+ export declare function uploadName(dir: string, mapFile: string, urlPrefix?: string): string;
26
+ export declare function uploadSourceMaps(options: UploadOptions): Promise<UploadResult>;
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.findMaps = findMaps;
37
+ exports.uploadName = uploadName;
38
+ exports.uploadSourceMaps = uploadSourceMaps;
39
+ const promises_1 = require("fs/promises");
40
+ const path = __importStar(require("path"));
41
+ const config_1 = require("./config");
42
+ const MAX_DEPTH = 20;
43
+ const CONCURRENCY = 4;
44
+ /** Every `*.map` under `dir`, without descending into node_modules. */
45
+ async function findMaps(dir, depth = 0) {
46
+ if (depth > MAX_DEPTH)
47
+ return [];
48
+ const entries = await (0, promises_1.readdir)(dir, { withFileTypes: true });
49
+ const found = [];
50
+ for (const entry of entries) {
51
+ const full = path.join(dir, entry.name);
52
+ if (entry.isDirectory()) {
53
+ if (entry.name !== "node_modules")
54
+ found.push(...(await findMaps(full, depth + 1)));
55
+ }
56
+ else if (entry.isFile() && entry.name.endsWith(".map")) {
57
+ found.push(full);
58
+ }
59
+ }
60
+ return found.sort();
61
+ }
62
+ /** `dist/assets/app-3f2a.js.map` -> `~/assets/app-3f2a.js`: the built file the map describes, as the server will match it. */
63
+ function uploadName(dir, mapFile, urlPrefix = "~") {
64
+ const relative = path.relative(dir, mapFile).split(path.sep).join("/").replace(/\.map$/, "");
65
+ return `${urlPrefix.replace(/\/+$/, "")}/${relative}`;
66
+ }
67
+ async function uploadSourceMaps(options) {
68
+ const log = options.log ?? (() => undefined);
69
+ if (!options.release?.trim())
70
+ throw new config_1.ConfigError("--release is required");
71
+ const apiKey = options.apiKey ?? (0, config_1.env)("MIDLINE_API_KEY");
72
+ if (!apiKey && !options.dryRun)
73
+ throw new config_1.ConfigError("Set MIDLINE_API_KEY (a project server key) or pass --api-key");
74
+ const origin = (0, config_1.resolveEndpointOrigin)(options.endpoint ?? (0, config_1.env)("MIDLINE_ENDPOINT") ?? config_1.DEFAULT_ENDPOINT);
75
+ const maps = await findMaps(options.dir).catch((error) => {
76
+ if (error.code === "ENOENT" || error.code === "ENOTDIR")
77
+ throw new config_1.ConfigError(`${options.dir} is not a directory`);
78
+ throw error;
79
+ });
80
+ if (!maps.length)
81
+ throw new config_1.ConfigError(`No .map files found under ${options.dir}. Enable source maps in your build.`);
82
+ const result = { uploaded: [], failed: [] };
83
+ const queue = [...maps];
84
+ const worker = async () => {
85
+ for (let file = queue.shift(); file; file = queue.shift()) {
86
+ const name = uploadName(options.dir, file, options.urlPrefix);
87
+ if (options.dryRun) {
88
+ log(`would upload ${name}`);
89
+ result.uploaded.push(name);
90
+ continue;
91
+ }
92
+ try {
93
+ const body = new FormData();
94
+ body.append("release", options.release);
95
+ body.append("name", name);
96
+ body.append("file", new Blob([await (0, promises_1.readFile)(file)]), path.basename(file));
97
+ const response = await fetch(new URL("/api/sourcemaps/upload", origin), {
98
+ method: "POST",
99
+ headers: { "x-midline-key": apiKey },
100
+ body,
101
+ });
102
+ if (!response.ok) {
103
+ const detail = await response.json().catch(() => undefined);
104
+ throw new Error(`${response.status} ${detail?.message ?? response.statusText}`);
105
+ }
106
+ log(`uploaded ${name}`);
107
+ result.uploaded.push(name);
108
+ }
109
+ catch (error) {
110
+ const message = error instanceof Error ? error.message : String(error);
111
+ log(`failed ${name}: ${message}`);
112
+ result.failed.push({ name, error: message });
113
+ }
114
+ }
115
+ };
116
+ await Promise.all(Array.from({ length: Math.min(CONCURRENCY, maps.length) }, worker));
117
+ return result;
118
+ }
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "midline-agent",
3
- "version": "0.5.0",
4
- "description": "Midline request, error and security monitoring for Node, and error, network, Web Vitals and pageview/visitor monitoring for browsers",
3
+ "version": "0.6.0",
4
+ "description": "Midline \u2014 request, error and security monitoring for Node, and error, network, Web Vitals and pageview/visitor monitoring for browsers",
5
+ "homepage": "https://usemidline.com",
5
6
  "main": "dist/index.js",
6
7
  "types": "dist/index.d.ts",
7
8
  "exports": {
@@ -38,16 +39,32 @@
38
39
  },
39
40
  "keywords": [
40
41
  "midline",
42
+ "api-monitoring",
43
+ "api-observability",
44
+ "apm",
45
+ "browser",
46
+ "console-capture",
47
+ "error-tracking",
48
+ "errors",
41
49
  "express",
50
+ "express-middleware",
51
+ "frontend",
52
+ "http-logging",
53
+ "issue-tracking",
54
+ "log-redaction",
42
55
  "monitoring",
56
+ "nestjs",
57
+ "nodejs",
58
+ "observability",
59
+ "proxy",
60
+ "real-user-monitoring",
61
+ "request",
62
+ "request-logging",
63
+ "reverse-proxy",
64
+ "rum",
43
65
  "sdk",
44
66
  "security",
45
- "request",
46
- "errors",
47
- "proxy",
48
- "browser",
49
- "web-vitals",
50
- "frontend"
67
+ "web-vitals"
51
68
  ],
52
69
  "author": "Your Name",
53
70
  "license": "MIT",
@@ -1,2 +1,2 @@
1
1
  /** Kept in step with package.json by test/browser.test.js; a browser bundle can't read package.json. */
2
- export const BROWSER_SDK_VERSION = "0.5.0";
2
+ export const BROWSER_SDK_VERSION = "0.6.0";
package/src/cli.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  import { MidlineAgent } from "./agent";
3
3
  import { ConfigError } from "./config";
4
4
  import { proxyAddress, resolveTarget, startMidlineProxy } from "./proxy";
5
+ import { uploadSourceMaps } from "./sourcemaps";
5
6
 
6
7
  const USAGE = `Usage: midline-agent proxy [--target <url>] [--port <n>] [--host <addr>]
7
8
 
@@ -16,6 +17,18 @@ Midline server:
16
17
  MIDLINE_ENDPOINT default https://api.usemidline.com
17
18
  MIDLINE_CUSTOM_CA extra CA for the Midline server only (PEM or file path)
18
19
  TARGET_API_CA extra CA for the destination only (PEM or file path)
20
+
21
+ Usage: midline-agent sourcemaps upload <dir> --release <name> [--url-prefix <prefix>] [--dry-run]
22
+
23
+ Uploads every .map file under <dir> so stack traces from that release show your
24
+ original code. Run it in CI after your build, with the same release name your SDK
25
+ reports.
26
+
27
+ --release Release the build belongs to (required)
28
+ --url-prefix What the built files are served under (default "~", any origin)
29
+ --endpoint Midline server (MIDLINE_ENDPOINT)
30
+ --api-key A project SERVER key, pk_ keys are refused (MIDLINE_API_KEY)
31
+ --dry-run List what would be uploaded
19
32
  `;
20
33
 
21
34
  function flag(args: string[], name: string): string | undefined {
@@ -25,8 +38,29 @@ function flag(args: string[], name: string): string | undefined {
25
38
  return inline?.slice(name.length + 3);
26
39
  }
27
40
 
41
+ async function sourcemaps(args: string[]): Promise<void> {
42
+ const dir = args[2] && !args[2].startsWith("--") ? args[2] : undefined;
43
+ if (args[1] !== "upload" || !dir || args.includes("--help") || args.includes("-h")) {
44
+ process.stdout.write(USAGE);
45
+ process.exitCode = args.includes("--help") || args.includes("-h") ? 0 : 1;
46
+ return;
47
+ }
48
+ const result = await uploadSourceMaps({
49
+ dir,
50
+ release: flag(args, "release") ?? "",
51
+ urlPrefix: flag(args, "url-prefix"),
52
+ endpoint: flag(args, "endpoint"),
53
+ apiKey: flag(args, "api-key"),
54
+ dryRun: args.includes("--dry-run"),
55
+ log: (line) => process.stdout.write(`midline sourcemaps: ${line}\n`),
56
+ });
57
+ process.stdout.write(`midline sourcemaps: ${result.uploaded.length} ${args.includes("--dry-run") ? "listed" : "uploaded"}, ${result.failed.length} failed\n`);
58
+ if (result.failed.length) process.exitCode = 1;
59
+ }
60
+
28
61
  async function main(): Promise<void> {
29
62
  const args = process.argv.slice(2);
63
+ if (args[0] === "sourcemaps") return sourcemaps(args);
30
64
  if (args[0] !== "proxy" || args.includes("--help") || args.includes("-h")) {
31
65
  process.stdout.write(USAGE);
32
66
  process.exitCode = args[0] === "proxy" || args.length === 0 ? 0 : 1;
@@ -64,6 +98,6 @@ async function main(): Promise<void> {
64
98
  }
65
99
 
66
100
  main().catch((err) => {
67
- process.stderr.write(`midline proxy: ${err instanceof ConfigError ? err.message : err?.stack ?? err}\n`);
101
+ process.stderr.write(`midline ${process.argv[2] === "sourcemaps" ? "sourcemaps" : "proxy"}: ${err instanceof ConfigError ? err.message : err?.stack ?? err}\n`);
68
102
  process.exit(1);
69
103
  });
@@ -0,0 +1,99 @@
1
+ import { readdir, readFile } from "fs/promises";
2
+ import * as path from "path";
3
+ import { DEFAULT_ENDPOINT, ConfigError, env, resolveEndpointOrigin } from "./config";
4
+
5
+ export interface UploadOptions {
6
+ /** Directory containing the build output (searched recursively for `*.map`). */
7
+ dir: string;
8
+ /** The release the build belongs to: the same string passed as `release` to the SDK. */
9
+ release: string;
10
+ /** What the built files are served under. Default `~`, meaning "any origin, this path". */
11
+ urlPrefix?: string;
12
+ /** Midline server. Env fallback: `MIDLINE_ENDPOINT`. */
13
+ endpoint?: string;
14
+ /** A project's **server** API key. Env fallback: `MIDLINE_API_KEY`. Browser keys are refused by the server. */
15
+ apiKey?: string;
16
+ dryRun?: boolean;
17
+ log?: (line: string) => void;
18
+ }
19
+
20
+ export interface UploadResult {
21
+ uploaded: string[];
22
+ failed: Array<{ name: string; error: string }>;
23
+ }
24
+
25
+ const MAX_DEPTH = 20;
26
+ const CONCURRENCY = 4;
27
+
28
+ /** Every `*.map` under `dir`, without descending into node_modules. */
29
+ export async function findMaps(dir: string, depth = 0): Promise<string[]> {
30
+ if (depth > MAX_DEPTH) return [];
31
+ const entries = await readdir(dir, { withFileTypes: true });
32
+ const found: string[] = [];
33
+ for (const entry of entries) {
34
+ const full = path.join(dir, entry.name);
35
+ if (entry.isDirectory()) {
36
+ if (entry.name !== "node_modules") found.push(...(await findMaps(full, depth + 1)));
37
+ } else if (entry.isFile() && entry.name.endsWith(".map")) {
38
+ found.push(full);
39
+ }
40
+ }
41
+ return found.sort();
42
+ }
43
+
44
+ /** `dist/assets/app-3f2a.js.map` -> `~/assets/app-3f2a.js`: the built file the map describes, as the server will match it. */
45
+ export function uploadName(dir: string, mapFile: string, urlPrefix = "~"): string {
46
+ const relative = path.relative(dir, mapFile).split(path.sep).join("/").replace(/\.map$/, "");
47
+ return `${urlPrefix.replace(/\/+$/, "")}/${relative}`;
48
+ }
49
+
50
+ export async function uploadSourceMaps(options: UploadOptions): Promise<UploadResult> {
51
+ const log = options.log ?? (() => undefined);
52
+ if (!options.release?.trim()) throw new ConfigError("--release is required");
53
+ const apiKey = options.apiKey ?? env("MIDLINE_API_KEY");
54
+ if (!apiKey && !options.dryRun) throw new ConfigError("Set MIDLINE_API_KEY (a project server key) or pass --api-key");
55
+ const origin = resolveEndpointOrigin(options.endpoint ?? env("MIDLINE_ENDPOINT") ?? DEFAULT_ENDPOINT);
56
+
57
+ const maps = await findMaps(options.dir).catch((error: NodeJS.ErrnoException) => {
58
+ if (error.code === "ENOENT" || error.code === "ENOTDIR") throw new ConfigError(`${options.dir} is not a directory`);
59
+ throw error;
60
+ });
61
+ if (!maps.length) throw new ConfigError(`No .map files found under ${options.dir}. Enable source maps in your build.`);
62
+
63
+ const result: UploadResult = { uploaded: [], failed: [] };
64
+ const queue = [...maps];
65
+
66
+ const worker = async () => {
67
+ for (let file = queue.shift(); file; file = queue.shift()) {
68
+ const name = uploadName(options.dir, file, options.urlPrefix);
69
+ if (options.dryRun) {
70
+ log(`would upload ${name}`);
71
+ result.uploaded.push(name);
72
+ continue;
73
+ }
74
+ try {
75
+ const body = new FormData();
76
+ body.append("release", options.release);
77
+ body.append("name", name);
78
+ body.append("file", new Blob([await readFile(file)]), path.basename(file));
79
+ const response = await fetch(new URL("/api/sourcemaps/upload", origin), {
80
+ method: "POST",
81
+ headers: { "x-midline-key": apiKey as string },
82
+ body,
83
+ });
84
+ if (!response.ok) {
85
+ const detail = await response.json().catch(() => undefined);
86
+ throw new Error(`${response.status} ${detail?.message ?? response.statusText}`);
87
+ }
88
+ log(`uploaded ${name}`);
89
+ result.uploaded.push(name);
90
+ } catch (error) {
91
+ const message = error instanceof Error ? error.message : String(error);
92
+ log(`failed ${name}: ${message}`);
93
+ result.failed.push({ name, error: message });
94
+ }
95
+ }
96
+ };
97
+ await Promise.all(Array.from({ length: Math.min(CONCURRENCY, maps.length) }, worker));
98
+ return result;
99
+ }
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+
3
+ const test = require("node:test");
4
+ const assert = require("node:assert/strict");
5
+ const fs = require("fs");
6
+ const http = require("http");
7
+ const os = require("os");
8
+ const path = require("path");
9
+ const { findMaps, uploadName, uploadSourceMaps } = require("../dist/sourcemaps");
10
+
11
+ function makeBuild() {
12
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "midline-maps-"));
13
+ fs.mkdirSync(path.join(dir, "assets"));
14
+ fs.mkdirSync(path.join(dir, "node_modules", "dep"), { recursive: true });
15
+ fs.writeFileSync(path.join(dir, "assets", "app-1.js.map"), '{"version":3,"mappings":"AAAA"}');
16
+ fs.writeFileSync(path.join(dir, "main.js.map"), '{"version":3,"mappings":"AAAA"}');
17
+ fs.writeFileSync(path.join(dir, "assets", "app-1.js"), "//");
18
+ fs.writeFileSync(path.join(dir, "node_modules", "dep", "x.js.map"), "{}");
19
+ return dir;
20
+ }
21
+
22
+ test("finds .map files, skipping node_modules, and names them by the built file they describe", async () => {
23
+ const dir = makeBuild();
24
+ const maps = await findMaps(dir);
25
+ assert.deepEqual(maps.map((m) => path.relative(dir, m)), [path.join("assets", "app-1.js.map"), "main.js.map"]);
26
+ assert.equal(uploadName(dir, maps[0]), "~/assets/app-1.js");
27
+ assert.equal(uploadName(dir, maps[1], "https://app.example.com/static/"), "https://app.example.com/static/main.js");
28
+ fs.rmSync(dir, { recursive: true, force: true });
29
+ });
30
+
31
+ test("uploads each map with the key, release and name, and reports failures", async () => {
32
+ const dir = makeBuild();
33
+ const seen = [];
34
+ const server = http.createServer((req, res) => {
35
+ let body = "";
36
+ req.on("data", (c) => (body += c));
37
+ req.on("end", () => {
38
+ seen.push({ url: req.url, key: req.headers["x-midline-key"], body });
39
+ const failing = body.includes("main.js");
40
+ res.writeHead(failing ? 400 : 201, { "content-type": "application/json" });
41
+ res.end(JSON.stringify(failing ? { message: "That is not a version 3 source map" } : { ok: true }));
42
+ });
43
+ });
44
+ await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
45
+ const endpoint = `http://127.0.0.1:${server.address().port}`;
46
+
47
+ const lines = [];
48
+ const result = await uploadSourceMaps({ dir, release: "1.2.3", endpoint, apiKey: "ak_test", log: (l) => lines.push(l) });
49
+ server.close();
50
+
51
+ assert.equal(seen.length, 2);
52
+ assert.ok(seen.every((s) => s.url === "/api/sourcemaps/upload" && s.key === "ak_test"));
53
+ assert.ok(seen.some((s) => s.body.includes("~/assets/app-1.js") && s.body.includes("1.2.3")));
54
+ assert.deepEqual(result.uploaded, ["~/assets/app-1.js"]);
55
+ assert.equal(result.failed.length, 1);
56
+ assert.match(result.failed[0].error, /400 That is not a version 3 source map/);
57
+ fs.rmSync(dir, { recursive: true, force: true });
58
+ });
59
+
60
+ test("dry run touches nothing, and misconfiguration fails clearly", async () => {
61
+ const dir = makeBuild();
62
+ const dry = await uploadSourceMaps({ dir, release: "1", dryRun: true });
63
+ assert.equal(dry.uploaded.length, 2);
64
+ await assert.rejects(uploadSourceMaps({ dir, release: "" }), /--release is required/);
65
+ await assert.rejects(uploadSourceMaps({ dir, release: "1", apiKey: "k", endpoint: "http://example.com" }), /https/);
66
+ await assert.rejects(uploadSourceMaps({ dir: os.tmpdir() + "/does-not-exist-midline", release: "1", apiKey: "k", endpoint: "https://api.example.com" }));
67
+ const empty = fs.mkdtempSync(path.join(os.tmpdir(), "midline-empty-"));
68
+ await assert.rejects(uploadSourceMaps({ dir: empty, release: "1", apiKey: "k", endpoint: "https://api.example.com" }), /No \.map files/);
69
+ fs.rmSync(dir, { recursive: true, force: true });
70
+ fs.rmSync(empty, { recursive: true, force: true });
71
+ });