Skip to content

Commit c8f910f

Browse files
committed
feat(desktop): add Datadog RUM and build tooling
- Datadog RUM: config, main process (init from db), renderer, ConnectEnvsToDatadog, ignoreErrors. - Main: read identities/datadogId from db and init Datadog when present. - Logger: forward to Datadog captureException when enabled. - Env, jest, eslint, rspack, sourcemaps, testSetup unmount type, pnpm-lock.
1 parent 3182cec commit c8f910f

29 files changed

+1606
-33
lines changed

apps/ledger-live-desktop/.env.production

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,10 @@ FIREBASE_MESSAGING_SENDER_ID="212042068804"
66
FIREBASE_APP_ID="1:212042068804:web:268d6f11671689c0b51d11"
77
BRAZE_API_KEY="d3af8483-c4ea-4325-b9fa-619038e98b99"
88
BRAZE_CUSTOM_ENDPOINT="sdk.fra-02.braze.eu"
9-
SEGMENT_WRITE_KEY="RduLzmQ1vSH5aeNrSDKWimFrivogTANI"
9+
SEGMENT_WRITE_KEY="RduLzmQ1vSH5aeNrSDKWimFrivogTANI"
10+
11+
# Datadog RUM (production)
12+
DATADOG_APPLICATION_ID="e269c93d-397f-4eae-aeb5-82b061018cd2"
13+
DATADOG_CLIENT_TOKEN="pub46b7d0392f192ba25ca6a00ad486362f"
14+
DATADOG_SITE="datadoghq.eu"
15+
DATADOG_ENV="production"

apps/ledger-live-desktop/.env.staging

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,10 @@ FIREBASE_MESSAGING_SENDER_ID="1008987457941"
66
FIREBASE_APP_ID="1:1008987457941:web:14f6fbee631e0438d6ce9c"
77
BRAZE_API_KEY="25715c7f-d18e-4ef2-9b09-cc5ad10aa514"
88
BRAZE_CUSTOM_ENDPOINT="sdk.fra-02.braze.eu"
9-
SEGMENT_WRITE_KEY="olBQc203GA3fXVa48rJB9c3826CY1axp"
9+
SEGMENT_WRITE_KEY="olBQc203GA3fXVa48rJB9c3826CY1axp"
10+
11+
# Datadog RUM (staging)
12+
DATADOG_APPLICATION_ID="afa5d47a-0092-4b07-bcfa-072040faba3e"
13+
DATADOG_CLIENT_TOKEN="pub8f96d7b432f3ef1cc6db970ca4b9a3ee"
14+
DATADOG_SITE="datadoghq.eu"
15+
DATADOG_ENV="staging"

