chore: add sentry metric [INS-4115] (#7727)

* chore: add landing duration metric

* chore: main process landing duration

* chore: project&organization switch duration

* chore: add cloud sync metrics

* feat: add workspace landing metric
This commit is contained in:
Curry Yang
2024-07-26 22:54:59 +08:00
committed by Curry Yang
parent 94a033eb67
commit bb1d2ab9d9
13 changed files with 176 additions and 37 deletions

View File

@@ -2,9 +2,27 @@ import { ClientOptions } from '@sentry/types';
import { getAppEnvironment, getAppVersion, getSentryDsn } from './constants';
export const APP_START_TIME = performance.now();
export const SENTRY_OPTIONS: Partial<ClientOptions> = {
sampleRate: 0.5,
dsn: getSentryDsn(),
environment: getAppEnvironment(),
release: getAppVersion(),
};
export const enum SentryMetrics {
APP_START_DURATION = 'app_start_duration',
MAIN_PROCESS_START_DURATION = 'main_process_start_duration',
ORGANIZATION_SWITCH_DURATION = 'organization_switch_duration',
PROJECT_SWITCH_DURATION = 'project_switch_duration',
CLOUD_SYNC_DURATION = 'cloud_sync_duration',
};
export const enum LandingPage {
ProjectDashboard = 'projectDashboard',
Onboarding = 'onboarding',
Login = 'login',
Scratchpad = 'scratchpad',
Workspace = 'workspace',
}

View File

@@ -183,7 +183,7 @@ const _launchApp = async () => {
console.log('[main] Open Deep Link URL sent from second instance', lastArg);
window.webContents.send('shell:open', lastArg);
});
window = windowUtils.createWindowsAndReturnMain();
window = windowUtils.createWindowsAndReturnMain({ firstLaunch: true });
const openDeepLinkUrl = (url: string) => {
console.log('[main] Open Deep Link URL', url);
window = windowUtils.createWindowsAndReturnMain();

View File

@@ -88,7 +88,7 @@ export const ipcMainOn = (
...args: any[]
) => Promise<void> | any
) => ipcMain.on(channel, listener);
export type OnceChannels = 'halfSecondAfterAppStart';
export type OnceChannels = 'halfSecondAfterAppStart' | 'landingPageRendered';
export const ipcMainOnce = (
channel: OnceChannels,
listener: (

View File

@@ -1,6 +1,8 @@
import * as Sentry from '@sentry/electron/main';
import { app, BrowserWindow, IpcRendererEvent, shell } from 'electron';
import fs from 'fs';
import { APP_START_TIME, LandingPage, SentryMetrics } from '../../common/sentry';
import type { HiddenBrowserWindowBridgeAPI } from '../../hidden-window';
import * as models from '../../models';
import { SegmentEvent, trackPageView, trackSegmentEvent } from '../analytics';
@@ -10,7 +12,7 @@ import installPlugin from '../install-plugin';
import { CurlBridgeAPI } from '../network/curl';
import { cancelCurlRequest, curlRequest } from '../network/libcurl-promise';
import { WebSocketBridgeAPI } from '../network/websocket';
import { ipcMainHandle, ipcMainOn, type RendererOnChannels } from './electron';
import { ipcMainHandle, ipcMainOn, ipcMainOnce, type RendererOnChannels } from './electron';
import { gRPCBridgeAPI } from './grpc';
export interface RendererToMainBridgeAPI {
@@ -41,6 +43,7 @@ export interface RendererToMainBridgeAPI {
};
};
hiddenBrowserWindow: HiddenBrowserWindowBridgeAPI;
landingPageRendered: (landingPage: LandingPage, tags?: Record<string, string>) => void;
}
export function registerMainHandlers() {
ipcMainHandle('database.caCertificate.create', async (_, options: { parentId: string; path: string }) => {
@@ -102,4 +105,15 @@ export function registerMainHandlers() {
shell.openExternal(href);
}
});
ipcMainOnce('landingPageRendered', (_, { landingPage, tags = {} }: { landingPage: LandingPage; tags?: Record<string, string> }) => {
const duration = performance.now() - APP_START_TIME;
Sentry.metrics.distribution(SentryMetrics.APP_START_DURATION, duration, {
tags: {
landingPage,
...tags,
},
unit: 'millisecond',
});
});
}

View File

@@ -1,3 +1,4 @@
import * as Sentry from '@sentry/electron/main';
import {
app,
BrowserWindow,
@@ -27,6 +28,7 @@ import {
} from '../common/constants';
import { docsBase } from '../common/documentation';
import * as log from '../common/log';
import { APP_START_TIME, SentryMetrics } from '../common/sentry';
import { invariant } from '../utils/invariant';
import { ipcMainOn } from './ipc/electron';
import LocalStorage from './local-storage';
@@ -162,7 +164,7 @@ export function stopHiddenBrowserWindow() {
browserWindows.get('HiddenBrowserWindow')?.close();
}
export function createWindow(): ElectronBrowserWindow {
export function createWindow({ firstLaunch }: { firstLaunch?: boolean } = {}): ElectronBrowserWindow {
const { bounds, fullscreen, maximize } = getBounds();
const { x, y, width, height } = bounds;
@@ -251,6 +253,12 @@ export function createWindow(): ElectronBrowserWindow {
const appUrl = process.env.APP_RENDER_URL || pathToFileURL(appPath).href;
console.log(`[main] Loading ${appUrl}`);
if (firstLaunch) {
const duration = performance.now() - APP_START_TIME;
Sentry.metrics.distribution(SentryMetrics.MAIN_PROCESS_START_DURATION, duration, {
unit: 'millisecond',
});
}
mainBrowserWindow.loadURL(appUrl);
// Emitted when the window is closed.
mainBrowserWindow.on('closed', () => {
@@ -773,8 +781,8 @@ function initLocalStorage() {
localStorage = new LocalStorage(localStoragePath);
}
export function createWindowsAndReturnMain() {
const mainWindow = browserWindows.get('Insomnia') ?? createWindow();
export function createWindowsAndReturnMain({ firstLaunch }: { firstLaunch?: boolean } = {}) {
const mainWindow = browserWindows.get('Insomnia') ?? createWindow({ firstLaunch });
if (!browserWindows.get('HiddenBrowserWindow')) {
createHiddenBrowserWindow();
}

View File

@@ -88,6 +88,10 @@ const main: Window['main'] = {
port.postMessage({ ...options, type: 'runPreRequestScript' });
}),
},
landingPageRendered: (landingPage, tags) => ipcRenderer.send('landingPageRendered', {
landingPage,
tags,
}),
};
ipcRenderer.on('hidden-browser-window-response-listener', event => {

View File

@@ -1,7 +1,8 @@
import React from 'react';
import React, { useEffect } from 'react';
import { Button, Dialog, DialogTrigger, Heading, Modal, ModalOverlay } from 'react-aria-components';
import { ActionFunction, Link, redirect, useFetcher, useNavigate } from 'react-router-dom';
import { LandingPage } from '../../common/sentry';
import { getAppWebsiteBaseURL } from '../../common/constants';
import { exportAllData } from '../../common/export-all-data';
import { SegmentEvent } from '../analytics';
@@ -63,6 +64,10 @@ const Login = () => {
});
};
useEffect(() => {
window.main.landingPageRendered(LandingPage.Login);
}, []);
return (
<div
className='flex flex-col gap-[--padding-md]'

View File

@@ -36,6 +36,7 @@ import {
import { DEFAULT_SIDEBAR_SIZE, SORT_ORDERS, SortOrder, sortOrderName } from '../../common/constants';
import { ChangeBufferEvent, database as db } from '../../common/database';
import { generateId } from '../../common/misc';
import { LandingPage } from '../../common/sentry';
import { PlatformKeyCombinations } from '../../common/settings';
import type { GrpcMethodInfo } from '../../main/ipc/grpc';
import * as models from '../../models';
@@ -55,6 +56,7 @@ import {
isWebSocketRequestId,
WebSocketRequest,
} from '../../models/websocket-request';
import { isScratchpad } from '../../models/workspace';
import { invariant } from '../../utils/invariant';
import { RequestActionsDropdown } from '../components/dropdowns/request-actions-dropdown';
import { RequestGroupActionsDropdown } from '../components/dropdowns/request-group-actions-dropdown';
@@ -682,6 +684,13 @@ export const Debug: FC = () => {
}
}, [settings.forceVerticalLayout, direction]);
useEffect(() => {
if (isScratchpad(activeWorkspace)) {
window.main.landingPageRendered(LandingPage.Scratchpad);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return (
<PanelGroup ref={sidebarPanelRef} autoSaveId="insomnia-sidebar" id="wrapper" className='new-sidebar w-full h-full text-[--color-font]' direction='horizontal'>
<Panel id="sidebar" className='sidebar theme--sidebar' maxSize={40} minSize={10} collapsible>

View File

@@ -1,7 +1,8 @@
import { IconName } from '@fortawesome/fontawesome-svg-core';
import React from 'react';
import React, { useEffect } from 'react';
import { Link, Route, Routes, useLocation } from 'react-router-dom';
import { LandingPage } from '../../common/sentry';
import { InsomniaLogo } from '../components/insomnia-icon';
import { TrailLinesContainer } from '../components/trail-lines-container';
import auto_pull from '../images/onboarding/auto_pull.png';
@@ -150,6 +151,10 @@ const FeatureWizardView = () => {
const Onboarding = () => {
const location = useLocation();
useEffect(() => {
window.main.landingPageRendered(LandingPage.Onboarding);
}, []);
return (
<div className="relative h-full w-full text-left flex bg-[--color-bg]">
<TrailLinesContainer>

View File

@@ -1,4 +1,5 @@
import React, { Fragment, useEffect, useState } from 'react';
import * as Sentry from '@sentry/electron/renderer';
import React, { Fragment, useEffect, useRef, useState } from 'react';
import {
Button,
Link,
@@ -28,6 +29,7 @@ import { useLocalStorage } from 'react-use';
import * as session from '../../account/session';
import { getAppWebsiteBaseURL } from '../../common/constants';
import { database } from '../../common/database';
import { SentryMetrics } from '../../common/sentry';
import { userSession } from '../../models';
import { updateLocalProjectToRemote } from '../../models/helpers/project';
import { isOwnerOfOrganization, isPersonalOrganization, isScratchpadOrganizationId, Organization } from '../../models/organization';
@@ -455,6 +457,20 @@ const OrganizationRoute = () => {
progress: loadingAIProgress,
} = useAIContext();
const nextOrganizationId = useRef<string>();
const startSwitchOrganizationTime = useRef<number>();
useEffect(() => {
if (nextOrganizationId.current && startSwitchOrganizationTime.current && nextOrganizationId.current === organizationId) {
const duration = performance.now() - startSwitchOrganizationTime.current;
Sentry.metrics.distribution(SentryMetrics.ORGANIZATION_SWITCH_DURATION, duration, {
unit: 'millisecond',
});
nextOrganizationId.current = undefined;
startSwitchOrganizationTime.current = undefined;
}
}, [organizationId]);
return (
<InsomniaEventStreamProvider>
<div className="w-full h-full">
@@ -638,32 +654,44 @@ const OrganizationRoute = () => {
`select-none text-[--color-font-surprise] hover:no-underline transition-all duration-150 bg-gradient-to-br box-border from-[#4000BF] to-[#154B62] font-bold outline-[3px] rounded-md w-[28px] h-[28px] flex items-center justify-center active:outline overflow-hidden outline-offset-[3px] outline ${isActive
? 'outline-[--color-font]'
: 'outline-transparent focus:outline-[--hl-md] hover:outline-[--hl-md]'
} ${isPending ? 'animate-pulse' : ''}`
}
to={`/organization/${organization.id}`}
}`}
onClick={async () => {
nextOrganizationId.current = organization.id;
startSwitchOrganizationTime.current = performance.now();
const routeForOrganization = await getInitialRouteForOrganization({ organizationId: organization.id });
navigate(routeForOrganization, {
state: {
asyncTaskList: [
// we only need sync projects when user switch to another organization
AsyncTask.SyncProjects,
],
},
});
}}
>
{isPersonalOrganization(organization) && isOwnerOfOrganization({
organization,
accountId: userSession.accountId || '',
}) ? (
<Icon icon="home" />
) : (
<OrganizationAvatar
alt={organization.display_name}
src={organization.branding?.logo_url || ''}
/>
)}
</div>
</Link>
<Tooltip
placement="right"
offset={8}
className="border select-none text-sm min-w-max border-solid border-[--hl-sm] shadow-lg bg-[--color-bg] text-[--color-font] px-4 py-2 rounded-md overflow-y-auto max-h-[85vh] focus:outline-none"
>
{isPersonalOrganization(organization) && isOwnerOfOrganization({
organization,
accountId: userSession.accountId || '',
}) ? (
<Icon icon="home" />
) : (
<OrganizationAvatar
alt={organization.display_name}
src={organization.branding?.logo_url || ''}
/>
)}
</NavLink>
</Link>
<Tooltip
placement="right"
offset={8}
className="border select-none text-sm min-w-max border-solid border-[--hl-sm] shadow-lg bg-[--color-bg] text-[--color-font] px-4 py-2 rounded-md overflow-y-auto max-h-[85vh] focus:outline-none"
>
<span>{organization.display_name}</span>
</Tooltip>
</TooltipTrigger>
))}
<span>{organization.display_name}</span>
</Tooltip>
</TooltipTrigger>
);
})}
<MenuTrigger>
<Button className="select-none text-[--color-font] hover:no-underline transition-all duration-150 box-border p-[--padding-sm] font-bold outline-none rounded-md w-[28px] h-[28px] flex items-center justify-center overflow-hidden">
<Icon icon="plus" />

View File

@@ -1,5 +1,6 @@
import { IconName } from '@fortawesome/fontawesome-svg-core';
import React, { FC, Fragment, useEffect, useState } from 'react';
import * as Sentry from '@sentry/electron/renderer';
import React, { FC, Fragment, useEffect, useRef, useState } from 'react';
import {
Button,
Dialog,
@@ -48,6 +49,7 @@ import {
} from '../../common/constants';
import { database } from '../../common/database';
import { fuzzyMatchAll, isNotNullOrUndefined } from '../../common/misc';
import { LandingPage, SentryMetrics } from '../../common/sentry';
import { descendingNumberSort, sortMethodMap } from '../../common/sorting';
import * as models from '../../models';
import { userSession } from '../../models';
@@ -914,6 +916,26 @@ const ProjectRoute: FC = () => {
const isRemoteProjectInconsistent = activeProject && isRemoteProject(activeProject) && storage === 'local_only';
const isLocalProjectInconsistent = activeProject && !isRemoteProject(activeProject) && storage === 'cloud_only';
const isProjectInconsistent = isRemoteProjectInconsistent || isLocalProjectInconsistent;
const showStorageRestrictionMessage = storage !== 'cloud_plus_local';
useEffect(() => {
window.main.landingPageRendered(LandingPage.ProjectDashboard);
}, []);
const nextProjectId = useRef<string>();
const startSwitchProjectTime = useRef<number>();
useEffect(() => {
if (nextProjectId.current && startSwitchProjectTime.current && nextProjectId.current === organizationId) {
const duration = performance.now() - startSwitchProjectTime.current;
Sentry.metrics.distribution(SentryMetrics.PROJECT_SWITCH_DURATION, duration, {
unit: 'millisecond',
});
nextProjectId.current = undefined;
startSwitchProjectTime.current = undefined;
}
}, [organizationId]);
return (
<ErrorBoundary>
<Fragment>
@@ -1007,6 +1029,8 @@ const ProjectRoute: FC = () => {
onSelectionChange={keys => {
if (keys !== 'all') {
const value = keys.values().next().value;
nextProjectId.current = value;
startSwitchProjectTime.current = performance.now();
navigate({
pathname: `/organization/${organizationId}/project/${value}`,
});

View File

@@ -1,7 +1,9 @@
import * as Sentry from '@sentry/electron/renderer';
import { ActionFunction, LoaderFunction, redirect } from 'react-router-dom';
import { database, Operation } from '../../common/database';
import { isNotNullOrUndefined } from '../../common/misc';
import { SentryMetrics } from '../../common/sentry';
import * as models from '../../models';
import { canSync } from '../../models';
import { ApiSpec } from '../../models/api-spec';
@@ -486,6 +488,7 @@ export const deleteBranchAction: ActionFunction = async ({
};
export const pullFromRemoteAction: ActionFunction = async ({ params }) => {
const startPullActionTime = performance.now();
const { organizationId, projectId, workspaceId } = params;
invariant(typeof projectId === 'string', 'Project Id is required');
invariant(typeof workspaceId === 'string', 'Workspace Id is required');
@@ -504,6 +507,12 @@ export const pullFromRemoteAction: ActionFunction = async ({ params }) => {
await database.batchModifyDocs(delta);
delete remoteCompareCache[workspaceId];
const duration = performance.now() - startPullActionTime;
Sentry.metrics.distribution(SentryMetrics.CLOUD_SYNC_DURATION, duration, {
unit: 'millisecond',
tags: { action: 'pull' },
});
} catch (err) {
const errorMessage =
err instanceof Error
@@ -561,6 +570,7 @@ export const fetchRemoteBranchAction: ActionFunction = async ({
};
export const pushToRemoteAction: ActionFunction = async ({ params }) => {
const startPushActionTime = performance.now();
const { projectId, workspaceId } = params;
invariant(typeof projectId === 'string', 'Project Id is required');
invariant(typeof workspaceId === 'string', 'Workspace Id is required');
@@ -576,6 +586,12 @@ export const pushToRemoteAction: ActionFunction = async ({ params }) => {
teamProjectId: project.remoteId,
});
delete remoteCompareCache[workspaceId];
const duration = performance.now() - startPushActionTime;
Sentry.metrics.distribution(SentryMetrics.CLOUD_SYNC_DURATION, duration, {
unit: 'millisecond',
tags: { action: 'push' },
});
} catch (err) {
const errorMessage =
err instanceof Error

View File

@@ -1,9 +1,10 @@
import React from 'react';
import { LoaderFunction, Outlet } from 'react-router-dom';
import React, { useEffect } from 'react';
import { LoaderFunction, Outlet, useLoaderData } from 'react-router-dom';
import { SortOrder } from '../../common/constants';
import { database } from '../../common/database';
import { fuzzyMatchAll } from '../../common/misc';
import { LandingPage } from '../../common/sentry';
import { sortMethodMap } from '../../common/sorting';
import * as models from '../../models';
import { ApiSpec } from '../../models/api-spec';
@@ -280,6 +281,13 @@ export const workspaceLoader: LoaderFunction = async ({
};
const WorkspaceRoute = () => {
const { activeWorkspace } = useLoaderData() as WorkspaceLoaderData;
useEffect(() => {
const { scope } = activeWorkspace;
window.main.landingPageRendered(LandingPage.Workspace, { scope });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <Outlet />;
};