Skip to content

Commit

Permalink
Fixes all over
Browse files Browse the repository at this point in the history
  • Loading branch information
benjackwhite committed Dec 5, 2024
1 parent 2cff552 commit a632f4a
Show file tree
Hide file tree
Showing 5 changed files with 218 additions and 102 deletions.
4 changes: 3 additions & 1 deletion playground/nextjs/src/posthog.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,10 +62,12 @@ if (typeof window !== 'undefined') {
persistence: cookieConsentGiven() ? 'localStorage+cookie' : 'memory',
person_profiles: PERSON_PROCESSING_MODE === 'never' ? 'identified_only' : PERSON_PROCESSING_MODE,
persistence_name: `${process.env.NEXT_PUBLIC_POSTHOG_KEY}_nextjs`,
opt_in_site_apps: true,
__preview_remote_config: true,
...configForConsent(),
})
// Help with debugging(window as any).posthog = posthog
// Help with debugging
;(window as any).posthog = posthog
}

export const posthogHelpers = {
Expand Down
184 changes: 123 additions & 61 deletions src/site-apps.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,53 @@
import { PostHog } from './posthog-core'
import { CaptureResult, RemoteConfig } from './types'
import { CaptureResult, Properties, RemoteConfig, SiteApp, SiteAppGlobals, SiteAppLoader } from './types'
import { assignableWindow } from './utils/globals'
import { logger } from './utils/logger'
import { logger as _logger } from './utils/logger'
import { isArray } from './utils/type-utils'

const logger = _logger.createLogger('[Site Apps]')

export class SiteApps {
instance: PostHog
enabled: boolean
missedInvocations: Record<string, any>[]
loaded: boolean
appsLoading: Set<string>

constructor(instance: PostHog) {
this.instance = instance
// can't use if site apps are disabled, or if we're not asking /decide for site apps
this.enabled = !!this.instance.config.opt_in_site_apps && !this.instance.config.advanced_disable_decide
apps: Record<string, SiteApp>

private stopBuffering?: () => void
private bufferedInvocations: SiteAppGlobals[]

constructor(private instance: PostHog) {
this.enabled = !!this.instance.config.opt_in_site_apps
// events captured between loading posthog-js and the site app; up to 1000 events
this.missedInvocations = []
// capture events until loaded
this.loaded = false
this.appsLoading = new Set()
this.bufferedInvocations = []
this.apps = {}
}

eventCollector(_eventName: string, eventPayload?: CaptureResult | undefined) {
if (!this.enabled) {
if (!eventPayload) {
return
}
if (!this.loaded && eventPayload) {
const globals = this.globalsForEvent(eventPayload)
this.missedInvocations.push(globals)
if (this.missedInvocations.length > 1000) {
this.missedInvocations = this.missedInvocations.slice(10)
}
const globals = this.globalsForEvent(eventPayload)
this.bufferedInvocations.push(globals)
if (this.bufferedInvocations.length > 1000) {
this.bufferedInvocations = this.bufferedInvocations.slice(10)
}
}

init() {
this.instance?._addCaptureHook(this.eventCollector.bind(this))
if (this.enabled) {
const stop = this.instance._addCaptureHook(this.eventCollector.bind(this))
this.stopBuffering = () => {
stop()
this.stopBuffering = undefined
}
}
}

globalsForEvent(event: CaptureResult): Record<string, any> {
globalsForEvent(event: CaptureResult): SiteAppGlobals {
if (!event) {
throw new Error('Event payload is required')
}
const groups: Record<string, Record<string, any>> = {}
const groups: SiteAppGlobals['groups'] = {}
const groupIds = this.instance.get_property('$groups') || []
const groupProperties = this.instance.get_property('$stored_group_properties') || {}
const groupProperties: Record<string, Properties> = this.instance.get_property('$stored_group_properties') || {}
for (const [type, properties] of Object.entries(groupProperties)) {
groups[type] = { id: groupIds[type], type, properties }
}
Expand Down Expand Up @@ -74,44 +76,104 @@ export class SiteApps {
return globals
}

onRemoteConfig(response?: RemoteConfig): void {
if (isArray(response?.siteApps) && response.siteApps.length > 0) {
if (this.enabled && this.instance.config.opt_in_site_apps) {
const checkIfAllLoaded = () => {
// Stop collecting events once all site apps are loaded
if (this.appsLoading.size === 0) {
this.loaded = true
this.missedInvocations = []
}
}
for (const { id, url } of response['siteApps']) {
// TODO: if we have opted out and "type" is "site_destination", ignore it... but do include "site_app" types
this.appsLoading.add(id)
assignableWindow[`__$$ph_site_app_${id}`] = this.instance
assignableWindow[`__$$ph_site_app_${id}_missed_invocations`] = () => this.missedInvocations
assignableWindow[`__$$ph_site_app_${id}_callback`] = () => {
this.appsLoading.delete(id)
checkIfAllLoaded()
}
assignableWindow.__PosthogExtensions__?.loadSiteApp?.(this.instance, url, (err) => {
if (err) {
this.appsLoading.delete(id)
checkIfAllLoaded()
return logger.error(`Error while initializing PostHog app with config id ${id}`, err)
}
})
setupSiteApp(loader: SiteAppLoader) {
const app: SiteApp = {
id: loader.id,
loaded: false,
errored: false,
}
this.apps[loader.id] = app

const onLoaded = (success: boolean) => {
this.apps[loader.id].errored = success
this.apps[loader.id].loaded = true

logger.info(`Site app with id ${loader.id} ${success ? 'loaded' : 'errored'}`)

if (success && this.bufferedInvocations.length) {
logger.info(`Processing ${this.bufferedInvocations.length} events for site app with id ${loader.id}`)
this.bufferedInvocations.forEach((globals) => app.processEvent?.(globals))
}

for (const app of Object.values(this.apps)) {
if (!app.loaded) {
// If any other apps are not loaded, we don't want to stop buffering
return
}
} else if (response['siteApps'].length > 0) {
logger.error('PostHog site apps are disabled. Enable the "opt_in_site_apps" config to proceed.')
this.loaded = true
} else {
this.loaded = true
}
} else {
this.loaded = true
this.enabled = false

this.stopBuffering?.()
}

try {
const { processEvent } = loader.init({
posthog: this.instance,
callback: (success) => {
onLoaded(success)
},
})

if (processEvent) {
app.processEvent = processEvent
}
} catch (e) {
logger.error(`Error while initializing PostHog app with config id ${loader.id}`, e)
onLoaded(false)
}
}

// TODO: opting out of stuff should disable this
private onCapturedEvent(event: CaptureResult) {
if (Object.keys(this.apps).length === 0) {
return
}

const globals = this.globalsForEvent(event)

for (const app of Object.values(this.apps)) {
try {
app.processEvent?.(globals)
} catch (e) {
logger.error(`Error while processing event ${event.event} for site app ${app.id}`, e)
}
}
}

onRemoteConfig(response: RemoteConfig): void {
if (isArray(assignableWindow._POSTHOG_JS_APPS)) {
if (!this.enabled) {
logger.error(`PostHog site apps are disabled. Enable the "opt_in_site_apps" config to proceed.`)
return
}

for (const app of assignableWindow._POSTHOG_JS_APPS) {
this.setupSiteApp(app)
}

if (!assignableWindow._POSTHOG_JS_APPS.length) {
this.stopBuffering?.()
} else {
// NOTE: We could improve this to only fire if we actually have listeners for the event
this.instance.on('eventCaptured', (event) => this.onCapturedEvent(event))
}

return
}

// NOTE: Below his is now only the fallback for legacy site app support. Once we have fully removed to the remote config loader we can get rid of this

this.stopBuffering?.()
if (!this.enabled) {
logger.error(`PostHog site apps are disabled. Enable the "opt_in_site_apps" config to proceed.`)
return
}

for (const { id, url } of response['siteApps']) {
assignableWindow[`__$$ph_site_app_${id}`] = this.instance
assignableWindow.__PosthogExtensions__?.loadSiteApp?.(this.instance, url, (err) => {
if (err) {
return logger.error(`Error while initializing PostHog app with config id ${id}`, err)
}
})
}
}
}
29 changes: 29 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,35 @@ export interface DecideResponse extends RemoteConfig {
errorsWhileComputingFlags: boolean
}

export type SiteAppGlobals = {
event: {
uuid: string
event: EventName
properties: Properties
timestamp?: Date
elements_chain?: string
distinct_id?: string
}
person: {
properties: Properties
}
groups: Record<string, { id: string; type: string; properties: Properties }>
}

export type SiteAppLoader = {
id: string
init: (config: { posthog: PostHog; callback: (success: boolean) => void }) => {
processEvent?: (globals: SiteAppGlobals) => void
}
}

export type SiteApp = {
id: string
loaded: boolean
errored: boolean
processEvent?: (globals: SiteAppGlobals) => void
}

export type FeatureFlagsCallback = (
flags: string[],
variants: Record<string, string | boolean>,
Expand Down
11 changes: 9 additions & 2 deletions src/utils/globals.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { ErrorProperties } from '../extensions/exception-autocapture/error-conversion'
import type { PostHog } from '../posthog-core'
import { SessionIdManager } from '../sessionid'
import { DeadClicksAutoCaptureConfig, ErrorEventArgs, ErrorMetadata, Properties, RemoteConfig } from '../types'
import {
DeadClicksAutoCaptureConfig,
ErrorEventArgs,
ErrorMetadata,
Properties,
RemoteConfig,
SiteAppLoader,
} from '../types'

/*
* Global helpers to protect access to browser globals in a way that is safer for different targets
Expand All @@ -21,7 +28,7 @@ export type AssignableWindow = Window &
Record<string, any> & {
__PosthogExtensions__?: PostHogExtensions
_POSTHOG_CONFIG?: RemoteConfig
_POSTHOG_SITE_APPS?: { token: string; load: (posthog: PostHog) => void }[]
_POSTHOG_JS_APPS?: SiteAppLoader[]
}

/**
Expand Down
92 changes: 54 additions & 38 deletions src/utils/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,44 +2,60 @@ import Config from '../config'
import { isUndefined } from './type-utils'
import { assignableWindow, window } from './globals'

const LOGGER_PREFIX = '[PostHog.js]'
export const logger = {
_log: (level: 'log' | 'warn' | 'error', ...args: any[]) => {
if (
window &&
(Config.DEBUG || assignableWindow.POSTHOG_DEBUG) &&
!isUndefined(window.console) &&
window.console
) {
const consoleLog =
'__rrweb_original__' in window.console[level]
? (window.console[level] as any)['__rrweb_original__']
: window.console[level]
export type Logger = {
_log: (level: 'log' | 'warn' | 'error', ...args: any[]) => void
info: (...args: any[]) => void
warn: (...args: any[]) => void
error: (...args: any[]) => void
critical: (...args: any[]) => void
uninitializedWarning: (methodName: string) => void
createLogger: (prefix: string) => Logger
}

const createLogger = (prefix: string): Logger => {
const logger: Logger = {
_log: (level: 'log' | 'warn' | 'error', ...args: any[]) => {
if (
window &&
(Config.DEBUG || assignableWindow.POSTHOG_DEBUG) &&
!isUndefined(window.console) &&
window.console
) {
const consoleLog =
'__rrweb_original__' in window.console[level]
? (window.console[level] as any)['__rrweb_original__']
: window.console[level]

// eslint-disable-next-line no-console
consoleLog(prefix, ...args)
}
},

info: (...args: any[]) => {
logger._log('log', ...args)
},

warn: (...args: any[]) => {
logger._log('warn', ...args)
},

error: (...args: any[]) => {
logger._log('error', ...args)
},

critical: (...args: any[]) => {
// Critical errors are always logged to the console
// eslint-disable-next-line no-console
consoleLog(LOGGER_PREFIX, ...args)
}
},

info: (...args: any[]) => {
logger._log('log', ...args)
},

warn: (...args: any[]) => {
logger._log('warn', ...args)
},

error: (...args: any[]) => {
logger._log('error', ...args)
},

critical: (...args: any[]) => {
// Critical errors are always logged to the console
// eslint-disable-next-line no-console
console.error(LOGGER_PREFIX, ...args)
},

uninitializedWarning: (methodName: string) => {
logger.error(`You must initialize PostHog before calling ${methodName}`)
},
console.error(prefix, ...args)
},

uninitializedWarning: (methodName: string) => {
logger.error(`You must initialize PostHog before calling ${methodName}`)
},

createLogger: (additionalPrefix: string) => createLogger(`${prefix} ${additionalPrefix}`),
}
return logger
}

export const logger = createLogger('[PostHog.js]')

0 comments on commit a632f4a

Please sign in to comment.