From ff48eae47cb16aa7d4404d7ebd447172266d94fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Renaudeau?= Date: Fri, 17 Jun 2022 12:17:01 +0200 Subject: [PATCH] Attach feature flags and envs to Sentry reports --- apps/ledger-live-desktop/release-notes.json | 7 +-- .../ledger-live-desktop/src/internal/index.js | 13 ++++- .../src/main/internal-lifecycle.js | 13 +++++ apps/ledger-live-desktop/src/renderer/App.js | 3 + .../components/ConnectEnvsToSentry.tsx | 56 +++++++++++++++++++ .../src/sentry/internal.js | 4 ++ apps/ledger-live-desktop/src/sentry/main.js | 4 ++ .../src/sentry/renderer.js | 4 ++ apps/ledger-live-mobile/index.js | 35 +++++++++++- .../src/components/FirebaseFeatureFlags.tsx | 49 ++++++++++------ apps/ledger-live-mobile/src/index.js | 1 - 11 files changed, 163 insertions(+), 26 deletions(-) create mode 100644 apps/ledger-live-desktop/src/renderer/components/ConnectEnvsToSentry.tsx diff --git a/apps/ledger-live-desktop/release-notes.json b/apps/ledger-live-desktop/release-notes.json index 77305cebdc30..0637a088a01e 100644 --- a/apps/ledger-live-desktop/release-notes.json +++ b/apps/ledger-live-desktop/release-notes.json @@ -1,6 +1 @@ -[ - { - "tag_name": "2.43.0", - "body": "\nWe’re constantly adding new integrations and working on performance improvements to make Ledger Live a world-class experience. Here is what’s new in this release.\n\n### 🚀 Features\nWe’re excited to announce that Ledger Live is launching support for Cardano, one of the biggest cryptocurrencies based on its total market value. Now you can send and receive Cardano’s native token, ADA. \n\n### 🐛 Fixes\nWe’ve done some bug fixes and made a few small but important changes behind the curtain.\n" - } -] \ No newline at end of file +[] \ No newline at end of file diff --git a/apps/ledger-live-desktop/src/internal/index.js b/apps/ledger-live-desktop/src/internal/index.js index a64aef91ab9d..382ed5808069 100644 --- a/apps/ledger-live-desktop/src/internal/index.js +++ b/apps/ledger-live-desktop/src/internal/index.js @@ -10,7 +10,7 @@ import logger from "~/logger"; import LoggerTransport from "~/logger/logger-transport-internal"; import { executeCommand, unsubscribeCommand, unsubscribeAllCommands } from "./commandHandler"; -import sentry from "~/sentry/internal"; +import sentry, { setTags } from "~/sentry/internal"; process.on("exit", () => { logger.debug("exiting process, unsubscribing all..."); @@ -42,6 +42,12 @@ let sentryEnabled = process.env.INITIAL_SENTRY_ENABLED !== "false"; const userId = process.env.SENTRY_USER_ID || ""; sentry(() => Boolean(userId) && sentryEnabled, userId); +const { INITIAL_SENTRY_TAGS } = process.env; +if (INITIAL_SENTRY_TAGS) { + const parsed = JSON.parse(INITIAL_SENTRY_TAGS); + if (parsed) setTags(parsed); +} + process.on("message", m => { switch (m.type) { case "command": @@ -74,6 +80,11 @@ process.on("message", m => { break; } + case "set-sentry-tags": { + setTags(JSON.parse(m.tagsJSON)); + break; + } + case "internalCrashTest": { logger.critical(new Error("CrashTestInternal")); break; diff --git a/apps/ledger-live-desktop/src/main/internal-lifecycle.js b/apps/ledger-live-desktop/src/main/internal-lifecycle.js index e612726dc2d4..7485c66e26d6 100644 --- a/apps/ledger-live-desktop/src/main/internal-lifecycle.js +++ b/apps/ledger-live-desktop/src/main/internal-lifecycle.js @@ -3,6 +3,7 @@ import { app, ipcMain } from "electron"; import path from "path"; import { setEnvUnsafe, getAllEnvs } from "@ledgerhq/live-common/lib/env"; import { isRestartNeeded } from "~/helpers/env"; +import { setTags } from "~/sentry/main"; import logger from "~/logger"; import { getMainWindow } from "./window-lifecycle"; import InternalProcess from "./InternalProcess"; @@ -19,6 +20,7 @@ const internal = new InternalProcess({ timeout: 3000 }); let sentryEnabled = null; let userId = null; +let sentryTags = null; export function getSentryEnabled(): boolean | null { return sentryEnabled; @@ -28,6 +30,16 @@ export function setUserId(id: string) { userId = id; } +ipcMain.handle("set-sentry-tags", (event, tags) => { + setTags(tags); + const tagsJSON = JSON.stringify(tags); + sentryTags = tagsJSON; + internal.send({ + type: "set-sentry-tags", + tagsJSON, + }); +}); + const spawnCoreProcess = () => { const env = { ...getAllEnvs(), @@ -35,6 +47,7 @@ const spawnCoreProcess = () => { ...process.env, IS_INTERNAL_PROCESS: 1, LEDGER_CONFIG_DIRECTORY, + INITIAL_SENTRY_TAGS: sentryTags, INITIAL_SENTRY_ENABLED: String(!!sentryEnabled), SENTRY_USER_ID: userId, }; diff --git a/apps/ledger-live-desktop/src/renderer/App.js b/apps/ledger-live-desktop/src/renderer/App.js index 6df15372d32a..ef4f6b878106 100644 --- a/apps/ledger-live-desktop/src/renderer/App.js +++ b/apps/ledger-live-desktop/src/renderer/App.js @@ -33,6 +33,8 @@ import { ToastProvider } from "@ledgerhq/live-common/lib/notifications/ToastProv import { themeSelector } from "./actions/general"; // $FlowFixMe import MarketDataProvider from "~/renderer/screens/market/MarketDataProviderWrapper"; +// $FlowFixMe +import { ConnectEnvsToSentry } from "~/renderer/components/ConnectEnvsToSentry"; const reloadApp = event => { if ((event.ctrlKey || event.metaKey) && event.key === "r") { @@ -73,6 +75,7 @@ const InnerApp = ({ initialCountervalues }: { initialCountervalues: * }) => { + diff --git a/apps/ledger-live-desktop/src/renderer/components/ConnectEnvsToSentry.tsx b/apps/ledger-live-desktop/src/renderer/components/ConnectEnvsToSentry.tsx new file mode 100644 index 000000000000..e262c43017d9 --- /dev/null +++ b/apps/ledger-live-desktop/src/renderer/components/ConnectEnvsToSentry.tsx @@ -0,0 +1,56 @@ +import React, { useEffect } from "react"; +import { ipcRenderer } from "electron"; +import { EnvName, getEnv } from "@ledgerhq/live-common/lib/env"; +import { defaultFeatures, useFeatureFlags } from "@ledgerhq/live-common/lib/featureFlags"; +import { FeatureId } from "@ledgerhq/live-common/lib/types"; +import { enabledExperimentalFeatures } from "../experimental"; +import { setTags } from "../../sentry/renderer"; + +function setSentryTagsEverywhere(tags: { [_: string]: any }) { + ipcRenderer.invoke("set-sentry-tags", tags); + setTags(tags); +} + +const MAX_KEYLEN = 32; +function safekey(k: string) { + if (k.length > MAX_KEYLEN) { + const sep = ".."; + const max = MAX_KEYLEN - sep.length; + const split1 = Math.floor(max / 2); + return k.slice(0, split1) + ".." + k.slice(k.length - (max - split1)); + } + return k; +} + +export const ConnectEnvsToSentry = () => { + const featureFlags = useFeatureFlags(); + useEffect(() => { + // This sync the Sentry tags to include the extra information in context of events + const syncTheTags = () => { + const tags: { [_: string]: any } = {}; + // if there are experimental on, we will add them in tags + enabledExperimentalFeatures().forEach((key: EnvName) => { + tags[safekey(key)] = getEnv(key); + }); + // if there are features on, we will add them in tags + const features: { [key in FeatureId]: boolean } = {}; + Object.keys(defaultFeatures).forEach(key => { + const value = featureFlags.getFeature(key); + if (value && value.enabled !== defaultFeatures[key].enabled) { + features[key] = value.enabled; + } + }); + + Object.keys(features).forEach(key => { + tags[safekey(`f_${key}`)] = features[key]; + }); + + setSentryTagsEverywhere(tags); + }; + // We need to wait firebase to load the data and then we set once for all the tags + setTimeout(syncTheTags, 5000); + // We also try to regularly update them so we are sure to get the correct tags (as these are dynamic) + setInterval(syncTheTags, 60000); + }, []); + return null; +}; diff --git a/apps/ledger-live-desktop/src/sentry/internal.js b/apps/ledger-live-desktop/src/sentry/internal.js index 790c9b793c4b..f546a3936460 100644 --- a/apps/ledger-live-desktop/src/sentry/internal.js +++ b/apps/ledger-live-desktop/src/sentry/internal.js @@ -17,3 +17,7 @@ export const captureException = (e: Error) => { export const captureBreadcrumb = (o: *) => { Sentry.addBreadcrumb(o); }; + +export const setTags = (tags: *) => { + Sentry.setTags(tags); +}; diff --git a/apps/ledger-live-desktop/src/sentry/main.js b/apps/ledger-live-desktop/src/sentry/main.js index 55915a9bbaab..ea7d285638eb 100644 --- a/apps/ledger-live-desktop/src/sentry/main.js +++ b/apps/ledger-live-desktop/src/sentry/main.js @@ -17,3 +17,7 @@ export const captureException = (e: Error) => { export const captureBreadcrumb = (o: *) => { Sentry.addBreadcrumb(o); }; + +export const setTags = (tags: *) => { + Sentry.setTags(tags); +}; diff --git a/apps/ledger-live-desktop/src/sentry/renderer.js b/apps/ledger-live-desktop/src/sentry/renderer.js index ef1ac1a3631f..402e40735d89 100644 --- a/apps/ledger-live-desktop/src/sentry/renderer.js +++ b/apps/ledger-live-desktop/src/sentry/renderer.js @@ -20,3 +20,7 @@ export const captureException = (e: Error) => { export const captureBreadcrumb = (o: *) => { Sentry.addBreadcrumb(o); }; + +export const setTags = (tags: *) => { + Sentry.setTags(tags); +}; diff --git a/apps/ledger-live-mobile/index.js b/apps/ledger-live-mobile/index.js index f18a4c14d170..fcfd3233012e 100644 --- a/apps/ledger-live-mobile/index.js +++ b/apps/ledger-live-mobile/index.js @@ -24,11 +24,14 @@ import * as Sentry from "@sentry/react-native"; import Config from "react-native-config"; import VersionNumber from "react-native-version-number"; +import { getEnv } from "@ledgerhq/live-common/lib/env"; import BackgroundRunnerService from "./services/BackgroundRunnerService"; import App, { routingInstrumentation } from "./src"; import { getEnabled } from "./src/components/HookSentry"; import logReport from "./src/log-report"; import pkg from "./package.json"; +import { getAllDivergedFlags } from "./src/components/FirebaseFeatureFlags"; +import { enabledExperimentalFeatures } from "./src/experimental"; // we exclude errors related to user's environment, not fixable by us const excludedErrorName = [ @@ -73,7 +76,7 @@ const excludedErrorDescription = [ "Unable to open URL", "Received an invalid JSON-RPC message", ]; -if (Config.SENTRY_DSN && !__DEV__ && !Config.MOCK) { +if (Config.SENTRY_DSN && (!__DEV__ || Config.FORCE_SENTRY) && !Config.MOCK) { Sentry.init({ dsn: Config.SENTRY_DSN, environment: Config.SENTRY_ENVIRONMENT, @@ -116,6 +119,36 @@ if (Config.SENTRY_DSN && !__DEV__ && !Config.MOCK) { return event; }, }); + + const MAX_KEYLEN = 32; + const safekey = (k: string) => { + if (k.length > MAX_KEYLEN) { + const sep = ".."; + const max = MAX_KEYLEN - sep.length; + const split1 = Math.floor(max / 2); + return k.slice(0, split1) + ".." + k.slice(k.length - (max - split1)); + } + return k; + }; + + // This sync the Sentry tags to include the extra information in context of events + const syncTheTags = () => { + const tags = {}; + // if there are experimental on, we will add them in tags + enabledExperimentalFeatures().forEach(key => { + tags[safekey(key)] = getEnv(key); + }); + // if there are features on, we will add them in tags + const features = getAllDivergedFlags(); + Object.keys(features).forEach(key => { + tags[safekey(`f_${key}`)] = features[key]; + }); + Sentry.setTags(tags); + }; + // We need to wait firebase to load the data and then we set once for all the tags + setTimeout(syncTheTags, 5000); + // We also try to regularly update them so we are sure to get the correct tags (as these are dynamic) + setInterval(syncTheTags, 60000); } if (Config.DISABLE_YELLOW_BOX) { diff --git a/apps/ledger-live-mobile/src/components/FirebaseFeatureFlags.tsx b/apps/ledger-live-mobile/src/components/FirebaseFeatureFlags.tsx index 929607af0e69..19962dbee5cb 100644 --- a/apps/ledger-live-mobile/src/components/FirebaseFeatureFlags.tsx +++ b/apps/ledger-live-mobile/src/components/FirebaseFeatureFlags.tsx @@ -1,6 +1,9 @@ import React, { ReactNode } from "react"; import remoteConfig from "@react-native-firebase/remote-config"; -import { FeatureFlagsProvider } from "@ledgerhq/live-common/lib/featureFlags"; +import { + FeatureFlagsProvider, + defaultFeatures, +} from "@ledgerhq/live-common/lib/featureFlags"; import { FeatureId } from "@ledgerhq/live-common/lib/types"; import { formatFeatureId } from "./FirebaseRemoteConfig"; @@ -9,22 +12,34 @@ type Props = { children?: ReactNode; }; -export const FirebaseFeatureFlagsProvider = ({ children }: Props) => { - const getFeature = (key: FeatureId) => { - try { - const value = remoteConfig().getValue(formatFeatureId(key)); - const feature = JSON.parse(value.asString()); +const getFeature = (key: FeatureId) => { + try { + const value = remoteConfig().getValue(formatFeatureId(key)); + const feature = JSON.parse(value.asString()); - return feature; - } catch (error) { - console.error(`Failed to retrieve feature "${key}"`); - return null; - } - }; + return feature; + } catch (error) { + console.error(`Failed to retrieve feature "${key}"`); + return null; + } +}; - return ( - - {children} - - ); +/** + * @returns all flags that have different defaults are exported as a key value map + */ +export const getAllDivergedFlags = (): { [key in FeatureId]: boolean } => { + const res: { [key in FeatureId]: boolean } = {}; + Object.keys(defaultFeatures).forEach(key => { + const value = getFeature(key); + if (value && value.enabled !== defaultFeatures[key].enabled) { + res[key] = value.enabled; + } + }); + return res; }; + +export const FirebaseFeatureFlagsProvider = ({ children }: Props) => ( + + {children} + +); diff --git a/apps/ledger-live-mobile/src/index.js b/apps/ledger-live-mobile/src/index.js index ba20486fc3a3..c6025bcf6ff4 100644 --- a/apps/ledger-live-mobile/src/index.js +++ b/apps/ledger-live-mobile/src/index.js @@ -35,7 +35,6 @@ import { checkLibs } from "@ledgerhq/live-common/lib/sanityChecks"; import { FeatureToggle } from "@ledgerhq/live-common/lib/featureFlags"; import { useCountervaluesExport } from "@ledgerhq/live-common/lib/countervalues/react"; import { pairId } from "@ledgerhq/live-common/lib/countervalues/helpers"; - import { NftMetadataProvider } from "@ledgerhq/live-common/lib/nft"; import { ToastProvider } from "@ledgerhq/live-common/lib/notifications/ToastProvider"; import { GlobalCatalogProvider } from "@ledgerhq/live-common/lib/platform/providers/GlobalCatalogProvider";