fix(stage-ui,stage-pocket,stage-web,stage-tamagotchi,ui-server-auth): analytics import not deferred or lazied
This commit is contained in:
@@ -112,7 +112,7 @@
|
||||
### PostHog(前端 / 外部数据源,产品侧)
|
||||
|
||||
已接入:
|
||||
- 前端 `posthog-js` 通过 `packages/stage-ui/src/stores/analytics/posthog.ts` 初始化;web / desktop / pocket / auth / docs 共用一个 project key,以 `app_surface` 区分运行端。
|
||||
- 前端 `posthog-js` 通过 `packages/stage-ui/src/stores/analytics/posthog.ts` 动态 adapter 初始化;web / desktop / pocket / auth / docs 共用一个 project key,以 `app_surface` 区分运行端。
|
||||
- Server 先把产品事实写入 `product_events`,再异步 best-effort 转发注册、支付、订阅等白名单业务事实到 PostHog;LLM / TTS per-request 事件不转发,PostHog 失败也不能影响请求主链路。
|
||||
- 前端 identity:`useSharedAnalyticsStore.initialize()` watch `authStore.isAuthenticated` 自动调 `posthog.identify(user.id)` / `reset()`
|
||||
- 平台统一写入 `app_surface`;`entry_surface` 只表示 `settings_flux` 这类业务入口,避免同名字段混用或覆盖 PostHog super property。
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { OnboardingDialog, OnboardingStepAnalyticsNotice, ToasterRoot } from '@proj-airi/stage-ui/components'
|
||||
import { useAuthProviderSync } from '@proj-airi/stage-ui/composables/use-auth-provider-sync'
|
||||
import { isPosthogAvailableInBuild, useSharedAnalyticsStore } from '@proj-airi/stage-ui/stores/analytics'
|
||||
import { isAnalyticsAvailableInBuild, useSharedAnalyticsStore } from '@proj-airi/stage-ui/stores/analytics'
|
||||
import { useCharacterOrchestratorStore } from '@proj-airi/stage-ui/stores/character'
|
||||
import { useDisplayModelsStore } from '@proj-airi/stage-ui/stores/display-models'
|
||||
import { useModsServerChannelStore } from '@proj-airi/stage-ui/stores/mods/api/channel-server'
|
||||
@@ -108,7 +108,7 @@ function handleSetupSkipped() {
|
||||
|
||||
const extraSteps = computed(() => [
|
||||
...(
|
||||
isPosthogAvailableInBuild()
|
||||
isAnalyticsAvailableInBuild()
|
||||
? [{ id: 'analytics-notice', component: OnboardingStepAnalyticsNotice }]
|
||||
: []
|
||||
),
|
||||
|
||||
@@ -7,6 +7,7 @@ import NProgress from 'nprogress'
|
||||
import { autoAnimatePlugin } from '@formkit/auto-animate/vue'
|
||||
import { isEnvTruthy } from '@proj-airi/stage-shared'
|
||||
import { trackButtonPlugin } from '@proj-airi/stage-ui/directives/track-button'
|
||||
import { configureAnalyticsAdapter } from '@proj-airi/stage-ui/stores/analytics/client'
|
||||
import { MotionPlugin } from '@vueuse/motion'
|
||||
import { createPinia } from 'pinia'
|
||||
import { setupLayouts } from 'virtual:generated-layouts'
|
||||
@@ -27,6 +28,11 @@ import 'vue-sonner/style.css'
|
||||
import './styles/main.css'
|
||||
import 'uno.css'
|
||||
|
||||
configureAnalyticsAdapter(async (options) => {
|
||||
const { createPosthogAdapter } = await import('@proj-airi/stage-ui/stores/analytics/posthog')
|
||||
return createPosthogAdapter(options)
|
||||
})
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
// TODO: vite-plugin-vue-layouts is long deprecated, replace with another layout solution
|
||||
|
||||
@@ -6,6 +6,7 @@ import Tres from '@tresjs/core'
|
||||
import { autoAnimatePlugin } from '@formkit/auto-animate/vue'
|
||||
import { PiniaColada } from '@pinia/colada'
|
||||
import { trackButtonPlugin } from '@proj-airi/stage-ui/directives/track-button'
|
||||
import { configureAnalyticsAdapter } from '@proj-airi/stage-ui/stores/analytics/client'
|
||||
import { MotionPlugin } from '@vueuse/motion'
|
||||
import { createPinia } from 'pinia'
|
||||
import { setupLayouts } from 'virtual:generated-layouts'
|
||||
@@ -37,6 +38,11 @@ import '@fontsource/kiwi-maru/index.css'
|
||||
import '@fontsource/m-plus-rounded-1c/index.css'
|
||||
import '@fontsource-variable/nunito/index.css'
|
||||
|
||||
configureAnalyticsAdapter(async (options) => {
|
||||
const { createPosthogAdapter } = await import('@proj-airi/stage-ui/stores/analytics/posthog')
|
||||
return createPosthogAdapter(options)
|
||||
})
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
const router = createRouter({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useElectronEventaInvoke } from '@proj-airi/electron-vueuse'
|
||||
import { OnboardingScreen, OnboardingStepAnalyticsNotice } from '@proj-airi/stage-ui/components'
|
||||
import { isPosthogAvailableInBuild } from '@proj-airi/stage-ui/stores/analytics'
|
||||
import { isAnalyticsAvailableInBuild } from '@proj-airi/stage-ui/stores/analytics'
|
||||
import { useAuthStore } from '@proj-airi/stage-ui/stores/auth'
|
||||
import { useOnboardingStore } from '@proj-airi/stage-ui/stores/onboarding'
|
||||
import { useTheme } from '@proj-airi/ui'
|
||||
@@ -30,7 +30,7 @@ watch(needsLogin, async (val) => {
|
||||
|
||||
const bgClass = computed(() => isDark.value ? 'bg-[#0f0f0f]' : 'bg-white')
|
||||
const extraSteps = computed(() => {
|
||||
return isPosthogAvailableInBuild()
|
||||
return isAnalyticsAvailableInBuild()
|
||||
? [{ id: 'analytics-notice', component: OnboardingStepAnalyticsNotice }]
|
||||
: []
|
||||
})
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { OnboardingDialog, OnboardingStepAnalyticsNotice, ToasterRoot } from '@proj-airi/stage-ui/components'
|
||||
import { useInferencePreload } from '@proj-airi/stage-ui/composables'
|
||||
import { useAuthProviderSync } from '@proj-airi/stage-ui/composables/use-auth-provider-sync'
|
||||
import { isPosthogAvailableInBuild, useSharedAnalyticsStore } from '@proj-airi/stage-ui/stores/analytics'
|
||||
import { isAnalyticsAvailableInBuild, useSharedAnalyticsStore } from '@proj-airi/stage-ui/stores/analytics'
|
||||
import { useCharacterOrchestratorStore } from '@proj-airi/stage-ui/stores/character'
|
||||
import { useChatSessionStore } from '@proj-airi/stage-ui/stores/chat/session-store'
|
||||
import { useDisplayModelsStore } from '@proj-airi/stage-ui/stores/display-models'
|
||||
@@ -65,7 +65,7 @@ const colors = computed(() => {
|
||||
})
|
||||
|
||||
const onboardingExtraSteps = computed(() => {
|
||||
return isPosthogAvailableInBuild()
|
||||
return isAnalyticsAvailableInBuild()
|
||||
? [{ id: 'analytics-notice', component: OnboardingStepAnalyticsNotice }]
|
||||
: []
|
||||
})
|
||||
|
||||
@@ -8,6 +8,7 @@ import { autoAnimatePlugin } from '@formkit/auto-animate/vue'
|
||||
import { PiniaColada } from '@pinia/colada'
|
||||
import { isEnvTruthy } from '@proj-airi/stage-shared'
|
||||
import { trackButtonPlugin } from '@proj-airi/stage-ui/directives/track-button'
|
||||
import { configureAnalyticsAdapter } from '@proj-airi/stage-ui/stores/analytics/client'
|
||||
import { MotionPlugin } from '@vueuse/motion'
|
||||
import { createPinia } from 'pinia'
|
||||
import { setupLayouts } from 'virtual:generated-layouts'
|
||||
@@ -27,6 +28,11 @@ import 'vue-sonner/style.css'
|
||||
import './styles/main.css'
|
||||
import 'uno.css'
|
||||
|
||||
configureAnalyticsAdapter(async (options) => {
|
||||
const { createPosthogAdapter } = await import('@proj-airi/stage-ui/stores/analytics/posthog')
|
||||
return createPosthogAdapter(options)
|
||||
})
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
// TODO: vite-plugin-vue-layouts is long deprecated, replace with another layout solution
|
||||
|
||||
@@ -91,6 +91,23 @@ export default defineConfig({
|
||||
},
|
||||
},
|
||||
build: {
|
||||
manifest: true,
|
||||
rolldownOptions: {
|
||||
output: {
|
||||
chunkFileNames: (chunkInfo) => {
|
||||
const containsAnalyticsModule = chunkInfo.moduleIds.some((moduleId) => {
|
||||
const normalizedModuleId = moduleId.replaceAll('\\', '/').toLowerCase()
|
||||
return normalizedModuleId.includes('analytics') || normalizedModuleId.includes('posthog')
|
||||
})
|
||||
|
||||
// Only analytics/provider chunks receive the manual neutral mapping;
|
||||
// all unrelated chunks retain Vite's readable default naming.
|
||||
return containsAnalyticsModule
|
||||
? 'assets/auxiliary-[hash].js'
|
||||
: 'assets/[name]-[hash].js'
|
||||
},
|
||||
},
|
||||
},
|
||||
sourcemap: true,
|
||||
},
|
||||
worker: {
|
||||
|
||||
@@ -14,7 +14,7 @@ import { routes } from 'vue-router/auto-routes'
|
||||
|
||||
import App from './App.vue'
|
||||
|
||||
import { initAuthAnalytics } from './modules/analytics'
|
||||
import { loadAnalyticsAdapter } from './modules/analytics'
|
||||
import { AUTH_UI_ROUTER_BASE_PATH } from './modules/auth-ui-base'
|
||||
import { i18n } from './modules/i18n'
|
||||
|
||||
@@ -24,7 +24,12 @@ import 'vue-sonner/style.css'
|
||||
import './styles/main.css'
|
||||
import 'uno.css'
|
||||
|
||||
initAuthAnalytics()
|
||||
if (isEnvTruthy(import.meta.env.VITE_ENABLE_POSTHOG)) {
|
||||
void loadAnalyticsAdapter(async () => {
|
||||
const { createPosthogAdapter } = await import('./modules/analytics-adapters/posthog')
|
||||
return createPosthogAdapter()
|
||||
})
|
||||
}
|
||||
|
||||
const pinia = createPinia()
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { AnalyticsAdapter } from '../analytics'
|
||||
|
||||
import posthog from 'posthog-js'
|
||||
|
||||
import {
|
||||
DEFAULT_POSTHOG_CONFIG,
|
||||
POSTHOG_PROJECT_KEY,
|
||||
} from '../../../../../posthog.config'
|
||||
|
||||
/** Creates the auth analytics adapter and initializes its provider SDK. */
|
||||
export function createPosthogAdapter(): AnalyticsAdapter {
|
||||
posthog.init(POSTHOG_PROJECT_KEY, { ...DEFAULT_POSTHOG_CONFIG })
|
||||
// The shared project distinguishes auth traffic through this super property.
|
||||
posthog.register({ app_surface: 'auth' })
|
||||
|
||||
return {
|
||||
capture(event, properties, options) {
|
||||
posthog.capture(
|
||||
event,
|
||||
properties,
|
||||
options?.beforeNavigation ? { send_instantly: true, transport: 'sendBeacon' } : undefined,
|
||||
)
|
||||
},
|
||||
identify(userId) {
|
||||
posthog.identify(userId)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,28 +1,22 @@
|
||||
import type { AnalyticsAdapter } from './analytics'
|
||||
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
import { initAuthAnalytics, trackSignupFormCompleted } from './analytics'
|
||||
import {
|
||||
AnalyticsClient,
|
||||
loadAnalyticsAdapter,
|
||||
trackSignupFormCompleted,
|
||||
} from './analytics'
|
||||
|
||||
const posthogMocks = vi.hoisted(() => ({
|
||||
const adapterMocks = {
|
||||
capture: vi.fn(),
|
||||
init: vi.fn(),
|
||||
register: vi.fn(),
|
||||
}))
|
||||
identify: vi.fn(),
|
||||
} satisfies AnalyticsAdapter
|
||||
|
||||
vi.mock('posthog-js', () => ({
|
||||
default: posthogMocks,
|
||||
}))
|
||||
|
||||
vi.mock('../../../../posthog.config', () => ({
|
||||
DEFAULT_POSTHOG_CONFIG: {},
|
||||
POSTHOG_ENABLED: true,
|
||||
POSTHOG_PROJECT_KEY: 'test-project-key',
|
||||
}))
|
||||
|
||||
describe('auth product analytics', () => {
|
||||
describe('auth analytics', () => {
|
||||
beforeEach(() => {
|
||||
posthogMocks.capture.mockClear()
|
||||
posthogMocks.init.mockClear()
|
||||
posthogMocks.register.mockClear()
|
||||
adapterMocks.capture.mockClear()
|
||||
adapterMocks.identify.mockClear()
|
||||
})
|
||||
|
||||
// ROOT CAUSE:
|
||||
@@ -33,16 +27,49 @@ describe('auth product analytics', () => {
|
||||
//
|
||||
// The anonymous UI milestone must use its own name. The identified server
|
||||
// event remains the only canonical `signup_completed` business fact.
|
||||
it('keeps anonymous signup UI completion separate from the canonical server signup fact', () => {
|
||||
expect(initAuthAnalytics()).toBe(true)
|
||||
expect(posthogMocks.register).toHaveBeenCalledWith({ app_surface: 'auth' })
|
||||
it('keeps anonymous signup UI completion separate from the canonical server signup fact', async () => {
|
||||
await expect(loadAnalyticsAdapter(async () => adapterMocks)).resolves.toBe(true)
|
||||
|
||||
trackSignupFormCompleted({ source: 'email', requires_verification: true })
|
||||
|
||||
expect(posthogMocks.capture).toHaveBeenCalledWith(
|
||||
expect(adapterMocks.capture).toHaveBeenCalledWith(
|
||||
'signup_form_completed',
|
||||
{ source: 'email', requires_verification: true },
|
||||
undefined,
|
||||
{ beforeNavigation: false },
|
||||
)
|
||||
})
|
||||
|
||||
it('flushes calls made while the optional adapter is loading', async () => {
|
||||
const client = new AnalyticsClient()
|
||||
let install: ((adapter: AnalyticsAdapter) => void) | undefined
|
||||
const loading = client.load(() => new Promise<AnalyticsAdapter>((resolve) => {
|
||||
install = resolve
|
||||
}))
|
||||
|
||||
client.capture('login_started', { method: 'github' }, { beforeNavigation: true })
|
||||
client.identify('user-1')
|
||||
await Promise.resolve()
|
||||
install?.(adapterMocks)
|
||||
|
||||
await expect(loading).resolves.toBe(true)
|
||||
expect(adapterMocks.capture).toHaveBeenCalledWith(
|
||||
'login_started',
|
||||
{ method: 'github' },
|
||||
{ beforeNavigation: true },
|
||||
)
|
||||
expect(adapterMocks.identify).toHaveBeenCalledWith('user-1')
|
||||
})
|
||||
|
||||
it('becomes a harmless no-op when a content blocker rejects the adapter import', async () => {
|
||||
const client = new AnalyticsClient()
|
||||
const loading = client.load(async () => {
|
||||
throw new TypeError('Failed to fetch dynamically imported module')
|
||||
})
|
||||
|
||||
client.capture('login_started', { method: 'google' })
|
||||
|
||||
await expect(loading).resolves.toBe(false)
|
||||
expect(() => client.capture('login_failed', { method: 'google' })).not.toThrow()
|
||||
expect(adapterMocks.capture).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/**
|
||||
* PostHog product analytics for the auth-only SPA (`apps/ui-server-auth`).
|
||||
* Provider-neutral analytics for the auth-only SPA (`apps/ui-server-auth`).
|
||||
*
|
||||
* This surface captures anonymous auth-UI milestones such as form completion,
|
||||
* sign-in attempts, email verification, and password recovery. Canonical
|
||||
@@ -9,44 +9,120 @@
|
||||
*
|
||||
* Unlike the stage apps there is no in-app analytics consent toggle here
|
||||
* (the user isn't signed in yet, so there's no settings store to read).
|
||||
* Capture posture matches the docs site: enabled in analytics-enabled
|
||||
* builds (`VITE_ENABLE_POSTHOG`), disclosed via the privacy policy linked
|
||||
* on the sign-in page.
|
||||
* Capture posture matches the docs site: the optional provider is enabled by
|
||||
* the application entry in configured builds and disclosed via the privacy
|
||||
* policy linked on the sign-in page.
|
||||
*/
|
||||
|
||||
import type { OauthCallbackFailureStage } from '@proj-airi/stage-ui/composables'
|
||||
|
||||
import posthog from 'posthog-js'
|
||||
|
||||
import {
|
||||
DEFAULT_POSTHOG_CONFIG,
|
||||
POSTHOG_ENABLED,
|
||||
POSTHOG_PROJECT_KEY,
|
||||
} from '../../../../posthog.config'
|
||||
|
||||
/** Login/signup credential kinds shown on the sign-in page. */
|
||||
export type AuthMethod = 'email' | 'github' | 'google'
|
||||
|
||||
let initialized = false
|
||||
interface CaptureOptions {
|
||||
/**
|
||||
* Set when navigation immediately follows capture. Adapters can select a
|
||||
* transport that survives document unload.
|
||||
*/
|
||||
beforeNavigation?: boolean
|
||||
}
|
||||
|
||||
/** Adapter contract installed by an optional analytics provider chunk. */
|
||||
export interface AnalyticsAdapter {
|
||||
capture: (event: string, properties: Record<string, unknown>, options?: CaptureOptions) => void
|
||||
identify: (userId: string) => void
|
||||
}
|
||||
|
||||
type PendingOperation
|
||||
= | { kind: 'capture', event: string, properties: Record<string, unknown>, options?: CaptureOptions }
|
||||
| { kind: 'identify', userId: string }
|
||||
|
||||
type LoadState = 'idle' | 'loading' | 'ready' | 'unavailable'
|
||||
|
||||
/**
|
||||
* Initialize PostHog for the auth surface. Call once from `main.ts` before
|
||||
* mount; later calls are no-ops. Returns whether capture is active so
|
||||
* callers can skip building event payloads in analytics-disabled builds.
|
||||
* Owns optional-adapter loading and guarantees that product-event calls never
|
||||
* make core auth UI wait for, or depend on, a provider SDK.
|
||||
*/
|
||||
export function initAuthAnalytics(): boolean {
|
||||
if (!POSTHOG_ENABLED)
|
||||
return false
|
||||
export class AnalyticsClient {
|
||||
private adapter: AnalyticsAdapter | undefined
|
||||
private loadPromise: Promise<boolean> | undefined
|
||||
private loadState: LoadState = 'idle'
|
||||
private readonly pendingOperations: PendingOperation[] = []
|
||||
|
||||
if (initialized)
|
||||
return true
|
||||
load(loader: () => Promise<AnalyticsAdapter>): Promise<boolean> {
|
||||
if (this.loadPromise)
|
||||
return this.loadPromise
|
||||
|
||||
posthog.init(POSTHOG_PROJECT_KEY, { ...DEFAULT_POSTHOG_CONFIG })
|
||||
// Same single-project setup as the stage apps: the `app_surface` super
|
||||
// property is how auth traffic is told apart in shared dashboards.
|
||||
posthog.register({ app_surface: 'auth' })
|
||||
initialized = true
|
||||
return true
|
||||
this.loadState = 'loading'
|
||||
this.loadPromise = Promise.resolve()
|
||||
.then(loader)
|
||||
.then((adapter) => {
|
||||
this.adapter = adapter
|
||||
this.loadState = 'ready'
|
||||
this.flush()
|
||||
return true
|
||||
})
|
||||
.catch(() => {
|
||||
// Content blockers commonly reject the provider's module request. The
|
||||
// provider is optional, so discard queued telemetry and stay no-op.
|
||||
this.pendingOperations.length = 0
|
||||
this.loadState = 'unavailable'
|
||||
return false
|
||||
})
|
||||
|
||||
return this.loadPromise
|
||||
}
|
||||
|
||||
capture(event: string, properties: Record<string, unknown>, options?: CaptureOptions): void {
|
||||
if (this.adapter) {
|
||||
this.adapter.capture(event, properties, options)
|
||||
return
|
||||
}
|
||||
|
||||
if (this.loadState === 'loading')
|
||||
this.enqueue({ kind: 'capture', event, properties, options })
|
||||
}
|
||||
|
||||
identify(userId: string): void {
|
||||
if (this.adapter) {
|
||||
this.adapter.identify(userId)
|
||||
return
|
||||
}
|
||||
|
||||
if (this.loadState === 'loading')
|
||||
this.enqueue({ kind: 'identify', userId })
|
||||
}
|
||||
|
||||
private enqueue(operation: PendingOperation): void {
|
||||
// A provider may remain slow indefinitely. Bound memory while preserving
|
||||
// the newest auth funnel steps, which are the most useful after recovery.
|
||||
if (this.pendingOperations.length === 100)
|
||||
this.pendingOperations.shift()
|
||||
this.pendingOperations.push(operation)
|
||||
}
|
||||
|
||||
private flush(): void {
|
||||
if (!this.adapter)
|
||||
return
|
||||
|
||||
for (const operation of this.pendingOperations) {
|
||||
if (operation.kind === 'identify')
|
||||
this.adapter.identify(operation.userId)
|
||||
else
|
||||
this.adapter.capture(operation.event, operation.properties, operation.options)
|
||||
}
|
||||
this.pendingOperations.length = 0
|
||||
}
|
||||
}
|
||||
|
||||
const analytics = new AnalyticsClient()
|
||||
|
||||
/**
|
||||
* Starts loading the optional provider adapter without exposing its SDK to
|
||||
* pages or to the application's static module graph.
|
||||
*/
|
||||
export function loadAnalyticsAdapter(loader: () => Promise<AnalyticsAdapter>): Promise<boolean> {
|
||||
return analytics.load(loader)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,29 +131,11 @@ export function initAuthAnalytics(): boolean {
|
||||
* uses as `distinctId` (see `apps/server` product events forwarding).
|
||||
*/
|
||||
export function identifyAuthUser(userId: string): void {
|
||||
if (!initialized)
|
||||
return
|
||||
posthog.identify(userId)
|
||||
}
|
||||
|
||||
interface CaptureOptions {
|
||||
/**
|
||||
* Set when navigation immediately follows the capture call
|
||||
* (`window.location.href = ...`). The batched queue would race the
|
||||
* unload and drop the event; sendBeacon survives it.
|
||||
*/
|
||||
beforeNavigation?: boolean
|
||||
analytics.identify(userId)
|
||||
}
|
||||
|
||||
function capture(event: string, properties: Record<string, unknown>, options?: CaptureOptions): void {
|
||||
if (!initialized)
|
||||
return
|
||||
|
||||
posthog.capture(
|
||||
event,
|
||||
properties,
|
||||
options?.beforeNavigation ? { send_instantly: true, transport: 'sendBeacon' } : undefined,
|
||||
)
|
||||
analytics.capture(event, properties, options)
|
||||
}
|
||||
|
||||
/** Anonymous email-signup UI milestone; the server owns the registration fact. */
|
||||
|
||||
@@ -45,18 +45,22 @@ export default defineConfig({
|
||||
},
|
||||
build: {
|
||||
emptyOutDir: true,
|
||||
manifest: true,
|
||||
outDir: resolve(join(import.meta.dirname, 'dist')),
|
||||
rolldownOptions: {
|
||||
output: {
|
||||
// NOTICE:
|
||||
// Safari content blockers can reject application chunks solely because a
|
||||
// semantic filename such as `analytics-*.js` matches a filtering rule.
|
||||
// Root cause: auth analytics is currently shared by the entry and routes,
|
||||
// so blocking that generated chunk prevents the Vue app from mounting.
|
||||
// Source/context: `apps/ui-server-auth/src/modules/analytics.ts`.
|
||||
// Removal condition: analytics is no longer an application-critical static
|
||||
// dependency and blocked optional modules cannot prevent app startup.
|
||||
chunkFileNames: 'assets/chunk-[hash].js',
|
||||
chunkFileNames: (chunkInfo) => {
|
||||
const containsAnalyticsModule = chunkInfo.moduleIds.some((moduleId) => {
|
||||
const normalizedModuleId = moduleId.replaceAll('\\', '/').toLowerCase()
|
||||
return normalizedModuleId.includes('analytics') || normalizedModuleId.includes('posthog')
|
||||
})
|
||||
|
||||
// Keep analytics as the source-domain name, but explicitly map its
|
||||
// public URL to a neutral chunk name that filter lists cannot infer.
|
||||
return containsAnalyticsModule
|
||||
? 'assets/chunk-[hash].js'
|
||||
: 'assets/[name]-[hash].js'
|
||||
},
|
||||
},
|
||||
},
|
||||
sourcemap: true,
|
||||
|
||||
Reference in New Issue
Block a user