Skip to content
Merged
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
49 changes: 17 additions & 32 deletions src/commands/crashlytics-sourcemap-upload.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ const PROJECT_NUMBER = "12345";
const BUCKET_NAME = "test-bucket";
const DIR_PATH = "src/test/fixtures/mapping-files";
const FILE_PATH = "src/test/fixtures/mapping-files/mock_mapping.js.map";
const TELEMETRY_SERVER_URL = "https://telemetry-api.com";

describe("crashlytics:sourcemap:upload", () => {
let sandbox: sinon.SinonSandbox;
Expand Down Expand Up @@ -69,17 +68,9 @@ describe("crashlytics:sourcemap:upload", () => {
);
});

it("should throw an error if no telemetry server URL is provided", async () => {
await expect(command.runner()("filename", { app: "test-app" })).to.be.rejectedWith(
FirebaseError,
"set --telemetry-server-url to a valid Firebase Telemetry server URL",
);
});

it("should create the default cloud storage bucket", async () => {
await command.runner()(FILE_PATH, {
app: "test-app",
telemetryServerUrl: TELEMETRY_SERVER_URL,
});
expect(gcsMock.upsertBucket).to.be.calledOnce;
const args = gcsMock.upsertBucket.firstCall.args;
Expand All @@ -91,7 +82,6 @@ describe("crashlytics:sourcemap:upload", () => {
const options = {
app: "test-app",
bucketLocation: "a-different-LoCaTiOn",
telemetryServerUrl: TELEMETRY_SERVER_URL,
};
await command.runner()(FILE_PATH, options);
expect(gcsMock.upsertBucket).to.be.calledOnce;
Expand All @@ -106,15 +96,13 @@ describe("crashlytics:sourcemap:upload", () => {
expect(
command.runner()("invalid/path", {
app: "test-app",
telemetryServerUrl: TELEMETRY_SERVER_URL,
}),
).to.be.rejectedWith(FirebaseError, "provide a valid file path or directory");
});

it("should upload a single mapping file", async () => {
await command.runner()(FILE_PATH, {
app: "test-app",
telemetryServerUrl: TELEMETRY_SERVER_URL,
});
expect(gcsMock.uploadObject).to.be.calledOnce;
expect(gcsMock.uploadObject).to.be.calledWith(sinon.match.any, BUCKET_NAME);
Expand All @@ -124,7 +112,22 @@ describe("crashlytics:sourcemap:upload", () => {
});

it("should find and upload mapping files in a directory", async () => {
await command.runner()(DIR_PATH, { app: "test-app", telemetryServerUrl: TELEMETRY_SERVER_URL });
await command.runner()(DIR_PATH, { app: "test-app" });
expect(gcsMock.uploadObject).to.be.calledTwice;
const uploadedFiles = gcsMock.uploadObject
.getCalls()
.map((call) => call.args[0].file)
.sort();
expect(uploadedFiles[0]).to.match(
/test-app-.*-src-test-fixtures-mapping-files-mock_mapping\.js\.map\.zip/,
);
expect(uploadedFiles[1]).to.match(
/test-app-.*-src-test-fixtures-mapping-files-subdir-subdir_mock_mapping\.js\.map\.zip/,
);
});

it("should find and upload mapping files in the current directory if no path is provided", async () => {
await command.runner()(undefined, { app: "test-app" });
expect(gcsMock.uploadObject).to.be.calledTwice;
const uploadedFiles = gcsMock.uploadObject
.getCalls()
Expand All @@ -142,7 +145,6 @@ describe("crashlytics:sourcemap:upload", () => {
await command.runner()(FILE_PATH, {
app: "test-app",
appVersion: "1.0.0",
telemetryServerUrl: TELEMETRY_SERVER_URL,
});
expect(gcsMock.uploadObject.firstCall.args[0].file).to.eq(
"test-app-1.0.0-src-test-fixtures-mapping-files-mock_mapping.js.map.zip",
Expand All @@ -152,7 +154,6 @@ describe("crashlytics:sourcemap:upload", () => {
it("should fall back to the git commit for app version", async () => {
await command.runner()(FILE_PATH, {
app: "test-app",
telemetryServerUrl: TELEMETRY_SERVER_URL,
});
expect(gcsMock.uploadObject.firstCall.args[0].file).to.match(
/test-app-a{40}-src-test-fixtures-mapping-files-mock_mapping.js.map.zip/,
Expand All @@ -167,7 +168,6 @@ describe("crashlytics:sourcemap:upload", () => {

await command.runner()(FILE_PATH, {
app: "test-app",
telemetryServerUrl: TELEMETRY_SERVER_URL,
});
expect(gcsMock.uploadObject.firstCall.args[0].file).to.eq(
"test-app-1.2.3-src-test-fixtures-mapping-files-mock_mapping.js.map.zip",
Expand All @@ -180,7 +180,6 @@ describe("crashlytics:sourcemap:upload", () => {

await command.runner()(FILE_PATH, {
app: "test-app",
telemetryServerUrl: TELEMETRY_SERVER_URL,
});
expect(gcsMock.uploadObject.firstCall.args[0].file).to.eq(
"test-app-unset-src-test-fixtures-mapping-files-mock_mapping.js.map.zip",
Expand All @@ -190,7 +189,6 @@ describe("crashlytics:sourcemap:upload", () => {
it("should register the source map after upload", async () => {
await command.runner()(FILE_PATH, {
app: "test-app",
telemetryServerUrl: TELEMETRY_SERVER_URL,
});
expect(clientPostStub).to.be.calledOnce;
const args = clientPostStub.firstCall.args;
Expand All @@ -210,7 +208,6 @@ describe("crashlytics:sourcemap:upload", () => {
clientPostStub.rejects(new Error("Registration failed"));
await command.runner()(FILE_PATH, {
app: "test-app",
telemetryServerUrl: TELEMETRY_SERVER_URL,
});
expect(clientPostStub).to.be.calledOnce;
expect(logLabeledWarningStub).to.be.calledOnceWith(
Expand All @@ -219,21 +216,9 @@ describe("crashlytics:sourcemap:upload", () => {
);
});

it("should use the provided telemetry server URL", async () => {
const customServerUrl = "https://custom-server.com";
clientPostStub.restore();
clientPostStub = sandbox.stub(Client.prototype, "post").callsFake(function (this: any) {
expect(this["opts"].urlPrefix).to.equal(customServerUrl);
return Promise.resolve({ status: 200, response: {} as any, body: {} });
});

await command.runner()(FILE_PATH, { app: "test-app", telemetryServerUrl: customServerUrl });
expect(clientPostStub).to.be.calledOnce;
});

it("should log failed files", async () => {
clientPostStub.rejects(new Error("Registration failed"));
await command.runner()(DIR_PATH, { app: "test-app", telemetryServerUrl: TELEMETRY_SERVER_URL });
await command.runner()(DIR_PATH, { app: "test-app" });

// Should verify that logLabeledBullet is called with the specific failed files
expect(logLabeledBulletStub).to.be.calledWith(
Expand Down
51 changes: 17 additions & 34 deletions src/commands/crashlytics-sourcemap-upload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ interface CommandOptions extends Options {
app?: string;
bucketLocation?: string;
appVersion?: string;
telemetryServerUrl?: string;
}

interface SourceMap {
Expand All @@ -29,7 +28,7 @@ interface SourceMap {
fileUri: string;
}

export const command = new Command("crashlytics:sourcemap:upload <mappingFiles>")
export const command = new Command("crashlytics:sourcemap:upload [mappingFiles]")
.description("upload javascript source maps to de-minify stack traces")
.option("--app <appID>", "the app id of your Firebase app")
.option(
Expand All @@ -40,10 +39,8 @@ export const command = new Command("crashlytics:sourcemap:upload <mappingFiles>"
"--app-version <appVersion>",
"the version of your Firebase app (defaults to Git commit hash, if available)",
)
.option("--telemetry-server-url <telemetryServerUrl>", "the url of the telemetry API")
.action(async (mappingFiles: string, options: CommandOptions) => {
.action(async (mappingFiles: string | undefined, options: CommandOptions) => {
checkGoogleAppID(options);
checkTelemetryServerUrl(options);

// App version
const appVersion = getAppVersion(options);
Expand All @@ -57,7 +54,7 @@ export const command = new Command("crashlytics:sourcemap:upload <mappingFiles>"

// Find and upload mapping files
const rootDir = options.projectRoot ?? process.cwd();
const filePath = path.relative(rootDir, mappingFiles);
const filePath = path.relative(rootDir, mappingFiles || ".") || ".";
let fstat: fs.Stats;
try {
fstat = statSync(filePath);
Expand All @@ -69,11 +66,11 @@ export const command = new Command("crashlytics:sourcemap:upload <mappingFiles>"
let successCount = 0;
const failedFiles: string[] = [];
if (fstat.isFile()) {
const success = await uploadMap(projectId, mappingFiles, bucketName, appVersion, options);
const success = await uploadMap(projectId, filePath, bucketName, appVersion, options);
if (success) {
successCount++;
} else {
failedFiles.push(mappingFiles);
failedFiles.push(filePath);
}
} else if (fstat.isDirectory()) {
logLabeledBullet("crashlytics", "Looking for mapping files in your directory...");
Expand Down Expand Up @@ -116,12 +113,6 @@ function checkGoogleAppID(options: CommandOptions): void {
}
}

function checkTelemetryServerUrl(options: CommandOptions): void {
if (!options.telemetryServerUrl) {
throw new FirebaseError("set --telemetry-server-url to a valid Firebase Telemetry server URL");
}
}

function getAppVersion(options: CommandOptions): string {
if (options.appVersion) {
return options.appVersion;
Expand Down Expand Up @@ -181,13 +172,12 @@ async function upsertBucket(

async function uploadMap(
projectId: string,
mappingFile: string,
filePath: string,
bucketName: string,
appVersion: string,
options: CommandOptions,
): Promise<boolean> {
try {
const filePath = path.relative(options.projectRoot ?? process.cwd(), mappingFile);
const tmpArchive = await archiveFile(filePath, { archivedFileName: "mapping.js.map" });
const gcsFile = `${options.app}-${appVersion}-${normalizeFileName(filePath)}.zip`;
const uid = murmurHashV3(`${appVersion}-${filePath}`);
Expand All @@ -202,22 +192,18 @@ async function uploadMap(
bucketName,
);
const fileUri = `gs://${bucket}/${object}`;
logger.debug(`Uploaded mapping file ${mappingFile} to ${fileUri}`);
logger.debug(`Uploaded mapping file ${filePath} to ${fileUri}`);

await registerSourceMap(
parent,
{
name,
version: appVersion,
obfuscatedFilePath: filePath,
fileUri,
},
options.telemetryServerUrl!,
);
await registerSourceMap(parent, {
name,
version: appVersion,
obfuscatedFilePath: filePath,
fileUri,
});

return true;
} catch (e) {
logLabeledWarning("crashlytics", `Failed to upload mapping file ${mappingFile}:\n${e}`);
logLabeledWarning("crashlytics", `Failed to upload mapping file ${filePath}:\n${e}`);
return false;
}
}
Expand All @@ -226,13 +212,10 @@ function normalizeFileName(fileName: string): string {
return fileName.replaceAll(/\//g, "-");
}

async function registerSourceMap(
parent: string,
sourceMap: SourceMap,
telemetryServerUrl: string,
): Promise<void> {
async function registerSourceMap(parent: string, sourceMap: SourceMap): Promise<void> {
const client = new Client({
urlPrefix: telemetryServerUrl,
// TODO(tonybaroneee): use the real telemetry server url when ready
urlPrefix: "http://localhost",
auth: true,
apiVersion: "v1",
});
Expand Down
Loading