Skip to content

Commit e9046ec

Browse files
committed
fix(apps): use app_builder_url from API response instead of constructing it
The backend now returns app_builder_url in upload and release responses, which reflects the correct host for orgs with custom domains (e.g. dd.datad0g.com). The previous hardcoded https://app.${site}/... produced wrong URLs for those orgs. The upload response's URL includes ?viewMode=preview (edit view); the release response's URL does not (published view) — this matches the backend behavior in ddoghq/dd-source#1038. Guards the release log against a missing/null app_builder_url (e.g. an older backend during a coordinated rollout) so it degrades to the plain "Published ... to live." message instead of crashing or logging "undefined" — mirrors the existing guard on the upload log. Related: ddoghq/dd-source#1038
1 parent a2ed1e9 commit e9046ec

2 files changed

Lines changed: 113 additions & 11 deletions

File tree

packages/plugins/apps/src/upload.test.ts

Lines changed: 102 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,8 @@ describe('Apps Plugin - upload', () => {
229229
version_id: 'v123',
230230
application_id: 'app123',
231231
app_builder_id: 'builder123',
232+
app_builder_url:
233+
'https://app.datadoghq.com/app-builder/apps/edit/builder123?viewMode=preview',
232234
} as any);
233235

234236
const { errors, warnings } = await uploadArchive(archive, context, logger);
@@ -248,7 +250,30 @@ describe('Apps Plugin - upload', () => {
248250
onRetry: expect.any(Function),
249251
});
250252
expect(mockLogFn).toHaveBeenCalledWith(
251-
expect.stringContaining('Your application is available at'),
253+
expect.stringContaining(
254+
'https://app.datadoghq.com/app-builder/apps/edit/builder123?viewMode=preview',
255+
),
256+
'info',
257+
);
258+
});
259+
260+
test('Should use app_builder_url from upload response', async () => {
261+
doAuthenticatedRequestMock.mockResolvedValueOnce({
262+
version_id: 'v123',
263+
application_id: 'app123',
264+
app_builder_id: 'builder123',
265+
app_builder_url:
266+
'https://dd.datad0g.com/app-builder/apps/edit/builder123?viewMode=preview',
267+
} as any);
268+
269+
await uploadArchive(archive, context, logger);
270+
271+
// Uses the exact URL from the response — proves it's not reconstructed from
272+
// context.site (datadoghq.com), which would produce a different domain.
273+
expect(mockLogFn).toHaveBeenCalledWith(
274+
expect.stringContaining(
275+
'https://dd.datad0g.com/app-builder/apps/edit/builder123?viewMode=preview',
276+
),
252277
'info',
253278
);
254279
});
@@ -258,6 +283,8 @@ describe('Apps Plugin - upload', () => {
258283
version_id: 'v123',
259284
application_id: 'app123',
260285
app_builder_id: 'builder123',
286+
app_builder_url:
287+
'https://app.datadoghq.com/app-builder/apps/edit/builder123?viewMode=preview',
261288
} as any);
262289

