0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(app-insights): add React context provider and hook

and fix AppInsights init issue for frontend projects.
This commit is contained in:
Gao Sun 2023-04-20 18:15:05 +08:00
parent 4331deb6f2
commit 748878ce5b
No known key found for this signature in database
GPG key ID: 13EBE123E4773688
35 changed files with 246 additions and 93 deletions

View file

@ -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

View file

@ -12,11 +12,15 @@
"./*": { "./*": {
"import": "./lib/*.js", "import": "./lib/*.js",
"types": "./lib/*.d.ts" "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.", "//": "This field is for parcel. Remove after https://github.com/parcel-bundler/parcel/pull/8807 published.",
"alias": { "alias": {
"./react": "./lib/react.js" "./react": "./lib/react/index.js"
}, },
"publishConfig": { "publishConfig": {
"access": "public" "access": "public"
@ -32,7 +36,9 @@
}, },
"devDependencies": { "devDependencies": {
"@silverhand/eslint-config": "3.0.1", "@silverhand/eslint-config": "3.0.1",
"@silverhand/eslint-config-react": "3.0.1",
"@silverhand/ts-config": "3.0.0", "@silverhand/ts-config": "3.0.0",
"@silverhand/ts-config-react": "3.0.0",
"@types/node": "^18.11.18", "@types/node": "^18.11.18",
"@types/react": "^18.0.31", "@types/react": "^18.0.31",
"eslint": "^8.34.0", "eslint": "^8.34.0",
@ -47,7 +53,7 @@
"node": "^18.12.0" "node": "^18.12.0"
}, },
"eslintConfig": { "eslintConfig": {
"extends": "@silverhand" "extends": "@silverhand/react"
}, },
"prettier": "@silverhand/eslint-config/.prettierrc", "prettier": "@silverhand/eslint-config/.prettierrc",
"dependencies": { "dependencies": {

View file

@ -8,9 +8,10 @@ import { type ComponentType } from 'react';
export type SetupConfig = { export type SetupConfig = {
connectionString?: string; connectionString?: string;
clickPlugin?: IClickAnalyticsConfiguration; clickPlugin?: IClickAnalyticsConfiguration;
cookieDomain?: string;
}; };
class AppInsightsReact { export class AppInsightsReact {
protected reactPlugin?: ReactPlugin; protected reactPlugin?: ReactPlugin;
protected clickAnalyticsPlugin?: ClickAnalyticsPlugin; protected clickAnalyticsPlugin?: ClickAnalyticsPlugin;
protected withAITracking?: typeof withAITracking; protected withAITracking?: typeof withAITracking;
@ -48,7 +49,8 @@ class AppInsightsReact {
const { ApplicationInsights } = await import('@microsoft/applicationinsights-web'); const { ApplicationInsights } = await import('@microsoft/applicationinsights-web');
// Conditionally load ClickAnalytics plugin // 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 initClickAnalyticsPlugin = async () => {
const { ClickAnalyticsPlugin } = await import( const { ClickAnalyticsPlugin } = await import(
'@microsoft/applicationinsights-clickanalytics-js' '@microsoft/applicationinsights-clickanalytics-js'
@ -62,13 +64,12 @@ class AppInsightsReact {
this.reactPlugin = new ReactPlugin(); this.reactPlugin = new ReactPlugin();
// Assign ClickAnalytics prop // Assign ClickAnalytics prop
this.clickAnalyticsPlugin = conditional( this.clickAnalyticsPlugin = conditional(clickPlugin && (await initClickAnalyticsPlugin()));
clickAnalyticsConfig && (await initClickAnalyticsPlugin())
);
// Init ApplicationInsights instance // Init ApplicationInsights instance
this.appInsights = new ApplicationInsights({ this.appInsights = new ApplicationInsights({
config: { config: {
cookieDomain,
connectionString, connectionString,
enableAutoRouteTracking: false, enableAutoRouteTracking: false,
extensions: conditionalArray<ITelemetryPlugin>( extensions: conditionalArray<ITelemetryPlugin>(
@ -77,18 +78,25 @@ class AppInsightsReact {
), ),
extensionConfig: conditional( extensionConfig: conditional(
this.clickAnalyticsPlugin && { this.clickAnalyticsPlugin && {
[this.clickAnalyticsPlugin.identifier]: clickAnalyticsConfig, [this.clickAnalyticsPlugin.identifier]: clickPlugin,
} }
), ),
}, },
}); });
this.appInsights.addTelemetryInitializer((item) => { this.appInsights.addTelemetryInitializer((item) => {
// The key 'ai.cloud.role' is extracted from Node SDK // @see https://github.com/microsoft/ApplicationInsights-JS#example-setting-cloud-role-name
// @see https://learn.microsoft.com/en-us/azure/azure-monitor/app/nodejs#multiple-roles-for-multi-component-applications
// @see https://github.com/microsoft/ApplicationInsights-node.js/blob/a573e40fc66981c6a3106bdc5b783d1d94f64231/Schema/PublicSchema/ContextTagKeys.bond#L83 // @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 }]; 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(); this.appInsights.loadAppInsights();
@ -112,3 +120,5 @@ class AppInsightsReact {
} }
export const appInsightsReact = new AppInsightsReact(); export const appInsightsReact = new AppInsightsReact();
export const withAppInsights = appInsightsReact.withAppInsights.bind(appInsightsReact);

View file

@ -0,0 +1,30 @@
import { type ReactNode, createContext, useMemo, useState } from 'react';
type Context = {
initialized: boolean;
setInitialized: React.Dispatch<React.SetStateAction<boolean>>;
};
export const AppInsightsContext = createContext<Context>({
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<Context>(
() => ({
initialized,
setInitialized,
}),
[initialized]
);
return <AppInsightsContext.Provider value={context}>{children}</AppInsightsContext.Provider>;
};

View file

@ -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';

View file

@ -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<typeof appInsightsReact.setup>) => {
const result = await appInsightsReact.setup(...args);
if (result) {
console.debug('Initialized ApplicationInsights');
setInitialized(true);
}
},
[setInitialized]
);
return { initialized, setup, appInsights: appInsightsReact };
};

View file

@ -0,0 +1 @@
export const getPrimaryDomain = () => window.location.hostname.split('.').slice(-2).join('.');

View file

@ -1,4 +1,7 @@
{ {
"extends": "./tsconfig", "extends": "./tsconfig",
"compilerOptions": {
"noEmit": false
},
"include": ["src"] "include": ["src"]
} }

View file

@ -1,5 +1,5 @@
{ {
"extends": "@silverhand/ts-config/tsconfig.base", "extends": "@silverhand/ts-config-react/tsconfig.base",
"compilerOptions": { "compilerOptions": {
"outDir": "lib", "outDir": "lib",
"types": ["node"] "types": ["node"]

View file

@ -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 { UserScope } from '@logto/core-kit';
import { LogtoProvider } from '@logto/react'; import { LogtoProvider } from '@logto/react';
import { adminConsoleApplicationId, PredefinedScope } from '@logto/schemas'; import { adminConsoleApplicationId, PredefinedScope } from '@logto/schemas';
import { conditionalArray, deduplicate } from '@silverhand/essentials'; import { conditionalArray, deduplicate } from '@silverhand/essentials';
import { useContext } from 'react'; import { useContext, useEffect } from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import 'overlayscrollbars/styles/overlayscrollbars.css'; import 'overlayscrollbars/styles/overlayscrollbars.css';
@ -26,17 +26,15 @@ import AppEndpointsProvider from './contexts/AppEndpointsProvider';
import { AppThemeProvider } from './contexts/AppThemeProvider'; import { AppThemeProvider } from './contexts/AppThemeProvider';
import TenantsProvider, { TenantsContext } from './contexts/TenantsProvider'; 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(); void initI18n();
function Content() { function Content() {
const { tenants, isSettle, currentTenantId } = useContext(TenantsContext); const { tenants, isSettle, currentTenantId } = useContext(TenantsContext);
const { setup } = useAppInsights();
useEffect(() => {
void setup('console', { cookieDomain: getPrimaryDomain() });
}, [setup]);
const resources = deduplicate( const resources = deduplicate(
conditionalArray( conditionalArray(
@ -90,9 +88,11 @@ function Content() {
function App() { function App() {
return ( return (
<TenantsProvider> <AppInsightsProvider>
<Content /> <TenantsProvider>
</TenantsProvider> <Content />
</TenantsProvider>
</AppInsightsProvider>
); );
} }

View file

@ -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 type { AdminConsoleKey } from '@logto/phrases';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
@ -14,6 +14,7 @@ type Props = {
function PageMeta({ titleKey, trackPageView = true }: Props) { function PageMeta({ titleKey, trackPageView = true }: Props) {
const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' }); const { t } = useTranslation(undefined, { keyPrefix: 'admin_console' });
const { initialized, appInsights } = useAppInsights();
const [pageViewTracked, setPageViewTracked] = useState(false); const [pageViewTracked, setPageViewTracked] = useState(false);
const keys = typeof titleKey === 'string' ? [titleKey] : titleKey; const keys = typeof titleKey === 'string' ? [titleKey] : titleKey;
const rawTitle = keys.map((key) => t(key, { lng: 'en' })).join(' - '); const rawTitle = keys.map((key) => t(key, { lng: 'en' })).join(' - ');
@ -21,11 +22,11 @@ function PageMeta({ titleKey, trackPageView = true }: Props) {
useEffect(() => { useEffect(() => {
// Only track once for the same page // Only track once for the same page
if (trackPageView && !pageViewTracked) { if (initialized && trackPageView && !pageViewTracked) {
appInsightsReact.trackPageView?.({ name: [rawTitle, mainTitle].join(' - ') }); appInsights.trackPageView?.({ name: [rawTitle, mainTitle].join(' - ') });
setPageViewTracked(true); setPageViewTracked(true);
} }
}, [pageViewTracked, rawTitle, trackPageView]); }, [appInsights, initialized, pageViewTracked, rawTitle, trackPageView]);
return <Helmet title={title} />; return <Helmet title={title} />;
} }

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react'; import { useAppInsights } from '@logto/app-insights/react';
import { useLogto } from '@logto/react'; import { useLogto } from '@logto/react';
import { trySafe } from '@silverhand/essentials'; import { trySafe } from '@silverhand/essentials';
import { useEffect } from 'react'; import { useEffect } from 'react';
@ -10,17 +10,19 @@ class NoIdTokenClaimsError extends Error {
const useTrackUserId = () => { const useTrackUserId = () => {
const { isAuthenticated, getIdTokenClaims } = useLogto(); const { isAuthenticated, getIdTokenClaims } = useLogto();
const {
initialized,
appInsights: { instance },
} = useAppInsights();
useEffect(() => { useEffect(() => {
const setUserId = async () => { const setUserId = async () => {
const { instance: appInsights } = appInsightsReact; if (!instance) {
if (!appInsights) {
return; return;
} }
if (!isAuthenticated) { if (!isAuthenticated) {
appInsights.clearAuthenticatedUserContext(); instance.clearAuthenticatedUserContext();
return; return;
} }
@ -28,14 +30,16 @@ const useTrackUserId = () => {
const claims = await trySafe(getIdTokenClaims()); const claims = await trySafe(getIdTokenClaims());
if (claims) { if (claims) {
appInsights.setAuthenticatedUserContext(claims.sub, claims.sub, true); instance.setAuthenticatedUserContext(claims.sub, claims.sub, true);
} else { } else {
appInsights.trackException({ exception: new NoIdTokenClaimsError() }); instance.trackException({ exception: new NoIdTokenClaimsError() });
} }
}; };
void setUserId(); if (initialized) {
}, [getIdTokenClaims, isAuthenticated]); void setUserId();
}
}, [getIdTokenClaims, initialized, instance, isAuthenticated]);
}; };
export default useTrackUserId; export default useTrackUserId;

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react'; import { withAppInsights } from '@logto/app-insights/react';
import { conditional } from '@silverhand/essentials'; import { conditional } from '@silverhand/essentials';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
@ -144,4 +144,4 @@ function About() {
); );
} }
export default appInsightsReact.withAppInsights(About); export default withAppInsights(About);

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react'; import { withAppInsights } from '@logto/app-insights/react';
import classNames from 'classnames'; import classNames from 'classnames';
import { useContext } from 'react'; import { useContext } from 'react';
import { Trans, useTranslation } from 'react-i18next'; import { Trans, useTranslation } from 'react-i18next';
@ -63,4 +63,4 @@ function Congrats() {
); );
} }
export default appInsightsReact.withAppInsights(Congrats); export default withAppInsights(Congrats);

View file

@ -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 type { SignInExperience as SignInExperienceType } from '@logto/schemas';
import { SignInIdentifier } from '@logto/schemas'; import { SignInIdentifier } from '@logto/schemas';
import { useCallback, useEffect, useMemo } from 'react'; import { useCallback, useEffect, useMemo } from 'react';
@ -255,4 +255,4 @@ function SignInExperience() {
); );
} }
export default appInsightsReact.withAppInsights(SignInExperience); export default withAppInsights(SignInExperience);

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react'; import { withAppInsights } from '@logto/app-insights/react';
import classNames from 'classnames'; import classNames from 'classnames';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form';
@ -111,4 +111,4 @@ function Welcome() {
); );
} }
export default appInsightsReact.withAppInsights(Welcome); export default withAppInsights(Welcome);

View file

@ -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 type { Resource } from '@logto/schemas';
import { isManagementApi, Theme } from '@logto/schemas'; import { isManagementApi, Theme } from '@logto/schemas';
import classNames from 'classnames'; import classNames from 'classnames';
@ -148,4 +148,4 @@ function ApiResourceDetails() {
); );
} }
export default appInsightsReact.withAppInsights(ApiResourceDetails); export default withAppInsights(ApiResourceDetails);

View file

@ -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 type { Resource } from '@logto/schemas';
import { Theme } from '@logto/schemas'; import { Theme } from '@logto/schemas';
import { toast } from 'react-hot-toast'; import { toast } from 'react-hot-toast';
@ -151,4 +151,4 @@ function ApiResources() {
); );
} }
export default appInsightsReact.withAppInsights(ApiResources); export default withAppInsights(ApiResources);

View file

@ -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 type { Application, ApplicationResponse, SnakeCaseOidcConfig } from '@logto/schemas';
import { ApplicationType } from '@logto/schemas'; import { ApplicationType } from '@logto/schemas';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@ -235,4 +235,4 @@ function ApplicationDetails() {
); );
} }
export default appInsightsReact.withAppInsights(ApplicationDetails); export default withAppInsights(ApplicationDetails);

View file

@ -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 type { Application } from '@logto/schemas';
import { ApplicationType } from '@logto/schemas'; import { ApplicationType } from '@logto/schemas';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -147,4 +147,4 @@ function Applications() {
); );
} }
export default appInsightsReact.withAppInsights(Applications); export default withAppInsights(Applications);

View file

@ -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 type { User, Log } from '@logto/schemas';
import { demoAppApplicationId } from '@logto/schemas'; import { demoAppApplicationId } from '@logto/schemas';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -122,4 +122,4 @@ function AuditLogDetails() {
); );
} }
export default appInsightsReact.withAppInsights(AuditLogDetails); export default withAppInsights(AuditLogDetails);

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react'; import { withAppInsights } from '@logto/app-insights/react';
import AuditLogTable from '@/components/AuditLogTable'; import AuditLogTable from '@/components/AuditLogTable';
import CardTitle from '@/components/CardTitle'; import CardTitle from '@/components/CardTitle';
@ -17,4 +17,4 @@ function AuditLogs() {
); );
} }
export default appInsightsReact.withAppInsights(AuditLogs); export default withAppInsights(AuditLogs);

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react'; import { withAppInsights } from '@logto/app-insights/react';
import { ConnectorType } from '@logto/schemas'; import { ConnectorType } from '@logto/schemas';
import type { ConnectorFactoryResponse, ConnectorResponse } from '@logto/schemas'; import type { ConnectorFactoryResponse, ConnectorResponse } from '@logto/schemas';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@ -235,4 +235,4 @@ function ConnectorDetails() {
); );
} }
export default appInsightsReact.withAppInsights(ConnectorDetails); export default withAppInsights(ConnectorDetails);

View file

@ -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 type { ConnectorFactoryResponse } from '@logto/schemas';
import { ConnectorType } from '@logto/schemas'; import { ConnectorType } from '@logto/schemas';
import { conditional } from '@silverhand/essentials'; import { conditional } from '@silverhand/essentials';
@ -225,4 +225,4 @@ function Connectors() {
); );
} }
export default appInsightsReact.withAppInsights(Connectors); export default withAppInsights(Connectors);

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react'; import { withAppInsights } from '@logto/app-insights/react';
import { format } from 'date-fns'; import { format } from 'date-fns';
import type { ChangeEventHandler } from 'react'; import type { ChangeEventHandler } from 'react';
import { useState } from 'react'; import { useState } from 'react';
@ -153,4 +153,4 @@ function Dashboard() {
); );
} }
export default appInsightsReact.withAppInsights(Dashboard); export default withAppInsights(Dashboard);

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react'; import { withAppInsights } from '@logto/app-insights/react';
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
@ -99,4 +99,4 @@ function GetStarted() {
); );
} }
export default appInsightsReact.withAppInsights(GetStarted); export default withAppInsights(GetStarted);

View file

@ -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 type { ConnectorResponse } from '@logto/schemas';
import { useState } from 'react'; import { useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -99,4 +99,4 @@ function Profile() {
); );
} }
export default appInsightsReact.withAppInsights(Profile); export default withAppInsights(Profile);

