Skip to content

Commit

Permalink
site functions
Browse files Browse the repository at this point in the history
  • Loading branch information
mariusandra committed Nov 22, 2024
1 parent d71ecd6 commit 4b6983d
Show file tree
Hide file tree
Showing 7 changed files with 135 additions and 37 deletions.
11 changes: 4 additions & 7 deletions src/autocapture.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,13 +213,10 @@ export function autocapturePropertiesForElement(

const props = extend(
getDefaultProperties(e.type),
elementsChainAsString
? {
$elements_chain: getElementsChainString(elementsJson),
}
: {
$elements: elementsJson,
},
// Sending "$elements" is deprecated. Only one client on US cloud uses this.
!elementsChainAsString ? { $elements: elementsJson } : {},
// Always send $elements_chain, as it's needed downstream in site app filtering
{ $elements_chain: getElementsChainString(elementsJson) },
elementsJson[0]?.['$el_text'] ? { $el_text: elementsJson[0]?.['$el_text'] } : {},
externalHref && e.type === 'click' ? { $external_click_url: externalHref } : {},
autocaptureAugmentProperties
Expand Down
17 changes: 1 addition & 16 deletions src/decide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Compression, DecideResponse } from './types'
import { STORED_GROUP_PROPERTIES_KEY, STORED_PERSON_PROPERTIES_KEY } from './constants'

import { logger } from './utils/logger'
import { document, assignableWindow } from './utils/globals'
import { document } from './utils/globals'

export class Decide {
constructor(private readonly instance: PostHog) {
Expand Down Expand Up @@ -64,20 +64,5 @@ export class Decide {
}

this.instance._afterDecideResponse(response)

if (response['siteApps']) {
if (this.instance.config.opt_in_site_apps) {
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)
}
})
}
} else if (response['siteApps'].length > 0) {
logger.error('PostHog site apps are disabled. Enable the "opt_in_site_apps" config to proceed.')
}
}
}
}
16 changes: 5 additions & 11 deletions src/entrypoints/external-scripts-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import type { PostHog } from '../posthog-core'
import { assignableWindow, document, PostHogExtensionKind } from '../utils/globals'
import { logger } from '../utils/logger'

