Skip to content

Commit bd9570b

Browse files
amitjoshi438Amit Joshiclaude
authored
Improve Dataverse error instrumentation for web extension (#1462)
* Improve Dataverse error instrumentation for web extension Fix empty "{}" error messages in telemetry for Dataverse save and fetch operations. The Response object from fetch has non-enumerable properties, causing JSON.stringify(response) to produce "{}". Changes: - Add errorHandlerUtil.ts with createHttpResponseError() to properly extract HTTP status, statusText, and response body from Response objects - Update remoteSaveProvider.ts, remoteFetchProvider.ts, etagHandlerService.ts, WebExtensionContext.ts, and graphClientService.ts to use the new utility - Update catch blocks to use isHttpResponseError() type guard for proper HTTP error routing to sendAPIFailureTelemetry with status codes This enables proper diagnosis of the 92% of save errors and 24% of fetch errors that were previously logged with empty error messages. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Fix tests for updated HTTP error handling Update test mocks to include required Response properties (status, url, clone, text) that createHttpResponseError needs. Also update test assertions to expect sendAPIFailureTelemetry instead of sendErrorTelemetry for HTTP errors, matching the new error handling flow. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Amit Joshi <amitjoshi@microsoft.com> Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 40796c5 commit bd9570b

10 files changed

Lines changed: 209 additions & 57 deletions

src/web/client/WebExtensionContext.ts

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { QuickPickProvider } from "./webViews/QuickPickProvider";
3535
import { UserCollaborationProvider } from "./webViews/userCollaborationProvider";
3636
import { GraphClientService } from "./services/graphClientService";
3737
import { ServiceEndpointCategory } from "../../common/services/Constants";
38+
import { createHttpResponseError, isHttpResponseError } from "./utilities/errorHandlerUtil";
3839

3940
export interface IWebExtensionContext {
4041
// From portalSchema properties
@@ -576,7 +577,7 @@ class WebExtensionContext implements IWebExtensionContext {
576577
headers: getCommonHeadersForDataverse(accessToken),
577578
});
578579
if (!response?.ok) {
579-
throw new Error(JSON.stringify(response));
580+
throw await createHttpResponseError(response);
580581
}
581582
this.telemetry.sendAPISuccessTelemetry(
582583
requestUrl,
@@ -592,8 +593,8 @@ class WebExtensionContext implements IWebExtensionContext {
592593
schema
593594
);
594595
} catch (error) {
595-
if ((error as Response)?.status > 0) {
596-
const errorMsg = (error as Error)?.message;
596+
const errorMsg = (error as Error)?.message;
597+
if (isHttpResponseError(error) && error.httpDetails) {
597598
this.telemetry.sendAPIFailureTelemetry(
598599
requestUrl,
599600
languageEntityName,
@@ -602,13 +603,13 @@ class WebExtensionContext implements IWebExtensionContext {
602603
this.populateLanguageIdToCode.name,
603604
errorMsg,
604605
'',
605-
(error as Response)?.status.toString(),
606+
error.httpDetails.statusCode.toString(),
606607
);
607608
} else {
608609
this.telemetry.sendErrorTelemetry(
609610
webExtensionTelemetryEventNames.WEB_EXTENSION_POPULATE_LANGUAGE_ID_TO_CODE_SYSTEM_ERROR,
610611
this.populateLanguageIdToCode.name,
611-
(error as Error)?.message,
612+
errorMsg,
612613
error as Error
613614
);
614615
}
@@ -642,7 +643,7 @@ class WebExtensionContext implements IWebExtensionContext {
642643
headers: getCommonHeadersForDataverse(accessToken),
643644
});
644645
if (!response?.ok) {
645-
throw new Error(JSON.stringify(response));
646+
throw await createHttpResponseError(response);
646647
}
647648
this.telemetry.sendAPISuccessTelemetry(
648649
requestUrl,
@@ -655,8 +656,8 @@ class WebExtensionContext implements IWebExtensionContext {
655656
this._websiteLanguageIdToPortalLanguageMap =
656657
getWebsiteLanguageIdToPortalLanguageIdMap(result, schema);
657658
} catch (error) {
658-
if ((error as Response)?.status > 0) {
659-
const errorMsg = (error as Error)?.message;
659+
const errorMsg = (error as Error)?.message;
660+
if (isHttpResponseError(error) && error.httpDetails) {
660661
this.telemetry.sendAPIFailureTelemetry(
661662
requestUrl,
662663
languageEntityName,
@@ -665,13 +666,13 @@ class WebExtensionContext implements IWebExtensionContext {
665666
this.populateWebsiteLanguageIdToPortalLanguageMap.name,
666667
errorMsg,
667668
'',
668-
(error as Response)?.status.toString()
669+
error.httpDetails.statusCode.toString()
669670
);
670671
} else {
671672
this.telemetry.sendErrorTelemetry(
672673
webExtensionTelemetryEventNames.WEB_EXTENSION_POPULATE_WEBSITE_LANGUAGE_ID_TO_PORTALLANGUAGE_SYSTEM_ERROR,
673674
this.populateWebsiteLanguageIdToPortalLanguageMap.name,
674-
(error as Error)?.message,
675+
errorMsg,
675676
error as Error
676677
);
677678
}
@@ -705,7 +706,7 @@ class WebExtensionContext implements IWebExtensionContext {
705706
});
706707

707708
if (!response?.ok) {
708-
throw new Error(JSON.stringify(response));
709+
throw await createHttpResponseError(response);
709710
}
710711
this.telemetry.sendAPISuccessTelemetry(
711712
requestUrl,
@@ -717,8 +718,8 @@ class WebExtensionContext implements IWebExtensionContext {
717718
const result = await response?.json();
718719
this._websiteIdToLanguage = getWebsiteIdToLcidMap(result, schema);
719720
} catch (error) {
720-
if ((error as Response)?.status > 0) {
721-
const errorMsg = (error as Error)?.message;
721+
const errorMsg = (error as Error)?.message;
722+
if (isHttpResponseError(error) && error.httpDetails) {
722723
this.telemetry.sendAPIFailureTelemetry(
723724
requestUrl,
724725
websiteEntityName,
@@ -727,13 +728,13 @@ class WebExtensionContext implements IWebExtensionContext {
727728
this.populateWebsiteIdToLanguageMap.name,
728729
errorMsg,
729730
'',
730-
(error as Response)?.status.toString()
731+
error.httpDetails.statusCode.toString()
731732
);
732733
} else {
733734
this.telemetry.sendErrorTelemetry(
734735
webExtensionTelemetryEventNames.WEB_EXTENSION_POPULATE_WEBSITE_ID_TO_LANGUAGE_SYSTEM_ERROR,
735736
this.populateWebsiteIdToLanguageMap.name,
736-
(error as Error)?.message,
737+
errorMsg,
737738
error as Error
738739
);
739740
}

src/web/client/dal/remoteFetchProvider.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { IAttributePath, IFileInfo } from "../common/interfaces";
3535
import { portal_schema_V2 } from "../schema/portalSchema";
3636
import { ERROR_CONSTANTS } from "../../../common/ErrorConstants";
3737
import { showErrorDialog } from "../../../common/utilities/errorHandlerUtil";
38+
import { createHttpResponseError, isHttpResponseError } from "../utilities/errorHandlerUtil";
3839
import { EnableServerLogicChanges, EnableDuplicateFileHandling, EnableBlogSupport } from "../../../common/ecs-features/ecsFeatureGates";
3940
import { ECSFeaturesClient } from "../../../common/ecs-features/ecsFeatureClient";
4041

@@ -138,7 +139,7 @@ async function fetchFromDataverseAndCreateFiles(
138139

139140
if (!response.ok) {
140141
makeRequestCall = false;
141-
throw new Error(JSON.stringify(response));
142+
throw await createHttpResponseError(response);
142143
}
143144

144145
const result = await response.json();
@@ -181,7 +182,8 @@ async function fetchFromDataverseAndCreateFiles(
181182
makeRequestCall = false;
182183
const errorMsg = (error as Error)?.message;
183184
console.error(vscode.l10n.t("Failed to fetch some files."));
184-
if ((error as Response)?.status > 0) {
185+
if (isHttpResponseError(error) && error.httpDetails) {
186+
// HTTP error - use API failure telemetry with status code
185187
WebExtensionContext.telemetry.sendAPIFailureTelemetry(
186188
requestUrl,
187189
entityName,
@@ -190,13 +192,14 @@ async function fetchFromDataverseAndCreateFiles(
190192
fetchFromDataverseAndCreateFiles.name,
191193
errorMsg,
192194
'',
193-
(error as Response)?.status.toString()
195+
error.httpDetails.statusCode.toString()
194196
);
195197
} else {
198+
// System error (network failure, timeout, etc.)
196199
WebExtensionContext.telemetry.sendErrorTelemetry(
197200
webExtensionTelemetryEventNames.WEB_EXTENSION_FETCH_DATAVERSE_AND_CREATE_FILES_SYSTEM_ERROR,
198201
fetchFromDataverseAndCreateFiles.name,
199-
(error as Error)?.message,
202+
errorMsg,
200203
error as Error
201204
);
202205
}
@@ -658,17 +661,18 @@ async function fetchMappingEntityContent(
658661
}
659662

660663
if (!response.ok) {
664+
const httpError = await createHttpResponseError(response);
661665
WebExtensionContext.telemetry.sendAPIFailureTelemetry(
662666
requestUrl,
663667
entity,
664668
Constants.httpMethod.GET,
665669
new Date().getTime() - requestSentAtTime,
666670
fetchMappingEntityContent.name,
667-
JSON.stringify(response),
671+
httpError.message,
668672
'',
669-
response?.status.toString()
673+
httpError.httpDetails.statusCode.toString()
670674
);
671-
throw new Error(response.statusText);
675+
throw httpError;
672676
}
673677

674678
WebExtensionContext.telemetry.sendAPISuccessTelemetry(

src/web/client/dal/remoteSaveProvider.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { IAttributePath } from "../common/interfaces";
2121
import { webExtensionTelemetryEventNames } from "../../../common/OneDSLoggerTelemetry/web/client/webExtensionTelemetryEvents";
2222
import { MultiFileSupportedEntityName, schemaEntityKey } from "../schema/constants";
2323
import { getEntityMappingEntityId } from "../utilities/fileAndEntityUtil";
24+
import { createHttpResponseError, isHttpResponseError } from "../utilities/errorHandlerUtil";
2425

2526
interface ISaveCallParameters {
2627
requestInit: RequestInit;
@@ -190,7 +191,7 @@ async function saveDataToDataverse(
190191
);
191192

192193
if (!response.ok) {
193-
throw new Error(JSON.stringify(response));
194+
throw await createHttpResponseError(response);
194195
}
195196

196197
WebExtensionContext.telemetry.sendAPISuccessTelemetry(
@@ -203,28 +204,33 @@ async function saveDataToDataverse(
203204
fileExtensionType
204205
);
205206
} catch (error) {
206-
const authError = (error as Error)?.message;
207-
if ((error as Response)?.status > 0) {
207+
const errorMessage = (error as Error)?.message;
208+
if (isHttpResponseError(error) && error.httpDetails) {
209+
// HTTP error - use API failure telemetry with status code
208210
WebExtensionContext.telemetry.sendAPIFailureTelemetry(
209211
saveCallParameters.requestUrl,
210212
entityName,
211213
httpMethod.PATCH,
212214
new Date().getTime() - requestSentAtTime,
213215
saveDataToDataverse.name,
214-
authError,
216+
errorMessage,
215217
fileExtensionType,
216-
(error as Response)?.status as unknown as string
218+
error.httpDetails.statusCode.toString()
217219
);
218220
} else {
221+
// System error (network failure, timeout, etc.)
219222
WebExtensionContext.telemetry.sendErrorTelemetry(
220223
webExtensionTelemetryEventNames.WEB_EXTENSION_SAVE_DATA_TO_DATAVERSE_API_ERROR,
221224
saveDataToDataverse.name,
222-
(error as Error)?.message,
225+
errorMessage,
223226
error as Error
224227
);
225228
}
226229

227-
if (typeof error === "string" && error.includes("Unauthorized")) {
230+
// Check for authorization failure (401) or "Unauthorized" in error message
231+
const isUnauthorized = (isHttpResponseError(error) && error.httpDetails?.statusCode === 401) ||
232+
errorMessage.includes("Unauthorized");
233+
if (isUnauthorized) {
228234
showErrorDialog(
229235
vscode.l10n.t(
230236
"Authorization Failed. Please run again to authorize it"
@@ -235,7 +241,7 @@ async function saveDataToDataverse(
235241
);
236242
} else {
237243
showErrorDialog(
238-
vscode.l10n.t("Theres a problem on the back end"),
244+
vscode.l10n.t("There's a problem on the back end"),
239245
vscode.l10n.t("Try again")
240246
);
241247
}

src/web/client/services/etagHandlerService.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import {
2020
} from "../utilities/fileAndEntityUtil";
2121
import { getRequestURL } from "../utilities/urlBuilderUtil";
2222
import WebExtensionContext from "../WebExtensionContext";
23+
import { createHttpResponseError, isHttpResponseError } from "../utilities/errorHandlerUtil";
2324

2425
export class EtagHandlerService {
2526
public static async getLatestFileContentAndUpdateMetadata(
@@ -109,7 +110,7 @@ export class EtagHandlerService {
109110
this.getLatestFileContentAndUpdateMetadata.name,
110111
response.statusText
111112
);
112-
throw new Error(JSON.stringify(response));
113+
throw await createHttpResponseError(response);
113114
}
114115

115116
WebExtensionContext.telemetry.sendAPISuccessTelemetry(
@@ -120,23 +121,25 @@ export class EtagHandlerService {
120121
this.getLatestFileContentAndUpdateMetadata.name
121122
);
122123
} catch (error) {
123-
if ((error as Response)?.status > 0) {
124-
const authError = (error as Error)?.message;
124+
const errorMessage = (error as Error)?.message;
125+
if (isHttpResponseError(error) && error.httpDetails) {
126+
// HTTP error - use API failure telemetry with status code
125127
WebExtensionContext.telemetry.sendAPIFailureTelemetry(
126128
requestUrl,
127129
entityName,
128130
httpMethod.GET,
129131
new Date().getTime() - requestSentAtTime,
130132
this.getLatestFileContentAndUpdateMetadata.name,
131-
authError,
133+
errorMessage,
132134
'',
133-
(error as Response)?.status.toString()
135+
error.httpDetails.statusCode.toString()
134136
);
135137
} else {
138+
// System error (network failure, timeout, etc.)
136139
WebExtensionContext.telemetry.sendErrorTelemetry(
137140
webExtensionTelemetryEventNames.WEB_EXTENSION_ETAG_HANDLER_SERVICE_ERROR,
138141
this.getLatestFileContentAndUpdateMetadata.name,
139-
(error as Error)?.message
142+
errorMessage
140143
);
141144
}
142145
}
@@ -192,7 +195,7 @@ export class EtagHandlerService {
192195
this.updateFileEtag.name,
193196
response.statusText
194197
);
195-
throw new Error(JSON.stringify(response));
198+
throw await createHttpResponseError(response);
196199
}
197200

198201
WebExtensionContext.telemetry.sendAPISuccessTelemetry(
@@ -203,23 +206,25 @@ export class EtagHandlerService {
203206
this.updateFileEtag.name
204207
);
205208
} catch (error) {
206-
if ((error as Response)?.status > 0) {
207-
const authError = (error as Error)?.message;
209+
const errorMessage = (error as Error)?.message;
210+
if (isHttpResponseError(error) && error.httpDetails) {
211+
// HTTP error - use API failure telemetry with status code
208212
WebExtensionContext.telemetry.sendAPIFailureTelemetry(
209213
requestUrl,
210214
entityName,
211215
httpMethod.GET,
212216
new Date().getTime() - requestSentAtTime,
213217
this.updateFileEtag.name,
214-
authError,
218+
errorMessage,
215219
'',
216-
(error as Response)?.status.toString()
220+
error.httpDetails.statusCode.toString()
217221
);
218222
} else {
223+
// System error (network failure, timeout, etc.)
219224
WebExtensionContext.telemetry.sendErrorTelemetry(
220225
webExtensionTelemetryEventNames.WEB_EXTENSION_ETAG_HANDLER_SERVICE_ERROR,
221226
this.updateFileEtag.name,
222-
(error as Error)?.message,
227+
errorMessage,
223228
error as Error
224229
);
225230
}

src/web/client/services/graphClientService.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import WebExtensionContext from "../WebExtensionContext";
88
import { getCommonHeaders, graphClientAuthentication } from "../../../common/services/AuthenticationProvider";
99
import * as Constants from "../common/constants";
1010
import { webExtensionTelemetryEventNames } from "../../../common/OneDSLoggerTelemetry/web/client/webExtensionTelemetryEvents";
11+
import { createHttpResponseError, isHttpResponseError } from "../utilities/errorHandlerUtil";
1112

1213
export class GraphClientService {
1314
private _graphToken: string;
@@ -85,7 +86,7 @@ export class GraphClientService {
8586
);
8687

8788
if (!response.ok) {
88-
throw new Error(JSON.stringify(response));
89+
throw await createHttpResponseError(response);
8990
}
9091

9192
WebExtensionContext.telemetry.sendAPISuccessTelemetry(
@@ -99,7 +100,8 @@ export class GraphClientService {
99100
return await response.json();
100101
} catch (error) {
101102
const errorMsg = (error as Error)?.message;
102-
if ((error as Response)?.status > 0) {
103+
if (isHttpResponseError(error) && error.httpDetails) {
104+
// HTTP error - use API failure telemetry with status code
103105
WebExtensionContext.telemetry.sendAPIFailureTelemetry(
104106
requestUrl.href,
105107
service,
@@ -108,9 +110,10 @@ export class GraphClientService {
108110
this.requestGraphClient.name,
109111
errorMsg,
110112
"",
111-
(error as Response)?.status.toString()
113+
error.httpDetails.statusCode.toString()
112114
);
113115
} else {
116+
// System error (network failure, timeout, etc.)
114117
WebExtensionContext.telemetry.sendErrorTelemetry(
115118
webExtensionTelemetryEventNames.WEB_EXTENSION_GET_FROM_GRAPH_CLIENT_FAILED,
116119
this.requestGraphClient.name,

src/web/client/test/integration/WebExtensionContext.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -615,9 +615,14 @@ describe("WebExtensionContext", () => {
615615
"getPortalLanguageIdToLcidMap"
616616
).returns(portalLanguageIdCodeMap);
617617

618+
const mockResponseBody = JSON.stringify({ value: "value" });
618619
const _mockFetch = stub(fetch, "default").resolves({
619620
ok: false,
620-
statusText: "statusText",
621+
status: 500,
622+
statusText: "Internal Server Error",
623+
url: "https://test.crm.dynamics.com",
624+
clone: function() { return this; },
625+
text: () => Promise.resolve(mockResponseBody),
621626
json: () => {
622627
return new Promise((resolve) => {
623628
return resolve({ value: "value" });

0 commit comments

Comments
 (0)