mirror of
https://github.com/logto-io/logto.git
synced 2024-12-30 20:33:54 -05:00
refactor: update tracking components (#3792)
* refactor: update tracking components * refactor: track once for the same session
This commit is contained in:
parent
61aaf7d98d
commit
5a8442712f
38 changed files with 230 additions and 35 deletions
9
.changeset/four-poets-change.md
Normal file
9
.changeset/four-poets-change.md
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
---
|
||||||
|
"@logto/app-insights": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
add custom events and new component
|
||||||
|
|
||||||
|
- implement `getEventName()` to create standard event name with the format `<component>/<event>[/data]`. E.g. `core/sign_in`.
|
||||||
|
- four new event components `core`, `console`, `blog`, and `website`.
|
||||||
|
- implement `<TrackOnce />` for tracking a custom event once
|
|
@ -20,7 +20,8 @@
|
||||||
},
|
},
|
||||||
"//": "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/index.js"
|
"./react": "./lib/react/index.js",
|
||||||
|
"./custom-event": "./lib/custom-event.js"
|
||||||
},
|
},
|
||||||
"publishConfig": {
|
"publishConfig": {
|
||||||
"access": "public"
|
"access": "public"
|
||||||
|
|
59
packages/app-insights/src/custom-event.ts
Normal file
59
packages/app-insights/src/custom-event.ts
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
/** The app (either frontend or backend) for a bunch of events. */
|
||||||
|
export enum Component {
|
||||||
|
/** Logto core service. */
|
||||||
|
Core = 'core',
|
||||||
|
/** Logto Console. */
|
||||||
|
Console = 'console',
|
||||||
|
/** Logto blog. */
|
||||||
|
Blog = 'blog',
|
||||||
|
/** Logto official website. */
|
||||||
|
Website = 'website',
|
||||||
|
}
|
||||||
|
|
||||||
|
/** General event enums that for frontend apps. */
|
||||||
|
export enum GeneralEvent {
|
||||||
|
/** A user visited the current app, it should be recorded once per session. */
|
||||||
|
Visit = 'visit',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum CoreEvent {
|
||||||
|
/** An existing user signed in via an interaction. */
|
||||||
|
SignIn = 'sign_in',
|
||||||
|
/** A new user has created in an interaction. */
|
||||||
|
Register = 'register',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum ConsoleEvent {
|
||||||
|
/** A user started the onboarding process. */
|
||||||
|
Onboard = 'onboard',
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum BlogEvent {
|
||||||
|
/** A user viewed a blog post. */
|
||||||
|
ViewPost = 'view_post',
|
||||||
|
}
|
||||||
|
|
||||||
|
export const eventsMap = Object.freeze({
|
||||||
|
[Component.Core]: CoreEvent,
|
||||||
|
[Component.Console]: { ...ConsoleEvent, ...GeneralEvent },
|
||||||
|
[Component.Blog]: { ...BlogEvent, ...GeneralEvent },
|
||||||
|
[Component.Website]: GeneralEvent,
|
||||||
|
}) satisfies Record<Component, unknown>;
|
||||||
|
|
||||||
|
export type EventsMap = typeof eventsMap;
|
||||||
|
|
||||||
|
export type EventType<C extends Component> = EventsMap[C][keyof EventsMap[C]];
|
||||||
|
|
||||||
|
export function getEventName(
|
||||||
|
component: Component.Blog,
|
||||||
|
event: EventType<Component.Blog>,
|
||||||
|
postUrl?: string
|
||||||
|
): string;
|
||||||
|
export function getEventName<C extends Component>(component: C, event: EventType<C>): string;
|
||||||
|
export function getEventName<C extends Component>(
|
||||||
|
component: C,
|
||||||
|
event: EventType<C>,
|
||||||
|
data?: string
|
||||||
|
): string {
|
||||||
|
return [component, event, data].filter(Boolean).join('/');
|
||||||
|
}
|
|
@ -3,19 +3,27 @@ import { type ReactNode, useContext, useEffect } from 'react';
|
||||||
import { AppInsightsContext, AppInsightsProvider } from './context.js';
|
import { AppInsightsContext, AppInsightsProvider } from './context.js';
|
||||||
import { getPrimaryDomain } from './utils.js';
|
import { getPrimaryDomain } from './utils.js';
|
||||||
|
|
||||||
const AppInsights = () => {
|
type AppInsightsProps = {
|
||||||
|
cloudRole: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AppInsights = ({ cloudRole }: AppInsightsProps) => {
|
||||||
const { needsSetup, setup } = useContext(AppInsightsContext);
|
const { needsSetup, setup } = useContext(AppInsightsContext);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const run = async () => {
|
||||||
|
await setup(cloudRole, { cookieDomain: getPrimaryDomain() });
|
||||||
|
};
|
||||||
|
|
||||||
if (needsSetup) {
|
if (needsSetup) {
|
||||||
void setup('console', { cookieDomain: getPrimaryDomain() });
|
void run();
|
||||||
}
|
}
|
||||||
}, [needsSetup, setup]);
|
}, [cloudRole, needsSetup, setup]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Props = {
|
type Props = AppInsightsProps & {
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -28,10 +36,10 @@ type Props = {
|
||||||
* cause issues for some context providers. For example, `useHandleSignInCallback` will be
|
* cause issues for some context providers. For example, `useHandleSignInCallback` will be
|
||||||
* called twice if you use this component to wrap a `<LogtoProvider />`.
|
* called twice if you use this component to wrap a `<LogtoProvider />`.
|
||||||
*/
|
*/
|
||||||
const AppInsightsBoundary = ({ children }: Props) => {
|
const AppInsightsBoundary = ({ children, ...rest }: Props) => {
|
||||||
return (
|
return (
|
||||||
<AppInsightsProvider>
|
<AppInsightsProvider>
|
||||||
<AppInsights />
|
<AppInsights {...rest} />
|
||||||
{children}
|
{children}
|
||||||
</AppInsightsProvider>
|
</AppInsightsProvider>
|
||||||
);
|
);
|
||||||
|
|
|
@ -12,6 +12,12 @@ export type SetupConfig = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export class AppInsightsReact {
|
export class AppInsightsReact {
|
||||||
|
/**
|
||||||
|
* URL search parameters that start with `utm_`. It is an empty object until you call `.setup()`,
|
||||||
|
* which will read the URL search string and store parameters in this property.
|
||||||
|
*/
|
||||||
|
utmParameters: Record<string, string> = {};
|
||||||
|
|
||||||
protected reactPlugin?: ReactPlugin;
|
protected reactPlugin?: ReactPlugin;
|
||||||
protected clickAnalyticsPlugin?: ClickAnalyticsPlugin;
|
protected clickAnalyticsPlugin?: ClickAnalyticsPlugin;
|
||||||
protected withAITracking?: typeof withAITracking;
|
protected withAITracking?: typeof withAITracking;
|
||||||
|
@ -84,18 +90,17 @@ export class AppInsightsReact {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Extract UTM parameters
|
||||||
|
const searchParams = [...new URLSearchParams(window.location.search).entries()];
|
||||||
|
this.utmParameters = Object.fromEntries(
|
||||||
|
searchParams.filter(([key]) => key.startsWith('utm_'))
|
||||||
|
);
|
||||||
|
|
||||||
this.appInsights.addTelemetryInitializer((item) => {
|
this.appInsights.addTelemetryInitializer((item) => {
|
||||||
// @see https://github.com/microsoft/ApplicationInsights-JS#example-setting-cloud-role-name
|
// @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
|
// @see https://github.com/microsoft/ApplicationInsights-node.js/blob/a573e40fc66981c6a3106bdc5b783d1d94f64231/Schema/PublicSchema/ContextTagKeys.bond#L83
|
||||||
/* eslint-disable @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 */
|
/* eslint-enable @silverhand/fp/no-mutation */
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
47
packages/app-insights/src/react/TrackOnce.tsx
Normal file
47
packages/app-insights/src/react/TrackOnce.tsx
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
import { type ICustomProperties } from '@microsoft/applicationinsights-web';
|
||||||
|
import { yes } from '@silverhand/essentials';
|
||||||
|
import { useContext, useEffect } from 'react';
|
||||||
|
|
||||||
|
import { getEventName, type Component, type EventType } from '../custom-event.js';
|
||||||
|
|
||||||
|
import { AppInsightsContext } from './context.js';
|
||||||
|
|
||||||
|
type Props<C extends Component> = {
|
||||||
|
component: C;
|
||||||
|
event: EventType<C>;
|
||||||
|
customProperties?: ICustomProperties;
|
||||||
|
};
|
||||||
|
|
||||||
|
const storageKeyPrefix = 'logto:insights:';
|
||||||
|
|
||||||
|
/** Track an event after AppInsights SDK is setup, but only once during the current session. */
|
||||||
|
const TrackOnce = <C extends Component>({ component, event, customProperties }: Props<C>) => {
|
||||||
|
const { isSetupFinished, appInsights } = useContext(AppInsightsContext);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const eventName = getEventName(component, event);
|
||||||
|
const storageKey = `${storageKeyPrefix}${eventName}`;
|
||||||
|
const tracked = yes(sessionStorage.getItem(storageKey));
|
||||||
|
|
||||||
|
if (isSetupFinished && !tracked) {
|
||||||
|
appInsights.instance?.trackEvent(
|
||||||
|
{
|
||||||
|
name: getEventName(component, event),
|
||||||
|
},
|
||||||
|
{ ...appInsights.utmParameters, ...customProperties }
|
||||||
|
);
|
||||||
|
sessionStorage.setItem(storageKey, '1');
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
appInsights.instance,
|
||||||
|
appInsights.utmParameters,
|
||||||
|
component,
|
||||||
|
customProperties,
|
||||||
|
event,
|
||||||
|
isSetupFinished,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default TrackOnce;
|
|
@ -2,3 +2,4 @@ export { AppInsightsReact, type SetupConfig, withAppInsights } from './AppInsigh
|
||||||
export * from './context.js';
|
export * from './context.js';
|
||||||
export * from './utils.js';
|
export * from './utils.js';
|
||||||
export { default as AppInsightsBoundary } from './AppInsightsBoundary.js';
|
export { default as AppInsightsBoundary } from './AppInsightsBoundary.js';
|
||||||
|
export { default as TrackOnce } from './TrackOnce.js';
|
||||||
|
|
|
@ -71,7 +71,7 @@ function Content() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AppThemeProvider>
|
<AppThemeProvider>
|
||||||
<AppInsightsBoundary>
|
<AppInsightsBoundary cloudRole="console">
|
||||||
<Helmet titleTemplate={`%s - ${mainTitle}`} defaultTitle={mainTitle} />
|
<Helmet titleTemplate={`%s - ${mainTitle}`} defaultTitle={mainTitle} />
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
{!isCloud || isSettle ? (
|
{!isCloud || isSettle ? (
|
||||||
|
|
|
@ -4,8 +4,6 @@ import { useContext, useEffect, useState } from 'react';
|
||||||
import { Helmet } from 'react-helmet';
|
import { Helmet } from 'react-helmet';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { mainTitle } from '@/consts/tenants';
|
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
titleKey: AdminConsoleKey | AdminConsoleKey[];
|
titleKey: AdminConsoleKey | AdminConsoleKey[];
|
||||||
// eslint-disable-next-line react/boolean-prop-naming
|
// eslint-disable-next-line react/boolean-prop-naming
|
||||||
|
@ -23,7 +21,7 @@ function PageMeta({ titleKey, trackPageView = true }: Props) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only track once for the same page
|
// Only track once for the same page
|
||||||
if (isSetupFinished && trackPageView && !pageViewTracked) {
|
if (isSetupFinished && trackPageView && !pageViewTracked) {
|
||||||
appInsights.trackPageView?.({ name: [rawTitle, mainTitle].join(' - ') });
|
appInsights.trackPageView?.({ name: `Console: ${rawTitle}` });
|
||||||
setPageViewTracked(true);
|
setPageViewTracked(true);
|
||||||
}
|
}
|
||||||
}, [appInsights, isSetupFinished, pageViewTracked, rawTitle, trackPageView]);
|
}, [appInsights, isSetupFinished, pageViewTracked, rawTitle, trackPageView]);
|
||||||
|
|
|
@ -5,9 +5,8 @@ export * from './resources';
|
||||||
export * from './tenants';
|
export * from './tenants';
|
||||||
export * from './page-tabs';
|
export * from './page-tabs';
|
||||||
export * from './external-links';
|
export * from './external-links';
|
||||||
|
export * from './storage';
|
||||||
|
|
||||||
export const appearanceModeStorageKey = 'logto:admin_console:appearance_mode';
|
|
||||||
export const profileSocialLinkingKeyPrefix = 'logto:admin_console:linking_social_connector';
|
|
||||||
export const requestTimeout = 20_000;
|
export const requestTimeout = 20_000;
|
||||||
export const defaultPageSize = 20;
|
export const defaultPageSize = 20;
|
||||||
|
|
||||||
|
|
13
packages/console/src/consts/storage.ts
Normal file
13
packages/console/src/consts/storage.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
export type CamelCase<T> = T extends `${infer A}_${infer B}`
|
||||||
|
? `${A}${Capitalize<CamelCase<B>>}`
|
||||||
|
: T;
|
||||||
|
|
||||||
|
export type StorageType = 'appearance_mode' | 'linking_social_connector';
|
||||||
|
|
||||||
|
export const getStorageKey = <T extends StorageType>(forType: T) =>
|
||||||
|
`logto:admin_console:${forType}` as const;
|
||||||
|
|
||||||
|
export const storageKeys = Object.freeze({
|
||||||
|
appearanceMode: getStorageKey('appearance_mode'),
|
||||||
|
linkingSocialConnector: getStorageKey('linking_social_connector'),
|
||||||
|
} satisfies Record<CamelCase<StorageType>, string>);
|
|
@ -3,7 +3,7 @@ import { conditionalString, noop, trySafe } from '@silverhand/essentials';
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useEffect, useMemo, useState, createContext } from 'react';
|
import { useEffect, useMemo, useState, createContext } from 'react';
|
||||||
|
|
||||||
import { appearanceModeStorageKey } from '@/consts';
|
import { storageKeys } from '@/consts';
|
||||||
import type { AppearanceMode } from '@/types/appearance-mode';
|
import type { AppearanceMode } from '@/types/appearance-mode';
|
||||||
import { appearanceModeGuard, DynamicAppearanceMode } from '@/types/appearance-mode';
|
import { appearanceModeGuard, DynamicAppearanceMode } from '@/types/appearance-mode';
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ const getThemeBySystemConfiguration = (): Theme =>
|
||||||
darkThemeWatchMedia.matches ? Theme.Dark : Theme.Light;
|
darkThemeWatchMedia.matches ? Theme.Dark : Theme.Light;
|
||||||
|
|
||||||
export const buildDefaultAppearanceMode = (): AppearanceMode =>
|
export const buildDefaultAppearanceMode = (): AppearanceMode =>
|
||||||
trySafe(() => appearanceModeGuard.parse(localStorage.getItem(appearanceModeStorageKey))) ??
|
trySafe(() => appearanceModeGuard.parse(localStorage.getItem(storageKeys.appearanceMode))) ??
|
||||||
DynamicAppearanceMode.System;
|
DynamicAppearanceMode.System;
|
||||||
|
|
||||||
const defaultAppearanceMode = buildDefaultAppearanceMode();
|
const defaultAppearanceMode = buildDefaultAppearanceMode();
|
||||||
|
@ -47,7 +47,7 @@ export function AppThemeProvider({ children }: Props) {
|
||||||
|
|
||||||
const setAppearanceMode = (mode: AppearanceMode) => {
|
const setAppearanceMode = (mode: AppearanceMode) => {
|
||||||
setMode(mode);
|
setMode(mode);
|
||||||
localStorage.setItem(appearanceModeStorageKey, mode);
|
localStorage.setItem(storageKeys.appearanceMode, mode);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { Component, ConsoleEvent } from '@logto/app-insights/custom-event';
|
||||||
|
import { TrackOnce } from '@logto/app-insights/react';
|
||||||
import { Theme } from '@logto/schemas';
|
import { Theme } from '@logto/schemas';
|
||||||
import { useContext, useEffect } from 'react';
|
import { useContext, useEffect } from 'react';
|
||||||
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom';
|
||||||
|
@ -46,6 +48,7 @@ function App() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BrowserRouter basename={getBasename()}>
|
<BrowserRouter basename={getBasename()}>
|
||||||
|
<TrackOnce component={Component.Console} event={ConsoleEvent.Onboard} />
|
||||||
<div className={styles.app}>
|
<div className={styles.app}>
|
||||||
<SWRConfig value={swrOptions}>
|
<SWRConfig value={swrOptions}>
|
||||||
<AppBoundary>
|
<AppBoundary>
|
||||||
|
|
|
@ -56,7 +56,7 @@ function About() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={pageLayout.page}>
|
<div className={pageLayout.page}>
|
||||||
<PageMeta titleKey="cloud.about.page_title" />
|
<PageMeta titleKey={['cloud.about.page_title', 'cloud.general.onboarding']} />
|
||||||
<OverlayScrollbar className={pageLayout.contentContainer}>
|
<OverlayScrollbar className={pageLayout.contentContainer}>
|
||||||
<div className={pageLayout.content}>
|
<div className={pageLayout.content}>
|
||||||
<Case />
|
<Case />
|
||||||
|
|
|
@ -31,7 +31,7 @@ function Congrats() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={pageLayout.page}>
|
<div className={pageLayout.page}>
|
||||||
<PageMeta titleKey="cloud.congrats.page_title" />
|
<PageMeta titleKey={['cloud.congrats.page_title', 'cloud.general.onboarding']} />
|
||||||
<OverlayScrollbar className={pageLayout.contentContainer}>
|
<OverlayScrollbar className={pageLayout.contentContainer}>
|
||||||
<div className={classNames(pageLayout.content, styles.content)}>
|
<div className={classNames(pageLayout.content, styles.content)}>
|
||||||
<CongratsImage className={styles.congratsImage} />
|
<CongratsImage className={styles.congratsImage} />
|
||||||
|
|
|
@ -107,7 +107,7 @@ function SignInExperience() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={pageLayout.page}>
|
<div className={pageLayout.page}>
|
||||||
<PageMeta titleKey="cloud.sie.page_title" />
|
<PageMeta titleKey={['cloud.sie.page_title', 'cloud.general.onboarding']} />
|
||||||
<OverlayScrollbar className={pageLayout.contentContainer}>
|
<OverlayScrollbar className={pageLayout.contentContainer}>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<div className={styles.config}>
|
<div className={styles.config}>
|
||||||
|
|
|
@ -53,7 +53,7 @@ function Welcome() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={pageLayout.page}>
|
<div className={pageLayout.page}>
|
||||||
<PageMeta titleKey="cloud.welcome.page_title" />
|
<PageMeta titleKey={['cloud.welcome.page_title', 'cloud.general.onboarding']} />
|
||||||
<OverlayScrollbar className={pageLayout.contentContainer}>
|
<OverlayScrollbar className={pageLayout.contentContainer}>
|
||||||
<div className={classNames(pageLayout.content, styles.content)}>
|
<div className={classNames(pageLayout.content, styles.content)}>
|
||||||
<WelcomeImage className={styles.congrats} />
|
<WelcomeImage className={styles.congrats} />
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { Component, GeneralEvent } from '@logto/app-insights/custom-event';
|
||||||
|
import { TrackOnce } from '@logto/app-insights/react';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
Route,
|
Route,
|
||||||
|
@ -41,6 +43,7 @@ function Main() {
|
||||||
return (
|
return (
|
||||||
<SWRConfig value={swrOptions}>
|
<SWRConfig value={swrOptions}>
|
||||||
<AppBoundary>
|
<AppBoundary>
|
||||||
|
<TrackOnce component={Component.Console} event={GeneralEvent.Visit} />
|
||||||
<Toast />
|
<Toast />
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</AppBoundary>
|
</AppBoundary>
|
||||||
|
|
|
@ -14,7 +14,7 @@ import FormCard from '@/components/FormCard';
|
||||||
import ImageWithErrorFallback from '@/components/ImageWithErrorFallback';
|
import ImageWithErrorFallback from '@/components/ImageWithErrorFallback';
|
||||||
import UnnamedTrans from '@/components/UnnamedTrans';
|
import UnnamedTrans from '@/components/UnnamedTrans';
|
||||||
import UserInfoCard from '@/components/UserInfoCard';
|
import UserInfoCard from '@/components/UserInfoCard';
|
||||||
import { adminTenantEndpoint, getBasename, meApi, profileSocialLinkingKeyPrefix } from '@/consts';
|
import { adminTenantEndpoint, getBasename, meApi, storageKeys } from '@/consts';
|
||||||
import { useStaticApi } from '@/hooks/use-api';
|
import { useStaticApi } from '@/hooks/use-api';
|
||||||
import { useConfirmModal } from '@/hooks/use-confirm-modal';
|
import { useConfirmModal } from '@/hooks/use-confirm-modal';
|
||||||
import useTheme from '@/hooks/use-theme';
|
import useTheme from '@/hooks/use-theme';
|
||||||
|
@ -48,7 +48,7 @@ function LinkAccountSection({ user, connectors, onUpdate }: Props) {
|
||||||
.post('me/social/authorization-uri', { json: { connectorId, state, redirectUri } })
|
.post('me/social/authorization-uri', { json: { connectorId, state, redirectUri } })
|
||||||
.json<{ redirectTo: string }>();
|
.json<{ redirectTo: string }>();
|
||||||
|
|
||||||
sessionStorage.setItem(profileSocialLinkingKeyPrefix, connectorId);
|
sessionStorage.setItem(storageKeys.linkingSocialConnector, connectorId);
|
||||||
|
|
||||||
return redirectTo;
|
return redirectTo;
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,7 +2,7 @@ import { useEffect } from 'react';
|
||||||
import { useLocation } from 'react-router-dom';
|
import { useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import AppLoading from '@/components/AppLoading';
|
import AppLoading from '@/components/AppLoading';
|
||||||
import { adminTenantEndpoint, meApi, profileSocialLinkingKeyPrefix } from '@/consts';
|
import { adminTenantEndpoint, meApi, storageKeys } from '@/consts';
|
||||||
import { useStaticApi } from '@/hooks/use-api';
|
import { useStaticApi } from '@/hooks/use-api';
|
||||||
import { useConfirmModal } from '@/hooks/use-confirm-modal';
|
import { useConfirmModal } from '@/hooks/use-confirm-modal';
|
||||||
|
|
||||||
|
@ -19,8 +19,8 @@ function HandleSocialCallback() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
const connectorId = sessionStorage.getItem(profileSocialLinkingKeyPrefix);
|
const connectorId = sessionStorage.getItem(storageKeys.linkingSocialConnector);
|
||||||
sessionStorage.removeItem(profileSocialLinkingKeyPrefix);
|
sessionStorage.removeItem(storageKeys.linkingSocialConnector);
|
||||||
|
|
||||||
if (connectorId) {
|
if (connectorId) {
|
||||||
const queries = new URLSearchParams(search);
|
const queries = new URLSearchParams(search);
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { Component, CoreEvent, getEventName } from '@logto/app-insights/custom-event';
|
||||||
|
import { appInsights } from '@logto/app-insights/node';
|
||||||
import type { User, Profile } from '@logto/schemas';
|
import type { User, Profile } from '@logto/schemas';
|
||||||
import {
|
import {
|
||||||
AdminTenantRole,
|
AdminTenantRole,
|
||||||
|
@ -196,6 +198,7 @@ export default async function submitInteraction(
|
||||||
await assignInteractionResults(ctx, provider, { login: { accountId: id } });
|
await assignInteractionResults(ctx, provider, { login: { accountId: id } });
|
||||||
|
|
||||||
log?.append({ userId: id });
|
log?.append({ userId: id });
|
||||||
|
appInsights.client?.trackEvent({ name: getEventName(Component.Core, CoreEvent.Register) });
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -208,9 +211,10 @@ export default async function submitInteraction(
|
||||||
const upsertProfile = await parseUserProfile(connectors, interaction, user);
|
const upsertProfile = await parseUserProfile(connectors, interaction, user);
|
||||||
|
|
||||||
await updateUserById(accountId, upsertProfile);
|
await updateUserById(accountId, upsertProfile);
|
||||||
|
|
||||||
await assignInteractionResults(ctx, provider, { login: { accountId } });
|
await assignInteractionResults(ctx, provider, { login: { accountId } });
|
||||||
|
|
||||||
|
appInsights.client?.trackEvent({ name: getEventName(Component.Core, CoreEvent.SignIn) });
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
const cloud = {
|
const cloud = {
|
||||||
|
general: {
|
||||||
|
onboarding: 'Einführung',
|
||||||
|
},
|
||||||
welcome: {
|
welcome: {
|
||||||
page_title: 'Willkommen',
|
page_title: 'Willkommen',
|
||||||
title: 'Willkommen, lass uns deine eigene Logto Cloud Preview erstellen',
|
title: 'Willkommen, lass uns deine eigene Logto Cloud Preview erstellen',
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
const cloud = {
|
const cloud = {
|
||||||
|
general: {
|
||||||
|
onboarding: 'Onboarding',
|
||||||
|
},
|
||||||
welcome: {
|
welcome: {
|
||||||
page_title: 'Welcome',
|
page_title: 'Welcome',
|
||||||
title: 'Welcome and let’s create your own Logto Cloud Preview',
|
title: 'Welcome and let’s create your own Logto Cloud Preview',
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
const cloud = {
|
const cloud = {
|
||||||
|
general: {
|
||||||
|
onboarding: 'Integración',
|
||||||
|
},
|
||||||
welcome: {
|
welcome: {
|
||||||
page_title: 'Bienvenido',
|
page_title: 'Bienvenido',
|
||||||
title: 'Bienvenido y creemos su propia vista previa de Logto Cloud',
|
title: 'Bienvenido y creemos su propia vista previa de Logto Cloud',
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
const cloud = {
|
const cloud = {
|
||||||
|
general: {
|
||||||
|
onboarding: 'Intégration',
|
||||||
|
},
|
||||||
welcome: {
|
welcome: {
|
||||||
page_title: 'Bienvenue',
|
page_title: 'Bienvenue',
|
||||||
title: 'Bienvenue et créons votre propre aperçu cloud de Logto',
|
title: 'Bienvenue et créons votre propre aperçu cloud de Logto',
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
const cloud = {
|
const cloud = {
|
||||||
|
general: {
|
||||||
|
onboarding: 'Inizio',
|
||||||
|
},
|
||||||
welcome: {
|
welcome: {
|
||||||
page_title: 'Benvenuto',
|
page_title: 'Benvenuto',
|
||||||
title: 'Benvenuto e creiamo insieme la tua anteprima di Logto Cloud',
|
title: 'Benvenuto e creiamo insieme la tua anteprima di Logto Cloud',
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
const cloud = {
|
const cloud = {
|
||||||
|
general: {
|
||||||
|
onboarding: 'オンボーディング',
|
||||||
|
},
|
||||||
welcome: {
|
welcome: {
|
||||||
page_title: 'ようこそ',
|
page_title: 'ようこそ',
|
||||||
title: 'ようこそ、あなたのLogto Cloud Previewを作成しましょう',
|
title: 'ようこそ、あなたのLogto Cloud Previewを作成しましょう',
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
const cloud = {
|
const cloud = {
|
||||||
|
general: {
|
||||||
|
onboarding: '온보딩',
|
||||||
|
},
|
||||||
welcome: {
|
welcome: {
|
||||||
page_title: '환영합니다',
|
page_title: '환영합니다',
|
||||||
title: '환영합니다. 당신 만의 Logto Cloud Preview를 만들어 보세요',
|
title: '환영합니다. 당신 만의 Logto Cloud Preview를 만들어 보세요',
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
const cloud = {
|
const cloud = {
|
||||||
|
general: {
|
||||||
|
onboarding: 'Wdrażanie',
|
||||||
|
},
|
||||||
welcome: {
|
welcome: {
|
||||||
page_title: 'Witaj',
|
page_title: 'Witaj',
|
||||||
title: 'Witaj i stwórz własny podgląd chmury Logto',
|
title: 'Witaj i stwórz własny podgląd chmury Logto',
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
const cloud = {
|
const cloud = {
|
||||||
|
general: {
|
||||||
|
onboarding: 'Integração',
|
||||||
|
},
|
||||||
welcome: {
|
welcome: {
|
||||||
page_title: 'Boas-vindas',
|
page_title: 'Boas-vindas',
|
||||||
title: 'Boas-vindas e vamos criar sua própria visualização de nuvem Logto',
|
title: 'Boas-vindas e vamos criar sua própria visualização de nuvem Logto',
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
const cloud = {
|
const cloud = {
|
||||||
|
general: {
|
||||||
|
onboarding: 'Introdução',
|
||||||
|
},
|
||||||
welcome: {
|
welcome: {
|
||||||
page_title: 'Bem-vindo',
|
page_title: 'Bem-vindo',
|
||||||
title: 'Bem-vindo e vamos criar a sua própria visualização da Logto Cloud',
|
title: 'Bem-vindo e vamos criar a sua própria visualização da Logto Cloud',
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
const cloud = {
|
const cloud = {
|
||||||
|
general: {
|
||||||
|
onboarding: 'Вводный курс',
|
||||||
|
},
|
||||||
welcome: {
|
welcome: {
|
||||||
page_title: 'Добро пожаловать',
|
page_title: 'Добро пожаловать',
|
||||||
title:
|
title:
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
const cloud = {
|
const cloud = {
|
||||||
|
general: {
|
||||||
|
onboarding: 'Başlatma',
|
||||||
|
},
|
||||||
welcome: {
|
welcome: {
|
||||||
page_title: 'Hoşgeldiniz',
|
page_title: 'Hoşgeldiniz',
|
||||||
title: 'Hoşgeldiniz ve kendi Logto Cloud Önizlemenizi oluşturalım',
|
title: 'Hoşgeldiniz ve kendi Logto Cloud Önizlemenizi oluşturalım',
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
const cloud = {
|
const cloud = {
|
||||||
|
general: {
|
||||||
|
onboarding: '入门',
|
||||||
|
},
|
||||||
welcome: {
|
welcome: {
|
||||||
page_title: '欢迎',
|
page_title: '欢迎',
|
||||||
title: '欢迎来到 Logto Cloud(预览版),让我们一起创建独属于你的体验',
|
title: '欢迎来到 Logto Cloud(预览版),让我们一起创建独属于你的体验',
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
const cloud = {
|
const cloud = {
|
||||||
|
general: {
|
||||||
|
onboarding: '入門',
|
||||||
|
},
|
||||||
welcome: {
|
welcome: {
|
||||||
page_title: '歡迎',
|
page_title: '歡迎',
|
||||||
title: '歡迎來到 Logto Cloud(預覽版),讓我們一起創建獨屬於你的體驗',
|
title: '歡迎來到 Logto Cloud(預覽版),讓我們一起創建獨屬於你的體驗',
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
const cloud = {
|
const cloud = {
|
||||||
|
general: {
|
||||||
|
onboarding: '入門',
|
||||||
|
},
|
||||||
welcome: {
|
welcome: {
|
||||||
page_title: '歡迎',
|
page_title: '歡迎',
|
||||||
title: '歡迎來到 Logto Cloud(預覽版),讓我們一起創建獨屬於你的體驗',
|
title: '歡迎來到 Logto Cloud(預覽版),讓我們一起創建獨屬於你的體驗',
|
||||||
|
|
|
@ -33,7 +33,7 @@ const App = () => {
|
||||||
<PageContextProvider>
|
<PageContextProvider>
|
||||||
<SettingsProvider>
|
<SettingsProvider>
|
||||||
<AppBoundary>
|
<AppBoundary>
|
||||||
<AppInsightsBoundary>
|
<AppInsightsBoundary cloudRole="ui">
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="sign-in/consent" element={<Consent />} />
|
<Route path="sign-in/consent" element={<Consent />} />
|
||||||
<Route element={<AppLayout />}>
|
<Route element={<AppLayout />}>
|
||||||
|
|
|
@ -23,7 +23,7 @@ const PageMeta = ({ titleKey, trackPageView = true }: Props) => {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Only track once for the same page
|
// Only track once for the same page
|
||||||
if (shouldTrack && isSetupFinished && trackPageView && !pageViewTracked) {
|
if (shouldTrack && isSetupFinished && trackPageView && !pageViewTracked) {
|
||||||
appInsights.trackPageView?.({ name: [rawTitle, 'SIE'].join(' - ') });
|
appInsights.trackPageView?.({ name: `Main flow: ${rawTitle}` });
|
||||||
setPageViewTracked(true);
|
setPageViewTracked(true);
|
||||||
}
|
}
|
||||||
}, [appInsights, isSetupFinished, pageViewTracked, rawTitle, trackPageView]);
|
}, [appInsights, isSetupFinished, pageViewTracked, rawTitle, trackPageView]);
|
||||||
|
|
Loading…
Reference in a new issue