From 748878ce5b99c881b980c370f655e0182939f529 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Thu, 20 Apr 2023 18:15:05 +0800 Subject: [PATCH] feat(app-insights): add React context provider and hook and fix AppInsights init issue for frontend projects. --- .changeset/witty-beers-complain.md | 7 +++ packages/app-insights/package.json | 10 +++- .../{react.ts => react/AppInsightsReact.ts} | 28 ++++++---- packages/app-insights/src/react/context.tsx | 30 +++++++++++ packages/app-insights/src/react/index.ts | 4 ++ .../src/react/use-app-insights.ts | 28 ++++++++++ packages/app-insights/src/react/utils.ts | 1 + packages/app-insights/tsconfig.build.json | 3 ++ packages/app-insights/tsconfig.json | 2 +- packages/console/src/App.tsx | 24 ++++----- .../console/src/components/PageMeta/index.tsx | 9 ++-- .../console/src/hooks/use-track-user-id.ts | 22 ++++---- .../src/onboarding/pages/About/index.tsx | 4 +- .../src/onboarding/pages/Congrats/index.tsx | 4 +- .../pages/SignInExperience/index.tsx | 4 +- .../src/onboarding/pages/Welcome/index.tsx | 4 +- .../src/pages/ApiResourceDetails/index.tsx | 4 +- .../console/src/pages/ApiResources/index.tsx | 4 +- .../src/pages/ApplicationDetails/index.tsx | 4 +- .../console/src/pages/Applications/index.tsx | 4 +- .../src/pages/AuditLogDetails/index.tsx | 4 +- .../console/src/pages/AuditLogs/index.tsx | 4 +- .../src/pages/ConnectorDetails/index.tsx | 4 +- .../console/src/pages/Connectors/index.tsx | 4 +- .../console/src/pages/Dashboard/index.tsx | 4 +- .../console/src/pages/GetStarted/index.tsx | 4 +- packages/console/src/pages/Profile/index.tsx | 4 +- .../console/src/pages/RoleDetails/index.tsx | 4 +- packages/console/src/pages/Roles/index.tsx | 4 +- .../src/pages/SignInExperience/index.tsx | 4 +- .../console/src/pages/UserDetails/index.tsx | 4 +- packages/console/src/pages/Users/index.tsx | 4 +- packages/ui/src/App.tsx | 31 ++++++----- packages/ui/src/components/PageMeta/index.tsx | 9 ++-- pnpm-lock.yaml | 51 +++++++++++++++++++ 35 files changed, 246 insertions(+), 93 deletions(-) create mode 100644 .changeset/witty-beers-complain.md rename packages/app-insights/src/{react.ts => react/AppInsightsReact.ts} (79%) create mode 100644 packages/app-insights/src/react/context.tsx create mode 100644 packages/app-insights/src/react/index.ts create mode 100644 packages/app-insights/src/react/use-app-insights.ts create mode 100644 packages/app-insights/src/react/utils.ts diff --git a/.changeset/witty-beers-complain.md b/.changeset/witty-beers-complain.md new file mode 100644 index 000000000..3fe8a35d5 --- /dev/null +++ b/.changeset/witty-beers-complain.md @@ -0,0 +1,7 @@ +--- +"@logto/app-insights": minor +"@logto/console": patch +"@logto/ui": patch +--- + +add React context and hook to app-insights, fix init issue for frontend projects diff --git a/packages/app-insights/package.json b/packages/app-insights/package.json index f415ddccf..c866cb2e8 100644 --- a/packages/app-insights/package.json +++ b/packages/app-insights/package.json @@ -12,11 +12,15 @@ "./*": { "import": "./lib/*.js", "types": "./lib/*.d.ts" + }, + "./react": { + "import": "./lib/react/index.js", + "types": "./lib/react/index.d.ts" } }, "//": "This field is for parcel. Remove after https://github.com/parcel-bundler/parcel/pull/8807 published.", "alias": { - "./react": "./lib/react.js" + "./react": "./lib/react/index.js" }, "publishConfig": { "access": "public" @@ -32,7 +36,9 @@ }, "devDependencies": { "@silverhand/eslint-config": "3.0.1", + "@silverhand/eslint-config-react": "3.0.1", "@silverhand/ts-config": "3.0.0", + "@silverhand/ts-config-react": "3.0.0", "@types/node": "^18.11.18", "@types/react": "^18.0.31", "eslint": "^8.34.0", @@ -47,7 +53,7 @@ "node": "^18.12.0" }, "eslintConfig": { - "extends": "@silverhand" + "extends": "@silverhand/react" }, "prettier": "@silverhand/eslint-config/.prettierrc", "dependencies": { diff --git a/packages/app-insights/src/react.ts b/packages/app-insights/src/react/AppInsightsReact.ts similarity index 79% rename from packages/app-insights/src/react.ts rename to packages/app-insights/src/react/AppInsightsReact.ts index f24a138b3..b178ea5eb 100644 --- a/packages/app-insights/src/react.ts +++ b/packages/app-insights/src/react/AppInsightsReact.ts @@ -8,9 +8,10 @@ import { type ComponentType } from 'react'; export type SetupConfig = { connectionString?: string; clickPlugin?: IClickAnalyticsConfiguration; + cookieDomain?: string; }; -class AppInsightsReact { +export class AppInsightsReact { protected reactPlugin?: ReactPlugin; protected clickAnalyticsPlugin?: ClickAnalyticsPlugin; protected withAITracking?: typeof withAITracking; @@ -48,7 +49,8 @@ class AppInsightsReact { const { ApplicationInsights } = await import('@microsoft/applicationinsights-web'); // Conditionally load ClickAnalytics plugin - const clickAnalyticsConfig = conditional(typeof config === 'object' && config.clickPlugin); + const configObject = conditional(typeof config === 'object' && config) ?? {}; + const { cookieDomain, clickPlugin } = configObject; const initClickAnalyticsPlugin = async () => { const { ClickAnalyticsPlugin } = await import( '@microsoft/applicationinsights-clickanalytics-js' @@ -62,13 +64,12 @@ class AppInsightsReact { this.reactPlugin = new ReactPlugin(); // Assign ClickAnalytics prop - this.clickAnalyticsPlugin = conditional( - clickAnalyticsConfig && (await initClickAnalyticsPlugin()) - ); + this.clickAnalyticsPlugin = conditional(clickPlugin && (await initClickAnalyticsPlugin())); // Init ApplicationInsights instance this.appInsights = new ApplicationInsights({ config: { + cookieDomain, connectionString, enableAutoRouteTracking: false, extensions: conditionalArray( @@ -77,18 +78,25 @@ class AppInsightsReact { ), extensionConfig: conditional( this.clickAnalyticsPlugin && { - [this.clickAnalyticsPlugin.identifier]: clickAnalyticsConfig, + [this.clickAnalyticsPlugin.identifier]: clickPlugin, } ), }, }); this.appInsights.addTelemetryInitializer((item) => { - // The key 'ai.cloud.role' is extracted from Node SDK - // @see https://learn.microsoft.com/en-us/azure/azure-monitor/app/nodejs#multiple-roles-for-multi-component-applications + // @see https://github.com/microsoft/ApplicationInsights-JS#example-setting-cloud-role-name // @see https://github.com/microsoft/ApplicationInsights-node.js/blob/a573e40fc66981c6a3106bdc5b783d1d94f64231/Schema/PublicSchema/ContextTagKeys.bond#L83 - // eslint-disable-next-line @silverhand/fp/no-mutation + /* eslint-disable @silverhand/fp/no-mutation */ item.tags = [...(item.tags ?? []), { 'ai.cloud.role': cloudRole }]; + + // Extract UTM parameters + const searchParams = [...new URLSearchParams(window.location.search).entries()]; + item.data = { + ...item.data, + ...Object.fromEntries(searchParams.filter(([key]) => key.startsWith('utm_'))), + }; + /* eslint-enable @silverhand/fp/no-mutation */ }); this.appInsights.loadAppInsights(); @@ -112,3 +120,5 @@ class AppInsightsReact { } export const appInsightsReact = new AppInsightsReact(); + +export const withAppInsights = appInsightsReact.withAppInsights.bind(appInsightsReact); diff --git a/packages/app-insights/src/react/context.tsx b/packages/app-insights/src/react/context.tsx new file mode 100644 index 000000000..d5f927c57 --- /dev/null +++ b/packages/app-insights/src/react/context.tsx @@ -0,0 +1,30 @@ +import { type ReactNode, createContext, useMemo, useState } from 'react'; + +type Context = { + initialized: boolean; + setInitialized: React.Dispatch>; +}; + +export const AppInsightsContext = createContext({ + initialized: false, + setInitialized: () => { + throw new Error('Not implemented'); + }, +}); + +type Properties = { + children: ReactNode; +}; + +export const AppInsightsProvider = ({ children }: Properties) => { + const [initialized, setInitialized] = useState(false); + const context = useMemo( + () => ({ + initialized, + setInitialized, + }), + [initialized] + ); + + return {children}; +}; diff --git a/packages/app-insights/src/react/index.ts b/packages/app-insights/src/react/index.ts new file mode 100644 index 000000000..6e16b6714 --- /dev/null +++ b/packages/app-insights/src/react/index.ts @@ -0,0 +1,4 @@ +export { AppInsightsReact, type SetupConfig, withAppInsights } from './AppInsightsReact.js'; +export * from './context.js'; +export * from './use-app-insights.js'; +export * from './utils.js'; diff --git a/packages/app-insights/src/react/use-app-insights.ts b/packages/app-insights/src/react/use-app-insights.ts new file mode 100644 index 000000000..aa08eeb60 --- /dev/null +++ b/packages/app-insights/src/react/use-app-insights.ts @@ -0,0 +1,28 @@ +import { useCallback, useContext } from 'react'; + +import { type AppInsightsReact, appInsightsReact } from './AppInsightsReact.js'; +import { AppInsightsContext } from './context.js'; + +export type UseAppInsights = { + initialized: boolean; + setup: typeof appInsightsReact.setup; + appInsights: AppInsightsReact; +}; + +export const useAppInsights = () => { + const { initialized, setInitialized } = useContext(AppInsightsContext); + + const setup = useCallback( + async (...args: Parameters) => { + const result = await appInsightsReact.setup(...args); + + if (result) { + console.debug('Initialized ApplicationInsights'); + setInitialized(true); + } + }, + [setInitialized] + ); + + return { initialized, setup, appInsights: appInsightsReact }; +}; diff --git a/packages/app-insights/src/react/utils.ts b/packages/app-insights/src/react/utils.ts new file mode 100644 index 000000000..97a36bf92 --- /dev/null +++ b/packages/app-insights/src/react/utils.ts @@ -0,0 +1 @@ +export const getPrimaryDomain = () => window.location.hostname.split('.').slice(-2).join('.'); diff --git a/packages/app-insights/tsconfig.build.json b/packages/app-insights/tsconfig.build.json index 695fba3f1..28541ea19 100644 --- a/packages/app-insights/tsconfig.build.json +++ b/packages/app-insights/tsconfig.build.json @@ -1,4 +1,7 @@ { "extends": "./tsconfig", + "compilerOptions": { + "noEmit": false + }, "include": ["src"] } diff --git a/packages/app-insights/tsconfig.json b/packages/app-insights/tsconfig.json index 225658205..dbfcba652 100644 --- a/packages/app-insights/tsconfig.json +++ b/packages/app-insights/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@silverhand/ts-config/tsconfig.base", + "extends": "@silverhand/ts-config-react/tsconfig.base", "compilerOptions": { "outDir": "lib", "types": ["node"] diff --git a/packages/console/src/App.tsx b/packages/console/src/App.tsx index c97da99b9..2d1f7a42a 100644 --- a/packages/console/src/App.tsx +++ b/packages/console/src/App.tsx @@ -1,9 +1,9 @@ -import { appInsightsReact } from '@logto/app-insights/react'; +import { AppInsightsProvider, getPrimaryDomain, useAppInsights } from '@logto/app-insights/react'; import { UserScope } from '@logto/core-kit'; import { LogtoProvider } from '@logto/react'; import { adminConsoleApplicationId, PredefinedScope } from '@logto/schemas'; import { conditionalArray, deduplicate } from '@silverhand/essentials'; -import { useContext } from 'react'; +import { useContext, useEffect } from 'react'; import { Helmet } from 'react-helmet'; import 'overlayscrollbars/styles/overlayscrollbars.css'; @@ -26,17 +26,15 @@ import AppEndpointsProvider from './contexts/AppEndpointsProvider'; import { AppThemeProvider } from './contexts/AppThemeProvider'; import TenantsProvider, { TenantsContext } from './contexts/TenantsProvider'; -// Use `.then()` for better compatibility, update to top-level await some day -// eslint-disable-next-line unicorn/prefer-top-level-await -void appInsightsReact.setup('console').then((success) => { - if (success) { - console.debug('Initialized ApplicationInsights'); - } -}); void initI18n(); function Content() { const { tenants, isSettle, currentTenantId } = useContext(TenantsContext); + const { setup } = useAppInsights(); + + useEffect(() => { + void setup('console', { cookieDomain: getPrimaryDomain() }); + }, [setup]); const resources = deduplicate( conditionalArray( @@ -90,9 +88,11 @@ function Content() { function App() { return ( - - - + + + + + ); } diff --git a/packages/console/src/components/PageMeta/index.tsx b/packages/console/src/components/PageMeta/index.tsx index 9b8a712b4..a6f24907d 100644 --- a/packages/console/src/components/PageMeta/index.tsx +++ b/packages/console/src/components/PageMeta/index.tsx @@ -1,4 +1,4 @@ -import { appInsightsReact } from '@logto/app-insights/react'; +import { useAppInsights } from '@logto/app-insights/react'; import type { AdminConsoleKey } from '@logto/phrases'; import { useEffect, useState } from 'react'; import { Helmet } from 'react-helmet'; @@ -14,6 +14,7 @@ type Props = { function PageMeta({ titleKey, trackPageView = true }: Props) { const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); + const { initialized, appInsights } = useAppInsights(); const [pageViewTracked, setPageViewTracked] = useState(false); const keys = typeof titleKey === 'string' ? [titleKey] : titleKey; const rawTitle = keys.map((key) => t(key, { lng: 'en' })).join(' - '); @@ -21,11 +22,11 @@ function PageMeta({ titleKey, trackPageView = true }: Props) { useEffect(() => { // Only track once for the same page - if (trackPageView && !pageViewTracked) { - appInsightsReact.trackPageView?.({ name: [rawTitle, mainTitle].join(' - ') }); + if (initialized && trackPageView && !pageViewTracked) { + appInsights.trackPageView?.({ name: [rawTitle, mainTitle].join(' - ') }); setPageViewTracked(true); } - }, [pageViewTracked, rawTitle, trackPageView]); + }, [appInsights, initialized, pageViewTracked, rawTitle, trackPageView]); return ; } diff --git a/packages/console/src/hooks/use-track-user-id.ts b/packages/console/src/hooks/use-track-user-id.ts index 757c70f4c..3fee90055 100644 --- a/packages/console/src/hooks/use-track-user-id.ts +++ b/packages/console/src/hooks/use-track-user-id.ts @@ -1,4 +1,4 @@ -import { appInsightsReact } from '@logto/app-insights/react'; +import { useAppInsights } from '@logto/app-insights/react'; import { useLogto } from '@logto/react'; import { trySafe } from '@silverhand/essentials'; import { useEffect } from 'react'; @@ -10,17 +10,19 @@ class NoIdTokenClaimsError extends Error { const useTrackUserId = () => { const { isAuthenticated, getIdTokenClaims } = useLogto(); + const { + initialized, + appInsights: { instance }, + } = useAppInsights(); useEffect(() => { const setUserId = async () => { - const { instance: appInsights } = appInsightsReact; - - if (!appInsights) { + if (!instance) { return; } if (!isAuthenticated) { - appInsights.clearAuthenticatedUserContext(); + instance.clearAuthenticatedUserContext(); return; } @@ -28,14 +30,16 @@ const useTrackUserId = () => { const claims = await trySafe(getIdTokenClaims()); if (claims) { - appInsights.setAuthenticatedUserContext(claims.sub, claims.sub, true); + instance.setAuthenticatedUserContext(claims.sub, claims.sub, true); } else { - appInsights.trackException({ exception: new NoIdTokenClaimsError() }); + instance.trackException({ exception: new NoIdTokenClaimsError() }); } }; - void setUserId(); - }, [getIdTokenClaims, isAuthenticated]); + if (initialized) { + void setUserId(); + } + }, [getIdTokenClaims, initialized, instance, isAuthenticated]); }; export default useTrackUserId; diff --git a/packages/console/src/onboarding/pages/About/index.tsx b/packages/console/src/onboarding/pages/About/index.tsx index 82900cdcf..36a29a426 100644 --- a/packages/console/src/onboarding/pages/About/index.tsx +++ b/packages/console/src/onboarding/pages/About/index.tsx @@ -1,4 +1,4 @@ -import { appInsightsReact } from '@logto/app-insights/react'; +import { withAppInsights } from '@logto/app-insights/react'; import { conditional } from '@silverhand/essentials'; import { useEffect } from 'react'; import { Controller, useForm } from 'react-hook-form'; @@ -144,4 +144,4 @@ function About() { ); } -export default appInsightsReact.withAppInsights(About); +export default withAppInsights(About); diff --git a/packages/console/src/onboarding/pages/Congrats/index.tsx b/packages/console/src/onboarding/pages/Congrats/index.tsx index fb4e6d7de..55cb66554 100644 --- a/packages/console/src/onboarding/pages/Congrats/index.tsx +++ b/packages/console/src/onboarding/pages/Congrats/index.tsx @@ -1,4 +1,4 @@ -import { appInsightsReact } from '@logto/app-insights/react'; +import { withAppInsights } from '@logto/app-insights/react'; import classNames from 'classnames'; import { useContext } from 'react'; import { Trans, useTranslation } from 'react-i18next'; @@ -63,4 +63,4 @@ function Congrats() { ); } -export default appInsightsReact.withAppInsights(Congrats); +export default withAppInsights(Congrats); diff --git a/packages/console/src/onboarding/pages/SignInExperience/index.tsx b/packages/console/src/onboarding/pages/SignInExperience/index.tsx index 59ca6acb9..00288dfc4 100644 --- a/packages/console/src/onboarding/pages/SignInExperience/index.tsx +++ b/packages/console/src/onboarding/pages/SignInExperience/index.tsx @@ -1,4 +1,4 @@ -import { appInsightsReact } from '@logto/app-insights/react'; +import { withAppInsights } from '@logto/app-insights/react'; import type { SignInExperience as SignInExperienceType } from '@logto/schemas'; import { SignInIdentifier } from '@logto/schemas'; import { useCallback, useEffect, useMemo } from 'react'; @@ -255,4 +255,4 @@ function SignInExperience() { ); } -export default appInsightsReact.withAppInsights(SignInExperience); +export default withAppInsights(SignInExperience); diff --git a/packages/console/src/onboarding/pages/Welcome/index.tsx b/packages/console/src/onboarding/pages/Welcome/index.tsx index 3f7a9ba2d..ca003e868 100644 --- a/packages/console/src/onboarding/pages/Welcome/index.tsx +++ b/packages/console/src/onboarding/pages/Welcome/index.tsx @@ -1,4 +1,4 @@ -import { appInsightsReact } from '@logto/app-insights/react'; +import { withAppInsights } from '@logto/app-insights/react'; import classNames from 'classnames'; import { useEffect } from 'react'; import { Controller, useForm } from 'react-hook-form'; @@ -111,4 +111,4 @@ function Welcome() { ); } -export default appInsightsReact.withAppInsights(Welcome); +export default withAppInsights(Welcome); diff --git a/packages/console/src/pages/ApiResourceDetails/index.tsx b/packages/console/src/pages/ApiResourceDetails/index.tsx index 3d2b6b0aa..916b16076 100644 --- a/packages/console/src/pages/ApiResourceDetails/index.tsx +++ b/packages/console/src/pages/ApiResourceDetails/index.tsx @@ -1,4 +1,4 @@ -import { appInsightsReact } from '@logto/app-insights/react'; +import { withAppInsights } from '@logto/app-insights/react'; import type { Resource } from '@logto/schemas'; import { isManagementApi, Theme } from '@logto/schemas'; import classNames from 'classnames'; @@ -148,4 +148,4 @@ function ApiResourceDetails() { ); } -export default appInsightsReact.withAppInsights(ApiResourceDetails); +export default withAppInsights(ApiResourceDetails); diff --git a/packages/console/src/pages/ApiResources/index.tsx b/packages/console/src/pages/ApiResources/index.tsx index 3d5041a63..73f130e12 100644 --- a/packages/console/src/pages/ApiResources/index.tsx +++ b/packages/console/src/pages/ApiResources/index.tsx @@ -1,4 +1,4 @@ -import { appInsightsReact } from '@logto/app-insights/react'; +import { withAppInsights } from '@logto/app-insights/react'; import type { Resource } from '@logto/schemas'; import { Theme } from '@logto/schemas'; import { toast } from 'react-hot-toast'; @@ -151,4 +151,4 @@ function ApiResources() { ); } -export default appInsightsReact.withAppInsights(ApiResources); +export default withAppInsights(ApiResources); diff --git a/packages/console/src/pages/ApplicationDetails/index.tsx b/packages/console/src/pages/ApplicationDetails/index.tsx index fe37665af..16fd2aa8d 100644 --- a/packages/console/src/pages/ApplicationDetails/index.tsx +++ b/packages/console/src/pages/ApplicationDetails/index.tsx @@ -1,4 +1,4 @@ -import { appInsightsReact } from '@logto/app-insights/react'; +import { withAppInsights } from '@logto/app-insights/react'; import type { Application, ApplicationResponse, SnakeCaseOidcConfig } from '@logto/schemas'; import { ApplicationType } from '@logto/schemas'; import { useEffect, useState } from 'react'; @@ -235,4 +235,4 @@ function ApplicationDetails() { ); } -export default appInsightsReact.withAppInsights(ApplicationDetails); +export default withAppInsights(ApplicationDetails); diff --git a/packages/console/src/pages/Applications/index.tsx b/packages/console/src/pages/Applications/index.tsx index fa446288b..cfa66cd09 100644 --- a/packages/console/src/pages/Applications/index.tsx +++ b/packages/console/src/pages/Applications/index.tsx @@ -1,4 +1,4 @@ -import { appInsightsReact } from '@logto/app-insights/react'; +import { withAppInsights } from '@logto/app-insights/react'; import type { Application } from '@logto/schemas'; import { ApplicationType } from '@logto/schemas'; import { useTranslation } from 'react-i18next'; @@ -147,4 +147,4 @@ function Applications() { ); } -export default appInsightsReact.withAppInsights(Applications); +export default withAppInsights(Applications); diff --git a/packages/console/src/pages/AuditLogDetails/index.tsx b/packages/console/src/pages/AuditLogDetails/index.tsx index cd7f4c79d..7c43d6ac7 100644 --- a/packages/console/src/pages/AuditLogDetails/index.tsx +++ b/packages/console/src/pages/AuditLogDetails/index.tsx @@ -1,4 +1,4 @@ -import { appInsightsReact } from '@logto/app-insights/react'; +import { withAppInsights } from '@logto/app-insights/react'; import type { User, Log } from '@logto/schemas'; import { demoAppApplicationId } from '@logto/schemas'; import { useTranslation } from 'react-i18next'; @@ -122,4 +122,4 @@ function AuditLogDetails() { ); } -export default appInsightsReact.withAppInsights(AuditLogDetails); +export default withAppInsights(AuditLogDetails); diff --git a/packages/console/src/pages/AuditLogs/index.tsx b/packages/console/src/pages/AuditLogs/index.tsx index 6b84f2cd5..17335e947 100644 --- a/packages/console/src/pages/AuditLogs/index.tsx +++ b/packages/console/src/pages/AuditLogs/index.tsx @@ -1,4 +1,4 @@ -import { appInsightsReact } from '@logto/app-insights/react'; +import { withAppInsights } from '@logto/app-insights/react'; import AuditLogTable from '@/components/AuditLogTable'; import CardTitle from '@/components/CardTitle'; @@ -17,4 +17,4 @@ function AuditLogs() { ); } -export default appInsightsReact.withAppInsights(AuditLogs); +export default withAppInsights(AuditLogs); diff --git a/packages/console/src/pages/ConnectorDetails/index.tsx b/packages/console/src/pages/ConnectorDetails/index.tsx index a67634ed1..f98cada9a 100644 --- a/packages/console/src/pages/ConnectorDetails/index.tsx +++ b/packages/console/src/pages/ConnectorDetails/index.tsx @@ -1,4 +1,4 @@ -import { appInsightsReact } from '@logto/app-insights/react'; +import { withAppInsights } from '@logto/app-insights/react'; import { ConnectorType } from '@logto/schemas'; import type { ConnectorFactoryResponse, ConnectorResponse } from '@logto/schemas'; import { useEffect, useState } from 'react'; @@ -235,4 +235,4 @@ function ConnectorDetails() { ); } -export default appInsightsReact.withAppInsights(ConnectorDetails); +export default withAppInsights(ConnectorDetails); diff --git a/packages/console/src/pages/Connectors/index.tsx b/packages/console/src/pages/Connectors/index.tsx index 4ffc71680..f91e74cf7 100644 --- a/packages/console/src/pages/Connectors/index.tsx +++ b/packages/console/src/pages/Connectors/index.tsx @@ -1,4 +1,4 @@ -import { appInsightsReact } from '@logto/app-insights/react'; +import { withAppInsights } from '@logto/app-insights/react'; import type { ConnectorFactoryResponse } from '@logto/schemas'; import { ConnectorType } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; @@ -225,4 +225,4 @@ function Connectors() { ); } -export default appInsightsReact.withAppInsights(Connectors); +export default withAppInsights(Connectors); diff --git a/packages/console/src/pages/Dashboard/index.tsx b/packages/console/src/pages/Dashboard/index.tsx index bb38ca2b8..b35157edb 100644 --- a/packages/console/src/pages/Dashboard/index.tsx +++ b/packages/console/src/pages/Dashboard/index.tsx @@ -1,4 +1,4 @@ -import { appInsightsReact } from '@logto/app-insights/react'; +import { withAppInsights } from '@logto/app-insights/react'; import { format } from 'date-fns'; import type { ChangeEventHandler } from 'react'; import { useState } from 'react'; @@ -153,4 +153,4 @@ function Dashboard() { ); } -export default appInsightsReact.withAppInsights(Dashboard); +export default withAppInsights(Dashboard); diff --git a/packages/console/src/pages/GetStarted/index.tsx b/packages/console/src/pages/GetStarted/index.tsx index 50ce145f8..6b4810dde 100644 --- a/packages/console/src/pages/GetStarted/index.tsx +++ b/packages/console/src/pages/GetStarted/index.tsx @@ -1,4 +1,4 @@ -import { appInsightsReact } from '@logto/app-insights/react'; +import { withAppInsights } from '@logto/app-insights/react'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; @@ -99,4 +99,4 @@ function GetStarted() { ); } -export default appInsightsReact.withAppInsights(GetStarted); +export default withAppInsights(GetStarted); diff --git a/packages/console/src/pages/Profile/index.tsx b/packages/console/src/pages/Profile/index.tsx index da67ae9b6..2816766e1 100644 --- a/packages/console/src/pages/Profile/index.tsx +++ b/packages/console/src/pages/Profile/index.tsx @@ -1,4 +1,4 @@ -import { appInsightsReact } from '@logto/app-insights/react'; +import { withAppInsights } from '@logto/app-insights/react'; import type { ConnectorResponse } from '@logto/schemas'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -99,4 +99,4 @@ function Profile() { ); } -export default appInsightsReact.withAppInsights(Profile); +export default withAppInsights(Profile); diff --git a/packages/console/src/pages/RoleDetails/index.tsx b/packages/console/src/pages/RoleDetails/index.tsx index 38cc1b1e4..fd44675cb 100644 --- a/packages/console/src/pages/RoleDetails/index.tsx +++ b/packages/console/src/pages/RoleDetails/index.tsx @@ -1,4 +1,4 @@ -import { appInsightsReact } from '@logto/app-insights/react'; +import { withAppInsights } from '@logto/app-insights/react'; import type { Role } from '@logto/schemas'; import classNames from 'classnames'; import { useEffect, useState } from 'react'; @@ -135,4 +135,4 @@ function RoleDetails() { ); } -export default appInsightsReact.withAppInsights(RoleDetails); +export default withAppInsights(RoleDetails); diff --git a/packages/console/src/pages/Roles/index.tsx b/packages/console/src/pages/Roles/index.tsx index 50a7f7c3c..9831353ff 100644 --- a/packages/console/src/pages/Roles/index.tsx +++ b/packages/console/src/pages/Roles/index.tsx @@ -1,4 +1,4 @@ -import { appInsightsReact } from '@logto/app-insights/react'; +import { withAppInsights } from '@logto/app-insights/react'; import type { RoleResponse } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; import { useTranslation } from 'react-i18next'; @@ -163,4 +163,4 @@ function Roles() { ); } -export default appInsightsReact.withAppInsights(Roles); +export default withAppInsights(Roles); diff --git a/packages/console/src/pages/SignInExperience/index.tsx b/packages/console/src/pages/SignInExperience/index.tsx index 79a10041f..77b7b6cf6 100644 --- a/packages/console/src/pages/SignInExperience/index.tsx +++ b/packages/console/src/pages/SignInExperience/index.tsx @@ -1,4 +1,4 @@ -import { appInsightsReact } from '@logto/app-insights/react'; +import { withAppInsights } from '@logto/app-insights/react'; import type { SignInExperience as SignInExperienceType } from '@logto/schemas'; import classNames from 'classnames'; import type { ReactNode } from 'react'; @@ -252,4 +252,4 @@ function SignInExperience() { ); } -export default appInsightsReact.withAppInsights(SignInExperience); +export default withAppInsights(SignInExperience); diff --git a/packages/console/src/pages/UserDetails/index.tsx b/packages/console/src/pages/UserDetails/index.tsx index 8893e2c93..b3cde4362 100644 --- a/packages/console/src/pages/UserDetails/index.tsx +++ b/packages/console/src/pages/UserDetails/index.tsx @@ -1,4 +1,4 @@ -import { appInsightsReact } from '@logto/app-insights/react'; +import { withAppInsights } from '@logto/app-insights/react'; import type { User } from '@logto/schemas'; import classNames from 'classnames'; import { useEffect, useState } from 'react'; @@ -250,4 +250,4 @@ function UserDetails() { ); } -export default appInsightsReact.withAppInsights(UserDetails); +export default withAppInsights(UserDetails); diff --git a/packages/console/src/pages/Users/index.tsx b/packages/console/src/pages/Users/index.tsx index 3c3f3f58b..bb3be818a 100644 --- a/packages/console/src/pages/Users/index.tsx +++ b/packages/console/src/pages/Users/index.tsx @@ -1,4 +1,4 @@ -import { appInsightsReact } from '@logto/app-insights/react'; +import { withAppInsights } from '@logto/app-insights/react'; import type { User } from '@logto/schemas'; import { conditional } from '@silverhand/essentials'; import { useTranslation } from 'react-i18next'; @@ -179,4 +179,4 @@ function Users() { ); } -export default appInsightsReact.withAppInsights(Users); +export default withAppInsights(Users); diff --git a/packages/ui/src/App.tsx b/packages/ui/src/App.tsx index 1e045abc0..25ab88b71 100644 --- a/packages/ui/src/App.tsx +++ b/packages/ui/src/App.tsx @@ -1,4 +1,5 @@ -import { appInsightsReact } from '@logto/app-insights/react'; +import { AppInsightsProvider, getPrimaryDomain, useAppInsights } from '@logto/app-insights/react'; +import { useEffect } from 'react'; import { Route, Routes, BrowserRouter } from 'react-router-dom'; import AppLayout from './Layout/AppLayout'; @@ -26,19 +27,17 @@ import { handleSearchParametersData } from './utils/search-parameters'; import './scss/normalized.scss'; -if (shouldTrack) { - // Use `.then()` for better compatibility, update to top-level await some day - // eslint-disable-next-line unicorn/prefer-top-level-await, promise/prefer-await-to-then - void appInsightsReact.setup('ui').then((success) => { - if (success) { - console.debug('Initialized ApplicationInsights'); - } - }); -} - handleSearchParametersData(); -const App = () => { +const Content = () => { + const { setup } = useAppInsights(); + + useEffect(() => { + if (shouldTrack) { + void setup('ui', { cookieDomain: getPrimaryDomain() }); + } + }, [setup]); + return ( @@ -99,4 +98,12 @@ const App = () => { ); }; +const App = () => { + return ( + + + + ); +}; + export default App; diff --git a/packages/ui/src/components/PageMeta/index.tsx b/packages/ui/src/components/PageMeta/index.tsx index bb3e733bf..352cce480 100644 --- a/packages/ui/src/components/PageMeta/index.tsx +++ b/packages/ui/src/components/PageMeta/index.tsx @@ -1,4 +1,4 @@ -import { appInsightsReact } from '@logto/app-insights/react'; +import { useAppInsights } from '@logto/app-insights/react'; import { useEffect, useState } from 'react'; import { Helmet } from 'react-helmet'; import { type TFuncKey, useTranslation } from 'react-i18next'; @@ -13,6 +13,7 @@ type Props = { const PageMeta = ({ titleKey, trackPageView = true }: Props) => { const { t } = useTranslation(); + const { initialized, appInsights } = useAppInsights(); const [pageViewTracked, setPageViewTracked] = useState(false); const keys = typeof titleKey === 'string' ? [titleKey] : titleKey; const rawTitle = keys.map((key) => t(key, { lng: 'en' })).join(' - '); @@ -20,11 +21,11 @@ const PageMeta = ({ titleKey, trackPageView = true }: Props) => { useEffect(() => { // Only track once for the same page - if (shouldTrack && trackPageView && !pageViewTracked) { - appInsightsReact.trackPageView?.({ name: [rawTitle, 'SIE'].join(' - ') }); + if (shouldTrack && initialized && trackPageView && !pageViewTracked) { + appInsights.trackPageView?.({ name: [rawTitle, 'SIE'].join(' - ') }); setPageViewTracked(true); } - }, [pageViewTracked, rawTitle, trackPageView]); + }, [appInsights, initialized, pageViewTracked, rawTitle, trackPageView]); return ; }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2913d46fd..3f94c1698 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -54,9 +54,15 @@ importers: '@silverhand/eslint-config': specifier: 3.0.1 version: 3.0.1(eslint@8.34.0)(prettier@2.8.4)(typescript@5.0.2) + '@silverhand/eslint-config-react': + specifier: 3.0.1 + version: 3.0.1(eslint@8.34.0)(postcss@8.4.21)(prettier@2.8.4)(stylelint@15.0.0)(typescript@5.0.2) '@silverhand/ts-config': specifier: 3.0.0 version: 3.0.0(typescript@5.0.2) + '@silverhand/ts-config-react': + specifier: 3.0.0 + version: 3.0.0(typescript@5.0.2) '@types/node': specifier: ^18.11.18 version: 18.11.18 @@ -8527,6 +8533,28 @@ packages: - typescript dev: true + /@silverhand/eslint-config-react@3.0.1(eslint@8.34.0)(postcss@8.4.21)(prettier@2.8.4)(stylelint@15.0.0)(typescript@5.0.2): + resolution: {integrity: sha512-8roPq3t5qgi4pxYh3It2BhS91LVsp740vhNXVn56RTID0ZdW27LMrV2yp5uTT6kVzLdFYis4nIPlOVFCnv+VKA==} + engines: {node: ^18.12.0} + peerDependencies: + stylelint: ^15.0.0 + dependencies: + '@silverhand/eslint-config': 3.0.1(eslint@8.34.0)(prettier@2.8.4)(typescript@5.0.2) + eslint-config-xo-react: 0.27.0(eslint-plugin-react-hooks@4.6.0)(eslint-plugin-react@7.31.10)(eslint@8.34.0) + eslint-plugin-jsx-a11y: 6.6.1(eslint@8.34.0) + eslint-plugin-react: 7.31.10(eslint@8.34.0) + eslint-plugin-react-hooks: 4.6.0(eslint@8.34.0) + stylelint: 15.0.0 + stylelint-config-xo-scss: 0.15.0(postcss@8.4.21)(stylelint@15.0.0) + transitivePeerDependencies: + - eslint + - eslint-import-resolver-webpack + - postcss + - prettier + - supports-color + - typescript + dev: true + /@silverhand/eslint-config-react@3.0.1(eslint@8.34.0)(postcss@8.4.6)(prettier@2.8.4)(stylelint@15.0.0)(typescript@5.0.2): resolution: {integrity: sha512-8roPq3t5qgi4pxYh3It2BhS91LVsp740vhNXVn56RTID0ZdW27LMrV2yp5uTT6kVzLdFYis4nIPlOVFCnv+VKA==} engines: {node: ^18.12.0} @@ -16914,6 +16942,15 @@ packages: postcss: 8.4.14 dev: true + /postcss-scss@4.0.5(postcss@8.4.21): + resolution: {integrity: sha512-F7xpB6TrXyqUh3GKdyB4Gkp3QL3DDW1+uI+gxx/oJnUt/qXI4trj5OGlp9rOKdoABGULuqtqeG+3HEVQk4DjmA==} + engines: {node: '>=12.0'} + peerDependencies: + postcss: ^8.3.3 + dependencies: + postcss: 8.4.21 + dev: true + /postcss-scss@4.0.5(postcss@8.4.6): resolution: {integrity: sha512-F7xpB6TrXyqUh3GKdyB4Gkp3QL3DDW1+uI+gxx/oJnUt/qXI4trj5OGlp9rOKdoABGULuqtqeG+3HEVQk4DjmA==} engines: {node: '>=12.0'} @@ -18821,6 +18858,20 @@ packages: - postcss dev: true + /stylelint-config-xo-scss@0.15.0(postcss@8.4.21)(stylelint@15.0.0): + resolution: {integrity: sha512-X9WD8cDofWFWy3uaKdwwm+DjEvgI/+h7AtlaPagkhNAeOWH/GFQoeciBvNvyJ8tB1p00SoIzCn2IIOIKXCbxYA==} + engines: {node: '>=12'} + peerDependencies: + stylelint: '>=14.5.1 || ^15.0.0' + dependencies: + postcss-scss: 4.0.5(postcss@8.4.21) + stylelint: 15.0.0 + stylelint-config-xo: 0.21.1(stylelint@15.0.0) + stylelint-scss: 4.3.0(stylelint@15.0.0) + transitivePeerDependencies: + - postcss + dev: true + /stylelint-config-xo-scss@0.15.0(postcss@8.4.6)(stylelint@15.0.0): resolution: {integrity: sha512-X9WD8cDofWFWy3uaKdwwm+DjEvgI/+h7AtlaPagkhNAeOWH/GFQoeciBvNvyJ8tB1p00SoIzCn2IIOIKXCbxYA==} engines: {node: '>=12'}