mirror of
https://github.com/Kong/insomnia.git
synced 2026-04-21 14:47:46 -04:00
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:
@@ -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',
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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: (
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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]'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
@@ -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}`,
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 />;
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user