apps/ledger-live-desktop/.eslintrc.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ module.exports = {
8989
Atomics: "readonly",
9090
SharedArrayBuffer: "readonly",
9191
__SENTRY_URL__: "readonly",
92+
__DATADOG_APPLICATION_ID__: "readonly",
93+
__DATADOG_CLIENT_TOKEN__: "readonly",
94+
__DATADOG_SITE__: "readonly",
95+
__DATADOG_ENV__: "readonly",
9296
__APP_VERSION__: "readonly",
9397
__GIT_REVISION__: "readonly",
9498
__PRERELEASE__: "readonly",

apps/ledger-live-desktop/index-types.d.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
declare const INDEX_URL: string;
22
declare const __SENTRY_URL__: string;
3+
declare const __DATADOG_APPLICATION_ID__: string | null;
4+
declare const __DATADOG_CLIENT_TOKEN__: string | null;
5+
declare const __DATADOG_SITE__: string | null;
6+
declare const __DATADOG_ENV__: string | null;
37
declare const __APP_VERSION__: string;
48
declare const __GIT_REVISION__: string;
59
declare const __PRERELEASE__: string;

apps/ledger-live-desktop/jest.config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ const commonConfig = {
6363
__SENTRY_URL__: null,
6464
__PRERELEASE__: "null",
6565
__CHANNEL__: "null",
66+
__DATADOG_APPLICATION_ID__: null,
67+
__DATADOG_CLIENT_TOKEN__: null,
68+
__DATADOG_SITE__: null,
69+
__DATADOG_ENV__: null,
6670
},
6771
moduleNameMapper,
6872
testPathIgnorePatterns,

apps/ledger-live-desktop/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
"test-deep-links": "ws --spa ledger-live-desktop-deeplinks.html"
5656
},
5757
"dependencies": {
58+
"@datadog/browser-rum": "6.27.1",
5859
"@braze/web-sdk": "6.4.0",
5960
"@electron/fuses": "2.0.0",
6061
"@features/market-banner": "workspace:^",
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { buildBeforeSend, getDatadogBuildConfig } from "./config";
2+
3+
jest.mock("~/sentry/anonymizer", () => ({
4+
__esModule: true,
5+
default: {
6+
filepathRecursiveReplacer: jest.fn((obj: Record<string, unknown>) => {
7+
obj._anonymized = true;
8+
}),
9+
},
10+
}));
11+
12+
jest.mock("./ignoreErrors", () => ({
13+
shouldIgnoreErrorMessage: jest.fn((msg: string) => msg.includes("IGNORE_ME")),
14+
}));
15+
16+
describe("datadog config", () => {
17+
beforeEach(() => {
18+
jest.clearAllMocks();
19+
});
20+
21+
describe("getDatadogBuildConfig", () => {
22+
it("should return config from build globals (null in Jest)", () => {
23+
const result = getDatadogBuildConfig();
24+
expect(result).toEqual({
25+
applicationId: null,
26+
clientToken: null,
27+
site: null,
28+
env: null,
29+
});
30+
});
31+
});
32+
33+
describe("buildBeforeSend", () => {
34+
it("should return false when shouldSend returns false", () => {
35+
const shouldSend = jest.fn().mockReturnValue(false);
36+
const beforeSend = buildBeforeSend(shouldSend);
37+
expect(beforeSend({}, undefined)).toBe(false);
38+
expect(shouldSend).toHaveBeenCalledTimes(1);
39+
});
40+
41+
it("should return true for non-object event", () => {
42+
const shouldSend = jest.fn().mockReturnValue(true);
43+
const beforeSend = buildBeforeSend(shouldSend);
44+
expect(beforeSend(null, undefined)).toBe(true);
45+
expect(beforeSend("string", undefined)).toBe(true);
46+
});
47+
48+
it("should return false when error message matches ignore list", () => {
49+
const shouldSend = jest.fn().mockReturnValue(true);
50+
const beforeSend = buildBeforeSend(shouldSend);
51+
const event = { error: { message: "IGNORE_ME please" } };
52+
expect(beforeSend(event, undefined)).toBe(false);
53+
});
54+
55+
it("should return false when event.message matches ignore list", () => {
56+
const shouldSend = jest.fn().mockReturnValue(true);
57+
const beforeSend = buildBeforeSend(shouldSend);
58+
expect(beforeSend({ message: "IGNORE_ME" }, undefined)).toBe(false);
59+
});
60+
61+
it("should remove server_name from event", () => {
62+
const shouldSend = jest.fn().mockReturnValue(true);
63+
const beforeSend = buildBeforeSend(shouldSend);
64+
const event = { server_name: "my-machine", message: "ok" };
65+
expect(beforeSend(event, undefined)).toBe(true);
66+
expect(event).not.toHaveProperty("server_name");
67+
});
68+
69+
it("should call anonymizer and return true for sendable event", () => {
70+
const anonymizer = jest.requireMock("~/sentry/anonymizer").default;
71+
const shouldSend = jest.fn().mockReturnValue(true);
72+
const beforeSend = buildBeforeSend(shouldSend);
73+
const event: Record<string, unknown> = { message: "Some error" };
74+
expect(beforeSend(event, undefined)).toBe(true);
75+
expect(anonymizer.filepathRecursiveReplacer).toHaveBeenCalledWith(event);
76+
expect(event._anonymized).toBe(true);
77+
});
78+
79+
it("should return true when anonymizer throws (logs and continues)", () => {
80+
const anonymizer = jest.requireMock("~/sentry/anonymizer").default;
81+
jest.mocked(anonymizer.filepathRecursiveReplacer).mockImplementationOnce(() => {
82+
throw new Error("anonymizer failed");
83+
});
84+
const consoleSpy = jest.spyOn(console, "error").mockImplementation();
85+
const shouldSend = jest.fn().mockReturnValue(true);
86+
const beforeSend = buildBeforeSend(shouldSend);
87+
const event = { message: "ok" };
88+
expect(beforeSend(event, undefined)).toBe(true);
89+
expect(consoleSpy).toHaveBeenCalledWith(
90+
"Datadog beforeSend: anonymization failed",
91+
expect.any(Error),
92+
);
93+
consoleSpy.mockRestore();
94+
});
95+
});
96+
});
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import anonymizer from "~/sentry/anonymizer";
2+
import { shouldIgnoreErrorMessage } from "./ignoreErrors";
3+
4+
export type ShouldSendCallback = () => boolean;
5+
6+
/**
7+
* Builds the beforeSend callback for Datadog RUM.
8+
* Drops events when opt-in is off or error message matches ignore list;
9+
* applies anonymization to the payload (parity with Sentry).
10+
*/
11+
export function buildBeforeSend(shouldSend: ShouldSendCallback) {
12+
return (event: unknown, _context?: unknown): boolean => {
13+
if (!shouldSend()) return false;
14+
if (typeof event !== "object" || event === null) return true;
15+
16+
const ev = event as Record<string, unknown>;
17+
18+
// Error events (event.type === 'error'): drop if message matches ignore list
19+
let message = "";
20+
if (ev.error && typeof ev.error === "object") {
21+
const errObj = ev.error as Record<string, unknown>;
22+
if (typeof errObj.message === "string") message = errObj.message;
23+
}
24+
if (!message && typeof ev.message === "string") message = ev.message;
25+
if (message && shouldIgnoreErrorMessage(message)) return false;
26+
27+
// Remove server_name (machine name)
28+
if ("server_name" in ev) delete ev.server_name;
29+
30+
// Anonymize file paths in payload (parity with Sentry)
31+
try {
32+
anonymizer.filepathRecursiveReplacer(ev);
33+
} catch (e) {
34+
console.error("Datadog beforeSend: anonymization failed", e);
35+
}
36+
37+
return true;
38+
};
39+
}
40+
41+
export function getDatadogBuildConfig(): {
42+
applicationId: string | null | undefined;
43+
clientToken: string | null | undefined;
44+
site: string | null | undefined;
45+
env: string | null | undefined;
46+
} {
47+
return {
48+
applicationId: __DATADOG_APPLICATION_ID__,
49+
clientToken: __DATADOG_CLIENT_TOKEN__,
50+
site: __DATADOG_SITE__,
51+
env: __DATADOG_ENV__,
52+
};
53+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { shouldIgnoreErrorMessage, IGNORE_ERROR_MESSAGES } from "./ignoreErrors";
2+
3+
describe("datadog ignoreErrors", () => {
4+
describe("shouldIgnoreErrorMessage", () => {
5+
it("should return false for message not matching any pattern", () => {
6+
expect(shouldIgnoreErrorMessage("Some random error")).toBe(false);
7+
expect(shouldIgnoreErrorMessage("UnknownError")).toBe(false);
8+
});
9+
10+
it("should return true when message includes a string pattern", () => {
11+
expect(shouldIgnoreErrorMessage("API HTTP 500")).toBe(true);
12+
expect(shouldIgnoreErrorMessage("ECONNREFUSED connection failed")).toBe(true);
13+
expect(shouldIgnoreErrorMessage("Network Error")).toBe(true);
14+
expect(shouldIgnoreErrorMessage("Failed to fetch")).toBe(true);
15+
expect(shouldIgnoreErrorMessage("request timed out")).toBe(true);
16+
expect(shouldIgnoreErrorMessage("UserRefusedOnDevice")).toBe(true);
17+
});
18+
19+
it("should return true when message matches RegExp pattern", () => {
20+
// IGNORE_ERROR_MESSAGES uses string includes; check one more
21+
expect(shouldIgnoreErrorMessage("status code 404")).toBe(true);
22+
});
23+
24+
it("should return false for empty string", () => {
25+
expect(shouldIgnoreErrorMessage("")).toBe(false);
26+
});
27+
});
28+
29+
describe("IGNORE_ERROR_MESSAGES", () => {
30+
it("should contain expected patterns", () => {
31+
expect(IGNORE_ERROR_MESSAGES).toContain("API HTTP");
32+
expect(IGNORE_ERROR_MESSAGES).toContain("ECONNREFUSED");
33+
expect(IGNORE_ERROR_MESSAGES).toContain("Failed to fetch");
34+
expect(IGNORE_ERROR_MESSAGES).toContain("UserRefusedOnDevice");
35+
});
36+
});
37+
});
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/**
2+
* Error messages matching any of these patterns will be dropped in Datadog beforeSend.
3+
* Ported from sentry/install.ts for parity. Supports string (includes) and RegExp.
4+
*/
5+
export const IGNORE_ERROR_MESSAGES: (string | RegExp)[] = [
6+
// networking conditions
7+
"API HTTP",
8+
"DisconnectedError",
9+
"EACCES",
10+
"ECONNABORTED",
11+
"ECONNREFUSED",
12+
"ECONNRESET",
13+
"EHOSTUNREACH",
14+
"ENETDOWN",
15+
"ENETUNREACH",
16+
"ENOSPC",
17+
"ENOTFOUND",
18+
"EPERM",
19+
"ERR_CONNECTION_RESET",
20+
"ERR_PROXY_CONNECTION_FAILED",
21+
"ERR_NAME_NOT_RESOLVED",
22+
"ERR_INTERNET_DISCONNECTED",
23+
"ERR_NETWORK_CHANGED",
24+
"ETIMEDOUT",
25+
"getaddrinfo",
26+
"HttpError",
27+
"Network Error",
28+
"NetworkDown",
29+
"NetworkError",
30+
"NotConnectedError",
31+
"socket disconnected",
32+
"socket hang up",
33+
"ERR_SSL_PROTOCOL_ERROR",
34+
"status code 404",
35+
"unable to get local issuer certificate",
36+
"Failed to fetch",
37+
"Failed to load",
38+
// API issues
39+
"LedgerAPI4xx",
40+
"LedgerAPI5xx",
41+
"<!DOCTYPE html",
42+
"Unexpected ''",
43+
"Unexpected '<'",
44+
"Service Unvailable",
45+
"HederaAddAccountError",
46+
// timeouts
47+
"ERR_CONNECTION_TIMED_OUT",
48+
"request timed out",
49+
"SolanaTxConfirmationTimeout",
50+
"timeout",
51+
"TimeoutError",
52+
"Time-out",
53+
"TronTransactionExpired",
54+
"WebsocketConnectionError",
55+
// bad usage of device
56+
"BleError",
57+
"BluetoothRequired",
58+
"CantOpenDevice",
59+
"could not read from HID device",
60+
"DeviceOnDashboardExpected",
61+
"EthAppPleaseEnableContractData",
62+
"CeloAppPleaseEnableContractData",
63+
"VechainAppPleaseEnableContractDataAndMultiClause",
64+
"failed with status code",
65+
"GetAppAndVersionUnsupportedFormat",
66+
"Invalid channel",
67+
"Ledger Device is busy",
68+
"DeviceDisconnectedWhileSendingError",
69+
"ManagerDeviceLocked",
70+
"LockedDeviceError",
71+
"UnresponsiveDeviceError",
72+
"PairingFailed",
73+
"Ledger device: UNKNOWN_ERROR",
74+
"UserRefusedOnDevice",
75+
"FirmwareNotRecognized",
76+
"HwTransportError",
77+
// other
78+
"AccountAwaitingSendPendingOperations",
79+
"AccountNeedResync",
80+
"Cannot update while running on a read-only volume",
81+
"DeviceAppVerifyNotSupported",
82+
"InvalidAddressError",
83+
"Received an invalid JSON-RPC message",
84+
"SwapNoAvailableProviders",
85+
"TransactionRefusedOnDevice",
86+
"Please reimport your Tezos accounts",
87+
"Transaction simulation failed",
88+
"failed to find a healthy working node",
89+
"was reached for request with last error",
90+
"530 undefined",
91+
"524 undefined",
92+
"Missing or invalid topic field",
93+
"Bad status on response: 503",
94+
"Render frame was disposed before WebFrameMain could be accessed",
95+
];
96+
97+
export function shouldIgnoreErrorMessage(message: string): boolean {
98+
return IGNORE_ERROR_MESSAGES.some(pattern => {
99+
if (typeof pattern === "string") {
100+
return message.includes(pattern);
101+
}
102+
return pattern.test(message);
103+
});
104+
}

0 commit comments

Comments
 (0)