Files
dify/web/utils/create-app-tracking.ts
Coding On Star 063e390c5d fix(web): preserve attribution from auth redirect (#38583)
Co-authored-by: CodingOnStar <hanxujiang@dify.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-07-09 07:40:55 +00:00

211 lines
5.9 KiB
TypeScript

import Cookies from 'js-cookie'
import { flushEvents, trackEvent } from '@/app/components/base/amplitude/utils'
import { AppModeEnum } from '@/types/app'
const CREATE_APP_EXTERNAL_ATTRIBUTION_STORAGE_KEY = 'create_app_external_attribution'
// const EXTERNAL_UTM_SOURCE_MAP = {
// 'blog': 'blog',
// 'dify_blog': 'blog',
// 'linkedin': 'linkedin',
// 'newsletter': 'blog',
// 'twitter': 'twitter/x',
// 'twitter/x': 'twitter/x',
// 'x': 'twitter/x',
// } as const
type SearchParamReader = {
get: (name: string) => string | null
}
type OriginalCreateAppMode = 'workflow' | 'chatflow' | 'agent'
type CreateAppSource
= | 'external'
| 'explore_template_list'
| 'explore_template_preview'
| 'studio_blank'
| 'studio_template_list'
| 'studio_template_preview'
| 'studio_upload'
export type TrackCreateAppParams = {
source: CreateAppSource
appMode: string
templateId?: string
}
type ExternalCreateAppAttribution = {
utmSource: string
slug?: string
}
const normalizeString = (value?: string | null) => {
const trimmed = value?.trim()
return trimmed || undefined
}
const getObjectStringValue = (value: unknown) => {
return typeof value === 'string' ? normalizeString(value) : undefined
}
const getSearchParamValue = (searchParams?: SearchParamReader | null, key?: string) => {
if (!searchParams || !key)
return undefined
return normalizeString(searchParams.get(key))
}
const parseJSONRecord = (value?: string | null): Record<string, unknown> | null => {
if (!value)
return null
try {
const parsed = JSON.parse(value)
return parsed && typeof parsed === 'object' ? parsed as Record<string, unknown> : null
}
catch {
return null
}
}
const getCookieUtmInfo = () => {
return parseJSONRecord(Cookies.get('utm_info'))
}
const padTimeValue = (value: number) => String(value).padStart(2, '0')
const formatCreateAppTime = (date: Date) => {
return `${padTimeValue(date.getMonth() + 1)}-${padTimeValue(date.getDate())}-${padTimeValue(date.getHours())}:${padTimeValue(date.getMinutes())}:${padTimeValue(date.getSeconds())}`
}
const mapOriginalCreateAppMode = (appMode: string): OriginalCreateAppMode => {
if (appMode === AppModeEnum.WORKFLOW)
return 'workflow'
if (appMode === AppModeEnum.AGENT_CHAT || appMode === 'agent')
return 'agent'
return 'chatflow'
}
export const extractExternalCreateAppAttribution = ({
searchParams,
utmInfo,
}: {
searchParams?: SearchParamReader | null
utmInfo?: Record<string, unknown> | null
}) => {
const rawSource = getSearchParamValue(searchParams, 'utm_source') ?? getObjectStringValue(utmInfo?.utm_source)
if (!rawSource)
return null
const slug = getSearchParamValue(searchParams, 'slug')
?? getSearchParamValue(searchParams, 'utm_campaign')
?? getObjectStringValue(utmInfo?.slug)
?? getObjectStringValue(utmInfo?.utm_campaign)
return {
utmSource: rawSource,
...(slug ? { slug } : {}),
} satisfies ExternalCreateAppAttribution
}
const readRememberedExternalCreateAppAttribution = (): ExternalCreateAppAttribution | null => {
const attribution = parseJSONRecord(window.sessionStorage.getItem(CREATE_APP_EXTERNAL_ATTRIBUTION_STORAGE_KEY))
const rawSource = getObjectStringValue(attribution?.utmSource) ?? getObjectStringValue(attribution?.utm_source)
if (!rawSource)
return null
const slug = getObjectStringValue(attribution?.slug)
?? getObjectStringValue(attribution?.utmCampaign)
?? getObjectStringValue(attribution?.utm_campaign)
return {
utmSource: rawSource,
...(slug ? { slug } : {}),
}
}
const writeRememberedExternalCreateAppAttribution = (attribution: ExternalCreateAppAttribution) => {
window.sessionStorage.setItem(CREATE_APP_EXTERNAL_ATTRIBUTION_STORAGE_KEY, JSON.stringify(attribution))
}
const clearRememberedExternalCreateAppAttribution = () => {
window.sessionStorage.removeItem(CREATE_APP_EXTERNAL_ATTRIBUTION_STORAGE_KEY)
}
export const rememberCreateAppExternalAttribution = ({
searchParams,
utmInfo,
}: {
searchParams?: SearchParamReader | null
utmInfo?: Record<string, unknown> | null
} = {}) => {
const attribution = extractExternalCreateAppAttribution({
searchParams,
utmInfo: utmInfo ?? getCookieUtmInfo(),
})
if (attribution && typeof window !== 'undefined')
writeRememberedExternalCreateAppAttribution(attribution)
return attribution
}
const resolveCurrentExternalCreateAppAttribution = () => {
if (typeof window === 'undefined')
return null
return readRememberedExternalCreateAppAttribution()
}
export const buildCreateAppEventPayload = (
params: TrackCreateAppParams,
externalAttribution?: ExternalCreateAppAttribution | null,
currentTime = new Date(),
) => {
const source = externalAttribution ? 'external' : params.source
if (source === 'external' && !externalAttribution)
return null
return {
source,
app_mode: mapOriginalCreateAppMode(params.appMode),
time: formatCreateAppTime(currentTime),
...(params.templateId ? { template_id: params.templateId } : {}),
...(externalAttribution
? {
utm_source: externalAttribution.utmSource,
...(externalAttribution.slug ? { slug: externalAttribution.slug } : {}),
}
: {}),
} satisfies Record<string, string>
}
export const trackCreateApp = (params: TrackCreateAppParams): Promise<void> | undefined => {
const externalAttribution = resolveCurrentExternalCreateAppAttribution()
const payload = buildCreateAppEventPayload(params, externalAttribution)
if (!payload)
return
if (externalAttribution)
clearRememberedExternalCreateAppAttribution()
const trackingResult = trackEvent('create_app', payload)
if (!trackingResult)
return
const flushResult = flushEvents()
const completionPromise = flushResult
? Promise.all([trackingResult.promise, flushResult.promise]).then(() => undefined)
: trackingResult.promise.then(() => undefined)
return completionPromise.catch(() => undefined)
}