View file

@ -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 type { Role } from '@logto/schemas';
import classNames from 'classnames'; import classNames from 'classnames';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@ -135,4 +135,4 @@ function RoleDetails() {
); );
} }
export default appInsightsReact.withAppInsights(RoleDetails); export default withAppInsights(RoleDetails);

View file

@ -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 type { RoleResponse } from '@logto/schemas';
import { conditional } from '@silverhand/essentials'; import { conditional } from '@silverhand/essentials';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -163,4 +163,4 @@ function Roles() {
); );
} }
export default appInsightsReact.withAppInsights(Roles); export default withAppInsights(Roles);

View file

@ -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 type { SignInExperience as SignInExperienceType } from '@logto/schemas';
import classNames from 'classnames'; import classNames from 'classnames';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
@ -252,4 +252,4 @@ function SignInExperience() {
); );
} }
export default appInsightsReact.withAppInsights(SignInExperience); export default withAppInsights(SignInExperience);

View file

@ -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 type { User } from '@logto/schemas';
import classNames from 'classnames'; import classNames from 'classnames';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
@ -250,4 +250,4 @@ function UserDetails() {
); );
} }
export default appInsightsReact.withAppInsights(UserDetails); export default withAppInsights(UserDetails);

View file

@ -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 type { User } from '@logto/schemas';
import { conditional } from '@silverhand/essentials'; import { conditional } from '@silverhand/essentials';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -179,4 +179,4 @@ function Users() {
); );
} }
export default appInsightsReact.withAppInsights(Users); export default withAppInsights(Users);