263290
const { errors, warnings } = await uploadArchive(
@@ -286,8 +313,12 @@ describe('Apps Plugin - upload', () => {
286313
version_id: 'v123',
287314
application_id: 'app123',
288315
app_builder_id: 'builder123',
316+
app_builder_url:
317+
'https://app.datadoghq.com/app-builder/apps/edit/builder123?viewMode=preview',
289318
})
290-
.mockResolvedValueOnce({});
319+
.mockResolvedValueOnce({
320+
app_builder_url: 'https://app.datadoghq.com/app-builder/apps/builder123',
321+
});
291322

292323
const { errors, warnings } = await uploadArchive(archive, context, logger);
293324

@@ -301,10 +332,72 @@ describe('Apps Plugin - upload', () => {
301332
getData: expect.any(Function),
302333
onRetry: expect.any(Function),
303334
});
304-
expect(mockLogFn).toHaveBeenCalledWith(
305-
expect.stringContaining('Your application is available at'),
306-
'info',
335+
// Pin down which log is which by its distinguishing message prefix — proves not
336+
// just that a matching call exists somewhere, but that the upload log specifically
337+
// carries ?viewMode=preview and the release log specifically doesn't.
338+
const uploadLog = mockLogFn.mock.calls.find(([message]) =>
339+
message.startsWith('Your application is available at'),
340+
);
341+
const releaseLog = mockLogFn.mock.calls.find(([message]) =>
342+
message.startsWith('Published uploaded version'),
343+
);
344+
expect(uploadLog?.[0]).toContain(
345+
'https://app.datadoghq.com/app-builder/apps/edit/builder123?viewMode=preview',
346+
);
347+
expect(releaseLog?.[0]).toContain(
348+
'https://app.datadoghq.com/app-builder/apps/builder123',
349+
);
350+
expect(releaseLog?.[0]).not.toContain('?viewMode');
351+
});
352+
353+
test('Should use app_builder_url from release response', async () => {
354+
doAuthenticatedRequestMock
355+
.mockResolvedValueOnce({
356+
version_id: 'v123',
357+
application_id: 'app123',
358+
app_builder_id: 'builder123',
359+
app_builder_url:
360+
'https://dd.datad0g.com/app-builder/apps/edit/builder123?viewMode=preview',
361+
})
362+
.mockResolvedValueOnce({
363+
app_builder_url: 'https://dd.datad0g.com/app-builder/apps/builder123',
364+
});
365+
366+
await uploadArchive(archive, context, logger);
367+
368+
// Match the release log by its distinguishing message prefix — proves the
369+
// published URL reflects the custom domain from the release response (not
370+
// context.site) and carries no ?viewMode (unlike the upload log).
371+
const releaseLog = mockLogFn.mock.calls.find(([message]) =>
372+
message.startsWith('Published uploaded version'),
373+
);
374+
expect(releaseLog?.[0]).toContain('https://dd.datad0g.com/app-builder/apps/builder123');
375+
expect(releaseLog?.[0]).not.toContain('?viewMode');
376+
});
377+
378+
test('Should degrade gracefully when release response has no app_builder_url', async () => {
379+
doAuthenticatedRequestMock
380+
.mockResolvedValueOnce({
381+
version_id: 'v123',
382+
application_id: 'app123',
383+
app_builder_id: 'builder123',
384+
app_builder_url:
385+
'https://app.datadoghq.com/app-builder/apps/edit/builder123?viewMode=preview',
386+
})
387+
// Simulates an older backend that doesn't yet return app_builder_url.
388+
.mockResolvedValueOnce({});
389+
390+
const { errors, warnings } = await uploadArchive(archive, context, logger);
391+
392+
expect(errors).toHaveLength(0);
393+
expect(warnings).toHaveLength(0);
394+
const releaseLog = mockLogFn.mock.calls.find(([message]) =>
395+
message.startsWith('Published uploaded version'),
307396
);
397+
expect(releaseLog?.[0]).toContain('to live.');
398+
// No second line with a URL — degrades to the plain message instead of
399+
// logging "undefined" or throwing on a missing app_builder_url.
400+
expect(releaseLog?.[0]).not.toContain('\n');
308401
});
309402

310403
test.each(['false', '0', 'False', 'FALSE', 'off', 'no'])(
@@ -317,6 +410,8 @@ describe('Apps Plugin - upload', () => {
317410
version_id: 'v123',
318411
application_id: 'app123',
319412
app_builder_id: 'builder123',
413+
app_builder_url:
414+
'https://app.datadoghq.com/app-builder/apps/edit/builder123?viewMode=preview',
320415
});
321416

322417
const { errors } = await uploadArchive(archive, context, logger);
@@ -342,6 +437,8 @@ describe('Apps Plugin - upload', () => {
342437
version_id: 'v123',
343438
application_id: 'app123',
344439
app_builder_id: 'builder123',
440+
app_builder_url:
441+
'https://app.datadoghq.com/app-builder/apps/edit/builder123?viewMode=preview',
345442
});
346443

347444
const { errors, warnings } = await uploadArchive(archive, context, logger);

packages/plugins/apps/src/upload.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,17 +121,15 @@ Would have uploaded ${summary}`,
121121

122122
log.debug(`Uploaded ${summary}\n`);
123123

124-
if (response.app_builder_id) {
125-
const appBuilderUrl = `https://app.${context.site}/app-builder/apps/${response.app_builder_id}`;
126-
127-
log.info(`Your application is available at:\n ${cyan(appBuilderUrl)}`);
124+
if (response.app_builder_url) {
125+
log.info(`Your application is available at:\n ${cyan(response.app_builder_url)}`);
128126
}
129127

130128
const shouldPublish = parseBoolEnv(getDDEnvValue('APPS_PUBLISH'), true);
131129

132130
if (response.version_id && shouldPublish) {
133131
const releaseUrl = getReleaseUrl(context.site, context.identifier);
134-
await doAuthenticatedRequest({
132+
const releaseResponse: any = await doAuthenticatedRequest({
135133
url: releaseUrl,
136134
method: 'PUT',
137135
type: 'json',
@@ -150,7 +148,14 @@ Would have uploaded ${summary}`,
150148
log.warn(message);
151149
},
152150
});
153-
log.info(`Published uploaded version ${bold(response.version_id)} to live.`);
151+
152+
if (releaseResponse?.app_builder_url) {
153+
log.info(
154+
`Published uploaded version ${bold(response.version_id)} to live.\n ${cyan(releaseResponse.app_builder_url)}`,
155+
);
156+
} else {
157+
log.info(`Published uploaded version ${bold(response.version_id)} to live.`);
158+
}
154159
} else if (response.version_id && !shouldPublish) {
155160
log.info(`Uploaded version ${bold(response.version_id)} as a draft (publish skipped).`);
156161
}

0 commit comments

Comments
 (0)