Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Features

- (debug-files) Add `debug-files prepare` to split WASM DWARF companions and upload them
- (build) Add dSYM support to IPA uploads ([#3393](https://github.com/getsentry/sentry-cli/pull/3393))

### Fixes
Expand Down
90 changes: 90 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ tokio = { version = "1.47", features = ["rt"] }
url = "2.3.1"
uuid = { version = "1.3.0", features = ["v4", "serde"] }
walkdir = "2.3.2"
wasmbin = { version = "0.8.1", features = ["exception-handling"] }
which = "4.4.0"
whoami = "1.5.2"
zip = "2.4.2"
Expand Down
110 changes: 110 additions & 0 deletions lib/debugFiles/__tests__/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
describe('SentryCli debug files', () => {
afterEach(() => {
jest.resetModules();
});

describe('with mock', () => {
let cli;
let mockExecute;
beforeAll(() => {
mockExecute = jest.fn(async () => {});
jest.doMock('../../helper', () => ({
...jest.requireActual('../../helper'),
execute: mockExecute,
}));
});
beforeEach(() => {
mockExecute.mockClear();
// eslint-disable-next-line global-require
const { SentryCli: SentryCliLocal } = require('../..');
cli = new SentryCliLocal();
});

describe('prepare', () => {
test('with single path', async () => {
await cli.debugFiles.prepare({ path: './dist' });
expect(mockExecute).toHaveBeenCalledWith(
['debug-files', 'prepare', './dist', '--ignore', 'node_modules'],
true,
false,
undefined,
{ silent: false }
);
});

test('with paths array', async () => {
await cli.debugFiles.prepare({ paths: ['./dist', './pkg'] });
expect(mockExecute).toHaveBeenCalledWith(
['debug-files', 'prepare', './dist', './pkg', '--ignore', 'node_modules'],
true,
false,
undefined,
{ silent: false }
);
});

test('with upload false adds --no-upload', async () => {
await cli.debugFiles.prepare({ path: './dist', upload: false });
expect(mockExecute).toHaveBeenCalledWith(
['debug-files', 'prepare', './dist', '--ignore', 'node_modules', '--no-upload'],
true,
false,
undefined,
{ silent: false }
);
});

test('with includeSources and wait', async () => {
await cli.debugFiles.prepare({
path: './dist',
includeSources: true,
wait: true,
});
expect(mockExecute).toHaveBeenCalledWith(
[
'debug-files',
'prepare',
'./dist',
'--ignore',
'node_modules',
'--include-sources',
'--wait',
],
true,
false,
undefined,
{ silent: false }
);
});

test('with dryRun and requireDwarf', async () => {
await cli.debugFiles.prepare({
path: './app.wasm',
dryRun: true,
requireDwarf: true,
});
expect(mockExecute).toHaveBeenCalledWith(
[
'debug-files',
'prepare',
'./app.wasm',
'--ignore',
'node_modules',
'--dry-run',
'--require-dwarf',
],
true,
false,
undefined,
{ silent: false }
);
});

test('throws when path is missing', async () => {
await expect(cli.debugFiles.prepare({})).rejects.toThrow(
'`options.path` or `options.paths` must contain at least one path.'
);
});
});
});
});
87 changes: 87 additions & 0 deletions lib/debugFiles/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
'use strict';

import { SentryCliDebugFilesPrepareOptions, SentryCliOptions } from '../types';
import { PREPARE_OPTIONS } from './options/prepare';
import * as helper from '../helper';

/**
* Default arguments for the `--ignore` option.
*/
const DEFAULT_IGNORE: string[] = ['node_modules'];

/**
* Manages debug information file operations on Sentry.
*/
export class DebugFiles {
constructor(
public options: SentryCliOptions = {},
private configFile: string | null
) {}

/**
* Split WebAssembly DWARF into `*.debug.wasm` companions and upload them.
*
* For every `.wasm` with DWARF, injects a `build_id` if missing, writes a
* debug companion that keeps the Code section, and strips DWARF from the
* deployable module. Name/symtab-only modules are skipped with a warning.
*
* @example
* await cli.debugFiles.prepare({
* path: './dist',
* upload: true,
* includeSources: true,
* wait: true,
* });
*
* @param options Options to configure prepare and upload.
* @returns A promise that resolves when prepare (and optional upload) has completed.
*/
async prepare(options: SentryCliDebugFilesPrepareOptions): Promise<string> {
const paths = normalizePreparePaths(options);
if (paths.length === 0) {
throw new Error('`options.path` or `options.paths` must contain at least one path.');
}

const newOptions: Record<string, unknown> = { ...options };
if (!newOptions.ignoreFile && !newOptions.ignore) {
newOptions.ignore = DEFAULT_IGNORE;
}

const args = helper.prepareCommand(
['debug-files', 'prepare', ...paths],
PREPARE_OPTIONS,
newOptions
);

return this.execute(args, true);
}

/**
* See {helper.execute} docs.
*/
async execute(args: string[], live: boolean): Promise<string> {
return helper.execute(args, live, this.options.silent, this.configFile, this.options);
}
}

function normalizePreparePaths(options: SentryCliDebugFilesPrepareOptions | undefined): string[] {
if (!options) {
return [];
}

const fromPath = options.path;
const fromPaths = options.paths;

const collected: string[] = [];
if (typeof fromPath === 'string') {
collected.push(fromPath);
} else if (Array.isArray(fromPath)) {
collected.push(...fromPath);
}

if (Array.isArray(fromPaths)) {
collected.push(...fromPaths);
}

return collected;
}
47 changes: 47 additions & 0 deletions lib/debugFiles/options/prepare.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { OptionsSchema } from '../../helper';

/**
* Schema for the `debug-files prepare` command.
*/
export const PREPARE_OPTIONS = {
ignore: {
param: '--ignore',
type: 'array',
},
ignoreFile: {
param: '--ignore-file',
type: 'string',
},
outDir: {
param: '--out-dir',
type: 'string',
},
stripNames: {
param: '--strip-names',
type: 'boolean',
},
upload: {
invertedParam: '--no-upload',
type: 'boolean',
},
dryRun: {
param: '--dry-run',
type: 'boolean',
},
requireDwarf: {
param: '--require-dwarf',
type: 'boolean',
},
includeSources: {
param: '--include-sources',
type: 'boolean',
},
wait: {
param: '--wait',
type: 'boolean',
},
waitFor: {
param: '--wait-for',
type: 'number',
},
} satisfies OptionsSchema;
Loading
Loading