View file

@ -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 { Route, Routes, BrowserRouter } from 'react-router-dom';
import AppLayout from './Layout/AppLayout'; import AppLayout from './Layout/AppLayout';
@ -26,19 +27,17 @@ import { handleSearchParametersData } from './utils/search-parameters';
import './scss/normalized.scss'; 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(); handleSearchParametersData();
const App = () => { const Content = () => {
const { setup } = useAppInsights();
useEffect(() => {
if (shouldTrack) {
void setup('ui', { cookieDomain: getPrimaryDomain() });
}
}, [setup]);
return ( return (
<BrowserRouter> <BrowserRouter>
<PageContextProvider> <PageContextProvider>
@ -99,4 +98,12 @@ const App = () => {
); );
}; };
const App = () => {
return (
<AppInsightsProvider>
<Content />
</AppInsightsProvider>
);
};
export default App; export default App;

View file

@ -1,4 +1,4 @@
import { appInsightsReact } from '@logto/app-insights/react'; import { useAppInsights } from '@logto/app-insights/react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { Helmet } from 'react-helmet'; import { Helmet } from 'react-helmet';
import { type TFuncKey, useTranslation } from 'react-i18next'; import { type TFuncKey, useTranslation } from 'react-i18next';
@ -13,6 +13,7 @@ type Props = {
const PageMeta = ({ titleKey, trackPageView = true }: Props) => { const PageMeta = ({ titleKey, trackPageView = true }: Props) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { initialized, appInsights } = useAppInsights();
const [pageViewTracked, setPageViewTracked] = useState(false); const [pageViewTracked, setPageViewTracked] = useState(false);
const keys = typeof titleKey === 'string' ? [titleKey] : titleKey; const keys = typeof titleKey === 'string' ? [titleKey] : titleKey;
const rawTitle = keys.map((key) => t(key, { lng: 'en' })).join(' - '); const rawTitle = keys.map((key) => t(key, { lng: 'en' })).join(' - ');
@ -20,11 +21,11 @@ const PageMeta = ({ titleKey, trackPageView = true }: Props) => {
useEffect(() => { useEffect(() => {
// Only track once for the same page // Only track once for the same page
if (shouldTrack && trackPageView && !pageViewTracked) { if (shouldTrack && initialized && trackPageView && !pageViewTracked) {
appInsightsReact.trackPageView?.({ name: [rawTitle, 'SIE'].join(' - ') }); appInsights.trackPageView?.({ name: [rawTitle, 'SIE'].join(' - ') });
setPageViewTracked(true); setPageViewTracked(true);
} }
}, [pageViewTracked, rawTitle, trackPageView]); }, [appInsights, initialized, pageViewTracked, rawTitle, trackPageView]);
return <Helmet title={title} />; return <Helmet title={title} />;
}; };

View file

@ -54,9 +54,15 @@ importers:
'@silverhand/eslint-config': '@silverhand/eslint-config':
specifier: 3.0.1 specifier: 3.0.1
version: 3.0.1(eslint@8.34.0)(prettier@2.8.4)(typescript@5.0.2) 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': '@silverhand/ts-config':
specifier: 3.0.0 specifier: 3.0.0
version: 3.0.0(typescript@5.0.2) 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': '@types/node':
specifier: ^18.11.18 specifier: ^18.11.18
version: 18.11.18 version: 18.11.18
@ -8527,6 +8533,28 @@ packages:
- typescript - typescript
dev: true 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): /@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==} resolution: {integrity: sha512-8roPq3t5qgi4pxYh3It2BhS91LVsp740vhNXVn56RTID0ZdW27LMrV2yp5uTT6kVzLdFYis4nIPlOVFCnv+VKA==}
engines: {node: ^18.12.0} engines: {node: ^18.12.0}
@ -16914,6 +16942,15 @@ packages:
postcss: 8.4.14 postcss: 8.4.14
dev: true 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): /postcss-scss@4.0.5(postcss@8.4.6):
resolution: {integrity: sha512-F7xpB6TrXyqUh3GKdyB4Gkp3QL3DDW1+uI+gxx/oJnUt/qXI4trj5OGlp9rOKdoABGULuqtqeG+3HEVQk4DjmA==} resolution: {integrity: sha512-F7xpB6TrXyqUh3GKdyB4Gkp3QL3DDW1+uI+gxx/oJnUt/qXI4trj5OGlp9rOKdoABGULuqtqeG+3HEVQk4DjmA==}
engines: {node: '>=12.0'} engines: {node: '>=12.0'}
@ -18821,6 +18858,20 @@ packages:
- postcss - postcss
dev: true 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): /stylelint-config-xo-scss@0.15.0(postcss@8.4.6)(stylelint@15.0.0):
resolution: {integrity: sha512-X9WD8cDofWFWy3uaKdwwm+DjEvgI/+h7AtlaPagkhNAeOWH/GFQoeciBvNvyJ8tB1p00SoIzCn2IIOIKXCbxYA==} resolution: {integrity: sha512-X9WD8cDofWFWy3uaKdwwm+DjEvgI/+h7AtlaPagkhNAeOWH/GFQoeciBvNvyJ8tB1p00SoIzCn2IIOIKXCbxYA==}
engines: {node: '>=12'} engines: {node: '>=12'}