mirror of https://github.com/langgenius/dify.git
integrate Amplitude analytics into the application (#29049)
Co-authored-by: CodingOnStar <hanxujiang@dify.ai> Co-authored-by: Joel <iamjoel007@gmail.com>
This commit is contained in:
parent
c7d2a13524
commit
fbb2d076f4
|
|
@ -3,6 +3,7 @@ import type { ReactNode } from 'react'
|
||||||
import SwrInitializer from '@/app/components/swr-initializer'
|
import SwrInitializer from '@/app/components/swr-initializer'
|
||||||
import { AppContextProvider } from '@/context/app-context'
|
import { AppContextProvider } from '@/context/app-context'
|
||||||
import GA, { GaType } from '@/app/components/base/ga'
|
import GA, { GaType } from '@/app/components/base/ga'
|
||||||
|
import AmplitudeProvider from '@/app/components/base/amplitude'
|
||||||
import HeaderWrapper from '@/app/components/header/header-wrapper'
|
import HeaderWrapper from '@/app/components/header/header-wrapper'
|
||||||
import Header from '@/app/components/header'
|
import Header from '@/app/components/header'
|
||||||
import { EventEmitterContextProvider } from '@/context/event-emitter'
|
import { EventEmitterContextProvider } from '@/context/event-emitter'
|
||||||
|
|
@ -18,6 +19,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<GA gaType={GaType.admin} />
|
<GA gaType={GaType.admin} />
|
||||||
|
<AmplitudeProvider />
|
||||||
<SwrInitializer>
|
<SwrInitializer>
|
||||||
<AppContextProvider>
|
<AppContextProvider>
|
||||||
<EventEmitterContextProvider>
|
<EventEmitterContextProvider>
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import { useProviderContext } from '@/context/provider-context'
|
||||||
import { LogOut01 } from '@/app/components/base/icons/src/vender/line/general'
|
import { LogOut01 } from '@/app/components/base/icons/src/vender/line/general'
|
||||||
import PremiumBadge from '@/app/components/base/premium-badge'
|
import PremiumBadge from '@/app/components/base/premium-badge'
|
||||||
import { useLogout } from '@/service/use-common'
|
import { useLogout } from '@/service/use-common'
|
||||||
|
import { resetUser } from '@/app/components/base/amplitude/utils'
|
||||||
|
|
||||||
export type IAppSelector = {
|
export type IAppSelector = {
|
||||||
isMobile: boolean
|
isMobile: boolean
|
||||||
|
|
@ -28,6 +29,7 @@ export default function AppSelector() {
|
||||||
await logout()
|
await logout()
|
||||||
|
|
||||||
localStorage.removeItem('setup_status')
|
localStorage.removeItem('setup_status')
|
||||||
|
resetUser()
|
||||||
// Tokens are now stored in cookies and cleared by backend
|
// Tokens are now stored in cookies and cleared by backend
|
||||||
|
|
||||||
router.push('/signin')
|
router.push('/signin')
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import Header from './header'
|
||||||
import SwrInitor from '@/app/components/swr-initializer'
|
import SwrInitor from '@/app/components/swr-initializer'
|
||||||
import { AppContextProvider } from '@/context/app-context'
|
import { AppContextProvider } from '@/context/app-context'
|
||||||
import GA, { GaType } from '@/app/components/base/ga'
|
import GA, { GaType } from '@/app/components/base/ga'
|
||||||
|
import AmplitudeProvider from '@/app/components/base/amplitude'
|
||||||
import HeaderWrapper from '@/app/components/header/header-wrapper'
|
import HeaderWrapper from '@/app/components/header/header-wrapper'
|
||||||
import { EventEmitterContextProvider } from '@/context/event-emitter'
|
import { EventEmitterContextProvider } from '@/context/event-emitter'
|
||||||
import { ProviderContextProvider } from '@/context/provider-context'
|
import { ProviderContextProvider } from '@/context/provider-context'
|
||||||
|
|
@ -13,6 +14,7 @@ const Layout = ({ children }: { children: ReactNode }) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<GA gaType={GaType.admin} />
|
<GA gaType={GaType.admin} />
|
||||||
|
<AmplitudeProvider />
|
||||||
<SwrInitor>
|
<SwrInitor>
|
||||||
<AppContextProvider>
|
<AppContextProvider>
|
||||||
<EventEmitterContextProvider>
|
<EventEmitterContextProvider>
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import Input from '@/app/components/base/input'
|
||||||
import { AppModeEnum } from '@/types/app'
|
import { AppModeEnum } from '@/types/app'
|
||||||
import { DSLImportMode } from '@/models/app'
|
import { DSLImportMode } from '@/models/app'
|
||||||
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
|
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
|
||||||
|
import { trackEvent } from '@/app/components/base/amplitude'
|
||||||
|
|
||||||
type AppsProps = {
|
type AppsProps = {
|
||||||
onSuccess?: () => void
|
onSuccess?: () => void
|
||||||
|
|
@ -141,6 +142,15 @@ const Apps = ({
|
||||||
icon_background,
|
icon_background,
|
||||||
description,
|
description,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Track app creation from template
|
||||||
|
trackEvent('create_app_with_template', {
|
||||||
|
app_mode: mode,
|
||||||
|
template_id: currApp?.app.id,
|
||||||
|
template_name: currApp?.app.name,
|
||||||
|
description,
|
||||||
|
})
|
||||||
|
|
||||||
setIsShowCreateModal(false)
|
setIsShowCreateModal(false)
|
||||||
Toast.notify({
|
Toast.notify({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import { getRedirection } from '@/utils/app-redirection'
|
||||||
import FullScreenModal from '@/app/components/base/fullscreen-modal'
|
import FullScreenModal from '@/app/components/base/fullscreen-modal'
|
||||||
import useTheme from '@/hooks/use-theme'
|
import useTheme from '@/hooks/use-theme'
|
||||||
import { useDocLink } from '@/context/i18n'
|
import { useDocLink } from '@/context/i18n'
|
||||||
|
import { trackEvent } from '@/app/components/base/amplitude'
|
||||||
|
|
||||||
type CreateAppProps = {
|
type CreateAppProps = {
|
||||||
onSuccess: () => void
|
onSuccess: () => void
|
||||||
|
|
@ -82,6 +83,13 @@ function CreateApp({ onClose, onSuccess, onCreateFromTemplate, defaultAppMode }:
|
||||||
icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined,
|
icon_background: appIcon.type === 'emoji' ? appIcon.background : undefined,
|
||||||
mode: appMode,
|
mode: appMode,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Track app creation success
|
||||||
|
trackEvent('create_app', {
|
||||||
|
app_mode: appMode,
|
||||||
|
description,
|
||||||
|
})
|
||||||
|
|
||||||
notify({ type: 'success', message: t('app.newApp.appCreated') })
|
notify({ type: 'success', message: t('app.newApp.appCreated') })
|
||||||
onSuccess()
|
onSuccess()
|
||||||
onClose()
|
onClose()
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import { getRedirection } from '@/utils/app-redirection'
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
|
import { usePluginDependencies } from '@/app/components/workflow/plugin-dependency/hooks'
|
||||||
import { noop } from 'lodash-es'
|
import { noop } from 'lodash-es'
|
||||||
|
import { trackEvent } from '@/app/components/base/amplitude'
|
||||||
|
|
||||||
type CreateFromDSLModalProps = {
|
type CreateFromDSLModalProps = {
|
||||||
show: boolean
|
show: boolean
|
||||||
|
|
@ -112,6 +113,13 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
|
||||||
return
|
return
|
||||||
const { id, status, app_id, app_mode, imported_dsl_version, current_dsl_version } = response
|
const { id, status, app_id, app_mode, imported_dsl_version, current_dsl_version } = response
|
||||||
if (status === DSLImportStatus.COMPLETED || status === DSLImportStatus.COMPLETED_WITH_WARNINGS) {
|
if (status === DSLImportStatus.COMPLETED || status === DSLImportStatus.COMPLETED_WITH_WARNINGS) {
|
||||||
|
// Track app creation from DSL import
|
||||||
|
trackEvent('create_app_with_dsl', {
|
||||||
|
app_mode,
|
||||||
|
creation_method: currentTab === CreateFromDSLModalTab.FROM_FILE ? 'dsl_file' : 'dsl_url',
|
||||||
|
has_warnings: status === DSLImportStatus.COMPLETED_WITH_WARNINGS,
|
||||||
|
})
|
||||||
|
|
||||||
if (onSuccess)
|
if (onSuccess)
|
||||||
onSuccess()
|
onSuccess()
|
||||||
if (onClose)
|
if (onClose)
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ import quarterOfYear from 'dayjs/plugin/quarterOfYear'
|
||||||
import type { QueryParam } from './index'
|
import type { QueryParam } from './index'
|
||||||
import Chip from '@/app/components/base/chip'
|
import Chip from '@/app/components/base/chip'
|
||||||
import Input from '@/app/components/base/input'
|
import Input from '@/app/components/base/input'
|
||||||
|
import { trackEvent } from '@/app/components/base/amplitude/utils'
|
||||||
dayjs.extend(quarterOfYear)
|
dayjs.extend(quarterOfYear)
|
||||||
|
|
||||||
const today = dayjs()
|
const today = dayjs()
|
||||||
|
|
@ -37,6 +38,9 @@ const Filter: FC<IFilterProps> = ({ queryParams, setQueryParams }: IFilterProps)
|
||||||
value={queryParams.status || 'all'}
|
value={queryParams.status || 'all'}
|
||||||
onSelect={(item) => {
|
onSelect={(item) => {
|
||||||
setQueryParams({ ...queryParams, status: item.value as string })
|
setQueryParams({ ...queryParams, status: item.value as string })
|
||||||
|
trackEvent('workflow_log_filter_status_selected', {
|
||||||
|
workflow_log_filter_status: item.value as string,
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
onClear={() => setQueryParams({ ...queryParams, status: 'all' })}
|
onClear={() => setQueryParams({ ...queryParams, status: 'all' })}
|
||||||
items={[{ value: 'all', name: 'All' },
|
items={[{ value: 'all', name: 'All' },
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,46 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import type { FC } from 'react'
|
||||||
|
import React, { useEffect } from 'react'
|
||||||
|
import * as amplitude from '@amplitude/analytics-browser'
|
||||||
|
import { sessionReplayPlugin } from '@amplitude/plugin-session-replay-browser'
|
||||||
|
import { IS_CLOUD_EDITION } from '@/config'
|
||||||
|
|
||||||
|
export type IAmplitudeProps = {
|
||||||
|
apiKey?: string
|
||||||
|
sessionReplaySampleRate?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
const AmplitudeProvider: FC<IAmplitudeProps> = ({
|
||||||
|
apiKey = process.env.NEXT_PUBLIC_AMPLITUDE_API_KEY ?? '',
|
||||||
|
sessionReplaySampleRate = 1,
|
||||||
|
}) => {
|
||||||
|
useEffect(() => {
|
||||||
|
// Only enable in Saas edition
|
||||||
|
if (!IS_CLOUD_EDITION)
|
||||||
|
return
|
||||||
|
|
||||||
|
// Initialize Amplitude
|
||||||
|
amplitude.init(apiKey, {
|
||||||
|
defaultTracking: {
|
||||||
|
sessions: true,
|
||||||
|
pageViews: true,
|
||||||
|
formInteractions: true,
|
||||||
|
fileDownloads: true,
|
||||||
|
},
|
||||||
|
// Enable debug logs in development environment
|
||||||
|
logLevel: amplitude.Types.LogLevel.Warn,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Add Session Replay plugin
|
||||||
|
const sessionReplay = sessionReplayPlugin({
|
||||||
|
sampleRate: sessionReplaySampleRate,
|
||||||
|
})
|
||||||
|
amplitude.add(sessionReplay)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
// This is a client component that renders nothing
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.memo(AmplitudeProvider)
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
export { default } from './AmplitudeProvider'
|
||||||
|
export { resetUser, setUserId, setUserProperties, trackEvent } from './utils'
|
||||||
|
|
@ -0,0 +1,37 @@
|
||||||
|
import * as amplitude from '@amplitude/analytics-browser'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track custom event
|
||||||
|
* @param eventName Event name
|
||||||
|
* @param eventProperties Event properties (optional)
|
||||||
|
*/
|
||||||
|
export const trackEvent = (eventName: string, eventProperties?: Record<string, any>) => {
|
||||||
|
amplitude.track(eventName, eventProperties)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set user ID
|
||||||
|
* @param userId User ID
|
||||||
|
*/
|
||||||
|
export const setUserId = (userId: string) => {
|
||||||
|
amplitude.setUserId(userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set user properties
|
||||||
|
* @param properties User properties
|
||||||
|
*/
|
||||||
|
export const setUserProperties = (properties: Record<string, any>) => {
|
||||||
|
const identifyEvent = new amplitude.Identify()
|
||||||
|
Object.entries(properties).forEach(([key, value]) => {
|
||||||
|
identifyEvent.set(key, value)
|
||||||
|
})
|
||||||
|
amplitude.identify(identifyEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset user (e.g., when user logs out)
|
||||||
|
*/
|
||||||
|
export const resetUser = () => {
|
||||||
|
amplitude.reset()
|
||||||
|
}
|
||||||
|
|
@ -34,6 +34,7 @@ import { useGlobalPublicStore } from '@/context/global-public-context'
|
||||||
import { useDocLink } from '@/context/i18n'
|
import { useDocLink } from '@/context/i18n'
|
||||||
import { useLogout } from '@/service/use-common'
|
import { useLogout } from '@/service/use-common'
|
||||||
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
import { ACCOUNT_SETTING_TAB } from '@/app/components/header/account-setting/constants'
|
||||||
|
import { resetUser } from '@/app/components/base/amplitude/utils'
|
||||||
|
|
||||||
export default function AppSelector() {
|
export default function AppSelector() {
|
||||||
const itemClassName = `
|
const itemClassName = `
|
||||||
|
|
@ -53,7 +54,7 @@ export default function AppSelector() {
|
||||||
const { mutateAsync: logout } = useLogout()
|
const { mutateAsync: logout } = useLogout()
|
||||||
const handleLogout = async () => {
|
const handleLogout = async () => {
|
||||||
await logout()
|
await logout()
|
||||||
|
resetUser()
|
||||||
localStorage.removeItem('setup_status')
|
localStorage.removeItem('setup_status')
|
||||||
// Tokens are now stored in cookies and cleared by backend
|
// Tokens are now stored in cookies and cleared by backend
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import Toast from '@/app/components/base/toast'
|
||||||
import { emailLoginWithCode, sendEMailLoginCode } from '@/service/common'
|
import { emailLoginWithCode, sendEMailLoginCode } from '@/service/common'
|
||||||
import I18NContext from '@/context/i18n'
|
import I18NContext from '@/context/i18n'
|
||||||
import { resolvePostLoginRedirect } from '../utils/post-login-redirect'
|
import { resolvePostLoginRedirect } from '../utils/post-login-redirect'
|
||||||
|
import { trackEvent } from '@/app/components/base/amplitude'
|
||||||
|
|
||||||
export default function CheckCode() {
|
export default function CheckCode() {
|
||||||
const { t, i18n } = useTranslation()
|
const { t, i18n } = useTranslation()
|
||||||
|
|
@ -44,6 +45,12 @@ export default function CheckCode() {
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
const ret = await emailLoginWithCode({ email, code, token, language })
|
const ret = await emailLoginWithCode({ email, code, token, language })
|
||||||
if (ret.result === 'success') {
|
if (ret.result === 'success') {
|
||||||
|
// Track login success event
|
||||||
|
trackEvent('user_login_success', {
|
||||||
|
method: 'email_code',
|
||||||
|
is_invite: !!invite_token,
|
||||||
|
})
|
||||||
|
|
||||||
if (invite_token) {
|
if (invite_token) {
|
||||||
router.replace(`/signin/invite-settings?${searchParams.toString()}`)
|
router.replace(`/signin/invite-settings?${searchParams.toString()}`)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ import I18NContext from '@/context/i18n'
|
||||||
import { noop } from 'lodash-es'
|
import { noop } from 'lodash-es'
|
||||||
import { resolvePostLoginRedirect } from '../utils/post-login-redirect'
|
import { resolvePostLoginRedirect } from '../utils/post-login-redirect'
|
||||||
import type { ResponseError } from '@/service/fetch'
|
import type { ResponseError } from '@/service/fetch'
|
||||||
|
import { trackEvent } from '@/app/components/base/amplitude'
|
||||||
|
|
||||||
type MailAndPasswordAuthProps = {
|
type MailAndPasswordAuthProps = {
|
||||||
isInvite: boolean
|
isInvite: boolean
|
||||||
|
|
@ -63,6 +64,12 @@ export default function MailAndPasswordAuth({ isInvite, isEmailSetup, allowRegis
|
||||||
body: loginData,
|
body: loginData,
|
||||||
})
|
})
|
||||||
if (res.result === 'success') {
|
if (res.result === 'success') {
|
||||||
|
// Track login success event
|
||||||
|
trackEvent('user_login_success', {
|
||||||
|
method: 'email_password',
|
||||||
|
is_invite: isInvite,
|
||||||
|
})
|
||||||
|
|
||||||
if (isInvite) {
|
if (isInvite) {
|
||||||
router.replace(`/signin/invite-settings?${searchParams.toString()}`)
|
router.replace(`/signin/invite-settings?${searchParams.toString()}`)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@ export default function CheckCode() {
|
||||||
}
|
}
|
||||||
setIsLoading(true)
|
setIsLoading(true)
|
||||||
const res = await verifyCode({ email, code, token })
|
const res = await verifyCode({ email, code, token })
|
||||||
console.log(res)
|
|
||||||
if ((res as MailValidityResponse).is_valid) {
|
if ((res as MailValidityResponse).is_valid) {
|
||||||
const params = new URLSearchParams(searchParams)
|
const params = new URLSearchParams(searchParams)
|
||||||
params.set('token', encodeURIComponent((res as MailValidityResponse).token))
|
params.set('token', encodeURIComponent((res as MailValidityResponse).token))
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import Input from '@/app/components/base/input'
|
||||||
import { validPassword } from '@/config'
|
import { validPassword } from '@/config'
|
||||||
import type { MailRegisterResponse } from '@/service/use-common'
|
import type { MailRegisterResponse } from '@/service/use-common'
|
||||||
import { useMailRegister } from '@/service/use-common'
|
import { useMailRegister } from '@/service/use-common'
|
||||||
|
import { trackEvent } from '@/app/components/base/amplitude'
|
||||||
|
|
||||||
const ChangePasswordForm = () => {
|
const ChangePasswordForm = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
|
@ -54,6 +55,11 @@ const ChangePasswordForm = () => {
|
||||||
})
|
})
|
||||||
const { result } = res as MailRegisterResponse
|
const { result } = res as MailRegisterResponse
|
||||||
if (result === 'success') {
|
if (result === 'success') {
|
||||||
|
// Track registration success event
|
||||||
|
trackEvent('user_registration_success', {
|
||||||
|
method: 'email',
|
||||||
|
})
|
||||||
|
|
||||||
Toast.notify({
|
Toast.notify({
|
||||||
type: 'success',
|
type: 'success',
|
||||||
message: t('common.api.actionSuccess'),
|
message: t('common.api.actionSuccess'),
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ import { noop } from 'lodash-es'
|
||||||
import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils'
|
import { setZendeskConversationFields } from '@/app/components/base/zendesk/utils'
|
||||||
import { ZENDESK_FIELD_IDS } from '@/config'
|
import { ZENDESK_FIELD_IDS } from '@/config'
|
||||||
import { useGlobalPublicStore } from './global-public-context'
|
import { useGlobalPublicStore } from './global-public-context'
|
||||||
|
import { setUserId, setUserProperties } from '@/app/components/base/amplitude'
|
||||||
|
|
||||||
export type AppContextValue = {
|
export type AppContextValue = {
|
||||||
userProfile: UserProfileResponse
|
userProfile: UserProfileResponse
|
||||||
|
|
@ -159,6 +160,28 @@ export const AppContextProvider: FC<AppContextProviderProps> = ({ children }) =>
|
||||||
}, [currentWorkspace?.id])
|
}, [currentWorkspace?.id])
|
||||||
// #endregion Zendesk conversation fields
|
// #endregion Zendesk conversation fields
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Report user and workspace info to Amplitude when loaded
|
||||||
|
if (userProfile?.id) {
|
||||||
|
setUserId(userProfile.email)
|
||||||
|
const properties: Record<string, any> = {
|
||||||
|
email: userProfile.email,
|
||||||
|
name: userProfile.name,
|
||||||
|
has_password: userProfile.is_password_set,
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentWorkspace?.id) {
|
||||||
|
properties.workspace_id = currentWorkspace.id
|
||||||
|
properties.workspace_name = currentWorkspace.name
|
||||||
|
properties.workspace_plan = currentWorkspace.plan
|
||||||
|
properties.workspace_status = currentWorkspace.status
|
||||||
|
properties.workspace_role = currentWorkspace.role
|
||||||
|
}
|
||||||
|
|
||||||
|
setUserProperties(properties)
|
||||||
|
}
|
||||||
|
}, [userProfile, currentWorkspace])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppContext.Provider value={{
|
<AppContext.Provider value={{
|
||||||
userProfile,
|
userProfile,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import type { NextRequest } from 'next/server'
|
import type { NextRequest } from 'next/server'
|
||||||
import { NextResponse } from 'next/server'
|
import { NextResponse } from 'next/server'
|
||||||
|
|
||||||
const NECESSARY_DOMAIN = '*.sentry.io http://localhost:* http://127.0.0.1:* https://analytics.google.com googletagmanager.com *.googletagmanager.com https://www.google-analytics.com https://api.github.com'
|
const NECESSARY_DOMAIN = '*.sentry.io http://localhost:* http://127.0.0.1:* https://analytics.google.com googletagmanager.com *.googletagmanager.com https://www.google-analytics.com https://api.github.com https://api2.amplitude.com *.amplitude.com'
|
||||||
|
|
||||||
const wrapResponseWithXFrameOptions = (response: NextResponse, pathname: string) => {
|
const wrapResponseWithXFrameOptions = (response: NextResponse, pathname: string) => {
|
||||||
// prevent clickjacking: https://owasp.org/www-community/attacks/Clickjacking
|
// prevent clickjacking: https://owasp.org/www-community/attacks/Clickjacking
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,8 @@
|
||||||
"knip": "knip"
|
"knip": "knip"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@amplitude/analytics-browser": "^2.31.3",
|
||||||
|
"@amplitude/plugin-session-replay-browser": "^1.23.6",
|
||||||
"@emoji-mart/data": "^1.2.1",
|
"@emoji-mart/data": "^1.2.1",
|
||||||
"@floating-ui/react": "^0.26.28",
|
"@floating-ui/react": "^0.26.28",
|
||||||
"@formatjs/intl-localematcher": "^0.5.10",
|
"@formatjs/intl-localematcher": "^0.5.10",
|
||||||
|
|
|
||||||
3591
web/pnpm-lock.yaml
3591
web/pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue