diff --git a/packages/insomnia/src/common/sentry.ts b/packages/insomnia/src/common/sentry.ts index a25882fac5..f0d2226b3d 100644 --- a/packages/insomnia/src/common/sentry.ts +++ b/packages/insomnia/src/common/sentry.ts @@ -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 = { 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', +} diff --git a/packages/insomnia/src/main.development.ts b/packages/insomnia/src/main.development.ts index 44d4b99fa7..c315d11b8c 100644 --- a/packages/insomnia/src/main.development.ts +++ b/packages/insomnia/src/main.development.ts @@ -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(); diff --git a/packages/insomnia/src/main/ipc/electron.ts b/packages/insomnia/src/main/ipc/electron.ts index a892267b75..aa3b2bb2f8 100644 --- a/packages/insomnia/src/main/ipc/electron.ts +++ b/packages/insomnia/src/main/ipc/electron.ts @@ -88,7 +88,7 @@ export const ipcMainOn = ( ...args: any[] ) => Promise | any ) => ipcMain.on(channel, listener); -export type OnceChannels = 'halfSecondAfterAppStart'; +export type OnceChannels = 'halfSecondAfterAppStart' | 'landingPageRendered'; export const ipcMainOnce = ( channel: OnceChannels, listener: ( diff --git a/packages/insomnia/src/main/ipc/main.ts b/packages/insomnia/src/main/ipc/main.ts index 800ee7529f..d99a5d60a0 100644 --- a/packages/insomnia/src/main/ipc/main.ts +++ b/packages/insomnia/src/main/ipc/main.ts @@ -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) => 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 }) => { + const duration = performance.now() - APP_START_TIME; + Sentry.metrics.distribution(SentryMetrics.APP_START_DURATION, duration, { + tags: { + landingPage, + ...tags, + }, + unit: 'millisecond', + }); + }); } diff --git a/packages/insomnia/src/main/window-utils.ts b/packages/insomnia/src/main/window-utils.ts index 4cb27f69d7..3638e88724 100644 --- a/packages/insomnia/src/main/window-utils.ts +++ b/packages/insomnia/src/main/window-utils.ts @@ -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(); } diff --git a/packages/insomnia/src/preload.ts b/packages/insomnia/src/preload.ts index c2680e25eb..6e2e4fe3f5 100644 --- a/packages/insomnia/src/preload.ts +++ b/packages/insomnia/src/preload.ts @@ -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 => { diff --git a/packages/insomnia/src/ui/routes/auth.login.tsx b/packages/insomnia/src/ui/routes/auth.login.tsx index 4fe7a0b36f..57f2fe7b4e 100644 --- a/packages/insomnia/src/ui/routes/auth.login.tsx +++ b/packages/insomnia/src/ui/routes/auth.login.tsx @@ -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 (
{ } }, [settings.forceVerticalLayout, direction]); + useEffect(() => { + if (isScratchpad(activeWorkspace)) { + window.main.landingPageRendered(LandingPage.Scratchpad); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + return ( diff --git a/packages/insomnia/src/ui/routes/onboarding.tsx b/packages/insomnia/src/ui/routes/onboarding.tsx index 70ccb9af25..a3c8ada080 100644 --- a/packages/insomnia/src/ui/routes/onboarding.tsx +++ b/packages/insomnia/src/ui/routes/onboarding.tsx @@ -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 (
diff --git a/packages/insomnia/src/ui/routes/organization.tsx b/packages/insomnia/src/ui/routes/organization.tsx index 36b7e86c87..870f4ddc25 100644 --- a/packages/insomnia/src/ui/routes/organization.tsx +++ b/packages/insomnia/src/ui/routes/organization.tsx @@ -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(); + const startSwitchOrganizationTime = useRef(); + + 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 (
@@ -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 || '', + }) ? ( + + ) : ( + + )} +
+ + - {isPersonalOrganization(organization) && isOwnerOfOrganization({ - organization, - accountId: userSession.accountId || '', - }) ? ( - - ) : ( - - )} - - - - {organization.display_name} - - - ))} + {organization.display_name} + + + ); + })}