Skip to content

Commit 1335d26

Browse files
amitjoshi438Amit Joshiclaude
authored
Fix web save flow error telemetry and dedupe dialog spam (#1571)
Fix missing error info and dedupe spam on web save flow events - webExtensionFetchGetOrCreateSharedWorkSpaceError: propagate the real underlying error.message/name/stack instead of the wrapper constant; use createHttpResponseError so HTTP failures route through sendAPIFailureTelemetry with a real status code (matches remoteFetchProvider pattern). - webExtensionGetSaveParametersError: include entityName + fsPath in payload so RCA isn't blocked by empty errorName/errorMessage; suppress repeat dialog/telemetry for the same fsPath within a session to remove the ~4.6x per-session spam users hit on save retry. Co-authored-by: Amit Joshi <amitjoshi@microsoft.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 34b9bee commit 1335d26

3 files changed

Lines changed: 70 additions & 13 deletions

File tree

src/web/client/dal/remoteSaveProvider.ts

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,16 @@ interface ISaveCallParameters {
2828
requestUrl: string;
2929
}
3030

31+
// Tracks fsPaths that have already triggered the get-save-parameters error
32+
// dialog/telemetry in this session. Subsequent failures for the same fsPath
33+
// are silenced to avoid 4.6x dialog/telemetry spam per session.
34+
const failedSaveParameterPaths = new Set<string>();
35+
36+
// Test-only export to reset the dedup set between cases.
37+
export function _resetFailedSaveParameterPaths() {
38+
failedSaveParameterPaths.clear();
39+
}
40+
3141
export async function saveData(fileUri: vscode.Uri) {
3242
const dataMap: Map<string, FileData> =
3343
WebExtensionContext.fileDataMap.getFileMap;
@@ -112,11 +122,14 @@ async function getSaveParameters(
112122
attributePath.source,
113123
requestUrl
114124
);
115-
} else {
125+
} else if (!failedSaveParameterPaths.has(fileUri.fsPath)) {
126+
failedSaveParameterPaths.add(fileUri.fsPath);
127+
const errorMessage = `${BAD_REQUEST}: missing attributePath for entity '${entityName ?? "unknown"}' at '${fileUri.fsPath}'`;
116128
WebExtensionContext.telemetry.sendErrorTelemetry(
117129
webExtensionTelemetryEventNames.WEB_EXTENSION_GET_SAVE_PARAMETERS_ERROR,
118130
getSaveParameters.name,
119-
BAD_REQUEST
131+
errorMessage,
132+
new Error(errorMessage)
120133
); // no API request is made in this case since we do not know in which column should we save the value
121134
showErrorDialog(
122135
vscode.l10n.t("Unable to complete the request"),

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

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import vscode from "vscode";
77
import * as fetch from "node-fetch";
88
import sinon, { stub, assert } from "sinon";
9-
import { saveData } from "../../dal/remoteSaveProvider";
9+
import { saveData, _resetFailedSaveParameterPaths } from "../../dal/remoteSaveProvider";
1010
import * as schemaHelperUtil from "../../utilities/schemaHelperUtil";
1111
import WebExtensionContext from "../../../client/WebExtensionContext";
1212
import { expect } from "chai";
@@ -20,6 +20,7 @@ import * as errorHandler from "../../../../common/utilities/errorHandlerUtil";
2020
describe("remoteSaveProvider", () => {
2121
afterEach(() => {
2222
sinon.restore();
23+
_resetFailedSaveParameterPaths();
2324
});
2425
it("saveData_whenFetchRetrnsOKAndIsWebFileV2IsFalse_shouldCallAllSuccessTelemetryMethods", async () => {
2526
//Act
@@ -149,15 +150,57 @@ describe("remoteSaveProvider", () => {
149150
);
150151
assert.calledOnce(showErrorDialog);
151152
assert.calledOnce(sendErrorTelemetry);
152-
assert.calledOnceWithExactly(
153-
sendErrorTelemetry,
154-
webExtensionTelemetryEventNames.WEB_EXTENSION_GET_SAVE_PARAMETERS_ERROR,
155-
"getSaveParameters",
156-
BAD_REQUEST
153+
const sendErrorTelemetryCall = sendErrorTelemetry.getCalls()[0];
154+
expect(sendErrorTelemetryCall.args[0]).eq(
155+
webExtensionTelemetryEventNames.WEB_EXTENSION_GET_SAVE_PARAMETERS_ERROR
156+
);
157+
expect(sendErrorTelemetryCall.args[1]).eq("getSaveParameters");
158+
expect(sendErrorTelemetryCall.args[2] as string).contains(BAD_REQUEST);
159+
expect(sendErrorTelemetryCall.args[2] as string).contains("missing attributePath");
160+
expect(sendErrorTelemetryCall.args[3]).instanceOf(Error);
161+
expect((sendErrorTelemetryCall.args[3] as Error).message).eq(
162+
sendErrorTelemetryCall.args[2] as string
157163
);
158164
assert.calledOnce(vscodeParse);
159165
});
160166

167+
it("saveData_whenAttributePathIsNullAndCalledMultipleTimes_shouldShowDialogAndSendTelemetryOnlyOnce", async () => {
168+
const fileUri: vscode.Uri = { fsPath: "dedupuri" } as vscode.Uri;
169+
170+
stub(vscode.Uri, "parse").returns({
171+
fsPath: "dedupuri",
172+
} as vscode.Uri);
173+
stub(schemaHelperUtil, "isWebFileV2").returns(false);
174+
WebExtensionContext.fileDataMap.setEntity(
175+
"",
176+
"",
177+
"entityName",
178+
"",
179+
"",
180+
"",
181+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
182+
null as any,
183+
true,
184+
"pdf"
185+
);
186+
187+
stub(urlBuilderUtil, "getRequestURL").returns(
188+
"https://orgedfe4d6c.crm10.dynamics.com"
189+
);
190+
const sendErrorTelemetry = stub(
191+
WebExtensionContext.telemetry,
192+
"sendErrorTelemetry"
193+
);
194+
const showErrorDialog = stub(errorHandler, "showErrorDialog");
195+
196+
await saveData(fileUri);
197+
await saveData(fileUri);
198+
await saveData(fileUri);
199+
200+
assert.calledOnce(showErrorDialog);
201+
assert.calledOnce(sendErrorTelemetry);
202+
});
203+
161204
it("saveData_shouldCallAllSuccessTelemetryMethods_whenFetchReturnsNotOKAndIsWebFileV2IsFalse", async () => {
162205
//Act
163206
const fileUri: vscode.Uri = { fsPath: "testuri" } as vscode.Uri;

src/web/client/utilities/urlBuilderUtil.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import { getAttributePath, getEntity, getEntityFetchQuery } from "./schemaHelper
2222
import { getWorkSpaceName } from "./commonUtil";
2323
import * as Constants from "../common/constants";
2424
import { webExtensionTelemetryEventNames } from "../../../common/OneDSLoggerTelemetry/web/client/webExtensionTelemetryEvents";
25+
import { createHttpResponseError, isHttpResponseError } from "./errorHandlerUtil";
2526

2627
export const getParameterizedRequestUrlTemplate = (
2728
useSingleEntityUrl: boolean
@@ -309,7 +310,7 @@ export async function getOrCreateSharedWorkspace(config: any) {
309310
)
310311

311312
if (!createWorkspaceResponse.ok) {
312-
throw new Error(JSON.stringify(createWorkspaceResponse));
313+
throw await createHttpResponseError(createWorkspaceResponse);
313314
}
314315

315316
WebExtensionContext.telemetry.sendAPISuccessTelemetry(
@@ -323,22 +324,22 @@ export async function getOrCreateSharedWorkspace(config: any) {
323324
return await createWorkspaceResponse.json();
324325
} catch (error) {
325326
const errorMsg = (error as Error)?.message;
326-
if ((error as Response)?.status > 0) {
327+
if (isHttpResponseError(error) && error.httpDetails) {
327328
WebExtensionContext.telemetry.sendAPIFailureTelemetry(
328329
requestUrl.href,
329330
config.entityName,
330-
Constants.httpMethod.GET,
331+
Constants.httpMethod.POST,
331332
new Date().getTime() - requestSentAtTime,
332333
getOrCreateSharedWorkspace.name,
333334
errorMsg,
334335
'',
335-
(error as Response)?.status.toString()
336+
error.httpDetails.statusCode.toString()
336337
);
337338
} else {
338339
WebExtensionContext.telemetry.sendErrorTelemetry(
339340
webExtensionTelemetryEventNames.WEB_EXTENSION_FETCH_GET_OR_CREATE_SHARED_WORK_SPACE_ERROR,
340341
getOrCreateSharedWorkspace.name,
341-
Constants.WEB_EXTENSION_FETCH_GET_OR_CREATE_SHARED_WORK_SPACE_ERROR,
342+
errorMsg,
342343
error as Error
343344
);
344345
}

0 commit comments

Comments
 (0)