diff --git a/app/client/src/AppErrorBoundry.tsx b/app/client/src/AppErrorBoundry.tsx index 5ee4d1346ff0..76a40a4a2db3 100644 --- a/app/client/src/AppErrorBoundry.tsx +++ b/app/client/src/AppErrorBoundry.tsx @@ -4,7 +4,7 @@ import AppCrashImage from "assets/images/404-image.png"; import log from "loglevel"; import AnalyticsUtil from "ee/utils/AnalyticsUtil"; import { Button } from "@appsmith/ads"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; const Wrapper = styled.div` display: flex; @@ -32,7 +32,9 @@ class AppErrorBoundary extends Component { componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { log.error({ error, errorInfo }); - captureException(error, { errorName: "AppErrorBoundary" }); + appsmithTelemetry.captureException(error, { + errorName: "AppErrorBoundary", + }); AnalyticsUtil.logEvent("APP_CRASH", { error, errorInfo }); this.setState({ hasError: true, diff --git a/app/client/src/PluginActionEditor/components/PluginActionForm/components/UQIEditor/FormRender.tsx b/app/client/src/PluginActionEditor/components/PluginActionForm/components/UQIEditor/FormRender.tsx index 3bcfdba1b515..d3e9205e8538 100644 --- a/app/client/src/PluginActionEditor/components/PluginActionForm/components/UQIEditor/FormRender.tsx +++ b/app/client/src/PluginActionEditor/components/PluginActionForm/components/UQIEditor/FormRender.tsx @@ -9,7 +9,7 @@ import { Tag } from "@blueprintjs/core"; import styled from "styled-components"; import { UIComponentTypes } from "entities/Plugin"; import log from "loglevel"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; import type { FormEvalOutput } from "reducers/evaluationReducers/formEvaluationReducer"; import { checkIfSectionCanRender, @@ -103,7 +103,7 @@ const FormRender = (props: Props) => { } } catch (e) { log.error(e); - captureException(e, { errorName: "FormRenderError" }); + appsmithTelemetry.captureException(e, { errorName: "FormRenderError" }); return ( => { if (!payload.id) { - captureException(new Error("Attempting to update page without page id"), { - errorName: "PageActions_UpdatePage", - }); + appsmithTelemetry.captureException( + new Error("Attempting to update page without page id"), + { + errorName: "PageActions_UpdatePage", + }, + ); } return { diff --git a/app/client/src/api/helpers/validateJsonResponseMeta.ts b/app/client/src/api/helpers/validateJsonResponseMeta.ts index 7afbea263391..f23d984cd4dd 100644 --- a/app/client/src/api/helpers/validateJsonResponseMeta.ts +++ b/app/client/src/api/helpers/validateJsonResponseMeta.ts @@ -1,4 +1,4 @@ -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; import type { AxiosResponse } from "axios"; import { CONTENT_TYPE_HEADER_KEY } from "PluginActionEditor/constants/CommonApiConstants"; @@ -7,9 +7,12 @@ export const validateJsonResponseMeta = (response: AxiosResponse) => { response.headers[CONTENT_TYPE_HEADER_KEY] === "application/json" && !response.data.responseMeta ) { - captureException(new Error("Api responded without response meta"), { - errorName: "ValidateJsonResponseMeta", - contexts: { response: response.data }, - }); + appsmithTelemetry.captureException( + new Error("Api responded without response meta"), + { + errorName: "ValidateJsonResponseMeta", + contexts: { response: response.data }, + }, + ); } }; diff --git a/app/client/src/api/interceptors/response/failureHandlers/handleMissingResponseMeta.ts b/app/client/src/api/interceptors/response/failureHandlers/handleMissingResponseMeta.ts index 9a7971c5e207..8cfdb305edc1 100644 --- a/app/client/src/api/interceptors/response/failureHandlers/handleMissingResponseMeta.ts +++ b/app/client/src/api/interceptors/response/failureHandlers/handleMissingResponseMeta.ts @@ -1,15 +1,18 @@ import type { AxiosError } from "axios"; import type { ApiResponse } from "api/types"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; export const handleMissingResponseMeta = async ( error: AxiosError, ) => { if (error.response?.data && !error.response.data.responseMeta) { - captureException(new Error("Api responded without response meta"), { - errorName: "MissingResponseMeta", - contexts: { response: { ...error.response.data } }, - }); + appsmithTelemetry.captureException( + new Error("Api responded without response meta"), + { + errorName: "MissingResponseMeta", + contexts: { response: { ...error.response.data } }, + }, + ); return Promise.reject(error.response.data); } diff --git a/app/client/src/api/interceptors/response/failureHandlers/handleNotFoundError.ts b/app/client/src/api/interceptors/response/failureHandlers/handleNotFoundError.ts index a9b3e27b0bd9..4fcdeeb97ed7 100644 --- a/app/client/src/api/interceptors/response/failureHandlers/handleNotFoundError.ts +++ b/app/client/src/api/interceptors/response/failureHandlers/handleNotFoundError.ts @@ -6,7 +6,7 @@ import { import type { AxiosError } from "axios"; import type { ApiResponse } from "api/types"; import { is404orAuthPath } from "api/helpers"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; export async function handleNotFoundError(error: AxiosError) { if (is404orAuthPath()) return null; @@ -20,7 +20,7 @@ export async function handleNotFoundError(error: AxiosError) { (SERVER_ERROR_CODES.RESOURCE_NOT_FOUND.includes(errorData.error?.code) || SERVER_ERROR_CODES.UNABLE_TO_FIND_PAGE.includes(errorData?.error?.code)) ) { - captureException(error, { errorName: "NotFoundError" }); + appsmithTelemetry.captureException(error, { errorName: "NotFoundError" }); return Promise.reject({ ...error, diff --git a/app/client/src/api/interceptors/response/failureHandlers/handleUnauthorizedError.ts b/app/client/src/api/interceptors/response/failureHandlers/handleUnauthorizedError.ts index f096a3eeeed1..7c6ba9a99171 100644 --- a/app/client/src/api/interceptors/response/failureHandlers/handleUnauthorizedError.ts +++ b/app/client/src/api/interceptors/response/failureHandlers/handleUnauthorizedError.ts @@ -4,7 +4,7 @@ import { is404orAuthPath } from "api/helpers"; import { logoutUser } from "actions/userActions"; import { AUTH_LOGIN_URL } from "constants/routes"; import { API_STATUS_CODES, ERROR_CODES } from "ee/constants/ApiConstants"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; export const handleUnauthorizedError = async (error: AxiosError) => { if (is404orAuthPath()) return null; @@ -20,7 +20,9 @@ export const handleUnauthorizedError = async (error: AxiosError) => { }), ); - captureException(error, { errorName: "UnauthorizedError" }); + appsmithTelemetry.captureException(error, { + errorName: "UnauthorizedError", + }); return Promise.reject({ ...error, diff --git a/app/client/src/ce/sagas/PageSagas.tsx b/app/client/src/ce/sagas/PageSagas.tsx index bbadd0ab499d..f69691b5c26b 100644 --- a/app/client/src/ce/sagas/PageSagas.tsx +++ b/app/client/src/ce/sagas/PageSagas.tsx @@ -151,7 +151,7 @@ import { selectCombinedPreviewMode, selectGitApplicationCurrentBranch, } from "selectors/gitModSelectors"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; export interface HandleWidgetNameUpdatePayload { newName: string; @@ -575,9 +575,12 @@ export function* savePageSaga(action: ReduxAction<{ isRetry?: boolean }>) { const { message } = incorrectBindingError; if (isRetry) { - captureException(new Error("Failed to correct binding paths"), { - errorName: "PageSagas_BindingPathCorrection", - }); + appsmithTelemetry.captureException( + new Error("Failed to correct binding paths"), + { + errorName: "PageSagas_BindingPathCorrection", + }, + ); yield put({ type: ReduxActionErrorTypes.FAILED_CORRECTING_BINDING_PATHS, payload: { diff --git a/app/client/src/ce/utils/AnalyticsUtil.tsx b/app/client/src/ce/utils/AnalyticsUtil.tsx index 6bca41a7ea2d..bcd16f595d46 100644 --- a/app/client/src/ce/utils/AnalyticsUtil.tsx +++ b/app/client/src/ce/utils/AnalyticsUtil.tsx @@ -9,7 +9,7 @@ import SegmentSingleton from "utils/Analytics/segment"; import MixpanelSingleton, { type SessionRecordingConfig, } from "utils/Analytics/mixpanel"; -import FaroUtil from "utils/Analytics/sentry"; +import { appsmithTelemetry } from "instrumentation"; import SmartlookUtil from "utils/Analytics/smartlook"; import TrackedUser from "ee/utils/Analytics/trackedUser"; @@ -94,7 +94,7 @@ async function identifyUser(userData: User, sendAdditionalData?: boolean) { await segmentAnalytics.identify(trackedUser.userId, userProperties); } - FaroUtil.identifyUser(trackedUser.userId, userData); + appsmithTelemetry.identifyUser(trackedUser.userId, userData); if (trackedUser.email) { SmartlookUtil.identify(trackedUser.userId, trackedUser.email); diff --git a/app/client/src/components/editorComponents/CodeEditor/EvaluatedValuePopup.tsx b/app/client/src/components/editorComponents/CodeEditor/EvaluatedValuePopup.tsx index 0af92d97beed..8538bc671908 100644 --- a/app/client/src/components/editorComponents/CodeEditor/EvaluatedValuePopup.tsx +++ b/app/client/src/components/editorComponents/CodeEditor/EvaluatedValuePopup.tsx @@ -28,7 +28,7 @@ import { getPathNavigationUrl } from "selectors/navigationSelectors"; import { Button, Icon, Link, toast, Tooltip } from "@appsmith/ads"; import type { EvaluationError } from "utils/DynamicBindingUtils"; import { DEBUGGER_TAB_KEYS } from "../Debugger/constants"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; const modifiers: IPopoverSharedProps["modifiers"] = { offset: { @@ -290,10 +290,13 @@ export function PreparedStatementViewer(props: { const { parameters, value } = props.evaluatedValue; if (!value) { - captureException(new Error("Prepared statement got no value"), { - errorName: "PreparedStatementError", - extra: { props }, - }); + appsmithTelemetry.captureException( + new Error("Prepared statement got no value"), + { + errorName: "PreparedStatementError", + extra: { props }, + }, + ); return
; } diff --git a/app/client/src/components/editorComponents/ErrorBoundry.tsx b/app/client/src/components/editorComponents/ErrorBoundry.tsx index e0d1efcf7b02..d43461e55cb3 100644 --- a/app/client/src/components/editorComponents/ErrorBoundry.tsx +++ b/app/client/src/components/editorComponents/ErrorBoundry.tsx @@ -1,7 +1,7 @@ import React from "react"; import styled from "styled-components"; import * as log from "loglevel"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; import type { ReactNode, CSSProperties } from "react"; @@ -39,7 +39,7 @@ class ErrorBoundary extends React.Component { // eslint-disable-next-line @typescript-eslint/no-explicit-any componentDidCatch(error: any, errorInfo: any) { log.error({ error, errorInfo }); - captureException(error, { errorName: "ErrorBoundary" }); + appsmithTelemetry.captureException(error, { errorName: "ErrorBoundary" }); } render() { diff --git a/app/client/src/components/propertyControls/TabControl.tsx b/app/client/src/components/propertyControls/TabControl.tsx index 94500969ba34..d8b648c94229 100644 --- a/app/client/src/components/propertyControls/TabControl.tsx +++ b/app/client/src/components/propertyControls/TabControl.tsx @@ -10,7 +10,7 @@ import isString from "lodash/isString"; import isUndefined from "lodash/isUndefined"; import includes from "lodash/includes"; import map from "lodash/map"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; import { useDispatch } from "react-redux"; import { ReduxActionTypes } from "ee/constants/ReduxActionConstants"; import { DraggableListControl } from "pages/Editor/PropertyPane/DraggableListControl"; @@ -131,7 +131,7 @@ class TabControl extends BaseControl { return parsedData; } catch (error) { - captureException( + appsmithTelemetry.captureException( { message: "Tab Migration Failed", oldData: this.props.propertyValue, diff --git a/app/client/src/ee/sagas/index.tsx b/app/client/src/ee/sagas/index.tsx index 1de97860848a..1589af1ad9bd 100644 --- a/app/client/src/ee/sagas/index.tsx +++ b/app/client/src/ee/sagas/index.tsx @@ -2,8 +2,8 @@ export * from "ce/sagas"; import { sagas as CE_Sagas } from "ce/sagas"; import { ReduxActionTypes } from "ee/constants/ReduxActionConstants"; import { call, all, spawn, race, take } from "redux-saga/effects"; +import { appsmithTelemetry } from "instrumentation"; import log from "loglevel"; -import captureException from "instrumentation/sendFaroErrors"; const sagasArr = [...CE_Sagas]; @@ -22,7 +22,9 @@ export function* rootSaga(sagasToRun = sagasArr): any { break; } catch (e) { log.error(e); - captureException(e, { errorName: "RootSagaError" }); + appsmithTelemetry.captureException(e, { + errorName: "RootSagaError", + }); } } }), diff --git a/app/client/src/git/sagas/helpers/handleApiErrors.ts b/app/client/src/git/sagas/helpers/handleApiErrors.ts index fd7fd69e91f5..fcf71fe13e10 100644 --- a/app/client/src/git/sagas/helpers/handleApiErrors.ts +++ b/app/client/src/git/sagas/helpers/handleApiErrors.ts @@ -1,4 +1,4 @@ -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; import type { ApiResponse } from "api/types"; import log from "loglevel"; @@ -22,7 +22,7 @@ export default function handleApiErrors(error?: Error, response?: ApiResponse) { } } else { log.error(error); - captureException(error, { errorName: "GitApiError" }); + appsmithTelemetry.captureException(error, { errorName: "GitApiError" }); } return apiError; diff --git a/app/client/src/index.tsx b/app/client/src/index.tsx index 2ebad23bccfa..de9bb46c9156 100755 --- a/app/client/src/index.tsx +++ b/app/client/src/index.tsx @@ -27,22 +27,18 @@ import GlobalStyles from "globalStyles"; import AppErrorBoundary from "./AppErrorBoundry"; import log from "loglevel"; import { FaroErrorBoundary } from "@grafana/faro-react"; -import { isTracingEnabled } from "instrumentation/utils"; runSagaMiddleware(); appInitializer(); -isTracingEnabled() && - (async () => { - try { - await import( - /* webpackChunkName: "instrumentation" */ "./instrumentation" - ); - } catch (e) { - log.error("Error loading telemetry script", e); - } - })(); +(async () => { + try { + await import(/* webpackChunkName: "instrumentation" */ "./instrumentation"); + } catch (e) { + log.error("Error loading telemetry script", e); + } +})(); function App() { return ( diff --git a/app/client/src/instrumentation/PageLoadInstrumentation.ts b/app/client/src/instrumentation/PageLoadInstrumentation.ts deleted file mode 100644 index d8d4d08b6695..000000000000 --- a/app/client/src/instrumentation/PageLoadInstrumentation.ts +++ /dev/null @@ -1,389 +0,0 @@ -import type { Span } from "instrumentation/types"; -import { InstrumentationBase } from "@opentelemetry/instrumentation"; -import { startRootSpan, startNestedSpan } from "./generateTraces"; -import { onLCP, onFCP } from "web-vitals/attribution"; -import type { - LCPMetricWithAttribution, - FCPMetricWithAttribution, - NavigationTimingPolyfillEntry, -} from "web-vitals"; -import isString from "lodash/isString"; - -type TNavigator = Navigator & { - deviceMemory: number; - connection: { - effectiveType: string; - downlink: number; - rtt: number; - saveData: boolean; - }; -}; - -export class PageLoadInstrumentation extends InstrumentationBase { - // PerformanceObserver to observe resource timings - resourceTimingObserver: PerformanceObserver | null = null; - // Root span for the page load instrumentation - rootSpan: Span; - // List of resource URLs to ignore - ignoreResourceUrls: string[] = []; - // Flag to check if navigation entry was pushed - wasNavigationEntryPushed: boolean = false; - // Set to keep track of resource entries - resourceEntriesSet: Set = new Set(); - // Timeout for polling resource entries - resourceEntryPollTimeout: number | null = null; - - constructor({ ignoreResourceUrls = [] }: { ignoreResourceUrls?: string[] }) { - // Initialize the base instrumentation with the name and version - super("appsmith-page-load-instrumentation", "1.0.0", { - enabled: true, - }); - this.ignoreResourceUrls = ignoreResourceUrls; - // Start the root span for the page load - this.rootSpan = startRootSpan("PAGE_LOAD", {}, 0); - - // Initialize the instrumentation after starting the root span - this.init(); - } - - init() { - // Register connection change listener - this.addConnectionAttributes(); - - // Add device attributes to the root span - this.addDeviceAttributes(); - - // Listen for LCP and FCP events - // reportAllChanges: true will report all LCP and FCP events - // binding the context to the class to access class properties - onLCP(this.onLCPReport.bind(this), { reportAllChanges: true }); - onFCP(this.onFCPReport.bind(this), { reportAllChanges: true }); - - // Check if PerformanceObserver is available - if (PerformanceObserver) { - this.observeResourceTimings(); - } else { - // If PerformanceObserver is not available, fallback to polling - this.pollResourceTimingEntries(); - } - } - - enable() { - // enable method is present in the base class and needs to be implemented - // Leaving it empty as there is no need to do anything here - } - - private addDeviceAttributes() { - this.rootSpan.setAttributes({ - deviceMemory: (navigator as TNavigator).deviceMemory, - hardwareConcurrency: navigator.hardwareConcurrency, - }); - } - - private addConnectionAttributes() { - if ((navigator as TNavigator).connection) { - const { downlink, effectiveType, rtt, saveData } = ( - navigator as TNavigator - ).connection; - - this.rootSpan.setAttributes({ - effectiveConnectionType: effectiveType, - connectionDownlink: downlink, - connectionRtt: rtt, - connectionSaveData: saveData, - }); - } - } - - // Handler for LCP report - private onLCPReport(metric: LCPMetricWithAttribution) { - const { - attribution: { lcpEntry }, - } = metric; - - if (lcpEntry) { - this.pushLcpTimingToSpan(lcpEntry); - } - } - - // Handler for FCP report - private onFCPReport(metric: FCPMetricWithAttribution) { - const { - attribution: { fcpEntry, navigationEntry }, - } = metric; - - // Push navigation entry only once - // This is to avoid pushing multiple navigation entries - if (navigationEntry && !this.wasNavigationEntryPushed) { - this.pushNavigationTimingToSpan(navigationEntry); - this.wasNavigationEntryPushed = true; - } - - if (fcpEntry) { - this.pushPaintTimingToSpan(fcpEntry); - } - } - - private getElementName(element?: Element | null, depth = 0): string { - // Limit the depth to 3 to avoid long element names - if (!element || depth > 3) { - return ""; - } - - const elementTestId = element.getAttribute("data-testid"); - const className = isString(element.className) - ? "." + element.className.split(" ").join(".") - : ""; - const elementId = element.id ? `#${element.id}` : ""; - - const elementName = `${element.tagName}${elementId}${className}:${elementTestId}`; - - // Recursively get the parent element names - const parentElementName = this.getElementName( - element.parentElement, - depth + 1, - ); - - return `${parentElementName} > ${elementName}`; - } - - // Convert kebab-case to SCREAMING_SNAKE_CASE - private kebabToScreamingSnakeCase(str: string) { - return str.replace(/-/g, "_").toUpperCase(); - } - - // Push paint timing to span - private pushPaintTimingToSpan(entry: PerformanceEntry) { - const paintSpan = startNestedSpan( - this.kebabToScreamingSnakeCase(entry.name), - this.rootSpan, - {}, - 0, - ); - - paintSpan.end(entry.startTime); - } - - // Push LCP timing to span - private pushLcpTimingToSpan(entry: LargestContentfulPaint) { - const { element, entryType, loadTime, renderTime, startTime, url } = entry; - - const lcpSpan = startNestedSpan( - this.kebabToScreamingSnakeCase(entryType), - this.rootSpan, - { - url, - renderTime, - element: this.getElementName(element), - entryType, - loadTime, - }, - 0, - ); - - lcpSpan.end(startTime); - } - - // Push navigation timing to span - private pushNavigationTimingToSpan( - entry: PerformanceNavigationTiming | NavigationTimingPolyfillEntry, - ) { - const { - connectEnd, - connectStart, - domainLookupEnd, - domainLookupStart, - domComplete, - domContentLoadedEventEnd, - domContentLoadedEventStart, - domInteractive, - entryType, - fetchStart, - loadEventEnd, - loadEventStart, - name: url, - redirectEnd, - redirectStart, - requestStart, - responseEnd, - responseStart, - secureConnectionStart, - startTime: navigationStartTime, - type: navigationType, - unloadEventEnd, - unloadEventStart, - workerStart, - } = entry; - - this.rootSpan.setAttributes({ - connectEnd, - connectStart, - decodedBodySize: - (entry as PerformanceNavigationTiming).decodedBodySize || 0, - domComplete, - domContentLoadedEventEnd, - domContentLoadedEventStart, - domInteractive, - domainLookupEnd, - domainLookupStart, - encodedBodySize: - (entry as PerformanceNavigationTiming).encodedBodySize || 0, - entryType, - fetchStart, - initiatorType: - (entry as PerformanceNavigationTiming).initiatorType || "navigation", - loadEventEnd, - loadEventStart, - nextHopProtocol: - (entry as PerformanceNavigationTiming).nextHopProtocol || "", - redirectCount: (entry as PerformanceNavigationTiming).redirectCount || 0, - redirectEnd, - redirectStart, - requestStart, - responseEnd, - responseStart, - secureConnectionStart, - navigationStartTime, - transferSize: (entry as PerformanceNavigationTiming).transferSize || 0, - navigationType, - url, - unloadEventEnd, - unloadEventStart, - workerStart, - }); - - this.rootSpan?.end(entry.domContentLoadedEventEnd); - } - - // Observe resource timings using PerformanceObserver - private observeResourceTimings() { - this.resourceTimingObserver = new PerformanceObserver((list) => { - const entries = list.getEntries() as PerformanceResourceTiming[]; - const resources = this.getResourcesToTrack(entries); - - resources.forEach((entry) => { - this.pushResourceTimingToSpan(entry); - }); - }); - - this.resourceTimingObserver.observe({ - type: "resource", - buffered: true, - }); - } - - // Filter out resources to track based on ignoreResourceUrls - private getResourcesToTrack(resources: PerformanceResourceTiming[]) { - return resources.filter(({ name }) => { - return !this.ignoreResourceUrls.some((ignoreUrl) => - name.includes(ignoreUrl), - ); - }); - } - - // Push resource timing to span - private pushResourceTimingToSpan(entry: PerformanceResourceTiming) { - const { - connectEnd, - connectStart, - decodedBodySize, - domainLookupEnd, - domainLookupStart, - duration: resourceDuration, - encodedBodySize, - entryType, - fetchStart, - initiatorType, - name: url, - nextHopProtocol, - redirectEnd, - redirectStart, - requestStart, - responseEnd, - responseStart, - secureConnectionStart, - transferSize, - workerStart, - } = entry; - - const resourceSpan = startNestedSpan( - entry.name, - this.rootSpan, - { - connectEnd, - connectStart, - decodedBodySize, - domainLookupEnd, - domainLookupStart, - encodedBodySize, - entryType, - fetchStart, - // TODO: Fix this the next time the file is edited - // eslint-disable-next-line @typescript-eslint/no-explicit-any - firstInterimResponseStart: (entry as any).firstInterimResponseStart, - initiatorType, - nextHopProtocol, - redirectEnd, - redirectStart, - requestStart, - responseEnd, - responseStart, - resourceDuration, - secureConnectionStart, - transferSize, - url, - workerStart, - // TODO: Fix this the next time the file is edited - // eslint-disable-next-line @typescript-eslint/no-explicit-any - renderBlockingStatus: (entry as any).renderBlockingStatus, - }, - entry.startTime, - ); - - resourceSpan.end(entry.startTime + entry.responseEnd); - } - - // Get unique key for a resource entry - private getResourceEntryKey(entry: PerformanceResourceTiming) { - return `${entry.name}:${entry.startTime}:${entry.entryType}`; - } - - // Poll resource timing entries periodically - private pollResourceTimingEntries() { - // Clear the previous timeout - if (this.resourceEntryPollTimeout) { - clearInterval(this.resourceEntryPollTimeout); - } - - const resources = performance.getEntriesByType( - "resource", - ) as PerformanceResourceTiming[]; - - const filteredResources = this.getResourcesToTrack(resources); - - filteredResources.forEach((entry) => { - const key = this.getResourceEntryKey(entry); - - if (!this.resourceEntriesSet.has(key)) { - this.pushResourceTimingToSpan(entry); - this.resourceEntriesSet.add(key); - } - }); - - // Poll every 5 seconds - this.resourceEntryPollTimeout = setTimeout( - this.pollResourceTimingEntries, - 5000, - ); - } - - disable(): void { - if (this.resourceTimingObserver) { - this.resourceTimingObserver.disconnect(); - } - - if (this.rootSpan) { - this.rootSpan.end(); - } - } -} diff --git a/app/client/src/instrumentation/generateTraces.ts b/app/client/src/instrumentation/generateTraces.ts index 5159239e58e2..892b9a10544c 100644 --- a/app/client/src/instrumentation/generateTraces.ts +++ b/app/client/src/instrumentation/generateTraces.ts @@ -5,11 +5,11 @@ import type { SpanOptions, } from "@opentelemetry/api"; import { SpanKind } from "@opentelemetry/api"; -import { getTraceAndContext } from "./index"; +import { appsmithTelemetry } from "./index"; import type { WebworkerSpanData } from "./types"; import { getCommonTelemetryAttributes } from "./utils"; -const { context, trace } = getTraceAndContext(); +const { context, trace } = appsmithTelemetry.getTraceAndContext(); const DEFAULT_TRACE = "default"; diff --git a/app/client/src/instrumentation/index.ts b/app/client/src/instrumentation/index.ts index 8363bcf04880..1db0c7089984 100644 --- a/app/client/src/instrumentation/index.ts +++ b/app/client/src/instrumentation/index.ts @@ -1,6 +1,5 @@ import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base"; import { WebTracerProvider } from "@opentelemetry/sdk-trace-web"; -import { ZoneContextManager } from "@opentelemetry/context-zone"; import { trace, context } from "@opentelemetry/api"; import { Resource } from "@opentelemetry/resources"; import { @@ -23,29 +22,27 @@ import { } from "@grafana/faro-web-tracing"; import log from "loglevel"; import { isTracingEnabled } from "instrumentation/utils"; +import { v4 as uuidv4 } from "uuid"; +import { error as errorLogger } from "loglevel"; +import type { User } from "constants/userConstants"; -declare global { - interface Window { - faro: Faro | null; - } -} - -const { appVersion, observability } = getAppsmithConfigs(); -const { deploymentName, serviceInstanceId, serviceName, tracingUrl } = - observability; +class AppsmithTelemetry { + private faro: Faro | null; + private ignoreUrls = ["smartlook.cloud"]; + private internalLoggerLevel: InternalLoggerLevel; + private static instance: AppsmithTelemetry | null; -let faro: Faro | null = null; + constructor() { + this.internalLoggerLevel = + log.getLevel() === log.levels.DEBUG + ? InternalLoggerLevel.ERROR + : InternalLoggerLevel.OFF; + const { appVersion, observability } = getAppsmithConfigs(); + const { deploymentName, serviceInstanceId, serviceName, tracingUrl } = + observability; -if (isTracingEnabled()) { - const ignoreUrls = ["smartlook.cloud"]; - const internalLoggerLevel = - log.getLevel() === log.levels.DEBUG - ? InternalLoggerLevel.ERROR - : InternalLoggerLevel.OFF; - - try { - if (!window.faro) { - faro = initializeFaro({ + if (isTracingEnabled()) { + this.faro = initializeFaro({ url: tracingUrl, app: { name: serviceName, @@ -56,7 +53,7 @@ if (isTracingEnabled()) { new ReactIntegration(), ...getWebInstrumentations({}), ], - ignoreUrls, + ignoreUrls: this.ignoreUrls, consoleInstrumentation: { disabledLevels: [ LogLevel.DEBUG, @@ -67,7 +64,7 @@ if (isTracingEnabled()) { }, trackResources: true, trackWebVitalsAttribution: true, - internalLoggerLevel, + internalLoggerLevel: this.internalLoggerLevel, sessionTracking: { generateSessionId: () => { // Disabling session tracing will not send any instrumentation data to the grafana backend @@ -76,46 +73,112 @@ if (isTracingEnabled()) { }, }, }); + + const tracerProvider = new WebTracerProvider({ + resource: new Resource({ + [ATTR_DEPLOYMENT_NAME]: deploymentName, + [ATTR_SERVICE_INSTANCE_ID]: serviceInstanceId, + [ATTR_SERVICE_NAME]: serviceName, + }), + }); + + tracerProvider.addSpanProcessor( + new FaroSessionSpanProcessor( + new BatchSpanProcessor(new FaroTraceExporter({ ...this.faro })), + this.faro.metas, + ), + ); + + this.faro.api.initOTEL(trace, context); + } else { + this.faro = null; } - } catch (error) { - // eslint-disable-next-line no-console - console.error(error); } - faro = window.faro; - - const tracerProvider = new WebTracerProvider({ - resource: new Resource({ - [ATTR_DEPLOYMENT_NAME]: deploymentName, - [ATTR_SERVICE_INSTANCE_ID]: serviceInstanceId, - [ATTR_SERVICE_NAME]: serviceName, - }), - }); - - if (faro) { - tracerProvider.addSpanProcessor( - new FaroSessionSpanProcessor( - new BatchSpanProcessor(new FaroTraceExporter({ ...faro })), - faro.metas, - ), - ); - - tracerProvider.register({ - contextManager: new ZoneContextManager(), - }); - - faro.api.initOTEL(trace, context); + public identifyUser(userId: string, userData: User) { + if (this.faro) { + this.faro.api.setUser({ + id: userId, + username: userData.username, + email: userData.email, + }); + } } -} -export const getTraceAndContext = () => { - if (!faro) { - return { trace, context }; + public static getInstance() { + if (!AppsmithTelemetry.instance) { + AppsmithTelemetry.instance = new AppsmithTelemetry(); + } + + return AppsmithTelemetry.instance; + } + + public getTraceAndContext() { + const otel = this.faro?.api.getOTEL(); + + if (!otel || !this.faro) { + return { trace, context, pushError: () => {} }; + } + + return { + trace: otel.trace, + context: otel.context, + }; } - // The return type of getOTEL is OTELApi | undefined so we need to check for undefined - // return default OTEL context and trace if faro is not initialized - return faro.api.getOTEL() || { trace, context }; -}; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + public captureException(exception: any, hint?: Record): string { + const eventId = uuidv4(); + + if (!this.faro) { + return eventId; + } + + // Exception context in push error is Record so we need to convert hint to that format + const context: Record = {}; + + // Iterate over hint and convert to Record + if (hint) { + for (const key in hint) { + if (typeof hint[key] === "string") { + context[key] = hint[key]; + } else { + context[key] = JSON.stringify(hint[key]); + } + } + } + + try { + this.faro.api.pushError( + exception instanceof Error ? exception : new Error(String(exception)), + { type: "error", context: context }, + ); + } catch (error) { + errorLogger(error); + } + + return eventId; + } + + public captureMeasurement( + value: Record, + context?: Record, + ) { + if (!this.faro) { + return; + } + + //add name inside cotext + try { + this.faro.api.pushMeasurement({ + type: "measurement", + values: value, + context: context, + }); + } catch (e) { + errorLogger(e); + } + } +} -export { faro }; +export const appsmithTelemetry = AppsmithTelemetry.getInstance(); diff --git a/app/client/src/instrumentation/sendFaroErrors.ts b/app/client/src/instrumentation/sendFaroErrors.ts deleted file mode 100644 index 66adfe7a07d8..000000000000 --- a/app/client/src/instrumentation/sendFaroErrors.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { v4 as uuidv4 } from "uuid"; -import { error as errorLogger } from "loglevel"; - -// eslint-disable-next-line @typescript-eslint/no-explicit-any -export function captureException(exception: any, hint?: any): string { - const eventId = uuidv4(); - const context = hint || {}; - - try { - window.faro?.api.pushError( - exception instanceof Error ? exception : new Error(String(exception)), - { type: "error", context: context }, - ); - } catch (error) { - errorLogger(error); - } - - return eventId; -} - -export default captureException; diff --git a/app/client/src/pages/Editor/Canvas.tsx b/app/client/src/pages/Editor/Canvas.tsx index 96d5c0534c38..91ffa7301a63 100644 --- a/app/client/src/pages/Editor/Canvas.tsx +++ b/app/client/src/pages/Editor/Canvas.tsx @@ -19,7 +19,7 @@ import { getAppThemeSettings } from "ee/selectors/applicationSelectors"; import CodeModeTooltip from "pages/Editor/WidgetsEditor/components/CodeModeTooltip"; import { getIsAnvilLayout } from "layoutSystems/anvil/integrations/selectors"; import { focusWidget } from "actions/widgetActions"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; interface CanvasProps { widgetsStructure: CanvasWidgetStructure; @@ -120,7 +120,7 @@ const Canvas = (props: CanvasProps) => { return renderChildren(); } catch (error) { log.error("Error rendering DSL", error); - captureException(error, { errorName: "Canvas" }); + appsmithTelemetry.captureException(error, { errorName: "Canvas" }); return null; } diff --git a/app/client/src/pages/UserAuth/Login.tsx b/app/client/src/pages/UserAuth/Login.tsx index 602669ff7c1f..a64888b15b67 100644 --- a/app/client/src/pages/UserAuth/Login.tsx +++ b/app/client/src/pages/UserAuth/Login.tsx @@ -52,7 +52,7 @@ import { useFeatureFlag } from "utils/hooks/useFeatureFlag"; import { FEATURE_FLAG } from "ee/entities/FeatureFlag"; import { getHTMLPageTitle } from "ee/utils/BusinessFeatures/brandingPageHelpers"; import CsrfTokenInput from "pages/UserAuth/CsrfTokenInput"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; import { getSafeErrorMessage } from "ee/constants/approvedErrorMessages"; const validate = (values: LoginFormValues, props: ValidateProps) => { @@ -116,7 +116,9 @@ export function Login(props: LoginFormProps) { if (queryParams.get("error")) { errorMessage = queryParams.get("message") || queryParams.get("error") || ""; showError = true; - captureException(new Error(errorMessage), { errorName: "LoginError" }); + appsmithTelemetry.captureException(new Error(errorMessage), { + errorName: "LoginError", + }); } let loginURL = "/api/v1/" + LOGIN_SUBMIT_PATH; diff --git a/app/client/src/pages/UserAuth/SignUp.tsx b/app/client/src/pages/UserAuth/SignUp.tsx index 1112ee89af8e..c260aa94657a 100644 --- a/app/client/src/pages/UserAuth/SignUp.tsx +++ b/app/client/src/pages/UserAuth/SignUp.tsx @@ -64,7 +64,7 @@ import CsrfTokenInput from "pages/UserAuth/CsrfTokenInput"; import { useIsCloudBillingEnabled } from "hooks"; import { isLoginHostname } from "utils/cloudBillingUtils"; import { getIsAiAgentFlowEnabled } from "ee/selectors/aiAgentSelectors"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; import { getSafeErrorMessage } from "ee/constants/approvedErrorMessages"; declare global { @@ -145,7 +145,7 @@ export function SignUp(props: SignUpFormProps) { if (queryParams.get("error")) { errorMessage = queryParams.get("error") || ""; showError = true; - captureException(new Error(errorMessage), { + appsmithTelemetry.captureException(new Error(errorMessage), { errorName: "SignUp", }); } diff --git a/app/client/src/pages/UserAuth/VerifyUser.tsx b/app/client/src/pages/UserAuth/VerifyUser.tsx index 80af8a62dfaf..268d967b835d 100644 --- a/app/client/src/pages/UserAuth/VerifyUser.tsx +++ b/app/client/src/pages/UserAuth/VerifyUser.tsx @@ -6,7 +6,7 @@ import { EMAIL_VERIFICATION_PATH } from "ee/constants/ApiConstants"; import { Redirect } from "react-router-dom"; import { VerificationErrorType } from "./VerificationError"; import CsrfTokenInput from "pages/UserAuth/CsrfTokenInput"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; const VerifyUser = ( props: RouteComponentProps<{ @@ -24,9 +24,12 @@ const VerifyUser = ( useEffect(() => { if (!token || !email) { - captureException(new Error("User email verification link is damaged"), { - errorName: "VerificationLinkDamaged", - }); + appsmithTelemetry.captureException( + new Error("User email verification link is damaged"), + { + errorName: "VerificationLinkDamaged", + }, + ); } const formElement: HTMLFormElement = document.getElementById( diff --git a/app/client/src/pages/UserAuth/helpers.ts b/app/client/src/pages/UserAuth/helpers.ts index 389a7a148c49..b91855140089 100644 --- a/app/client/src/pages/UserAuth/helpers.ts +++ b/app/client/src/pages/UserAuth/helpers.ts @@ -5,7 +5,7 @@ import type { Dispatch } from "redux"; import UserApi from "ee/api/UserApi"; import { toast } from "@appsmith/ads"; import type { ApiResponse } from "../../api/ApiResponses"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; export interface LoginFormValues { username?: string; @@ -95,7 +95,7 @@ export const useResendEmailVerification = ( if (!email) { const errorMessage = "Email not found for retry verification"; - captureException(new Error(errorMessage), { + appsmithTelemetry.captureException(new Error(errorMessage), { errorName: "EmailVerificationRetryError", }); toast.show(errorMessage, { kind: "error" }); diff --git a/app/client/src/reducers/evaluationReducers/treeReducer.ts b/app/client/src/reducers/evaluationReducers/treeReducer.ts index fc1f3f60a17e..814cde1fe74e 100644 --- a/app/client/src/reducers/evaluationReducers/treeReducer.ts +++ b/app/client/src/reducers/evaluationReducers/treeReducer.ts @@ -3,7 +3,7 @@ import { ReduxActionTypes } from "ee/constants/ReduxActionConstants"; import { applyChange, type Diff } from "deep-diff"; import type { DataTree } from "entities/DataTree/dataTreeTypes"; import { createImmerReducer } from "utils/ReducerUtils"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; export type EvaluatedTreeState = DataTree; @@ -32,7 +32,7 @@ const evaluatedTreeReducer = createImmerReducer(initialState, { applyChange(state, undefined, update); } catch (e) { - captureException(e, { + appsmithTelemetry.captureException(e, { errorName: "TreeReducer", extra: { update, diff --git a/app/client/src/sagas/ActionExecution/PluginActionSaga.ts b/app/client/src/sagas/ActionExecution/PluginActionSaga.ts index df5e52e894af..6cc37fd6351f 100644 --- a/app/client/src/sagas/ActionExecution/PluginActionSaga.ts +++ b/app/client/src/sagas/ActionExecution/PluginActionSaga.ts @@ -169,7 +169,7 @@ import { selectGitOpsModalOpen, } from "selectors/gitModSelectors"; import { createActionExecutionResponse } from "./PluginActionSagaUtils"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; interface FilePickerInstumentationObject { numberOfFiles: number; @@ -989,7 +989,7 @@ function* executeOnPageLoadJSAction(pageAction: PageAction) { ); if (!collection) { - captureException( + appsmithTelemetry.captureException( new Error( "Collection present in layoutOnLoadActions but no collection exists ", ), diff --git a/app/client/src/sagas/AppThemingSaga.tsx b/app/client/src/sagas/AppThemingSaga.tsx index 5d1054aed5eb..a3c6a059b9e9 100644 --- a/app/client/src/sagas/AppThemingSaga.tsx +++ b/app/client/src/sagas/AppThemingSaga.tsx @@ -44,7 +44,7 @@ import { selectApplicationVersion, } from "selectors/editorSelectors"; import { find } from "lodash"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; import { getAllPageIdentities } from "./selectors"; import type { SagaIterator } from "@redux-saga/types"; import type { AxiosPromise } from "axios"; @@ -126,16 +126,19 @@ export function* fetchAppSelectedTheme( payload: response.data, }); } else { - captureException(new Error("Unable to fetch the selected theme"), { - errorName: "ThemeFetchError", - extra: { - pageIdentities, - applicationId, - applicationVersion, - userDetails, - themeResponse: response, + appsmithTelemetry.captureException( + new Error("Unable to fetch the selected theme"), + { + errorName: "ThemeFetchError", + extra: { + pageIdentities, + applicationId, + applicationVersion, + userDetails, + themeResponse: response, + }, }, - }); + ); // If the response.data is undefined then we set selectedTheme to default Theme yield put({ diff --git a/app/client/src/sagas/ErrorSagas.tsx b/app/client/src/sagas/ErrorSagas.tsx index 176ace8f1162..32205e6f0a21 100644 --- a/app/client/src/sagas/ErrorSagas.tsx +++ b/app/client/src/sagas/ErrorSagas.tsx @@ -41,7 +41,7 @@ import AppsmithConsole from "../utils/AppsmithConsole"; import type { SourceEntity } from "../entities/AppsmithConsole"; import { getAppMode } from "ee/selectors/applicationSelectors"; import { APP_MODE } from "../entities/App"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; const shouldShowToast = (action: string) => { return action in toastMessageErrorTypes; @@ -298,7 +298,13 @@ export function* errorSaga(errorAction: ReduxAction) { break; } case ErrorEffectTypes.LOG_TO_SENTRY: { - yield call(captureException, error, { errorName: "ErrorSagaError" }); + yield call( + [appsmithTelemetry, appsmithTelemetry.captureException], + error, + { + errorName: "ErrorSagaError", + }, + ); break; } } diff --git a/app/client/src/sagas/EvalErrorHandler.ts b/app/client/src/sagas/EvalErrorHandler.ts index 1664dff50d72..abe81d714898 100644 --- a/app/client/src/sagas/EvalErrorHandler.ts +++ b/app/client/src/sagas/EvalErrorHandler.ts @@ -17,7 +17,7 @@ import { get } from "lodash"; import LOG_TYPE from "entities/AppsmithConsole/logtype"; import { select } from "redux-saga/effects"; import AppsmithConsole from "utils/AppsmithConsole"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; import AnalyticsUtil from "ee/utils/AnalyticsUtil"; import { createMessage, @@ -235,7 +235,7 @@ export function* evalErrorHandler( if (error.context.logToSentry) { // Send the generic error message to sentry for better grouping - captureException(reconstructedError, { + appsmithTelemetry.captureException(reconstructedError, { errorName: "CyclicalDependencyError", tags: { node, @@ -268,18 +268,20 @@ export function* evalErrorHandler( kind: "error", }); log.error(error); - captureException(reconstructedError, { errorName: "EvalTreeError" }); + appsmithTelemetry.captureException(reconstructedError, { + errorName: "EvalTreeError", + }); break; } case EvalErrorTypes.BAD_UNEVAL_TREE_ERROR: { log.error(error); - captureException(reconstructedError, { + appsmithTelemetry.captureException(reconstructedError, { errorName: "BadUnevalTreeError", }); break; } case EvalErrorTypes.EVAL_PROPERTY_ERROR: { - captureException(reconstructedError, { + appsmithTelemetry.captureException(reconstructedError, { errorName: "EvalPropertyError", }); log.error(error); @@ -287,7 +289,7 @@ export function* evalErrorHandler( } case EvalErrorTypes.CLONE_ERROR: { log.debug(error); - captureException(reconstructedError, { + appsmithTelemetry.captureException(reconstructedError, { errorName: "CloneError", extra: { request: error.context, @@ -303,14 +305,14 @@ export function* evalErrorHandler( text: `${error.message} at: ${error.context?.propertyPath}`, }); log.error(error); - captureException(reconstructedError, { + appsmithTelemetry.captureException(reconstructedError, { errorName: "ParseJSError", entity: error.context, }); break; } case EvalErrorTypes.EXTRACT_DEPENDENCY_ERROR: { - captureException(reconstructedError, { + appsmithTelemetry.captureException(reconstructedError, { errorName: "ExtractDependencyError", extra: error.context, }); @@ -318,7 +320,7 @@ export function* evalErrorHandler( } case EvalErrorTypes.UPDATE_DATA_TREE_ERROR: { // Log to Sentry with additional context - captureException(reconstructedError, { + appsmithTelemetry.captureException(reconstructedError, { errorName: "UpdateDataTreeError", }); // Log locally with error details @@ -329,12 +331,14 @@ export function* evalErrorHandler( } case EvalErrorTypes.CACHE_ERROR: { log.error(error); - captureException(error, { errorName: "CacheError" }); + appsmithTelemetry.captureException(error, { errorName: "CacheError" }); break; } default: { log.error(error); - captureException(reconstructedError, { errorName: "UnknownEvalError" }); + appsmithTelemetry.captureException(reconstructedError, { + errorName: "UnknownEvalError", + }); } } }); diff --git a/app/client/src/sagas/EvaluationsSaga.ts b/app/client/src/sagas/EvaluationsSaga.ts index 626133a3cf7d..9d6a5d1e47fb 100644 --- a/app/client/src/sagas/EvaluationsSaga.ts +++ b/app/client/src/sagas/EvaluationsSaga.ts @@ -123,7 +123,7 @@ import type { AffectedJSObjects, EvaluationReduxAction, } from "actions/EvaluationReduxActionTypes"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; const APPSMITH_CONFIGS = getAppsmithConfigs(); @@ -935,7 +935,7 @@ export function* evaluateActionSelectorFieldSaga(action: any) { ); } catch (e) { log.error(e); - captureException(e, { errorName: "EvaluationError" }); + appsmithTelemetry.captureException(e, { errorName: "EvaluationError" }); } } @@ -1006,7 +1006,7 @@ export default function* evaluationSagaListeners() { yield call(evaluationChangeListenerSaga); } catch (e) { log.error(e); - captureException(e, { errorName: "EvaluationError" }); + appsmithTelemetry.captureException(e, { errorName: "EvaluationError" }); } } } diff --git a/app/client/src/sagas/FormEvaluationSaga.ts b/app/client/src/sagas/FormEvaluationSaga.ts index c5f9cbeff504..8bd938c6e653 100644 --- a/app/client/src/sagas/FormEvaluationSaga.ts +++ b/app/client/src/sagas/FormEvaluationSaga.ts @@ -11,7 +11,7 @@ import { import type { ReduxAction } from "actions/ReduxActionTypes"; import { ReduxActionTypes } from "ee/constants/ReduxActionConstants"; import log from "loglevel"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; import { getFormEvaluationState } from "selectors/formSelectors"; import { evalFormConfig } from "./EvaluationsSaga"; import type { @@ -372,7 +372,9 @@ export default function* formEvaluationChangeListener() { yield call(formEvaluationChangeListenerSaga); } catch (e) { log.error(e); - captureException(e, { errorName: "FormEvaluationError" }); + appsmithTelemetry.captureException(e, { + errorName: "FormEvaluationError", + }); } } } diff --git a/app/client/src/sagas/InitSagas.ts b/app/client/src/sagas/InitSagas.ts index 34f88ff154e8..4ada86731263 100644 --- a/app/client/src/sagas/InitSagas.ts +++ b/app/client/src/sagas/InitSagas.ts @@ -92,7 +92,7 @@ import type { ApplicationPayload } from "entities/Application"; import type { Page } from "entities/Page"; import type { PACKAGE_PULL_STATUS } from "ee/constants/ModuleConstants"; import { validateSessionToken } from "utils/SessionUtils"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; export const URL_CHANGE_ACTIONS = [ ReduxActionTypes.CURRENT_APPLICATION_NAME_UPDATE, @@ -278,7 +278,7 @@ export function* getInitResponses({ ReduxActionTypes.END_CONSOLIDATED_PAGE_LOAD, shouldInitialiseUserDetails, ); - captureException( + appsmithTelemetry.captureException( new Error(`consolidated api failure for ${JSON.stringify(params)}`), { errorName: "ConsolidatedApiError", @@ -374,7 +374,7 @@ export function* startAppEngine(action: ReduxAction) { if (e instanceof AppEngineApiError) return; - captureException(e, { errorName: "AppEngineError" }); + appsmithTelemetry.captureException(e, { errorName: "AppEngineError" }); yield put(safeCrashAppRequest()); } finally { endSpan(rootSpan); @@ -471,7 +471,9 @@ function* eagerPageInitSaga() { } catch (error) { // Log error but don't block the rest of the initialization log.error("Error validating session token:", error); - captureException(error, { errorName: "SessionValidationError" }); + appsmithTelemetry.captureException(error, { + errorName: "SessionValidationError", + }); } const url = window.location.pathname; diff --git a/app/client/src/sagas/ReplaySaga.ts b/app/client/src/sagas/ReplaySaga.ts index cea53117d96a..b6205b603bc8 100644 --- a/app/client/src/sagas/ReplaySaga.ts +++ b/app/client/src/sagas/ReplaySaga.ts @@ -8,7 +8,7 @@ import { takeLatest, } from "redux-saga/effects"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; import log from "loglevel"; import { @@ -133,7 +133,9 @@ export function* openPropertyPaneSaga(replay: any) { ); } catch (e) { log.error(e); - captureException(e, { errorName: "OpenPropertyPaneError" }); + appsmithTelemetry.captureException(e, { + errorName: "OpenPropertyPaneError", + }); } } @@ -165,7 +167,7 @@ export function* postUndoRedoSaga(replay: any) { scrollWidgetIntoView(widgetIds[0]); } catch (e) { log.error(e); - captureException(e, { errorName: "PostUndoRedoError" }); + appsmithTelemetry.captureException(e, { errorName: "PostUndoRedoError" }); } } @@ -260,7 +262,7 @@ export function* undoRedoSaga(action: ReduxAction) { } } catch (e) { log.error(e); - captureException(e, { errorName: "UndoRedoSagaError" }); + appsmithTelemetry.captureException(e, { errorName: "UndoRedoSagaError" }); } } diff --git a/app/client/src/sagas/WidgetLoadingSaga.ts b/app/client/src/sagas/WidgetLoadingSaga.ts index 9c5987f0c937..247020825942 100644 --- a/app/client/src/sagas/WidgetLoadingSaga.ts +++ b/app/client/src/sagas/WidgetLoadingSaga.ts @@ -16,7 +16,7 @@ import { ReduxActionTypes, } from "ee/constants/ReduxActionConstants"; import log from "loglevel"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; import { findLoadingEntities } from "utils/WidgetLoadingStateUtils"; const actionExecutionRequestActions = [ @@ -101,7 +101,9 @@ export default function* actionExecutionChangeListeners() { yield call(actionExecutionChangeListenerSaga); } catch (e) { log.error(e); - captureException(e, { errorName: "WidgetLoadingError" }); + appsmithTelemetry.captureException(e, { + errorName: "WidgetLoadingError", + }); } } } diff --git a/app/client/src/sagas/layoutConversionSagas.ts b/app/client/src/sagas/layoutConversionSagas.ts index d6fd62fe7c21..93f5346a5224 100644 --- a/app/client/src/sagas/layoutConversionSagas.ts +++ b/app/client/src/sagas/layoutConversionSagas.ts @@ -26,7 +26,7 @@ import { import { updateApplicationLayoutType } from "./AutoLayoutUpdateSagas"; import AnalyticsUtil from "ee/utils/AnalyticsUtil"; import { nestDSL } from "@shared/dsl"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; /** * This method is used to convert from auto-layout to fixed layout @@ -214,7 +214,9 @@ function* logLayoutConversionErrorSaga() { (state: AppState) => state.ui.layoutConversion.conversionError, ); - yield call(captureException, error, { errorName: "LayoutConversionError" }); + yield call(appsmithTelemetry.captureException, error, { + errorName: "LayoutConversionError", + }); } catch (e) { throw e; } diff --git a/app/client/src/utils/Analytics/sentry.ts b/app/client/src/utils/Analytics/sentry.ts deleted file mode 100644 index ea4bd48fb9c5..000000000000 --- a/app/client/src/utils/Analytics/sentry.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { captureException } from "instrumentation/sendFaroErrors"; -import type { User } from "constants/userConstants"; - -class FaroUtil { - static init() { - // No initialization needed - } - - public static identifyUser(userId: string, userData: User) { - // Set user context for error reporting - window.faro?.api.setUser({ - id: userId, - username: userData.username, - email: userData.email, - }); - } - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - public static captureException(error: Error, context?: any) { - captureException(error, context); - } -} - -export default FaroUtil; diff --git a/app/client/src/utils/getPathAndValueFromActionDiffObject.ts b/app/client/src/utils/getPathAndValueFromActionDiffObject.ts index a7a1f68ae99d..b34477017622 100644 --- a/app/client/src/utils/getPathAndValueFromActionDiffObject.ts +++ b/app/client/src/utils/getPathAndValueFromActionDiffObject.ts @@ -1,4 +1,4 @@ -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; //Following function is the fix for the missing where key /** @@ -47,7 +47,7 @@ export function getPathAndValueFromActionDiffObject(actionObjectDiff: any) { return acc; } catch (error) { - captureException( + appsmithTelemetry.captureException( { message: `Adding key: where failed, cannot create path`, oldData: actionObjectDiff, diff --git a/app/client/src/utils/helpers.test.ts b/app/client/src/utils/helpers.test.ts index 2f0d35768c0b..6d0c3eecd71b 100644 --- a/app/client/src/utils/helpers.test.ts +++ b/app/client/src/utils/helpers.test.ts @@ -16,9 +16,22 @@ import { } from "./helpers"; import WidgetFactory from "../WidgetProvider/factory"; import { Colors } from "constants/Colors"; -import * as FaroErrors from "instrumentation/sendFaroErrors"; - -jest.mock("instrumentation/sendFaroErrors"); +import { appsmithTelemetry } from "instrumentation"; + +// Mock appsmithTelemetry +jest.mock("instrumentation", () => ({ + appsmithTelemetry: { + getTraceAndContext: jest.fn().mockReturnValue({ + context: {}, + trace: { + getTracer: jest.fn().mockReturnValue({ + startSpan: jest.fn(), + }), + }, + }), + captureException: jest.fn(), + }, +})); describe("flattenObject test", () => { it("Check if non nested object is returned correctly", () => { @@ -545,7 +558,7 @@ describe("#captureInvalidDynamicBindingPath", () => { const mockCaptureException = jest.fn(); - (FaroErrors.captureException as jest.Mock).mockImplementation( + (appsmithTelemetry.captureException as jest.Mock).mockImplementation( mockCaptureException, ); diff --git a/app/client/src/utils/helpers.tsx b/app/client/src/utils/helpers.tsx index bd4d83897b02..a4b30371b7d3 100644 --- a/app/client/src/utils/helpers.tsx +++ b/app/client/src/utils/helpers.tsx @@ -44,7 +44,7 @@ import { klona as klonaJson } from "klona/json"; import { startAndEndSpanForFn } from "instrumentation/generateTraces"; import type { Property } from "entities/Action"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; export const snapToGrid = ( columnWidth: number, @@ -944,7 +944,7 @@ export const captureInvalidDynamicBindingPath = ( * Checks if dynamicBindingPathList contains a property path that doesn't have a binding */ if (!isDynamicValue(pathValue)) { - captureException( + appsmithTelemetry.captureException( new Error( `INVALID_DynamicPathBinding_CLIENT_ERROR: Invalid dynamic path binding list: ${currentDSL.widgetName}.${dBindingPath.key}`, ), diff --git a/app/client/src/widgets/CurrencyInputWidget/widget/index.tsx b/app/client/src/widgets/CurrencyInputWidget/widget/index.tsx index 99a27b137f4e..af75f5ab7478 100644 --- a/app/client/src/widgets/CurrencyInputWidget/widget/index.tsx +++ b/app/client/src/widgets/CurrencyInputWidget/widget/index.tsx @@ -43,7 +43,7 @@ import { getDefaultCurrency } from "../component/CurrencyCodeDropdown"; import IconSVG from "../icon.svg"; import ThumbnailSVG from "../thumbnail.svg"; import { WIDGET_TAGS } from "constants/WidgetConstants"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; export function defaultValueValidation( // TODO: Fix this the next time the file is edited @@ -499,7 +499,9 @@ class CurrencyInputWidget extends BaseInputWidget< this.props.updateWidgetMetaProperty("text", formattedValue); } catch (e) { log.error(e); - captureException(e, { errorName: "CurrencyInputWidget" }); + appsmithTelemetry.captureException(e, { + errorName: "CurrencyInputWidget", + }); } } } @@ -517,7 +519,9 @@ class CurrencyInputWidget extends BaseInputWidget< } catch (e) { formattedValue = value; log.error(e); - captureException(e, { errorName: "CurrencyInputWidget" }); + appsmithTelemetry.captureException(e, { + errorName: "CurrencyInputWidget", + }); } // text is stored as what user has typed @@ -575,7 +579,9 @@ class CurrencyInputWidget extends BaseInputWidget< } } catch (e) { log.error(e); - captureException(e, { errorName: "CurrencyInputWidget" }); + appsmithTelemetry.captureException(e, { + errorName: "CurrencyInputWidget", + }); this.props.updateWidgetMetaProperty("text", this.props.text); } diff --git a/app/client/src/widgets/JSONFormWidget/fields/CurrencyInputField.tsx b/app/client/src/widgets/JSONFormWidget/fields/CurrencyInputField.tsx index aca1efe75199..7555ec680ae1 100644 --- a/app/client/src/widgets/JSONFormWidget/fields/CurrencyInputField.tsx +++ b/app/client/src/widgets/JSONFormWidget/fields/CurrencyInputField.tsx @@ -14,7 +14,7 @@ import derived from "widgets/CurrencyInputWidget/widget/derived"; import { isEmpty } from "../helper"; import { BASE_LABEL_TEXT_SIZE } from "../component/FieldLabel"; import { getLocaleDecimalSeperator } from "widgets/WidgetUtils"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; type CurrencyInputComponentProps = BaseInputComponentProps & { currencyCountryCode: string; @@ -132,7 +132,9 @@ function CurrencyInputField({ } } catch (e) { text = inputValue; - captureException(e, { errorName: "JSONFormWidget_CurrencyInputField" }); + appsmithTelemetry.captureException(e, { + errorName: "JSONFormWidget_CurrencyInputField", + }); } const value = derived.value({ text }); diff --git a/app/client/src/widgets/JSONFormWidget/fields/useRegisterFieldValidity.ts b/app/client/src/widgets/JSONFormWidget/fields/useRegisterFieldValidity.ts index b24a193b1882..3a3cfe97b878 100644 --- a/app/client/src/widgets/JSONFormWidget/fields/useRegisterFieldValidity.ts +++ b/app/client/src/widgets/JSONFormWidget/fields/useRegisterFieldValidity.ts @@ -7,7 +7,7 @@ import FormContext from "../FormContext"; import type { FieldType } from "../constants"; import { startAndEndSpanForFn } from "instrumentation/generateTraces"; import { klonaRegularWithTelemetry } from "utils/helpers"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; export interface UseRegisterFieldValidityProps { isValid: boolean; @@ -53,7 +53,7 @@ function useRegisterFieldValidity({ } } } catch (e) { - captureException(e, { + appsmithTelemetry.captureException(e, { errorName: "JSONFormWidget_useRegisterFieldValidity", }); } diff --git a/app/client/src/widgets/MapChartWidget/component/utilities.ts b/app/client/src/widgets/MapChartWidget/component/utilities.ts index 20babb089d1c..bb72de25a475 100644 --- a/app/client/src/widgets/MapChartWidget/component/utilities.ts +++ b/app/client/src/widgets/MapChartWidget/component/utilities.ts @@ -3,7 +3,7 @@ import countryDetails from "./countryDetails"; import { MapTypes } from "../constants"; import { geoAlbers, geoAzimuthalEqualArea, geoMercator } from "d3-geo"; import log from "loglevel"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; import { retryPromise } from "utils/AppsmithUtils"; interface GeoSpecialAreas { @@ -75,7 +75,7 @@ export const loadMapGenerator = () => { if (error.code !== 20) { log.error({ error }); - captureException(error, { + appsmithTelemetry.captureException(error, { errorName: "MapChartWidget_utilities", }); } diff --git a/app/client/src/widgets/PhoneInputWidget/widget/index.tsx b/app/client/src/widgets/PhoneInputWidget/widget/index.tsx index c04b2c8b49d8..a116fc741166 100644 --- a/app/client/src/widgets/PhoneInputWidget/widget/index.tsx +++ b/app/client/src/widgets/PhoneInputWidget/widget/index.tsx @@ -38,7 +38,7 @@ import { getDefaultISDCode } from "../component/ISDCodeDropdown"; import IconSVG from "../icon.svg"; import ThumbnailSVG from "../thumbnail.svg"; import { WIDGET_TAGS } from "constants/WidgetConstants"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; export function defaultValueValidation( // TODO: Fix this the next time the file is edited @@ -348,7 +348,9 @@ class PhoneInputWidget extends BaseInputWidget< this.props.updateWidgetMetaProperty("text", formattedValue); } catch (e) { log.error(e); - captureException(e, { errorName: "PhoneInputWidget" }); + appsmithTelemetry.captureException(e, { + errorName: "PhoneInputWidget", + }); } } } diff --git a/app/client/src/widgets/TableWidgetV2/component/cellComponents/InlineCellEditor.tsx b/app/client/src/widgets/TableWidgetV2/component/cellComponents/InlineCellEditor.tsx index f024c3ffad25..ebd8a900493f 100644 --- a/app/client/src/widgets/TableWidgetV2/component/cellComponents/InlineCellEditor.tsx +++ b/app/client/src/widgets/TableWidgetV2/component/cellComponents/InlineCellEditor.tsx @@ -20,7 +20,7 @@ import { } from "widgets/WidgetUtils"; import { limitDecimalValue } from "widgets/CurrencyInputWidget/component/utilities"; import { getLocale } from "utils/helpers"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; const FOCUS_CLASS = "has-focus"; @@ -237,7 +237,9 @@ export function InlineCellEditor({ value = convertToNumber(inputValue); } catch (e) { - captureException(e, { errorName: "TableWidgetV2_InlineCellEditor" }); + appsmithTelemetry.captureException(e, { + errorName: "TableWidgetV2_InlineCellEditor", + }); } } diff --git a/app/client/src/widgets/TableWidgetV2/component/cellComponents/PlainTextCell.tsx b/app/client/src/widgets/TableWidgetV2/component/cellComponents/PlainTextCell.tsx index f107324e09f6..8dc13b8193ee 100644 --- a/app/client/src/widgets/TableWidgetV2/component/cellComponents/PlainTextCell.tsx +++ b/app/client/src/widgets/TableWidgetV2/component/cellComponents/PlainTextCell.tsx @@ -20,7 +20,7 @@ import CurrencyTypeDropdown, { } from "widgets/CurrencyInputWidget/component/CurrencyCodeDropdown"; import { getLocale } from "utils/helpers"; import { getLocaleThousandSeparator } from "widgets/WidgetUtils"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; const Container = styled.div<{ isCellEditMode?: boolean; @@ -227,7 +227,9 @@ function PlainTextCell( return currency?.id + " " + formattedValue; } } catch (e) { - captureException(e, { errorName: "TableWidgetV2_PlainTextCell" }); + appsmithTelemetry.captureException(e, { + errorName: "TableWidgetV2_PlainTextCell", + }); return value; } diff --git a/app/client/src/widgets/TabsMigrator/widget/index.tsx b/app/client/src/widgets/TabsMigrator/widget/index.tsx index b126624151dd..547bce85c77c 100644 --- a/app/client/src/widgets/TabsMigrator/widget/index.tsx +++ b/app/client/src/widgets/TabsMigrator/widget/index.tsx @@ -12,7 +12,7 @@ import { EVAL_VALUE_PATH } from "utils/DynamicBindingUtils"; import { AutocompleteDataType } from "utils/autocomplete/AutocompleteDataType"; import type { DSLWidget } from "WidgetProvider/constants"; import { DATA_BIND_REGEX_GLOBAL } from "constants/BindingsConstants"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; function migrateTabsDataUsingMigrator(currentDSL: DSLWidget) { if (currentDSL.type === "TABS_WIDGET" && currentDSL.version === 1) { @@ -20,7 +20,7 @@ function migrateTabsDataUsingMigrator(currentDSL: DSLWidget) { currentDSL.type = "TABS_MIGRATOR_WIDGET"; currentDSL.version = 1; } catch (error) { - captureException(error, { + appsmithTelemetry.captureException(error, { errorName: "TabsMigrator", message: "Tabs Migration Failed", oldData: currentDSL.tabs, @@ -117,7 +117,7 @@ const migrateTabsData = (currentDSL: DSLWidget) => { currentDSL.version = 2; delete currentDSL.tabs; } catch (error) { - captureException(error, { + appsmithTelemetry.captureException(error, { errorName: "TabsMigrator", message: "Tabs Migration Failed", oldData: currentDSL.tabs, diff --git a/app/client/src/widgets/wds/WDSCurrencyInputWidget/widget/index.tsx b/app/client/src/widgets/wds/WDSCurrencyInputWidget/widget/index.tsx index 640de05edc6b..768541792a6f 100644 --- a/app/client/src/widgets/wds/WDSCurrencyInputWidget/widget/index.tsx +++ b/app/client/src/widgets/wds/WDSCurrencyInputWidget/widget/index.tsx @@ -29,7 +29,7 @@ import type { CurrencyInputWidgetProps } from "./types"; import { WDSBaseInputWidget } from "widgets/wds/WDSBaseInputWidget"; import { getCountryCodeFromCurrencyCode, validateInput } from "./helpers"; import type { KeyDownEvent } from "widgets/wds/WDSBaseInputWidget/component/types"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; class WDSCurrencyInputWidget extends WDSBaseInputWidget< CurrencyInputWidgetProps, @@ -189,7 +189,9 @@ class WDSCurrencyInputWidget extends WDSBaseInputWidget< } catch (e) { formattedValue = value; log.error(e); - captureException(e, { errorName: "WDSCurrencyInputWidget" }); + appsmithTelemetry.captureException(e, { + errorName: "WDSCurrencyInputWidget", + }); } this.props.updateWidgetMetaProperty("text", String(formattedValue)); @@ -248,7 +250,9 @@ class WDSCurrencyInputWidget extends WDSBaseInputWidget< } } catch (e) { log.error(e); - captureException(e, { errorName: "WDSCurrencyInputWidget" }); + appsmithTelemetry.captureException(e, { + errorName: "WDSCurrencyInputWidget", + }); this.props.updateWidgetMetaProperty("text", this.props.text); } @@ -311,7 +315,9 @@ class WDSCurrencyInputWidget extends WDSBaseInputWidget< this.props.updateWidgetMetaProperty("text", formattedValue); } catch (e) { log.error(e); - captureException(e, { errorName: "WDSCurrencyInputWidget" }); + appsmithTelemetry.captureException(e, { + errorName: "WDSCurrencyInputWidget", + }); } } } diff --git a/app/client/src/widgets/wds/WDSPhoneInputWidget/widget/index.tsx b/app/client/src/widgets/wds/WDSPhoneInputWidget/widget/index.tsx index f3c4e5b1db3b..f71b963331a0 100644 --- a/app/client/src/widgets/wds/WDSPhoneInputWidget/widget/index.tsx +++ b/app/client/src/widgets/wds/WDSPhoneInputWidget/widget/index.tsx @@ -20,7 +20,7 @@ import * as config from "../config"; import { PhoneInputComponent } from "../component"; import type { PhoneInputWidgetProps } from "./types"; import { getCountryCode, validateInput } from "./helpers"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; class WDSPhoneInputWidget extends WDSBaseInputWidget< PhoneInputWidgetProps, @@ -163,7 +163,9 @@ class WDSPhoneInputWidget extends WDSBaseInputWidget< this.props.updateWidgetMetaProperty("text", formattedValue); } catch (e) { log.error(e); - captureException(e, { errorName: "WDSPhoneInputWidget" }); + appsmithTelemetry.captureException(e, { + errorName: "WDSPhoneInputWidget", + }); } } } diff --git a/app/client/src/workers/Evaluation/errorModifier.ts b/app/client/src/workers/Evaluation/errorModifier.ts index 69cd601ab689..a4143d9cf76c 100644 --- a/app/client/src/workers/Evaluation/errorModifier.ts +++ b/app/client/src/workers/Evaluation/errorModifier.ts @@ -13,7 +13,7 @@ import { APP_MODE } from "entities/App"; import { isAction } from "ee/workers/Evaluation/evaluationUtils"; import log from "loglevel"; import { getMemberExpressionObjectFromProperty } from "@shared/ast"; -import captureException from "instrumentation/sendFaroErrors"; +import { appsmithTelemetry } from "instrumentation"; interface ErrorMetaData { userScript: string; @@ -224,7 +224,9 @@ export function convertAllDataTypesToString(e: any) { return JSON.stringify(e); } catch (error) { log.debug(error); - captureException(error, { errorName: "ErrorModifier_StringifyError" }); + appsmithTelemetry.captureException(error, { + errorName: "ErrorModifier_StringifyError", + }); } } }