const loadScript = (posthog: PostHog, url: string, callback: (error?: string | Event, event?: Event) => void) => {
export const loadScript = (
posthog: PostHog,
url: string,
callback: (error?: string | Event, event?: Event) => void
) => {
if (posthog.config.disable_external_dependency_loading) {
logger.warn(`${url} was requested but loading of external scripts is disabled.`)
return callback('Loading of external scripts is disabled')
Expand Down Expand Up @@ -56,13 +60,3 @@ assignableWindow.__PosthogExtensions__.loadExternalDependency = (

loadScript(posthog, url, callback)
}

assignableWindow.__PosthogExtensions__.loadSiteApp = (
posthog: PostHog,
url: string,
callback: (error?: string | Event, event?: Event) => void
): void => {
const scriptUrl = posthog.requestRouter.endpointFor('api', url)

loadScript(posthog, scriptUrl, callback)
}
6 changes: 6 additions & 0 deletions src/posthog-core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ import { ExceptionObserver } from './extensions/exception-autocapture'
import { WebVitalsAutocapture } from './extensions/web-vitals'
import { WebExperiments } from './web-experiments'
import { PostHogExceptions } from './posthog-exceptions'
import { SiteApps } from './site-apps'
import { DeadClicksAutocapture, isDeadClicksEnabledForAutocapture } from './extensions/dead-clicks-autocapture'

/*
Expand Down Expand Up @@ -258,6 +259,7 @@ export class PostHog {
sessionManager?: SessionIdManager
sessionPropsManager?: SessionPropsManager
requestRouter: RequestRouter
siteApps?: SiteApps
autocapture?: Autocapture
heatmaps?: Heatmaps
webVitalsAutocapture?: WebVitalsAutocapture
Expand Down Expand Up @@ -432,6 +434,9 @@ export class PostHog {

new TracingHeaders(this).startIfEnabledOrStop()

this.siteApps = new SiteApps(this)
this.siteApps?.init()

this.sessionRecording = new SessionRecording(this)
this.sessionRecording.startIfEnabledOrStop()

Expand Down Expand Up @@ -562,6 +567,7 @@ export class PostHog {
: 'always',
})

this.siteApps?.afterDecideResponse(response)
this.sessionRecording?.afterDecideResponse(response)
this.autocapture?.afterDecideResponse(response)
this.heatmaps?.afterDecideResponse(response)
Expand Down
118 changes: 118 additions & 0 deletions src/site-apps.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { loadScript } from './entrypoints/external-scripts-loader'
import { PostHog } from './posthog-core'
import { CaptureResult, DecideResponse } from './types'
import { assignableWindow } from './utils/globals'
import { logger } from './utils/logger'

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
// 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()
}

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

init() {
this.instance?._addCaptureHook(this.eventCollector.bind(this))
}

globalsForEvent(event: CaptureResult): Record<string, any> {
if (!event) {
throw new Error('Event payload is required')
}
const groups: Record<string, Record<string, any>> = {}
const groupIds = this.instance.get_property('$groups') || []
const groupProperties = this.instance.get_property('$stored_group_properties') || {}
for (const [type, properties] of Object.entries(groupProperties)) {
groups[type] = { id: groupIds[type], type, properties }
}
const { $set_once, $set, ..._event } = event
const globals = {
event: {
..._event,
properties: {
...event.properties,
...($set ? { $set: { ...(event.properties?.$set ?? {}), ...$set } } : {}),
...($set_once ? { $set_once: { ...(event.properties?.$set_once ?? {}), ...$set_once } } : {}),
},
elements_chain: event.properties?.['$elements_chain'] ?? '',
// TODO:
// - elements_chain_href: '',
// - elements_chain_texts: [] as string[],
// - elements_chain_ids: [] as string[],
// - elements_chain_elements: [] as string[],
distinct_id: event.properties?.['distinct_id'],
},
person: {
properties: this.instance.get_property('$stored_person_properties'),
},
groups,
}
return globals
}

loadSiteApp(posthog: PostHog, url: string, callback: (error?: string | Event, event?: Event) => void) {
const scriptUrl = posthog.requestRouter.endpointFor('api', url)
loadScript(posthog, scriptUrl, callback)
}

afterDecideResponse(response?: DecideResponse): void {
if (response?.['siteApps']) {
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']) {
this.appsLoading.add(id)
assignableWindow[`__$$ph_site_app_${id}_posthog`] = this.instance
assignableWindow[`__$$ph_site_app_${id}_missed_invocations`] = () => this.missedInvocations
assignableWindow[`__$$ph_site_app_${id}_callback`] = () => {
this.appsLoading.delete(id)
checkIfAllLoaded()
}
this.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)
}
})
}
} else if (response['siteApps'].length > 0) {
logger.error('PostHog site apps are disabled. Enable the "opt_in_site_apps" config to proceed.')
}
} else {
this.loaded = true
this.enabled = false
}
}

// TODO: opting out of stuff should disable this
}
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -523,7 +523,7 @@ export interface DecideResponse {
editorParams?: ToolbarParams /** @deprecated, renamed to toolbarParams, still present on older API responses */
toolbarVersion: 'toolbar' /** @deprecated, moved to toolbarParams */
isAuthenticated: boolean
siteApps: { id: number; url: string }[]
siteApps: { id: string; url: string }[]
heatmaps?: boolean
defaultIdentifiedOnly?: boolean
captureDeadClicks?: boolean
Expand Down
2 changes: 0 additions & 2 deletions src/utils/globals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,6 @@ interface PostHogExtensions {
callback: (error?: string | Event, event?: Event) => void
) => void

loadSiteApp?: (posthog: PostHog, appUrl: string, callback: (error?: string | Event, event?: Event) => void) => void

parseErrorAsProperties?: (
[event, source, lineno, colno, error]: ErrorEventArgs,
metadata?: ErrorMetadata
Expand Down

0 comments on commit 4b6983d

Please sign in to comment.