mirror of
https://github.com/langgenius/dify.git
synced 2026-07-21 10:38:32 +08:00
106 lines
3.4 KiB
TypeScript
106 lines
3.4 KiB
TypeScript
'use client'
|
|
|
|
import Cookies from 'js-cookie'
|
|
import { useEffect } from 'react'
|
|
import { IS_CLOUD_EDITION } from '@/config'
|
|
import { useSearchParams } from '@/next/navigation'
|
|
import { rememberCreateAppExternalAttribution } from '@/utils/create-app-tracking'
|
|
|
|
const UTM_INFO_COOKIE = 'utm_info'
|
|
const UTM_INFO_COOKIE_EXPIRES_DAYS = 1
|
|
const UTM_INFO_QUERY_KEYS = [
|
|
'utm_source',
|
|
'utm_medium',
|
|
'utm_campaign',
|
|
'utm_content',
|
|
'utm_term',
|
|
'slug',
|
|
] as const
|
|
|
|
type SearchParamReader = {
|
|
get: (name: string) => string | null
|
|
}
|
|
|
|
const normalizeString = (value?: string | null) => {
|
|
const trimmed = value?.trim()
|
|
return trimmed || undefined
|
|
}
|
|
|
|
const getSearchParamValue = (searchParams: SearchParamReader, key: string) =>
|
|
normalizeString(searchParams.get(key))
|
|
|
|
const parseRedirectUrlSearchParams = (redirectUrl: string) => {
|
|
const baseUrl = window.location.origin
|
|
|
|
try {
|
|
const url = new URL(redirectUrl, baseUrl)
|
|
if (url.origin !== baseUrl) return null
|
|
|
|
return url.searchParams
|
|
} catch {
|
|
return null
|
|
}
|
|
}
|
|
|
|
const resolveAttributionSearchParams = (
|
|
searchParams: SearchParamReader,
|
|
): SearchParamReader | null => {
|
|
if (getSearchParamValue(searchParams, 'utm_source')) return searchParams
|
|
|
|
const redirectUrl = getSearchParamValue(searchParams, 'redirect_url')
|
|
if (!redirectUrl) return null
|
|
|
|
return parseRedirectUrlSearchParams(redirectUrl)
|
|
}
|
|
|
|
/**
|
|
* Captures external-campaign params (utm_* + blog `slug`) from the landing URL.
|
|
*
|
|
* Blog links point straight at cloud.dify.ai (e.g. `/apps?utm_source=dify_blog&slug=…`),
|
|
* bypassing the marketing site that normally seeds the `utm_info` cookie. A new visitor
|
|
* is bounced to sign-up, and the URL params are lost on that redirect — so we persist
|
|
* them here, on the landing render, before the redirect happens:
|
|
*
|
|
* - `utm_info` cookie → read by the registration trackers, so slug is reported on
|
|
* registration even when the user registers before creating an app.
|
|
* - create_app sessionStorage → read by `trackCreateApp`, so slug is reported on
|
|
* create_app.
|
|
*
|
|
* slug is intentionally NOT attached to page-view events; only these conversion events.
|
|
*/
|
|
const ExternalAttributionRecorder = () => {
|
|
const searchParams = useSearchParams()
|
|
|
|
useEffect(() => {
|
|
if (!IS_CLOUD_EDITION) return
|
|
|
|
const attributionSearchParams = resolveAttributionSearchParams(searchParams)
|
|
if (!attributionSearchParams) return
|
|
|
|
const utmSource = getSearchParamValue(attributionSearchParams, 'utm_source')
|
|
if (!utmSource) return
|
|
|
|
// create_app conversion attribution (utm_source + slug).
|
|
rememberCreateAppExternalAttribution({ searchParams: attributionSearchParams })
|
|
|
|
// Seed the utm_info cookie the registration trackers read. A campaign click always
|
|
// overwrites any previous value, so the most recent blog link wins (last-touch) and
|
|
// a stale cookie from an earlier, un-converted visit can't shadow the new slug. This
|
|
// mirrors the create_app attribution refreshed just above.
|
|
const utmInfo: Record<string, string> = {}
|
|
UTM_INFO_QUERY_KEYS.forEach((key) => {
|
|
const value = getSearchParamValue(attributionSearchParams, key)
|
|
if (value) utmInfo[key] = value
|
|
})
|
|
|
|
Cookies.set(UTM_INFO_COOKIE, JSON.stringify(utmInfo), {
|
|
expires: UTM_INFO_COOKIE_EXPIRES_DAYS,
|
|
path: '/',
|
|
})
|
|
}, [searchParams])
|
|
|
|
return null
|
|
}
|
|
|
|
export default ExternalAttributionRecorder
|