@@ -350,7 +354,7 @@ export const createOpenTextOrLinkPopup = (
${surveyDescription ? `${surveyDescription}` : ''}
${
surveyQuestionType === 'open'
- ? ``
: ''
@@ -358,9 +362,9 @@ export const createOpenTextOrLinkPopup = (
-
+
Survey by ${posthogLogo}
@@ -380,6 +384,9 @@ export const createOpenTextOrLinkPopup = (
$survey_question: survey.questions[0].question,
$survey_response: surveyQuestionType === 'open' ? e.target.survey.value : 'link clicked',
sessionRecordingUrl: posthog.get_session_replay_url?.(),
+ $set: {
+ [`$survey_responded/${survey.id}`]: true,
+ },
})
if (surveyQuestionType === 'link') {
window.open(question.link || undefined)
@@ -401,13 +408,17 @@ export const createOpenTextOrLinkPopup = (
}
})
}
-
- formElement.addEventListener('input', (e: any) => {
- if (formElement.querySelector('.form-submit')) {
- const submitButton = formElement.querySelector('.form-submit') as HTMLButtonElement
- submitButton.disabled = !e.data
+ if (!isOptional) {
+ if (surveyQuestionType === 'open') {
+ ;(formElement.querySelector('.form-submit') as HTMLButtonElement).disabled = true
}
- })
+ formElement.addEventListener('input', (e: any) => {
+ if (formElement.querySelector('.form-submit')) {
+ const submitButton = formElement.querySelector('.form-submit') as HTMLButtonElement
+ submitButton.disabled = !e.target.value
+ }
+ })
+ }
return formElement
}
@@ -441,31 +452,41 @@ export const addCancelListeners = (
surveyId: string,
surveyEventName: string
) => {
- const cancelButton = surveyPopup.getElementsByClassName('form-cancel')?.[0] as HTMLButtonElement
-
- cancelButton.addEventListener('click', (e) => {
- e.preventDefault()
- Object.assign(surveyPopup.style, { display: 'none' })
- localStorage.setItem(`seenSurvey_${surveyId}`, 'true')
- posthog.capture('survey dismissed', {
- $survey_name: surveyEventName,
- $survey_id: surveyId,
- sessionRecordingUrl: posthog.get_session_replay_url(),
+ const cancelButtons = surveyPopup.getElementsByClassName('form-cancel')
+ for (const button of cancelButtons) {
+ button.addEventListener('click', (e) => {
+ e.preventDefault()
+ closeSurveyPopup(surveyId, surveyPopup)
+ posthog.capture('survey dismissed', {
+ $survey_name: surveyEventName,
+ $survey_id: surveyId,
+ sessionRecordingUrl: posthog.get_session_replay_url?.(),
+ $set: {
+ [`$survey_dismissed/${surveyId}`]: true,
+ },
+ })
})
- window.dispatchEvent(new Event('PHSurveyClosed'))
- })
+ }
+ window.dispatchEvent(new Event('PHSurveyClosed'))
}
-export const createRatingsPopup = (posthog: PostHog, survey: Survey, question: RatingSurveyQuestion) => {
+export const createRatingsPopup = (
+ posthog: PostHog,
+ survey: Survey,
+ question: RatingSurveyQuestion,
+ questionIndex: number
+) => {
const scale = question.scale
+ const starting = question.scale === 10 ? 0 : 1
const displayType = question.display
+ const isOptional = !!question.optional
const ratingOptionsElement = document.createElement('div')
if (displayType === 'number') {
ratingOptionsElement.className = 'rating-options-buttons'
- ratingOptionsElement.style.gridTemplateColumns = `repeat(${scale}, minmax(0, 1fr))`
- for (let i = 1; i <= scale; i++) {
+ ratingOptionsElement.style.gridTemplateColumns = `repeat(${scale - starting + 1}, minmax(0, 1fr))`
+ for (let i = starting; i <= scale; i++) {
const buttonElement = document.createElement('button')
- buttonElement.className = `ratings-number rating_${i} auto-text-color`
+ buttonElement.className = `ratings-number question-${questionIndex}-rating-${i} auto-text-color`
buttonElement.type = 'submit'
buttonElement.value = `${i}`
buttonElement.innerHTML = `${i}`
@@ -477,7 +498,7 @@ export const createRatingsPopup = (posthog: PostHog, survey: Survey, question: R
const fiveEmojis = [veryDissatisfiedEmoji, dissatisfiedEmoji, neutralEmoji, satisfiedEmoji, verySatisfiedEmoji]
for (let i = 1; i <= scale; i++) {
const emojiElement = document.createElement('button')
- emojiElement.className = `ratings-emoji rating_${i}`
+ emojiElement.className = `ratings-emoji question-${questionIndex}-rating-${i}`
emojiElement.type = 'submit'
emojiElement.value = `${i}`
emojiElement.innerHTML = scale === 3 ? threeEmojis[i - 1] : fiveEmojis[i - 1]
@@ -504,9 +525,9 @@ export const createRatingsPopup = (posthog: PostHog, survey: Survey, question: R
}
-
+
Survey by ${posthogLogo}
@@ -541,10 +562,10 @@ export const createRatingsPopup = (posthog: PostHog, survey: Survey, question: R
innerHTML: ratingsForm,
})
}
-
formElement.getElementsByClassName('rating-options')[0].insertAdjacentElement('afterbegin', ratingOptionsElement)
- for (const x of Array(question.scale).keys()) {
- const ratingEl = formElement.getElementsByClassName(`rating_${x + 1}`)[0]
+ const allElements = question.scale === 10 ? [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] : [1, 2, 3, 4, 5]
+ for (const x of allElements) {
+ const ratingEl = formElement.getElementsByClassName(`question-${questionIndex}-rating-${x}`)[0]
ratingEl.addEventListener('click', (e) => {
e.preventDefault()
for (const activeRatingEl of formElement.getElementsByClassName('rating-active')) {
@@ -561,11 +582,18 @@ export const createRatingsPopup = (posthog: PostHog, survey: Survey, question: R
return formElement
}
-export const createMultipleChoicePopup = (posthog: PostHog, survey: Survey, question: MultipleSurveyQuestion) => {
+export const createMultipleChoicePopup = (
+ posthog: PostHog,
+ survey: Survey,
+ question: MultipleSurveyQuestion,
+ questionIndex: number
+) => {
const surveyQuestion = question.question
const surveyDescription = question.description
const surveyQuestionChoices = question.choices
const singleOrMultiSelect = question.type
+ const isOptional = !!question.optional
+
const form = `
-
+
Survey by ${posthogLogo}
@@ -628,19 +656,19 @@ export const createMultipleChoicePopup = (posthog: PostHog, survey: Survey, ques
innerHTML: form,
})
}
- formElement.addEventListener('change', () => {
- const selectedChoices =
- singleOrMultiSelect === 'single_choice'
- ? formElement.querySelectorAll('input[type=radio]:checked')
- : formElement.querySelectorAll('input[type=checkbox]:checked')
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore // TODO: Fix this, error because it doesn't recognize node list as an array
- if ((selectedChoices.length ?? 0) > 0) {
- ;(formElement.querySelector('.form-submit') as HTMLButtonElement).disabled = false
- } else {
- ;(formElement.querySelector('.form-submit') as HTMLButtonElement).disabled = true
- }
- })
+ if (!isOptional) {
+ formElement.addEventListener('change', () => {
+ const selectedChoices: NodeListOf
=
+ singleOrMultiSelect === 'single_choice'
+ ? formElement.querySelectorAll('input[type=radio]:checked')
+ : formElement.querySelectorAll('input[type=checkbox]:checked')
+ if ((selectedChoices.length ?? 0) > 0) {
+ ;(formElement.querySelector('.form-submit') as HTMLButtonElement).disabled = false
+ } else {
+ ;(formElement.querySelector('.form-submit') as HTMLButtonElement).disabled = true
+ }
+ })
+ }
return formElement
}
@@ -742,8 +770,7 @@ export const createMultipleQuestionSurvey = (posthog: PostHog, survey: Survey) =
e.preventDefault()
const multipleQuestionResponses: Record = {}
const allTabs = (e.target as HTMLDivElement).getElementsByClassName('tab')
- let idx = 0
- for (const tab of allTabs) {
+ for (const [index, tab] of [...allTabs].entries()) {
const classes = tab.classList
const questionType = classes[2]
let response
@@ -764,14 +791,17 @@ export const createMultipleQuestionSurvey = (posthog: PostHog, survey: Survey) =
].map((choice) => choice.value)
response = selectedChoices
}
- if (response !== undefined) {
- if (idx === 0) {
+ const isQuestionOptional = survey.questions[index].optional
+ if (isQuestionOptional && _isUndefined(response)) {
+ response = null
+ }
+ if (!_isUndefined(response)) {
+ if (index === 0) {
multipleQuestionResponses['$survey_response'] = response
} else {
- multipleQuestionResponses[`$survey_response_${idx}`] = response
+ multipleQuestionResponses[`$survey_response_${index}`] = response
}
}
- idx++
}
posthog.capture('survey sent', {
$survey_name: survey.name,
@@ -790,7 +820,7 @@ export const createMultipleQuestionSurvey = (posthog: PostHog, survey: Survey) =
questions.map((question, idx) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // TODO: Fix this, error because of survey question type mapping
- const questionElement = questionTypeMapping[question.type](posthog, survey, question)
+ const questionElement = questionTypeMapping[question.type](posthog, survey, question, idx)
const questionTab = document.createElement('div')
questionTab.className = `tab question-${idx} ${question.type}`
if (idx < questions.length - 1) {
@@ -814,11 +844,11 @@ export const createMultipleQuestionSurvey = (posthog: PostHog, survey: Survey) =
const createSingleQuestionSurvey = (posthog: PostHog, survey: Survey, question: SurveyQuestion) => {
const questionType = question.type
if (questionType === 'rating') {
- return createRatingsPopup(posthog, survey, question)
+ return createRatingsPopup(posthog, survey, question, 0)
} else if (questionType === 'open' || questionType === 'link') {
- return createOpenTextOrLinkPopup(posthog, survey, question)
+ return createOpenTextOrLinkPopup(posthog, survey, question, 0)
} else if (questionType === 'single_choice' || questionType === 'multiple_choice') {
- return createMultipleChoicePopup(posthog, survey, question)
+ return createMultipleChoicePopup(posthog, survey, question, 0)
}
return null
}
@@ -865,13 +895,8 @@ function nextQuestion(currentQuestionIdx: number, surveyId: string) {
export function generateSurveys(posthog: PostHog) {
callSurveys(posthog, true)
- let currentUrl = location.href
- if (location.href) {
- setInterval(() => {
- if (location.href !== currentUrl) {
- currentUrl = location.href
- callSurveys(posthog, false)
- }
- }, 1500)
- }
+ // recalculate surveys every 3 seconds to check if URL or selectors have changed
+ setInterval(() => {
+ callSurveys(posthog, false)
+ }, 3000)
}
diff --git a/src/extensions/toolbar.ts b/src/extensions/toolbar.ts
index f4e2af8ee..bf91cf914 100644
--- a/src/extensions/toolbar.ts
+++ b/src/extensions/toolbar.ts
@@ -1,7 +1,16 @@
-import { _getHashParam, _register_event, loadScript, logger } from '../utils'
+import { _register_event, _try, loadScript } from '../utils'
import { PostHog } from '../posthog-core'
import { DecideResponse, ToolbarParams } from '../types'
import { POSTHOG_MANAGED_HOSTS } from './cloud'
+import { _getHashParam } from '../utils/request-utils'
+import { logger } from '../utils/logger'
+import { window } from '../utils/globals'
+
+// TRICKY: Many web frameworks will modify the route on load, potentially before posthog is initialized.
+// To get ahead of this we grab it as soon as the posthog-js is parsed
+const STATE_FROM_WINDOW = window.location
+ ? _getHashParam(window.location.hash, '__posthog') || _getHashParam(location.hash, 'state')
+ : null
export class Toolbar {
instance: PostHog
@@ -49,10 +58,24 @@ export class Toolbar {
localStorage = window.localStorage
}
- const stateHash = _getHashParam(location.hash, '__posthog') || _getHashParam(location.hash, 'state')
- const state = stateHash ? JSON.parse(decodeURIComponent(stateHash)) : null
- const parseFromUrl = state && state['action'] === 'ph_authorize'
+ /**
+ * Info about the state
+ * The state is a json object
+ * 1. (Legacy) The state can be `state={}` as a urlencoded object of info. In this case
+ * 2. The state should now be found in `__posthog={}` and can be base64 encoded or urlencoded.
+ * 3. Base64 encoding is preferred and will gradually be rolled out everywhere
+ */
+
+ const stateHash =
+ STATE_FROM_WINDOW || _getHashParam(location.hash, '__posthog') || _getHashParam(location.hash, 'state')
+
let toolbarParams: ToolbarParams
+ const state = stateHash
+ ? _try(() => JSON.parse(atob(decodeURIComponent(stateHash)))) ||
+ _try(() => JSON.parse(decodeURIComponent(stateHash)))
+ : null
+
+ const parseFromUrl = state && state['action'] === 'ph_authorize'
if (parseFromUrl) {
// happens if they are initializing the toolbar using an old snippet
diff --git a/src/gdpr-utils.ts b/src/gdpr-utils.ts
index 6d40f0a56..1904d3d58 100644
--- a/src/gdpr-utils.ts
+++ b/src/gdpr-utils.ts
@@ -11,11 +11,15 @@
* These functions are used internally by the SDK and are not intended to be publicly exposed.
*/
-import { _each, _includes, _isNumber, _isString, window } from './utils'
+import { _each, _includes } from './utils'
+import { window } from './utils/globals'
import { cookieStore, localStore, localPlusCookieStore } from './storage'
import { GDPROptions, PersistentStore } from './types'
import { PostHog } from './posthog-core'
+import { _isNumber, _isString } from './utils/type-utils'
+import { logger } from './utils/logger'
+
/**
* A function used to capture a PostHog event (e.g. PostHogLib.capture)
* @callback captureFunction
@@ -188,7 +192,7 @@ function _hasDoNotTrackFlagOn(options: GDPROptions) {
*/
function _optInOut(optValue: boolean, token: string, options: GDPROptions) {
if (!_isString(token) || !token.length) {
- console.error('gdpr.' + (optValue ? 'optIn' : 'optOut') + ' called with an invalid token')
+ logger.error('gdpr.' + (optValue ? 'optIn' : 'optOut') + ' called with an invalid token')
return
}
@@ -210,7 +214,7 @@ function _optInOut(optValue: boolean, token: string, options: GDPROptions) {
}
}
-export function userOptedOut(posthog: PostHog, silenceErrors: boolean | undefined) {
+export function userOptedOut(posthog: PostHog) {
let optedOut = false
try {
@@ -230,41 +234,7 @@ export function userOptedOut(posthog: PostHog, silenceErrors: boolean | undefine
})
}
} catch (err) {
- if (!silenceErrors) {
- console.error('Unexpected error when checking capturing opt-out status: ' + err)
- }
+ logger.error('Unexpected error when checking capturing opt-out status: ' + err)
}
return optedOut
}
-
-/**
- * Wrap a method with a check for whether the user is opted out of data capturing and cookies/localstorage for the given token
- * If the user has opted out, return early instead of executing the method.
- * If a callback argument was provided, execute it passing the 0 error code.
- * @param {PostHog} posthog - the posthog instance
- * @param {function} method - wrapped method to be executed if the user has not opted out
- * @param silenceErrors
- * @returns {*} the result of executing method OR undefined if the user has opted out
- */
-export function addOptOutCheck any = (...args: any[]) => any>(
- posthog: PostHog,
- method: M,
- silenceErrors?: boolean
-): M {
- return function (...args) {
- const optedOut = userOptedOut(posthog, silenceErrors)
-
- if (!optedOut) {
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- return method.apply(this, args)
- }
-
- const callback = args[args.length - 1]
- if (typeof callback === 'function') {
- callback(0)
- }
-
- return
- } as M
-}
diff --git a/src/loader-exception-autocapture.ts b/src/loader-exception-autocapture.ts
new file mode 100644
index 000000000..c46ca95e1
--- /dev/null
+++ b/src/loader-exception-autocapture.ts
@@ -0,0 +1,9 @@
+import { extendPostHog } from './extensions/exception-autocapture'
+
+import { _isUndefined } from './utils/type-utils'
+
+const win: Window & typeof globalThis = _isUndefined(window) ? ({} as typeof window) : window
+
+;(win as any).extendPostHogWithExceptionAutoCapture = extendPostHog
+
+export default extendPostHog
diff --git a/src/loader-module.ts b/src/loader-module.ts
index 1f37bc5a8..c967ca2a5 100644
--- a/src/loader-module.ts
+++ b/src/loader-module.ts
@@ -1,5 +1,6 @@
import { init_as_module } from './posthog-core'
export { PostHog } from './posthog-core'
export * from './types'
+export * from './posthog-surveys-types'
export const posthog = init_as_module()
export default posthog
diff --git a/src/loader-recorder-v2.ts b/src/loader-recorder-v2.ts
index 66a243879..bd06c7c71 100644
--- a/src/loader-recorder-v2.ts
+++ b/src/loader-recorder-v2.ts
@@ -7,10 +7,10 @@ import rrwebRecord from 'rrweb/es/rrweb/packages/rrweb/src/record'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { getRecordConsolePlugin } from 'rrweb/es/rrweb/packages/rrweb/src/plugins/console/record'
-// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-// @ts-ignore
-const win: Window & typeof globalThis = typeof window !== 'undefined' ? window : ({} as typeof window)
+import { _isUndefined } from './utils/type-utils'
+
+const win: Window & typeof globalThis = _isUndefined(window) ? ({} as typeof window) : window
;(win as any).rrweb = { record: rrwebRecord, version: 'v2', rrwebVersion: version }
;(win as any).rrwebConsoleRecord = { getRecordConsolePlugin }
diff --git a/src/loader-recorder.ts b/src/loader-recorder.ts
index 20d1ff696..7ac507e79 100644
--- a/src/loader-recorder.ts
+++ b/src/loader-recorder.ts
@@ -7,10 +7,12 @@ import rrwebRecord from 'rrweb-v1/es/rrweb/packages/rrweb/src/record'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import { getRecordConsolePlugin } from 'rrweb-v1/es/rrweb/packages/rrweb/src/plugins/console/record'
+
+import { _isUndefined } from './utils/type-utils'
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
-const win: Window & typeof globalThis = typeof window !== 'undefined' ? window : ({} as typeof window)
+const win: Window & typeof globalThis = _isUndefined(window) ? ({} as typeof window) : window
;(win as any).rrweb = { record: rrwebRecord, version: 'v1', rrwebVersion: version }
;(win as any).rrwebConsoleRecord = { getRecordConsolePlugin }
diff --git a/src/loader-surveys.ts b/src/loader-surveys.ts
index 489ddefb0..4925804f1 100644
--- a/src/loader-surveys.ts
+++ b/src/loader-surveys.ts
@@ -1,6 +1,8 @@
import { generateSurveys } from './extensions/surveys'
-const win: Window & typeof globalThis = typeof window !== 'undefined' ? window : ({} as typeof window)
+import { _isUndefined } from './utils/type-utils'
+
+const win: Window & typeof globalThis = _isUndefined(window) ? ({} as typeof window) : window
;(win as any).extendPostHogWithSurveys = generateSurveys
diff --git a/src/page-view.ts b/src/page-view.ts
index d2beb9248..35de57395 100644
--- a/src/page-view.ts
+++ b/src/page-view.ts
@@ -1,4 +1,4 @@
-import { window } from './utils'
+import { window } from './utils/globals'
interface PageViewData {
pathname: string
diff --git a/src/posthog-core.ts b/src/posthog-core.ts
index 9f8dbf11d..5f45bb463 100644
--- a/src/posthog-core.ts
+++ b/src/posthog-core.ts
@@ -4,25 +4,18 @@ import {
_each,
_eachArray,
_extend,
- _info,
- _isArray,
_isBlockedUA,
- _isEmptyObject,
- _isObject,
- _isUndefined,
_register_event,
_safewrap_class,
- document,
- logger,
- userAgent,
- window,
+ isCrossDomainCookie,
} from './utils'
+import { window } from './utils/globals'
import { autocapture } from './autocapture'
import { PostHogFeatureFlags } from './posthog-featureflags'
import { PostHogPersistence } from './posthog-persistence'
import { ALIAS_ID_KEY, FLAG_CALL_REPORTED, PEOPLE_DISTINCT_ID_KEY } from './constants'
-import { SessionRecording } from './extensions/sessionrecording'
-import { WebPerformanceObserver } from './extensions/web-performance'
+import { SessionRecording } from './extensions/replay/sessionrecording'
+import { WebPerformanceObserver } from './extensions/replay/web-performance'
import { Decide } from './decide'
import { Toolbar } from './extensions/toolbar'
import { clearOptInOut, hasOptedIn, hasOptedOut, optIn, optOut, userOptedOut } from './gdpr-utils'
@@ -37,6 +30,7 @@ import {
CaptureOptions,
CaptureResult,
Compression,
+ DecideResponse,
EarlyAccessFeatureCallback,
GDPROptions,
isFeatureEnabledOptions,
@@ -55,11 +49,15 @@ import {
import { SentryIntegration } from './extensions/sentry-integration'
import { createSegmentIntegration } from './extensions/segment-integration'
import { PageViewManager } from './page-view'
-import { ExceptionObserver } from './extensions/exceptions/exception-autocapture'
import { PostHogSurveys } from './posthog-surveys'
import { RateLimiter } from './rate-limiter'
import { uuidv7 } from './uuidv7'
import { SurveyCallback } from './posthog-surveys-types'
+import { _isArray, _isEmptyObject, _isFunction, _isObject, _isString, _isUndefined } from './utils/type-utils'
+import { _info } from './utils/event-utils'
+import { logger } from './utils/logger'
+import { document, userAgent } from './utils/globals'
+import { SessionPropsManager } from './session-props'
/*
SIMPLE STYLE GUIDE:
@@ -109,7 +107,7 @@ const defaultConfig = (): PostHogConfig => ({
token: '',
autocapture: true,
rageclick: true,
- cross_subdomain_cookie: document?.location?.hostname?.indexOf('herokuapp.com') === -1,
+ cross_subdomain_cookie: isCrossDomainCookie(document?.location),
persistence: 'cookie',
persistence_name: '',
cookie_name: '',
@@ -152,7 +150,7 @@ const defaultConfig = (): PostHogConfig => ({
advanced_disable_toolbar_metrics: false,
on_xhr_error: (req) => {
const error = 'Bad HTTP status: ' + req.status + ' ' + req.statusText
- console.error(error)
+ logger.error(error)
},
get_device_id: (uuid) => uuid,
// Used for internal testing
@@ -199,7 +197,7 @@ const create_phlib = function (
instance = target as any
} else {
if (target && !_isArray(target)) {
- console.error('You have already initialized ' + name)
+ logger.error('You have already initialized ' + name)
// TODO: throw something instead?
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
@@ -221,8 +219,6 @@ const create_phlib = function (
instance.pageViewManager.startMeasuringScrollPosition()
}
- instance.exceptionAutocapture = new ExceptionObserver(instance)
-
instance.__autocapture = instance.config.autocapture
autocapture._setIsAutocaptureEnabled(instance)
if (autocapture._isAutocaptureEnabled) {
@@ -231,10 +227,10 @@ const create_phlib = function (
const num_enabled_buckets = 100
if (!autocapture.enabledForProject(instance.config.token, num_buckets, num_enabled_buckets)) {
instance.__autocapture = false
- logger.log('Not in active bucket: disabling Automatic Event Collection.')
+ logger.info('Not in active bucket: disabling Automatic Event Collection.')
} else if (!autocapture.isBrowserSupported()) {
instance.__autocapture = false
- logger.log('Disabling Automatic Event Collection because this browser is not supported')
+ logger.info('Disabling Automatic Event Collection because this browser is not supported')
} else {
autocapture.init(instance)
}
@@ -246,7 +242,7 @@ const create_phlib = function (
// if target is not defined, we called init after the lib already
// loaded, so there won't be an array of things to execute
- if (typeof target !== 'undefined' && _isArray(target)) {
+ if (!_isUndefined(target) && _isArray(target)) {
// Crunch through the people queue first - we queue this data up &
// flush on identify, so it's better to do all these operations first
instance._execute_array.call(instance.people, (target as any).people)
@@ -276,12 +272,12 @@ export class PostHog {
persistence?: PostHogPersistence
sessionPersistence?: PostHogPersistence
sessionManager?: SessionIdManager
+ sessionPropsManager?: SessionPropsManager
_requestQueue?: RequestQueue
_retryQueue?: RetryQueue
sessionRecording?: SessionRecording
webPerformance?: WebPerformanceObserver
- exceptionAutocapture?: ExceptionObserver
_triggered_notifs: any
compression: Partial>
@@ -290,6 +286,7 @@ export class PostHog {
__request_queue: [url: string, data: Record, options: XHROptions, callback?: RequestCallback][]
__autocapture: boolean | AutocaptureConfig | undefined
decideEndpointWasHit: boolean
+ analyticsDefaultEndpoint: string
SentryIntegration: typeof SentryIntegration
segmentIntegration: () => any
@@ -312,6 +309,7 @@ export class PostHog {
this.__loaded_recorder_version = undefined
this.__autocapture = undefined
this._jsc = function () {} as JSC
+ this.analyticsDefaultEndpoint = '/e/'
this.featureFlags = new PostHogFeatureFlags(this)
this.toolbar = new Toolbar(this)
@@ -322,12 +320,12 @@ export class PostHog {
// NOTE: See the property definition for deprecation notice
this.people = {
set: (prop: string | Properties, to?: string, callback?: RequestCallback) => {
- const setProps = typeof prop === 'string' ? { [prop]: to } : prop
+ const setProps = _isString(prop) ? { [prop]: to } : prop
this.setPersonProperties(setProps)
callback?.({})
},
set_once: (prop: string | Properties, to?: string, callback?: RequestCallback) => {
- const setProps = typeof prop === 'string' ? { [prop]: to } : prop
+ const setProps = _isString(prop) ? { [prop]: to } : prop
this.setPersonProperties(undefined, setProps)
callback?.({})
},
@@ -354,11 +352,13 @@ export class PostHog {
*/
init(token: string, config?: Partial, name?: string): PostHog | void {
if (_isUndefined(name)) {
- console.error('You must name your new library: init(token, config, name)')
+ logger.critical('You must name your new library: init(token, config, name)')
return
}
if (name === PRIMARY_INSTANCE_NAME) {
- console.error('You must initialize the main posthog object right after you include the PostHog js snippet')
+ logger.critical(
+ 'You must initialize the main posthog object right after you include the PostHog js snippet'
+ )
return
}
@@ -439,6 +439,7 @@ export class PostHog {
this.__request_queue = []
this.sessionManager = new SessionIdManager(this.config, this.persistence)
+ this.sessionPropsManager = new SessionPropsManager(this.sessionManager, this.persistence)
this.sessionPersistence =
this.config.persistence === 'sessionStorage'
? this.persistence
@@ -463,6 +464,8 @@ export class PostHog {
updateInitComplete('segmentRegister')()
}
+ // isUndefined doesn't provide typehint here so wouldn't reduce bundle as we'd need to assign
+ // eslint-disable-next-line posthog-js/no-direct-undefined-check
if (config.bootstrap?.distinctID !== undefined) {
const uuid = this.config.get_device_id(uuidv7())
const deviceID = config.bootstrap?.isIdentifiedID ? uuid : config.bootstrap.distinctID
@@ -521,6 +524,21 @@ export class PostHog {
// Private methods
+ _afterDecideResponse(response: DecideResponse) {
+ this.compression = {}
+ if (response.supportedCompression && !this.config.disable_compression) {
+ const compression: Partial> = {}
+ for (const method of response['supportedCompression']) {
+ compression[method] = true
+ }
+ this.compression = compression
+ }
+
+ if (response.analytics?.endpoint) {
+ this.analyticsDefaultEndpoint = response.analytics.endpoint
+ }
+ }
+
_loaded(): void {
// Pause `reloadFeatureFlags` calls in config.loaded callback.
// These feature flags are loaded in the decide call made right
@@ -533,7 +551,7 @@ export class PostHog {
try {
this.config.loaded(this)
} catch (err) {
- console.error('`loaded` function failed', err)
+ logger.critical('`loaded` function failed', err)
}
this._start_queue_if_opted_in()
@@ -693,7 +711,7 @@ export class PostHog {
onResponse: this.rateLimiter.checkForLimiting,
})
} catch (e) {
- console.error(e)
+ logger.error(e)
}
} else {
const script = document.createElement('script')
@@ -728,15 +746,11 @@ export class PostHog {
fn_name = item[0]
if (_isArray(fn_name)) {
capturing_calls.push(item) // chained call e.g. posthog.get_group().set()
- } else if (typeof item === 'function') {
+ } else if (_isFunction(item)) {
;(item as any).call(this)
} else if (_isArray(item) && fn_name === 'alias') {
alias_calls.push(item)
- } else if (
- _isArray(item) &&
- fn_name.indexOf('capture') !== -1 &&
- typeof (this as any)[fn_name] === 'function'
- ) {
+ } else if (_isArray(item) && fn_name.indexOf('capture') !== -1 && _isFunction((this as any)[fn_name])) {
capturing_calls.push(item)
} else {
other_calls.push(item)
@@ -792,23 +806,6 @@ export class PostHog {
this._execute_array([item])
}
- /*
- * PostHog supports exception autocapture, however, this function
- * is used to manually capture an exception
- * and can be used to add more context to that exception
- *
- * Properties passed as the second option will be merged with the properties
- * of the exception event.
- * Where there is a key in both generated exception and passed properties,
- * the generated exception property takes precedence.
- */
- captureException(exception: Error, properties?: Properties): void {
- this.exceptionAutocapture?.captureException(
- [exception.name, undefined, undefined, undefined, exception],
- properties
- )
- }
-
/**
* Capture an event. This is the most important and
* frequently used PostHog function.
@@ -835,10 +832,10 @@ export class PostHog {
// While developing, a developer might purposefully _not_ call init(),
// in this case, we would like capture to be a noop.
if (!this.__loaded || !this.sessionPersistence || !this._requestQueue) {
- return logger.unintializedWarning('posthog.capture')
+ return logger.uninitializedWarning('posthog.capture')
}
- if (userOptedOut(this, false)) {
+ if (userOptedOut(this)) {
return
}
@@ -849,8 +846,8 @@ export class PostHog {
}
// typing doesn't prevent interesting data
- if (_isUndefined(event_name) || typeof event_name !== 'string') {
- console.error('No event name provided to posthog.capture')
+ if (_isUndefined(event_name) || !_isString(event_name)) {
+ logger.error('No event name provided to posthog.capture')
return
}
@@ -889,12 +886,10 @@ export class PostHog {
this.setPersonPropertiesForFlags(finalSet)
}
- if (this.config.debug) {
- logger.log('PostHog.js send', data)
- }
+ logger.info('send', data)
const jsonData = JSON.stringify(data)
- const url = this.config.api_host + (options.endpoint || '/e/')
+ const url = this.config.api_host + (options.endpoint || this.analyticsDefaultEndpoint)
const has_unique_traits = options !== __NOOPTIONS
@@ -942,6 +937,15 @@ export class PostHog {
properties['$window_id'] = windowId
}
+ if (
+ this.sessionPropsManager &&
+ this.config.__preview_send_client_session_params &&
+ (event_name === '$pageview' || event_name === '$pageleave' || event_name === '$autocapture')
+ ) {
+ const sessionProps = this.sessionPropsManager.getSessionProps()
+ properties = _extend(properties, sessionProps)
+ }
+
if (this.config.__preview_measure_pageview_stats) {
let performanceProperties: Record = {}
if (event_name === '$pageview') {
@@ -965,7 +969,7 @@ export class PostHog {
}
// set $duration if time_event was previously called for this event
- if (typeof start_timestamp !== 'undefined') {
+ if (!_isUndefined(start_timestamp)) {
const duration_in_ms = new Date().getTime() - start_timestamp
properties['$duration'] = parseFloat((duration_in_ms / 1000).toFixed(3))
}
@@ -989,7 +993,7 @@ export class PostHog {
delete properties[blacklisted_prop]
})
} else {
- console.error('Invalid value for property_blacklist config: ' + property_blacklist)
+ logger.error('Invalid value for property_blacklist config: ' + property_blacklist)
}
const sanitize_properties = this.config.sanitize_properties
@@ -1256,11 +1260,11 @@ export class PostHog {
*/
identify(new_distinct_id?: string, userPropertiesToSet?: Properties, userPropertiesToSetOnce?: Properties): void {
if (!this.__loaded || !this.persistence) {
- return logger.unintializedWarning('posthog.identify')
+ return logger.uninitializedWarning('posthog.identify')
}
//if the new_distinct_id has not been set ignore the identify event
if (!new_distinct_id) {
- console.error('Unique user id has not been set in posthog.identify')
+ logger.error('Unique user id has not been set in posthog.identify')
return
}
@@ -1350,7 +1354,7 @@ export class PostHog {
*/
group(groupType: string, groupKey: string, groupPropertiesToSet?: Properties): void {
if (!groupType || !groupKey) {
- console.error('posthog.group requires a group type and group key')
+ logger.error('posthog.group requires a group type and group key')
return
}
@@ -1425,7 +1429,7 @@ export class PostHog {
*/
reset(reset_device_id?: boolean): void {
if (!this.__loaded) {
- return logger.unintializedWarning('posthog.reset')
+ return logger.uninitializedWarning('posthog.reset')
}
const device_id = this.get_property('$device_id')
this.persistence?.clear()
@@ -1543,7 +1547,7 @@ export class PostHog {
this._register_single(ALIAS_ID_KEY, alias)
return this.capture('$create_alias', { alias: alias, distinct_id: original })
} else {
- console.error('alias matches current distinct_id - skipping api call.')
+ logger.warn('alias matches current distinct_id - skipping api call.')
this.identify(alias)
return -1
}
@@ -1704,7 +1708,7 @@ export class PostHog {
Config.DEBUG = true
}
- if (this.sessionRecording && typeof config.disable_session_recording !== 'undefined') {
+ if (this.sessionRecording && !_isUndefined(config.disable_session_recording)) {
if (oldConfig.disable_session_recording !== config.disable_session_recording) {
if (config.disable_session_recording) {
this.sessionRecording.stopRecording()
@@ -1737,7 +1741,7 @@ export class PostHog {
* is currently running
*/
sessionRecordingStarted(): boolean {
- return !!this.sessionRecording?.started()
+ return !!this.sessionRecording?.started
}
/**
@@ -2150,7 +2154,7 @@ export function init_from_snippet(): void {
if (posthog_master['__loaded'] || (posthog_master['config'] && posthog_master['persistence'])) {
// lib has already been loaded at least once; we don't want to override the global object this time so bomb early
- console.error('PostHog library has already been downloaded at least once.')
+ logger.critical('PostHog library has already been downloaded at least once.')
return
}
diff --git a/src/posthog-featureflags.ts b/src/posthog-featureflags.ts
index 4b9e3f873..2a80cf324 100644
--- a/src/posthog-featureflags.ts
+++ b/src/posthog-featureflags.ts
@@ -1,4 +1,4 @@
-import { _base64Encode, _entries, _extend, logger } from './utils'
+import { _base64Encode, _entries, _extend } from './utils'
import { PostHog } from './posthog-core'
import {
DecideResponse,
@@ -19,6 +19,9 @@ import {
FLAG_CALL_REPORTED,
} from './constants'
+import { _isArray } from './utils/type-utils'
+import { logger } from './utils/logger'
+
const PERSISTENCE_ACTIVE_FEATURE_FLAGS = '$active_feature_flags'
const PERSISTENCE_OVERRIDE_FEATURE_FLAGS = '$override_feature_flags'
const PERSISTENCE_FEATURE_FLAG_PAYLOADS = '$feature_flag_payloads'
@@ -43,7 +46,7 @@ export const parseFeatureFlagDecideResponse = (
const flagPayloads = response['featureFlagPayloads']
if (flags) {
// using the v1 api
- if (Array.isArray(flags)) {
+ if (_isArray(flags)) {
const $enabled_feature_flags: Record = {}
if (flags) {
for (let i = 0; i < flags.length; i++) {
@@ -112,7 +115,7 @@ export class PostHogFeatureFlags {
}
}
if (!this._override_warning) {
- console.warn('[PostHog] Overriding feature flags!', {
+ logger.warn(' Overriding feature flags!', {
enabledFlags,
overriddenFlags,
finalFlags,
@@ -211,7 +214,7 @@ export class PostHogFeatureFlags {
*/
getFeatureFlag(key: string, options: { send_event?: boolean } = {}): boolean | string | undefined {
if (!this.instance.decideEndpointWasHit && !(this.getFlags() && this.getFlags().length > 0)) {
- console.warn('getFeatureFlag for key "' + key + '" failed. Feature flags didn\'t load in time.')
+ logger.warn('getFeatureFlag for key "' + key + '" failed. Feature flags didn\'t load in time.')
return undefined
}
const flagValue = this.getFlagVariants()[key]
@@ -220,7 +223,7 @@ export class PostHogFeatureFlags {
if (options.send_event || !('send_event' in options)) {
if (!(key in flagCallReported) || !flagCallReported[key].includes(flagReportValue)) {
- if (Array.isArray(flagCallReported[key])) {
+ if (_isArray(flagCallReported[key])) {
flagCallReported[key].push(flagReportValue)
} else {
flagCallReported[key] = [flagReportValue]
@@ -250,7 +253,7 @@ export class PostHogFeatureFlags {
*/
isFeatureEnabled(key: string, options: { send_event?: boolean } = {}): boolean | undefined {
if (!this.instance.decideEndpointWasHit && !(this.getFlags() && this.getFlags().length > 0)) {
- console.warn('isFeatureEnabled for key "' + key + '" failed. Feature flags didn\'t load in time.')
+ logger.warn('isFeatureEnabled for key "' + key + '" failed. Feature flags didn\'t load in time.')
return undefined
}
return !!this.getFeatureFlag(key, options)
@@ -288,14 +291,14 @@ export class PostHogFeatureFlags {
*/
override(flags: boolean | string[] | Record): void {
if (!this.instance.__loaded || !this.instance.persistence) {
- return logger.unintializedWarning('posthog.feature_flags.override')
+ return logger.uninitializedWarning('posthog.feature_flags.override')
}
this._override_warning = false
if (flags === false) {
this.instance.persistence.unregister(PERSISTENCE_OVERRIDE_FEATURE_FLAGS)
- } else if (Array.isArray(flags)) {
+ } else if (_isArray(flags)) {
const flagsObj: Record = {}
for (let i = 0; i < flags.length; i++) {
flagsObj[flags[i]] = true
diff --git a/src/posthog-persistence.ts b/src/posthog-persistence.ts
index 5514e1a03..7ccbec21f 100644
--- a/src/posthog-persistence.ts
+++ b/src/posthog-persistence.ts
@@ -1,6 +1,6 @@
/* eslint camelcase: "off" */
-import { _each, _extend, _include, _info, _isObject, _isUndefined, _strip_empty_properties, logger } from './utils'
+import { _each, _extend, _include, _strip_empty_properties } from './utils'
import { cookieStore, localStore, localPlusCookieStore, memoryStore, sessionStore } from './storage'
import { PersistentStore, PostHogConfig, Properties } from './types'
import {
@@ -11,6 +11,10 @@ import {
USER_STATE,
} from './constants'
+import { _isObject, _isUndefined } from './utils/type-utils'
+import { _info } from './utils/event-utils'
+import { logger } from './utils/logger'
+
const CASE_INSENSITIVE_PERSISTENCE_TYPES: readonly Lowercase[] = [
'cookie',
'localstorage',
@@ -88,7 +92,7 @@ export class PostHogPersistence {
const p: Properties = {}
// Filter out reserved properties
_each(this.props, function (v, k) {
- if (k === ENABLED_FEATURE_FLAGS && typeof v === 'object') {
+ if (k === ENABLED_FEATURE_FLAGS && _isObject(v)) {
const keys = Object.keys(v)
for (let i = 0; i < keys.length; i++) {
p[`$feature/${keys[i]}`] = v[keys[i]]
@@ -146,10 +150,10 @@ export class PostHogPersistence {
register_once(props: Properties, default_value: any, days?: number): boolean {
if (_isObject(props)) {
- if (typeof default_value === 'undefined') {
+ if (_isUndefined(default_value)) {
default_value = 'None'
}
- this.expire_days = typeof days === 'undefined' ? this.default_expiry : days
+ this.expire_days = _isUndefined(days) ? this.default_expiry : days
let hasChanges = false
@@ -175,7 +179,7 @@ export class PostHogPersistence {
register(props: Properties, days?: number): boolean {
if (_isObject(props)) {
- this.expire_days = typeof days === 'undefined' ? this.default_expiry : days
+ this.expire_days = _isUndefined(days) ? this.default_expiry : days
let hasChanges = false
diff --git a/src/posthog-surveys-types.ts b/src/posthog-surveys-types.ts
index 310e240b9..e5862f02b 100644
--- a/src/posthog-surveys-types.ts
+++ b/src/posthog-surveys-types.ts
@@ -28,9 +28,6 @@ export interface SurveyAppearance {
export enum SurveyType {
Popover = 'popover',
- Button = 'button',
- FullScreen = 'full_screen',
- Email = 'email',
API = 'api',
}
@@ -39,7 +36,7 @@ export type SurveyQuestion = BasicSurveyQuestion | LinkSurveyQuestion | RatingSu
interface SurveyQuestionBase {
question: string
description?: string | null
- required?: boolean
+ optional?: boolean
}
export interface BasicSurveyQuestion extends SurveyQuestionBase {
diff --git a/src/posthog-surveys.ts b/src/posthog-surveys.ts
index 5011ced91..730b8d25d 100644
--- a/src/posthog-surveys.ts
+++ b/src/posthog-surveys.ts
@@ -1,7 +1,7 @@
import { PostHog } from './posthog-core'
import { SURVEYS } from './constants'
-import { _isUrlMatchingRegex } from './utils'
-import { SurveyCallback, SurveyUrlMatchType } from 'posthog-surveys-types'
+import { SurveyCallback, SurveyUrlMatchType } from './posthog-surveys-types'
+import { _isUrlMatchingRegex } from './utils/request-utils'
export const surveyUrlValidationMap: Record boolean> = {
icontains: (conditionsUrl) => window.location.href.toLowerCase().indexOf(conditionsUrl.toLowerCase()) > -1,
diff --git a/src/rate-limiter.ts b/src/rate-limiter.ts
index b5d039640..5eea45fcf 100644
--- a/src/rate-limiter.ts
+++ b/src/rate-limiter.ts
@@ -1,4 +1,4 @@
-import { logger } from './utils'
+import { logger } from './utils/logger'
const oneMinuteInMilliseconds = 60 * 1000
@@ -20,10 +20,15 @@ export class RateLimiter {
public checkForLimiting = (xmlHttpRequest: XMLHttpRequest): void => {
try {
- const response: CaptureResponse = JSON.parse(xmlHttpRequest.responseText)
+ const text = xmlHttpRequest.responseText
+ if (!text || !text.length) {
+ return
+ }
+
+ const response: CaptureResponse = JSON.parse(text)
const quotaLimitedProducts = response.quota_limited || []
quotaLimitedProducts.forEach((batchKey) => {
- logger.log(`[PostHog RateLimiter] ${batchKey || 'events'} is quota limited.`)
+ logger.info(`[RateLimiter] ${batchKey || 'events'} is quota limited.`)
this.limits[batchKey] = new Date().getTime() + oneMinuteInMilliseconds
})
} catch (e) {
diff --git a/src/request-queue.ts b/src/request-queue.ts
index 65ec37d60..2b2ab27e0 100644
--- a/src/request-queue.ts
+++ b/src/request-queue.ts
@@ -2,6 +2,8 @@ import { RequestQueueScaffold } from './base-request-queue'
import { _each } from './utils'
import { Properties, QueuedRequestData, XHROptions } from './types'
+import { _isUndefined } from './utils/type-utils'
+
export class RequestQueue extends RequestQueueScaffold {
handlePollRequest: (url: string, data: Properties, options?: XHROptions) => void
@@ -77,7 +79,7 @@ export class RequestQueue extends RequestQueueScaffold {
_each(this._event_queue, (request) => {
const { url, data, options } = request
const key = (options ? options._batchKey : null) || url
- if (requests[key] === undefined) {
+ if (_isUndefined(requests[key])) {
requests[key] = { data: [], url, options }
}
diff --git a/src/retry-queue.ts b/src/retry-queue.ts
index 0fda82069..f848e2a96 100644
--- a/src/retry-queue.ts
+++ b/src/retry-queue.ts
@@ -1,9 +1,11 @@
import { RequestQueueScaffold } from './base-request-queue'
import { encodePostData, xhr } from './send-request'
import { QueuedRequestData, RetryQueueElement } from './types'
-import Config from './config'
import { RateLimiter } from './rate-limiter'
+import { _isUndefined } from './utils/type-utils'
+import { logger } from './utils/logger'
+
const thirtyMinutes = 30 * 60 * 1000
/**
@@ -40,7 +42,7 @@ export class RetryQueue extends RequestQueueScaffold {
this.onXHRError = onXHRError
this.rateLimiter = rateLimiter
- if (typeof window !== 'undefined' && 'onLine' in window.navigator) {
+ if (!_isUndefined(window) && 'onLine' in window.navigator) {
this.areWeOnline = window.navigator.onLine
window.addEventListener('online', () => {
this._handleWeAreNowOnline()
@@ -60,7 +62,13 @@ export class RetryQueue extends RequestQueueScaffold {
const retryAt = new Date(Date.now() + msToNextRetry)
this.queue.push({ retryAt, requestData })
- console.warn(`Enqueued failed request for retry in ${msToNextRetry}`)
+
+ let logMessage = `Enqueued failed request for retry in ${msToNextRetry}`
+ if (!navigator.onLine) {
+ logMessage += ' (Browser is offline)'
+ }
+ logger.warn(logMessage)
+
if (!this.isPolling) {
this.isPolling = true
this.poll()
@@ -99,9 +107,7 @@ export class RetryQueue extends RequestQueueScaffold {
const { url, data, options } = requestData
if (this.rateLimiter.isRateLimited(options._batchKey)) {
- if (Config.DEBUG) {
- console.warn('[PostHog RetryQueue] is quota limited. Dropping request.')
- }
+ logger.warn('[RetryQueue] is quota limited. Dropping request.')
continue
}
@@ -112,9 +118,7 @@ export class RetryQueue extends RequestQueueScaffold {
} catch (e) {
// Note sendBeacon automatically retries, and after the first retry it will lose reference to contextual `this`.
// This means in some cases `this.getConfig` will be undefined.
- if (Config.DEBUG) {
- console.error(e)
- }
+ logger.error(e)
}
}
this.queue = []
diff --git a/src/send-request.ts b/src/send-request.ts
index 7ad121693..08bc0dace 100644
--- a/src/send-request.ts
+++ b/src/send-request.ts
@@ -1,6 +1,10 @@
-import { _each, _HTTPBuildQuery, logger } from './utils'
+import { _each } from './utils'
import Config from './config'
import { PostData, XHROptions, XHRParams } from './types'
+import { _HTTPBuildQuery } from './utils/request-utils'
+
+import { _isArray, _isFunction, _isNumber, _isUint8Array, _isUndefined } from './utils/type-utils'
+import { logger } from './utils/logger'
export const addParamsToURL = (
url: string,
@@ -17,7 +21,7 @@ export const addParamsToURL = (
const params = halves[1].split('&')
for (const p of params) {
const key = p.split('=')[0]
- if (args[key]) {
+ if (!_isUndefined(args[key])) {
delete args[key]
}
}
@@ -29,7 +33,7 @@ export const addParamsToURL = (
export const encodePostData = (data: PostData | Uint8Array, options: Partial): string | BlobPart | null => {
if (options.blob && data.buffer) {
- return new Blob([data.buffer], { type: 'text/plain' })
+ return new Blob([_isUint8Array(data) ? data : data.buffer], { type: 'text/plain' })
}
if (options.sendBeacon || options.blob) {
@@ -42,8 +46,8 @@ export const encodePostData = (data: PostData | Uint8Array, options: Partial Object.prototype.toString.call(d) === '[object Uint8Array]'
- if (Array.isArray(data) || isUint8Array(data)) {
+
+ if (_isArray(data) || _isUint8Array(data)) {
// TODO: eh? passing an Array here?
body_data = 'data=' + encodeURIComponent(data as any)
} else {
@@ -69,6 +73,10 @@ export const xhr = ({
timeout = 60000,
onResponse,
}: XHRParams) => {
+ if (_isNumber(retriesPerformedSoFar) && retriesPerformedSoFar > 0) {
+ url = addParamsToURL(url, { retry_count: retriesPerformedSoFar }, {})
+ }
+
const req = new XMLHttpRequest()
req.open(options.method || 'GET', url, true)
@@ -102,7 +110,7 @@ export const xhr = ({
callback(response)
}
} else {
- if (typeof onXHRError === 'function') {
+ if (_isFunction(onXHRError)) {
onXHRError(req)
}
diff --git a/src/session-props.ts b/src/session-props.ts
new file mode 100644
index 000000000..15f0d2725
--- /dev/null
+++ b/src/session-props.ts
@@ -0,0 +1,88 @@
+/* Client-side session parameters. These are primarily used by web analytics,
+ * which relies on these for session analytics without the plugin server being
+ * available for the person level set-once properties. Obviously not consistent
+ * between client-side events and server-side events but this is acceptable
+ * as web analytics only uses client-side.
+ *
+ * These have the same lifespan as a session_id
+ */
+import { window } from './utils/globals'
+import { _info } from './utils/event-utils'
+import { SessionIdManager } from './sessionid'
+import { PostHogPersistence } from './posthog-persistence'
+import { CLIENT_SESSION_PROPS } from './constants'
+
+interface SessionSourceProps {
+ initialPathName: string
+ referringDomain: string // Is actually host, but named domain for internal consistency. Should contain a port if there is one.
+ utm_medium?: string
+ utm_source?: string
+ utm_campaign?: string
+ utm_content?: string
+ utm_term?: string
+}
+
+interface StoredSessionSourceProps {
+ sessionId: string
+ props: SessionSourceProps
+}
+
+export const generateSessionSourceParams = (): SessionSourceProps => {
+ return {
+ initialPathName: window.location.pathname,
+ referringDomain: _info.referringDomain(),
+ ..._info.campaignParams(),
+ }
+}
+
+export class SessionPropsManager {
+ private readonly _sessionIdManager: SessionIdManager
+ private readonly _persistence: PostHogPersistence
+ private readonly _sessionSourceParamGenerator: typeof generateSessionSourceParams
+
+ constructor(
+ sessionIdManager: SessionIdManager,
+ persistence: PostHogPersistence,
+ sessionSourceParamGenerator?: typeof generateSessionSourceParams
+ ) {
+ this._sessionIdManager = sessionIdManager
+ this._persistence = persistence
+ this._sessionSourceParamGenerator = sessionSourceParamGenerator || generateSessionSourceParams
+
+ this._sessionIdManager.onSessionId(this._onSessionIdCallback)
+ }
+
+ _getStoredProps(): StoredSessionSourceProps | undefined {
+ return this._persistence.props[CLIENT_SESSION_PROPS]
+ }
+
+ _onSessionIdCallback = (sessionId: string) => {
+ const stored = this._getStoredProps()
+ if (stored && stored.sessionId === sessionId) {
+ return
+ }
+
+ const newProps: StoredSessionSourceProps = {
+ sessionId,
+ props: this._sessionSourceParamGenerator(),
+ }
+ this._persistence.register({ [CLIENT_SESSION_PROPS]: newProps })
+ }
+
+ getSessionProps() {
+ const p = this._getStoredProps()?.props
+ if (!p) {
+ return {}
+ }
+
+ return {
+ $client_session_initial_referring_host: p.referringDomain,
+ $client_session_initial_pathname: p.initialPathName,
+ $client_session_initial_utm_source: p.utm_source,
+ $client_session_initial_utm_campaign: p.utm_campaign,
+ $client_session_initial_utm_medium: p.utm_medium,
+ $client_session_initial_utm_content: p.utm_content,
+ $client_session_initial_utm_term: p.utm_term,
+ }
+ }
+}
diff --git a/src/sessionid.ts b/src/sessionid.ts
index 93bcb12b6..fca582a16 100644
--- a/src/sessionid.ts
+++ b/src/sessionid.ts
@@ -3,45 +3,55 @@ import { SESSION_ID } from './constants'
import { sessionStore } from './storage'
import { PostHogConfig, SessionIdChangedCallback } from './types'
import { uuidv7 } from './uuidv7'
+import { window } from './utils/globals'
-const MAX_SESSION_IDLE_TIMEOUT = 30 * 60 // 30 mins
-const MIN_SESSION_IDLE_TIMEOUT = 60 // 1 mins
+import { _isArray, _isNumber, _isUndefined } from './utils/type-utils'
+import { logger } from './utils/logger'
+
+const MAX_SESSION_IDLE_TIMEOUT = 30 * 60 // 30 minutes
+const MIN_SESSION_IDLE_TIMEOUT = 60 // 1 minute
const SESSION_LENGTH_LIMIT = 24 * 3600 * 1000 // 24 hours
export class SessionIdManager {
+ private readonly _sessionIdGenerator: () => string
+ private readonly _windowIdGenerator: () => string
private config: Partial
private persistence: PostHogPersistence
private _windowId: string | null | undefined
private _sessionId: string | null | undefined
- private _window_id_storage_key: string
- private _primary_window_exists_storage_key: string
+ private readonly _window_id_storage_key: string
+ private readonly _primary_window_exists_storage_key: string
private _sessionStartTimestamp: number | null
+
private _sessionActivityTimestamp: number | null
- private _sessionTimeoutMs: number
+ private readonly _sessionTimeoutMs: number
private _sessionIdChangedHandlers: SessionIdChangedCallback[] = []
- constructor(config: Partial, persistence: PostHogPersistence) {
+ constructor(
+ config: Partial,
+ persistence: PostHogPersistence,
+ sessionIdGenerator?: () => string,
+ windowIdGenerator?: () => string
+ ) {
this.config = config
this.persistence = persistence
this._windowId = undefined
this._sessionId = undefined
this._sessionStartTimestamp = null
this._sessionActivityTimestamp = null
+ this._sessionIdGenerator = sessionIdGenerator || uuidv7
+ this._windowIdGenerator = windowIdGenerator || uuidv7
const persistenceName = config['persistence_name'] || config['token']
let desiredTimeout = config['session_idle_timeout_seconds'] || MAX_SESSION_IDLE_TIMEOUT
- if (typeof desiredTimeout !== 'number') {
- console.warn('[PostHog] session_idle_timeout_seconds must be a number. Defaulting to 30 minutes.')
+ if (!_isNumber(desiredTimeout)) {
+ logger.warn('session_idle_timeout_seconds must be a number. Defaulting to 30 minutes.')
desiredTimeout = MAX_SESSION_IDLE_TIMEOUT
} else if (desiredTimeout > MAX_SESSION_IDLE_TIMEOUT) {
- console.warn(
- '[PostHog] session_idle_timeout_seconds cannot be greater than 30 minutes. Using 30 minutes instead.'
- )
+ logger.warn('session_idle_timeout_seconds cannot be greater than 30 minutes. Using 30 minutes instead.')
} else if (desiredTimeout < MIN_SESSION_IDLE_TIMEOUT) {
- console.warn(
- '[PostHog] session_idle_timeout_seconds cannot be less than 60 seconds. Using 60 seconds instead.'
- )
+ logger.warn('session_idle_timeout_seconds cannot be less than 60 seconds. Using 60 seconds instead.')
}
this._sessionTimeoutMs =
@@ -72,7 +82,7 @@ export class SessionIdManager {
onSessionId(callback: SessionIdChangedCallback): () => void {
// KLUDGE: when running in tests the handlers array was always undefined
// it's yucky but safe to set it here so that it's always definitely available
- if (this._sessionIdChangedHandlers === undefined) {
+ if (_isUndefined(this._sessionIdChangedHandlers)) {
this._sessionIdChangedHandlers = []
}
@@ -129,6 +139,7 @@ export class SessionIdManager {
this._sessionStartTimestamp = sessionStartTimestamp
this._sessionActivityTimestamp = sessionActivityTimestamp
this._sessionId = sessionId
+
this.persistence.register({
[SESSION_ID]: [sessionActivityTimestamp, sessionId, sessionStartTimestamp],
})
@@ -141,7 +152,7 @@ export class SessionIdManager {
}
const sessionId = this.persistence.props[SESSION_ID]
- if (Array.isArray(sessionId) && sessionId.length === 2) {
+ if (_isArray(sessionId) && sessionId.length === 2) {
// Storage does not yet have a session start time. Add the last activity timestamp as the start time
sessionId.push(sessionId[0])
}
@@ -171,8 +182,8 @@ export class SessionIdManager {
/*
* This function returns the current sessionId and windowId. It should be used to
- * access these values over directly calling `._sessionId` or `._windowId`. In addition
- * to returning the sessionId and windowId, this function also manages cycling the
+ * access these values over directly calling `._sessionId` or `._windowId`.
+ * In addition to returning the sessionId and windowId, this function also manages cycling the
* sessionId and windowId when appropriate by doing the following:
*
* 1. If the sessionId or windowId is not set, it will generate a new one and store it.
@@ -196,17 +207,15 @@ export class SessionIdManager {
startTimestamp && startTimestamp > 0 && Math.abs(timestamp - startTimestamp) > SESSION_LENGTH_LIMIT
let valuesChanged = false
- if (
- !sessionId ||
- (!readOnly && Math.abs(timestamp - lastTimestamp) > this._sessionTimeoutMs) ||
- sessionPastMaximumLength
- ) {
- sessionId = uuidv7()
- windowId = uuidv7()
+ const noSessionId = !sessionId
+ const activityTimeout = !readOnly && Math.abs(timestamp - lastTimestamp) > this._sessionTimeoutMs
+ if (noSessionId || activityTimeout || sessionPastMaximumLength) {
+ sessionId = this._sessionIdGenerator()
+ windowId = this._windowIdGenerator()
startTimestamp = timestamp
valuesChanged = true
} else if (!windowId) {
- windowId = uuidv7()
+ windowId = this._windowIdGenerator()
valuesChanged = true
}
diff --git a/src/storage.ts b/src/storage.ts
index 97b1cb2d9..a7a7a5606 100644
--- a/src/storage.ts
+++ b/src/storage.ts
@@ -1,9 +1,11 @@
-import { _extend, logger } from './utils'
+import { _extend } from './utils'
import { PersistentStore, Properties } from './types'
-import Config from './config'
-import { DISTINCT_ID, SESSION_ID } from './constants'
+import { DISTINCT_ID, SESSION_ID, SESSION_RECORDING_IS_SAMPLED } from './constants'
-const DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]+\.[a-z.]{2,6}$/i
+import { _isNull, _isUndefined } from './utils/type-utils'
+import { logger } from './utils/logger'
+
+const DOMAIN_MATCH_REGEX = /[a-z0-9][a-z0-9-]+\.[a-z]{2,}$/i
// Methods partially borrowed from quirksmode.org/js/cookies.html
export const cookieStore: PersistentStore = {
@@ -92,12 +94,12 @@ let _localStorage_supported: boolean | null = null
export const localStore: PersistentStore = {
is_supported: function () {
- if (_localStorage_supported !== null) {
+ if (!_isNull(_localStorage_supported)) {
return _localStorage_supported
}
let supported = true
- if (typeof window !== 'undefined') {
+ if (!_isUndefined(window)) {
try {
const key = '__mplssupport__',
val = 'xyz'
@@ -162,7 +164,7 @@ export const localStore: PersistentStore = {
// Use localstorage for most data but still use cookie for COOKIE_PERSISTED_PROPERTIES
// This solves issues with cookies having too much data in them causing headers too large
// Also makes sure we don't have to send a ton of data to the server
-const COOKIE_PERSISTED_PROPERTIES = [DISTINCT_ID, SESSION_ID]
+const COOKIE_PERSISTED_PROPERTIES = [DISTINCT_ID, SESSION_ID, SESSION_RECORDING_IS_SAMPLED]
export const localPlusCookieStore: PersistentStore = {
...localStore,
@@ -246,11 +248,11 @@ export const resetSessionStorageSupported = () => {
// Storage that only lasts the length of a tab/window. Survives page refreshes
export const sessionStore: PersistentStore = {
is_supported: function () {
- if (sessionStorageSupported !== null) {
+ if (!_isNull(sessionStorageSupported)) {
return sessionStorageSupported
}
sessionStorageSupported = true
- if (typeof window !== 'undefined') {
+ if (!_isUndefined(window)) {
try {
const key = '__support__',
val = 'xyz'
@@ -269,9 +271,7 @@ export const sessionStore: PersistentStore = {
},
error: function (msg) {
- if (Config.DEBUG) {
- logger.error('sessionStorage error: ', msg)
- }
+ logger.error('sessionStorage error: ', msg)
},
get: function (name) {
diff --git a/src/types.ts b/src/types.ts
index 7fd90f86e..ed607a8ab 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -124,6 +124,7 @@ export interface PostHogConfig {
}
segment?: any
__preview_measure_pageview_stats?: boolean
+ __preview_send_client_session_params?: boolean
}
export interface OptInOutCapturingOptions {
@@ -221,6 +222,9 @@ export interface DecideResponse {
errorsWhileComputingFlags: boolean
autocapture_opt_out?: boolean
capturePerformance?: boolean
+ analytics?: {
+ endpoint?: string
+ }
// this is currently in development and may have breaking changes without a major version bump
autocaptureExceptions?:
| boolean
@@ -232,6 +236,10 @@ export interface DecideResponse {
endpoint?: string
consoleLogRecordingEnabled?: boolean
recorderVersion?: 'v1' | 'v2'
+ // the API returns a decimal between 0 and 1 as a string
+ sampleRate?: string | null
+ minimumDurationMilliseconds?: number
+ linkedFlag?: string | null
}
surveys?: boolean
toolbarParams: ToolbarParams
diff --git a/src/utils.ts b/src/utils.ts
deleted file mode 100644
index 88b2b78b8..000000000
--- a/src/utils.ts
+++ /dev/null
@@ -1,978 +0,0 @@
-import Config from './config'
-import { Breaker, EventHandler, Properties } from './types'
-
-/*
- * Saved references to long variable names, so that closure compiler can
- * minimize file size.
- */
-
-const ArrayProto = Array.prototype
-const ObjProto = Object.prototype
-const toString = ObjProto.toString
-const hasOwnProperty = ObjProto.hasOwnProperty
-const win: Window & typeof globalThis = typeof window !== 'undefined' ? window : ({} as typeof window)
-const navigator = win.navigator || { userAgent: '' }
-const document = win.document || {}
-const userAgent = navigator.userAgent
-const localDomains = ['localhost', '127.0.0.1']
-
-const nativeForEach = ArrayProto.forEach,
- nativeIndexOf = ArrayProto.indexOf,
- nativeIsArray = Array.isArray,
- breaker: Breaker = {}
-
-// Console override
-const logger = {
- /** @type {function(...*)} */
- log: function (...args: any[]) {
- if (Config.DEBUG && !_isUndefined(window.console) && window.console) {
- // Don't log PostHog debug messages in rrweb
- const log =
- '__rrweb_original__' in window.console.log
- ? (window.console.log as any)['__rrweb_original__']
- : window.console.log
-
- try {
- log.apply(window.console, args)
- } catch (err) {
- _eachArray(args, function (arg) {
- log(arg)
- })
- }
- }
- },
- /** @type {function(...*)} */
- error: function (..._args: any[]) {
- if (Config.DEBUG && !_isUndefined(window.console) && window.console) {
- const args = ['PostHog error:', ..._args]
- // Don't log PostHog debug messages in rrweb
- const error =
- '__rrweb_original__' in window.console.error
- ? (window.console.error as any)['__rrweb_original__']
- : window.console.error
- try {
- error.apply(window.console, args)
- } catch (err) {
- _eachArray(args, function (arg) {
- error(arg)
- })
- }
- }
- },
- /** @type {function(...*)} */
- critical: function (..._args: any[]) {
- if (!_isUndefined(window.console) && window.console) {
- const args = ['PostHog error:', ..._args]
- // Don't log PostHog debug messages in rrweb
- const error =
- '__rrweb_original__' in window.console.error
- ? (window.console.error as any)['__rrweb_original__']
- : window.console.error
- try {
- error.apply(window.console, args)
- } catch (err) {
- _eachArray(args, function (arg) {
- error(arg)
- })
- }
- }
- },
- unintializedWarning: function (methodName: string): void {
- if (Config.DEBUG && !_isUndefined(window.console) && window.console) {
- logger.error(`[PostHog] You must initialize PostHog before calling ${methodName}`)
- }
- },
-}
-
-// UNDERSCORE
-// Embed part of the Underscore Library
-export const _trim = function (str: string): string {
- return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '')
-}
-
-export const _bind_instance_methods = function (obj: Record): void {
- for (const func in obj) {
- if (typeof obj[func] === 'function') {
- obj[func] = obj[func].bind(obj)
- }
- }
-}
-
-export function _eachArray(
- obj: E[] | null | undefined,
- iterator: (value: E, key: number) => void | Breaker,
- thisArg?: any
-): void {
- if (Array.isArray(obj)) {
- if (nativeForEach && obj.forEach === nativeForEach) {
- obj.forEach(iterator, thisArg)
- } else if ('length' in obj && obj.length === +obj.length) {
- for (let i = 0, l = obj.length; i < l; i++) {
- if (i in obj && iterator.call(thisArg, obj[i], i) === breaker) {
- return
- }
- }
- }
- }
-}
-
-/**
- * @param {*=} obj
- * @param {function(...*)=} iterator
- * @param {Object=} thisArg
- */
-export function _each(obj: any, iterator: (value: any, key: any) => void | Breaker, thisArg?: any): void {
- if (obj === null || obj === undefined) {
- return
- }
- if (Array.isArray(obj)) {
- return _eachArray(obj, iterator, thisArg)
- }
- for (const key in obj) {
- if (hasOwnProperty.call(obj, key)) {
- if (iterator.call(thisArg, obj[key], key) === breaker) {
- return
- }
- }
- }
-}
-
-export const _extend = function (obj: Record, ...args: Record[]): Record {
- _eachArray(args, function (source) {
- for (const prop in source) {
- if (source[prop] !== void 0) {
- obj[prop] = source[prop]
- }
- }
- })
- return obj
-}
-
-export const _isArray =
- nativeIsArray ||
- function (obj: any): obj is any[] {
- return toString.call(obj) === '[object Array]'
- }
-
-// from a comment on http://dbj.org/dbj/?p=286
-// fails on only one very rare and deliberate custom object:
-// let bomb = { toString : undefined, valueOf: function(o) { return "function BOMBA!"; }};
-export const _isFunction = function (f: any): f is (...args: any[]) => any {
- try {
- return /^\s*\bfunction\b/.test(f)
- } catch (x) {
- return false
- }
-}
-
-export const _include = function (
- obj: null | string | Array | Record,
- target: any
-): boolean | Breaker {
- let found = false
- if (obj === null) {
- return found
- }
- if (nativeIndexOf && obj.indexOf === nativeIndexOf) {
- return obj.indexOf(target) != -1
- }
- _each(obj, function (value) {
- if (found || (found = value === target)) {
- return breaker
- }
- return
- })
- return found
-}
-
-export function _includes(str: T[] | string, needle: T): boolean {
- return (str as any).indexOf(needle) !== -1
-}
-
-/**
- * Object.entries() polyfill
- * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries
- */
-export function _entries(obj: Record): [string, T][] {
- const ownProps = Object.keys(obj)
- let i = ownProps.length
- const resArray = new Array(i) // preallocate the Array
-
- while (i--) {
- resArray[i] = [ownProps[i], obj[ownProps[i]]]
- }
- return resArray
-}
-
-// Underscore Addons
-export const _isObject = function (obj: any): obj is Record {
- return obj === Object(obj) && !_isArray(obj)
-}
-
-export const _isEmptyObject = function (obj: any): obj is Record {
- if (_isObject(obj)) {
- for (const key in obj) {
- if (hasOwnProperty.call(obj, key)) {
- return false
- }
- }
- return true
- }
- return false
-}
-
-export const _isUndefined = function (obj: any): obj is undefined {
- return obj === void 0
-}
-
-export const _isString = function (obj: any): obj is string {
- return toString.call(obj) == '[object String]'
-}
-
-export const _isDate = function (obj: any): obj is Date {
- return toString.call(obj) == '[object Date]'
-}
-
-export const _isNumber = function (obj: any): obj is number {
- return toString.call(obj) == '[object Number]'
-}
-
-export const _isValidRegex = function (str: string): boolean {
- try {
- new RegExp(str)
- } catch (error) {
- return false
- }
- return true
-}
-
-export const _isUrlMatchingRegex = function (url: string, pattern: string): boolean {
- if (!_isValidRegex(pattern)) return false
- return new RegExp(pattern).test(url)
-}
-
-export const _encodeDates = function (obj: Properties): Properties {
- _each(obj, function (v, k) {
- if (_isDate(v)) {
- obj[k] = _formatDate(v)
- } else if (_isObject(v)) {
- obj[k] = _encodeDates(v) // recurse
- }
- })
- return obj
-}
-
-export const _timestamp = function (): number {
- Date.now =
- Date.now ||
- function () {
- return +new Date()
- }
- return Date.now()
-}
-
-export const _formatDate = function (d: Date): string {
- // YYYY-MM-DDTHH:MM:SS in UTC
- function pad(n: number) {
- return n < 10 ? '0' + n : n
- }
- return (
- d.getUTCFullYear() +
- '-' +
- pad(d.getUTCMonth() + 1) +
- '-' +
- pad(d.getUTCDate()) +
- 'T' +
- pad(d.getUTCHours()) +
- ':' +
- pad(d.getUTCMinutes()) +
- ':' +
- pad(d.getUTCSeconds())
- )
-}
-
-export const _safewrap = function any = (...args: any[]) => any>(f: F): F {
- return function (...args) {
- try {
- // eslint-disable-next-line @typescript-eslint/ban-ts-comment
- // @ts-ignore
- return f.apply(this, args)
- } catch (e) {
- logger.critical('Implementation error. Please turn on debug and contact support@posthog.com.')
- logger.critical(e)
- }
- } as F
-}
-
-// eslint-disable-next-line @typescript-eslint/ban-types
-export const _safewrap_class = function (klass: Function, functions: string[]): void {
- for (let i = 0; i < functions.length; i++) {
- klass.prototype[functions[i]] = _safewrap(klass.prototype[functions[i]])
- }
-}
-
-export const _safewrap_instance_methods = function (obj: Record): void {
- for (const func in obj) {
- if (typeof obj[func] === 'function') {
- obj[func] = _safewrap(obj[func])
- }
- }
-}
-
-export const _strip_empty_properties = function (p: Properties): Properties {
- const ret: Properties = {}
- _each(p, function (v, k) {
- if (_isString(v) && v.length > 0) {
- ret[k] = v
- }
- })
- return ret
-}
-
-/**
- * Deep copies an object.
- * It handles cycles by replacing all references to them with `undefined`
- * Also supports customizing native values
- *
- * @param value
- * @param customizer
- * @returns {{}|undefined|*}
- */
-function deepCircularCopy = Record>(
- value: T,
- customizer?: (value: T[K], key?: K) => T[K]
-): T | undefined {
- const COPY_IN_PROGRESS_SET = new Set()
-
- function internalDeepCircularCopy(value: T, key?: string): T | undefined {
- if (value !== Object(value)) return customizer ? customizer(value as any, key) : value // primitive value
-
- if (COPY_IN_PROGRESS_SET.has(value)) return undefined
- COPY_IN_PROGRESS_SET.add(value)
- let result: T
-
- if (_isArray(value)) {
- result = [] as any as T
- _eachArray(value, (it) => {
- result.push(internalDeepCircularCopy(it))
- })
- } else {
- result = {} as T
- _each(value, (val, key) => {
- if (!COPY_IN_PROGRESS_SET.has(val)) {
- ;(result as any)[key] = internalDeepCircularCopy(val, key)
- }
- })
- }
- return result
- }
- return internalDeepCircularCopy(value)
-}
-
-const LONG_STRINGS_ALLOW_LIST = ['$performance_raw']
-
-export function _copyAndTruncateStrings = Record>(
- object: T,
- maxStringLength: number | null
-): T {
- return deepCircularCopy(object, (value: any, key) => {
- if (key && LONG_STRINGS_ALLOW_LIST.indexOf(key as string) > -1) {
- return value
- }
- if (typeof value === 'string' && maxStringLength !== null) {
- return (value as string).slice(0, maxStringLength)
- }
- return value
- }) as T
-}
-
-export function _base64Encode(data: null): null
-export function _base64Encode(data: undefined): undefined
-export function _base64Encode(data: string): string
-export function _base64Encode(data: string | null | undefined): string | null | undefined {
- const b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
- let o1,
- o2,
- o3,
- h1,
- h2,
- h3,
- h4,
- bits,
- i = 0,
- ac = 0,
- enc = ''
- const tmp_arr: string[] = []
-
- if (!data) {
- return data
- }
-
- data = _utf8Encode(data)
-
- do {
- // pack three octets into four hexets
- o1 = data.charCodeAt(i++)
- o2 = data.charCodeAt(i++)
- o3 = data.charCodeAt(i++)
-
- bits = (o1 << 16) | (o2 << 8) | o3
-
- h1 = (bits >> 18) & 0x3f
- h2 = (bits >> 12) & 0x3f
- h3 = (bits >> 6) & 0x3f
- h4 = bits & 0x3f
-
- // use hexets to index into b64, and append result to encoded string
- tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4)
- } while (i < data.length)
-
- enc = tmp_arr.join('')
-
- switch (data.length % 3) {
- case 1:
- enc = enc.slice(0, -2) + '=='
- break
- case 2:
- enc = enc.slice(0, -1) + '='
- break
- }
-
- return enc
-}
-
-export const _utf8Encode = function (string: string): string {
- string = (string + '').replace(/\r\n/g, '\n').replace(/\r/g, '\n')
-
- let utftext = '',
- start,
- end
- let stringl = 0,
- n
-
- start = end = 0
- stringl = string.length
-
- for (n = 0; n < stringl; n++) {
- const c1 = string.charCodeAt(n)
- let enc = null
-
- if (c1 < 128) {
- end++
- } else if (c1 > 127 && c1 < 2048) {
- enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128)
- } else {
- enc = String.fromCharCode((c1 >> 12) | 224, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128)
- }
- if (enc !== null) {
- if (end > start) {
- utftext += string.substring(start, end)
- }
- utftext += enc
- start = end = n + 1
- }
- }
-
- if (end > start) {
- utftext += string.substring(start, string.length)
- }
-
- return utftext
-}
-
-export const DEFAULT_BLOCKED_UA_STRS = [
- 'ahrefsbot',
- 'applebot',
- 'baiduspider',
- 'bingbot',
- 'bingpreview',
- 'bot.htm',
- 'bot.php',
- 'crawler',
- 'duckduckbot',
- 'facebookexternal',
- 'facebookcatalog',
- 'gptbot',
- 'hubspot',
- 'linkedinbot',
- 'mj12bot',
- 'petalbot',
- 'pinterest',
- 'prerender',
- 'rogerbot',
- 'screaming frog',
- 'semrushbot',
- 'sitebulb',
- 'twitterbot',
- 'yahoo! slurp',
- 'yandexbot',
-
- // a whole bunch of goog-specific crawlers
- // https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers
- 'adsbot-google',
- 'apis-google',
- 'duplexweb-google',
- 'feedfetcher-google',
- 'google favicon',
- 'google web preview',
- 'google-read-aloud',
- 'googlebot',
- 'googleweblight',
- 'mediapartners-google',
- 'storebot-google',
-]
-
-// _.isBlockedUA()
-// This is to block various web spiders from executing our JS and
-// sending false capturing data
-export const _isBlockedUA = function (ua: string, customBlockedUserAgents: string[]): boolean {
- return DEFAULT_BLOCKED_UA_STRS.concat(customBlockedUserAgents).some((blockedUA) => {
- if (ua.includes) {
- return ua.includes(blockedUA)
- } else {
- // IE 11 :/
- return ua.indexOf(blockedUA) !== -1
- }
- })
-}
-
-/**
- * @param {Object=} formdata
- * @param {string=} arg_separator
- */
-export const _HTTPBuildQuery = function (formdata: Record, arg_separator = '&'): string {
- let use_val: string
- let use_key: string
- const tph_arr: string[] = []
-
- _each(formdata, function (val, key) {
- use_val = encodeURIComponent(val.toString())
- use_key = encodeURIComponent(key)
- tph_arr[tph_arr.length] = use_key + '=' + use_val
- })
-
- return tph_arr.join(arg_separator)
-}
-
-export const _getQueryParam = function (url: string, param: string): string {
- // Expects a raw URL
-
- const cleanParam = param.replace(/[[]/, '\\[').replace(/[\]]/, '\\]')
- const regexS = '[\\?&]' + cleanParam + '=([^]*)'
- const regex = new RegExp(regexS)
- const results = regex.exec(url)
- if (results === null || (results && typeof results[1] !== 'string' && (results[1] as any).length)) {
- return ''
- } else {
- let result = results[1]
- try {
- result = decodeURIComponent(result)
- } catch (err) {
- logger.error('Skipping decoding for malformed query param: ' + result)
- }
- return result.replace(/\+/g, ' ')
- }
-}
-
-export const _getHashParam = function (hash: string, param: string): string | null {
- const matches = hash.match(new RegExp(param + '=([^&]*)'))
- return matches ? matches[1] : null
-}
-
-export const _register_event = (function () {
- // written by Dean Edwards, 2005
- // with input from Tino Zijdel - crisp@xs4all.nl
- // with input from Carl Sverre - mail@carlsverre.com
- // with input from PostHog
- // http://dean.edwards.name/weblog/2005/10/add-event/
- // https://gist.github.com/1930440
-
- /**
- * @param {Object} element
- * @param {string} type
- * @param {function(...*)} handler
- * @param {boolean=} oldSchool
- * @param {boolean=} useCapture
- */
- const register_event = function (
- element: Element | Window | Document | Node,
- type: string,
- handler: EventHandler,
- oldSchool?: boolean,
- useCapture?: boolean
- ) {
- if (!element) {
- logger.error('No valid element provided to register_event')
- return
- }
-
- if (element.addEventListener && !oldSchool) {
- element.addEventListener(type, handler, !!useCapture)
- } else {
- const ontype = 'on' + type
- const old_handler = (element as any)[ontype] // can be undefined
- ;(element as any)[ontype] = makeHandler(element, handler, old_handler)
- }
- }
-
- function makeHandler(
- element: Element | Window | Document | Node,
- new_handler: EventHandler,
- old_handlers: EventHandler
- ) {
- return function (event: Event): boolean | void {
- event = event || fixEvent(window.event)
-
- // this basically happens in firefox whenever another script
- // overwrites the onload callback and doesn't pass the event
- // object to previously defined callbacks. All the browsers
- // that don't define window.event implement addEventListener
- // so the dom_loaded handler will still be fired as usual.
- if (!event) {
- return undefined
- }
-
- let ret = true
- let old_result: any
-
- if (_isFunction(old_handlers)) {
- old_result = old_handlers(event)
- }
- const new_result = new_handler.call(element, event)
-
- if (false === old_result || false === new_result) {
- ret = false
- }
-
- return ret
- }
- }
-
- function fixEvent(event: Event | undefined): Event | undefined {
- if (event) {
- event.preventDefault = fixEvent.preventDefault
- event.stopPropagation = fixEvent.stopPropagation
- }
- return event
- }
- fixEvent.preventDefault = function () {
- ;(this as any as Event).returnValue = false
- }
- fixEvent.stopPropagation = function () {
- ;(this as any as Event).cancelBubble = true
- }
-
- return register_event
-})()
-
-export const isLocalhost = (): boolean => {
- return localDomains.includes(location.hostname)
-}
-
-export function loadScript(scriptUrlToLoad: string, callback: (error?: string | Event, event?: Event) => void): void {
- const addScript = () => {
- const scriptTag = document.createElement('script')
- scriptTag.type = 'text/javascript'
- scriptTag.src = scriptUrlToLoad
- scriptTag.onload = (event) => callback(undefined, event)
- scriptTag.onerror = (error) => callback(error)
-
- const scripts = document.querySelectorAll('body > script')
- if (scripts.length > 0) {
- scripts[0].parentNode?.insertBefore(scriptTag, scripts[0])
- } else {
- // In exceptional situations this call might load before the DOM is fully ready.
- document.body.appendChild(scriptTag)
- }
- }
-
- if (document.body) {
- addScript()
- } else {
- document.addEventListener('DOMContentLoaded', addScript)
- }
-}
-
-export const _info = {
- campaignParams: function (customParams?: string[]): Record {
- const campaign_keywords = [
- 'utm_source',
- 'utm_medium',
- 'utm_campaign',
- 'utm_content',
- 'utm_term',
- 'gclid',
- 'fbclid',
- 'msclkid',
- ].concat(customParams || [])
-
- const params: Record = {}
- _each(campaign_keywords, function (kwkey) {
- const kw = _getQueryParam(document.URL, kwkey)
- if (kw.length) {
- params[kwkey] = kw
- }
- })
-
- return params
- },
-
- searchEngine: function (): string | null {
- const referrer = document.referrer
- if (!referrer) {
- return null
- } else if (referrer.search('https?://(.*)google.([^/?]*)') === 0) {
- return 'google'
- } else if (referrer.search('https?://(.*)bing.com') === 0) {
- return 'bing'
- } else if (referrer.search('https?://(.*)yahoo.com') === 0) {
- return 'yahoo'
- } else if (referrer.search('https?://(.*)duckduckgo.com') === 0) {
- return 'duckduckgo'
- } else {
- return null
- }
- },
-
- searchInfo: function (): Record {
- const search = _info.searchEngine(),
- param = search != 'yahoo' ? 'q' : 'p',
- ret: Record = {}
-
- if (search !== null) {
- ret['$search_engine'] = search
-
- const keyword = _getQueryParam(document.referrer, param)
- if (keyword.length) {
- ret['ph_keyword'] = keyword
- }
- }
-
- return ret
- },
-
- /**
- * This function detects which browser is running this script.
- * The order of the checks are important since many user agents
- * include key words used in later checks.
- */
- browser: function (user_agent: string, vendor: string, opera?: any): string {
- vendor = vendor || '' // vendor is undefined for at least IE9
- if (opera || _includes(user_agent, ' OPR/')) {
- if (_includes(user_agent, 'Mini')) {
- return 'Opera Mini'
- }
- return 'Opera'
- } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) {
- return 'BlackBerry'
- } else if (_includes(user_agent, 'IEMobile') || _includes(user_agent, 'WPDesktop')) {
- return 'Internet Explorer Mobile'
- } else if (_includes(user_agent, 'SamsungBrowser/')) {
- // https://developer.samsung.com/internet/user-agent-string-format
- return 'Samsung Internet'
- } else if (_includes(user_agent, 'Edge') || _includes(user_agent, 'Edg/')) {
- return 'Microsoft Edge'
- } else if (_includes(user_agent, 'FBIOS')) {
- return 'Facebook Mobile'
- } else if (_includes(user_agent, 'Chrome')) {
- return 'Chrome'
- } else if (_includes(user_agent, 'CriOS')) {
- return 'Chrome iOS'
- } else if (_includes(user_agent, 'UCWEB') || _includes(user_agent, 'UCBrowser')) {
- return 'UC Browser'
- } else if (_includes(user_agent, 'FxiOS')) {
- return 'Firefox iOS'
- } else if (_includes(vendor, 'Apple')) {
- if (_includes(user_agent, 'Mobile')) {
- return 'Mobile Safari'
- }
- return 'Safari'
- } else if (_includes(user_agent, 'Android')) {
- return 'Android Mobile'
- } else if (_includes(user_agent, 'Konqueror')) {
- return 'Konqueror'
- } else if (_includes(user_agent, 'Firefox')) {
- return 'Firefox'
- } else if (_includes(user_agent, 'MSIE') || _includes(user_agent, 'Trident/')) {
- return 'Internet Explorer'
- } else if (_includes(user_agent, 'Gecko')) {
- return 'Mozilla'
- } else {
- return ''
- }
- },
-
- /**
- * This function detects which browser version is running this script,
- * parsing major and minor version (e.g., 42.1). User agent strings from:
- * http://www.useragentstring.com/pages/useragentstring.php
- */
- browserVersion: function (userAgent: string, vendor: string, opera: string): number | null {
- const browser = _info.browser(userAgent, vendor, opera)
- const versionRegexs = {
- 'Internet Explorer Mobile': /rv:(\d+(\.\d+)?)/,
- 'Microsoft Edge': /Edge?\/(\d+(\.\d+)?)/,
- Chrome: /Chrome\/(\d+(\.\d+)?)/,
- 'Chrome iOS': /CriOS\/(\d+(\.\d+)?)/,
- 'UC Browser': /(UCBrowser|UCWEB)\/(\d+(\.\d+)?)/,
- Safari: /Version\/(\d+(\.\d+)?)/,
- 'Mobile Safari': /Version\/(\d+(\.\d+)?)/,
- Opera: /(Opera|OPR)\/(\d+(\.\d+)?)/,
- Firefox: /Firefox\/(\d+(\.\d+)?)/,
- 'Firefox iOS': /FxiOS\/(\d+(\.\d+)?)/,
- Konqueror: /Konqueror:(\d+(\.\d+)?)/,
- BlackBerry: /BlackBerry (\d+(\.\d+)?)/,
- 'Android Mobile': /android\s(\d+(\.\d+)?)/,
- 'Samsung Internet': /SamsungBrowser\/(\d+(\.\d+)?)/,
- 'Internet Explorer': /(rv:|MSIE )(\d+(\.\d+)?)/,
- Mozilla: /rv:(\d+(\.\d+)?)/,
- }
- const regex: RegExp | undefined = versionRegexs[browser as keyof typeof versionRegexs]
- if (regex === undefined) {
- return null
- }
- const matches = userAgent.match(regex)
- if (!matches) {
- return null
- }
- return parseFloat(matches[matches.length - 2])
- },
-
- browserLanguage: function (): string {
- return (
- navigator.language || // Any modern browser
- (navigator as Record).userLanguage // IE11
- )
- },
-
- os: function (user_agent: string): { os_name: string; os_version: string } {
- if (/Windows/i.test(user_agent)) {
- if (/Phone/.test(user_agent) || /WPDesktop/.test(user_agent)) {
- return { os_name: 'Windows Phone', os_version: '' }
- }
- const match = /Windows NT ([0-9.]+)/i.exec(user_agent)
- if (match && match[1]) {
- const version = match[1]
- return { os_name: 'Windows', os_version: version }
- }
- return { os_name: 'Windows', os_version: '' }
- } else if (/(iPhone|iPad|iPod)/.test(user_agent)) {
- const match = /OS (\d+)_(\d+)_?(\d+)?/i.exec(user_agent)
- if (match && match[1]) {
- const versionParts = [match[1], match[2], match[3] || '0']
- return { os_name: 'iOS', os_version: versionParts.join('.') }
- }
- return { os_name: 'iOS', os_version: '' }
- } else if (/Android/.test(user_agent)) {
- const match = /Android (\d+)\.(\d+)\.?(\d+)?/i.exec(user_agent)
- if (match && match[1]) {
- const versionParts = [match[1], match[2], match[3] || '0']
- return { os_name: 'Android', os_version: versionParts.join('.') }
- }
- return { os_name: 'Android', os_version: '' }
- } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) {
- return { os_name: 'BlackBerry', os_version: '' }
- } else if (/Mac/i.test(user_agent)) {
- const match = /Mac OS X (\d+)[_.](\d+)[_.]?(\d+)?/i.exec(user_agent)
- if (match && match[1]) {
- const versionParts = [match[1], match[2], match[3] || '0']
- return { os_name: 'Mac OS X', os_version: versionParts.join('.') }
- }
- return { os_name: 'Mac OS X', os_version: '' }
- } else if (/Linux/.test(user_agent)) {
- return { os_name: 'Linux', os_version: '' }
- } else if (/CrOS/.test(user_agent)) {
- return { os_name: 'Chrome OS', os_version: '' }
- } else {
- return { os_name: '', os_version: '' }
- }
- },
-
- device: function (user_agent: string): string {
- if (/Windows Phone/i.test(user_agent) || /WPDesktop/.test(user_agent)) {
- return 'Windows Phone'
- } else if (/iPad/.test(user_agent)) {
- return 'iPad'
- } else if (/iPod/.test(user_agent)) {
- return 'iPod Touch'
- } else if (/iPhone/.test(user_agent)) {
- return 'iPhone'
- } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) {
- return 'BlackBerry'
- } else if (/Android/.test(user_agent) && !/Mobile/.test(user_agent)) {
- return 'Android Tablet'
- } else if (/Android/.test(user_agent)) {
- return 'Android'
- } else {
- return ''
- }
- },
-
- deviceType: function (user_agent: string): string {
- const device = this.device(user_agent)
- if (device === 'iPad' || device === 'Android Tablet') {
- return 'Tablet'
- } else if (device) {
- return 'Mobile'
- } else {
- return 'Desktop'
- }
- },
-
- referrer: function (): string {
- return document.referrer || '$direct'
- },
-
- referringDomain: function (): string {
- if (!document.referrer) {
- return '$direct'
- }
- const parser = document.createElement('a') // Unfortunately we cannot use new URL due to IE11
- parser.href = document.referrer
- return parser.host
- },
-
- properties: function (): Properties {
- const { os_name, os_version } = _info.os(userAgent)
- return _extend(
- _strip_empty_properties({
- $os: os_name,
- $os_version: os_version,
- $browser: _info.browser(userAgent, navigator.vendor, (win as any).opera),
- $device: _info.device(userAgent),
- $device_type: _info.deviceType(userAgent),
- }),
- {
- $current_url: win?.location.href,
- $host: win?.location.host,
- $pathname: win?.location.pathname,
- $browser_version: _info.browserVersion(userAgent, navigator.vendor, (win as any).opera),
- $browser_language: _info.browserLanguage(),
- $screen_height: win?.screen.height,
- $screen_width: win?.screen.width,
- $viewport_height: win?.innerHeight,
- $viewport_width: win?.innerWidth,
- $lib: 'web',
- $lib_version: Config.LIB_VERSION,
- $insert_id: Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 10),
- $time: _timestamp() / 1000, // epoch time in seconds
- }
- )
- },
-
- people_properties: function (): Properties {
- const { os_name, os_version } = _info.os(userAgent)
- return _extend(
- _strip_empty_properties({
- $os: os_name,
- $os_version: os_version,
- $browser: _info.browser(userAgent, navigator.vendor, (win as any).opera),
- }),
- {
- $browser_version: _info.browserVersion(userAgent, navigator.vendor, (win as any).opera),
- }
- )
- },
-}
-
-export { win as window, userAgent, logger, document }
diff --git a/src/utils/event-utils.ts b/src/utils/event-utils.ts
new file mode 100644
index 000000000..b3a629d57
--- /dev/null
+++ b/src/utils/event-utils.ts
@@ -0,0 +1,306 @@
+import { _getQueryParam } from './request-utils'
+import { _isNull, _isUndefined } from './type-utils'
+import { Properties } from '../types'
+import Config from '../config'
+import { _each, _extend, _includes, _strip_empty_properties, _timestamp } from './index'
+import { document, userAgent } from './globals'
+
+/**
+ * Safari detection turns out to be complicted. For e.g. https://stackoverflow.com/a/29696509
+ * We can be slightly loose because some options have been ruled out (e.g. firefox on iOS)
+ * before this check is made
+ */
+function isSafari(userAgent: string): boolean {
+ return _includes(userAgent, 'Safari') && !_includes(userAgent, 'Chrome') && !_includes(userAgent, 'Android')
+}
+
+export const _info = {
+ campaignParams: function (customParams?: string[]): Record {
+ const campaign_keywords = [
+ 'utm_source',
+ 'utm_medium',
+ 'utm_campaign',
+ 'utm_content',
+ 'utm_term',
+ 'gclid',
+ 'fbclid',
+ 'msclkid',
+ ].concat(customParams || [])
+
+ const params: Record = {}
+ _each(campaign_keywords, function (kwkey) {
+ const kw = _getQueryParam(document.URL, kwkey)
+ if (kw.length) {
+ params[kwkey] = kw
+ }
+ })
+
+ return params
+ },
+
+ searchEngine: function (): string | null {
+ const referrer = document.referrer
+ if (!referrer) {
+ return null
+ } else if (referrer.search('https?://(.*)google.([^/?]*)') === 0) {
+ return 'google'
+ } else if (referrer.search('https?://(.*)bing.com') === 0) {
+ return 'bing'
+ } else if (referrer.search('https?://(.*)yahoo.com') === 0) {
+ return 'yahoo'
+ } else if (referrer.search('https?://(.*)duckduckgo.com') === 0) {
+ return 'duckduckgo'
+ } else {
+ return null
+ }
+ },
+
+ searchInfo: function (): Record {
+ const search = _info.searchEngine(),
+ param = search != 'yahoo' ? 'q' : 'p',
+ ret: Record = {}
+
+ if (!_isNull(search)) {
+ ret['$search_engine'] = search
+
+ const keyword = _getQueryParam(document.referrer, param)
+ if (keyword.length) {
+ ret['ph_keyword'] = keyword
+ }
+ }
+
+ return ret
+ },
+
+ /**
+ * This function detects which browser is running this script.
+ * The order of the checks are important since many user agents
+ * include key words used in later checks.
+ */
+ browser: function (user_agent: string, vendor: string | undefined, opera?: any): string {
+ vendor = vendor || '' // vendor is undefined for at least IE9
+ if (opera || _includes(user_agent, ' OPR/')) {
+ if (_includes(user_agent, 'Mini')) {
+ return 'Opera Mini'
+ }
+ return 'Opera'
+ } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) {
+ return 'BlackBerry'
+ } else if (_includes(user_agent, 'IEMobile') || _includes(user_agent, 'WPDesktop')) {
+ return 'Internet Explorer Mobile'
+ } else if (_includes(user_agent, 'SamsungBrowser/')) {
+ // https://developer.samsung.com/internet/user-agent-string-format
+ return 'Samsung Internet'
+ } else if (_includes(user_agent, 'Edge') || _includes(user_agent, 'Edg/')) {
+ return 'Microsoft Edge'
+ } else if (_includes(user_agent, 'FBIOS')) {
+ return 'Facebook Mobile'
+ } else if (_includes(user_agent, 'Chrome')) {
+ return 'Chrome'
+ } else if (_includes(user_agent, 'CriOS')) {
+ return 'Chrome iOS'
+ } else if (_includes(user_agent, 'UCWEB') || _includes(user_agent, 'UCBrowser')) {
+ return 'UC Browser'
+ } else if (_includes(user_agent, 'FxiOS')) {
+ return 'Firefox iOS'
+ } else if (_includes(vendor, 'Apple') || isSafari(user_agent)) {
+ if (_includes(user_agent, 'Mobile')) {
+ return 'Mobile Safari'
+ }
+ return 'Safari'
+ } else if (_includes(user_agent, 'Android')) {
+ return 'Android Mobile'
+ } else if (_includes(user_agent, 'Konqueror') || _includes(user_agent, 'konqueror')) {
+ return 'Konqueror'
+ } else if (_includes(user_agent, 'Firefox')) {
+ return 'Firefox'
+ } else if (_includes(user_agent, 'MSIE') || _includes(user_agent, 'Trident/')) {
+ return 'Internet Explorer'
+ } else if (_includes(user_agent, 'Gecko')) {
+ return 'Mozilla'
+ } else {
+ return ''
+ }
+ },
+
+ /**
+ * This function detects which browser version is running this script,
+ * parsing major and minor version (e.g., 42.1). User agent strings from:
+ * http://www.useragentstring.com/pages/useragentstring.php
+ *
+ * `navigator.vendor` is passed in and used to help with detecting certain browsers
+ * NB `navigator.vendor` is deprecated and not present in every browser
+ */
+ browserVersion: function (userAgent: string, vendor: string | undefined, opera: string): number | null {
+ const browser = _info.browser(userAgent, vendor, opera)
+ const versionRegexes: Record = {
+ 'Internet Explorer Mobile': [/rv:(\d+(\.\d+)?)/],
+ 'Microsoft Edge': [/Edge?\/(\d+(\.\d+)?)/],
+ Chrome: [/Chrome\/(\d+(\.\d+)?)/],
+ 'Chrome iOS': [/CriOS\/(\d+(\.\d+)?)/],
+ 'UC Browser': [/(UCBrowser|UCWEB)\/(\d+(\.\d+)?)/],
+ Safari: [/Version\/(\d+(\.\d+)?)/],
+ 'Mobile Safari': [/Version\/(\d+(\.\d+)?)/],
+ Opera: [/(Opera|OPR)\/(\d+(\.\d+)?)/],
+ Firefox: [/Firefox\/(\d+(\.\d+)?)/],
+ 'Firefox iOS': [/FxiOS\/(\d+(\.\d+)?)/],
+ Konqueror: [/Konqueror[:/]?(\d+(\.\d+)?)/i],
+ // not every blackberry user agent has the version after the name
+ BlackBerry: [/BlackBerry (\d+(\.\d+)?)/, /Version\/(\d+(\.\d+)?)/],
+ 'Android Mobile': [/android\s(\d+(\.\d+)?)/],
+ 'Samsung Internet': [/SamsungBrowser\/(\d+(\.\d+)?)/],
+ 'Internet Explorer': [/(rv:|MSIE )(\d+(\.\d+)?)/],
+ Mozilla: [/rv:(\d+(\.\d+)?)/],
+ }
+ const regexes: RegExp[] | undefined = versionRegexes[browser as keyof typeof versionRegexes]
+ if (_isUndefined(regexes)) {
+ return null
+ }
+
+ for (let i = 0; i < regexes.length; i++) {
+ const regex = regexes[i]
+ const matches = userAgent.match(regex)
+ if (matches) {
+ return parseFloat(matches[matches.length - 2])
+ }
+ }
+ return null
+ },
+
+ browserLanguage: function (): string {
+ return (
+ navigator.language || // Any modern browser
+ (navigator as Record).userLanguage // IE11
+ )
+ },
+
+ os: function (user_agent: string): { os_name: string; os_version: string } {
+ if (/Windows/i.test(user_agent)) {
+ if (/Phone/.test(user_agent) || /WPDesktop/.test(user_agent)) {
+ return { os_name: 'Windows Phone', os_version: '' }
+ }
+ const match = /Windows NT ([0-9.]+)/i.exec(user_agent)
+ if (match && match[1]) {
+ const version = match[1]
+ return { os_name: 'Windows', os_version: version }
+ }
+ return { os_name: 'Windows', os_version: '' }
+ } else if (/(iPhone|iPad|iPod)/.test(user_agent)) {
+ const match = /OS (\d+)_(\d+)_?(\d+)?/i.exec(user_agent)
+ if (match && match[1]) {
+ const versionParts = [match[1], match[2], match[3] || '0']
+ return { os_name: 'iOS', os_version: versionParts.join('.') }
+ }
+ return { os_name: 'iOS', os_version: '' }
+ } else if (/Android/.test(user_agent)) {
+ const match = /Android (\d+)\.(\d+)\.?(\d+)?/i.exec(user_agent)
+ if (match && match[1]) {
+ const versionParts = [match[1], match[2], match[3] || '0']
+ return { os_name: 'Android', os_version: versionParts.join('.') }
+ }
+ return { os_name: 'Android', os_version: '' }
+ } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) {
+ return { os_name: 'BlackBerry', os_version: '' }
+ } else if (/Mac/i.test(user_agent)) {
+ const match = /Mac OS X (\d+)[_.](\d+)[_.]?(\d+)?/i.exec(user_agent)
+ if (match && match[1]) {
+ const versionParts = [match[1], match[2], match[3] || '0']
+ return { os_name: 'Mac OS X', os_version: versionParts.join('.') }
+ }
+ return { os_name: 'Mac OS X', os_version: '' }
+ } else if (/Linux/.test(user_agent)) {
+ return { os_name: 'Linux', os_version: '' }
+ } else if (/CrOS/.test(user_agent)) {
+ return { os_name: 'Chrome OS', os_version: '' }
+ } else {
+ return { os_name: '', os_version: '' }
+ }
+ },
+
+ device: function (user_agent: string): string {
+ if (/Windows Phone/i.test(user_agent) || /WPDesktop/.test(user_agent)) {
+ return 'Windows Phone'
+ } else if (/iPad/.test(user_agent)) {
+ return 'iPad'
+ } else if (/iPod/.test(user_agent)) {
+ return 'iPod Touch'
+ } else if (/iPhone/.test(user_agent)) {
+ return 'iPhone'
+ } else if (/(BlackBerry|PlayBook|BB10)/i.test(user_agent)) {
+ return 'BlackBerry'
+ } else if (/Android/.test(user_agent) && !/Mobile/.test(user_agent)) {
+ return 'Android Tablet'
+ } else if (/Android/.test(user_agent)) {
+ return 'Android'
+ } else {
+ return ''
+ }
+ },
+
+ deviceType: function (user_agent: string): string {
+ const device = this.device(user_agent)
+ if (device === 'iPad' || device === 'Android Tablet') {
+ return 'Tablet'
+ } else if (device) {
+ return 'Mobile'
+ } else {
+ return 'Desktop'
+ }
+ },
+
+ referrer: function (): string {
+ return document.referrer || '$direct'
+ },
+
+ referringDomain: function (): string {
+ if (!document.referrer) {
+ return '$direct'
+ }
+ const parser = document.createElement('a') // Unfortunately we cannot use new URL due to IE11
+ parser.href = document.referrer
+ return parser.host
+ },
+
+ properties: function (): Properties {
+ const { os_name, os_version } = _info.os(userAgent)
+ return _extend(
+ _strip_empty_properties({
+ $os: os_name,
+ $os_version: os_version,
+ $browser: _info.browser(userAgent, navigator.vendor, (window as any).opera),
+ $device: _info.device(userAgent),
+ $device_type: _info.deviceType(userAgent),
+ }),
+ {
+ $current_url: window?.location.href,
+ $host: window?.location.host,
+ $pathname: window?.location.pathname,
+ $raw_user_agent: userAgent.length > 1000 ? userAgent.substring(0, 997) + '...' : userAgent,
+ $browser_version: _info.browserVersion(userAgent, navigator.vendor, (window as any).opera),
+ $browser_language: _info.browserLanguage(),
+ $screen_height: window?.screen.height,
+ $screen_width: window?.screen.width,
+ $viewport_height: window?.innerHeight,
+ $viewport_width: window?.innerWidth,
+ $lib: 'web',
+ $lib_version: Config.LIB_VERSION,
+ $insert_id: Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 10),
+ $time: _timestamp() / 1000, // epoch time in seconds
+ }
+ )
+ },
+
+ people_properties: function (): Properties {
+ const { os_name, os_version } = _info.os(userAgent)
+ return _extend(
+ _strip_empty_properties({
+ $os: os_name,
+ $os_version: os_version,
+ $browser: _info.browser(userAgent, navigator.vendor, (window as any).opera),
+ }),
+ {
+ $browser_version: _info.browserVersion(userAgent, navigator.vendor, (window as any).opera),
+ }
+ )
+ },
+}
diff --git a/src/utils/globals.ts b/src/utils/globals.ts
new file mode 100644
index 000000000..42c3f6800
--- /dev/null
+++ b/src/utils/globals.ts
@@ -0,0 +1,12 @@
+/*
+ * Saved references to long variable names, so that bundling can minimize file size.
+ */
+export const ArrayProto = Array.prototype
+export const nativeForEach = ArrayProto.forEach
+export const nativeIndexOf = ArrayProto.indexOf
+export const win: Window & typeof globalThis = typeof window !== 'undefined' ? window : ({} as typeof window)
+const navigator = win.navigator || { userAgent: '' }
+const document = win.document || {}
+const userAgent = navigator.userAgent
+
+export { win as window, userAgent, document }
diff --git a/src/utils/index.ts b/src/utils/index.ts
new file mode 100644
index 000000000..99a5d237e
--- /dev/null
+++ b/src/utils/index.ts
@@ -0,0 +1,547 @@
+import { Breaker, EventHandler, Properties } from '../types'
+import {
+ _isArray,
+ _isDate,
+ _isFunction,
+ _isNull,
+ _isObject,
+ _isString,
+ _isUndefined,
+ hasOwnProperty,
+} from './type-utils'
+import { logger } from './logger'
+import { document, nativeForEach, nativeIndexOf } from './globals'
+
+const breaker: Breaker = {}
+
+// UNDERSCORE
+// Embed part of the Underscore Library
+export const _trim = function (str: string): string {
+ return str.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '')
+}
+
+export const _bind_instance_methods = function (obj: Record): void {
+ for (const func in obj) {
+ if (_isFunction(obj[func])) {
+ obj[func] = obj[func].bind(obj)
+ }
+ }
+}
+
+export function _eachArray(
+ obj: E[] | null | undefined,
+ iterator: (value: E, key: number) => void | Breaker,
+ thisArg?: any
+): void {
+ if (_isArray(obj)) {
+ if (nativeForEach && obj.forEach === nativeForEach) {
+ obj.forEach(iterator, thisArg)
+ } else if ('length' in obj && obj.length === +obj.length) {
+ for (let i = 0, l = obj.length; i < l; i++) {
+ if (i in obj && iterator.call(thisArg, obj[i], i) === breaker) {
+ return
+ }
+ }
+ }
+ }
+}
+
+/**
+ * @param {*=} obj
+ * @param {function(...*)=} iterator
+ * @param {Object=} thisArg
+ */
+export function _each(obj: any, iterator: (value: any, key: any) => void | Breaker, thisArg?: any): void {
+ if (_isNull(obj) || _isUndefined(obj)) {
+ return
+ }
+ if (_isArray(obj)) {
+ return _eachArray(obj, iterator, thisArg)
+ }
+ for (const key in obj) {
+ if (hasOwnProperty.call(obj, key)) {
+ if (iterator.call(thisArg, obj[key], key) === breaker) {
+ return
+ }
+ }
+ }
+}
+
+export const _extend = function (obj: Record, ...args: Record[]): Record {
+ _eachArray(args, function (source) {
+ for (const prop in source) {
+ if (source[prop] !== void 0) {
+ obj[prop] = source[prop]
+ }
+ }
+ })
+ return obj
+}
+
+export const _include = function (
+ obj: null | string | Array | Record,
+ target: any
+): boolean | Breaker {
+ let found = false
+ if (_isNull(obj)) {
+ return found
+ }
+ if (nativeIndexOf && obj.indexOf === nativeIndexOf) {
+ return obj.indexOf(target) != -1
+ }
+ _each(obj, function (value) {
+ if (found || (found = value === target)) {
+ return breaker
+ }
+ return
+ })
+ return found
+}
+
+export function _includes(str: T[] | string, needle: T): boolean {
+ return (str as any).indexOf(needle) !== -1
+}
+
+/**
+ * Object.entries() polyfill
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries
+ */
+export function _entries(obj: Record): [string, T][] {
+ const ownProps = Object.keys(obj)
+ let i = ownProps.length
+ const resArray = new Array(i) // preallocate the Array
+
+ while (i--) {
+ resArray[i] = [ownProps[i], obj[ownProps[i]]]
+ }
+ return resArray
+}
+
+export const _isValidRegex = function (str: string): boolean {
+ try {
+ new RegExp(str)
+ } catch (error) {
+ return false
+ }
+ return true
+}
+
+export const _encodeDates = function (obj: Properties): Properties {
+ _each(obj, function (v, k) {
+ if (_isDate(v)) {
+ obj[k] = _formatDate(v)
+ } else if (_isObject(v)) {
+ obj[k] = _encodeDates(v) // recurse
+ }
+ })
+ return obj
+}
+
+export const _timestamp = function (): number {
+ Date.now =
+ Date.now ||
+ function () {
+ return +new Date()
+ }
+ return Date.now()
+}
+
+export const _formatDate = function (d: Date): string {
+ // YYYY-MM-DDTHH:MM:SS in UTC
+ function pad(n: number) {
+ return n < 10 ? '0' + n : n
+ }
+ return (
+ d.getUTCFullYear() +
+ '-' +
+ pad(d.getUTCMonth() + 1) +
+ '-' +
+ pad(d.getUTCDate()) +
+ 'T' +
+ pad(d.getUTCHours()) +
+ ':' +
+ pad(d.getUTCMinutes()) +
+ ':' +
+ pad(d.getUTCSeconds())
+ )
+}
+
+export const _try = function (fn: () => T): T | undefined {
+ try {
+ return fn()
+ } catch (e) {
+ return undefined
+ }
+}
+
+export const _safewrap = function any = (...args: any[]) => any>(f: F): F {
+ return function (...args) {
+ try {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ return f.apply(this, args)
+ } catch (e) {
+ logger.critical('Implementation error. Please turn on debug and contact support@posthog.com.')
+ logger.critical(e)
+ }
+ } as F
+}
+
+// eslint-disable-next-line @typescript-eslint/ban-types
+export const _safewrap_class = function (klass: Function, functions: string[]): void {
+ for (let i = 0; i < functions.length; i++) {
+ klass.prototype[functions[i]] = _safewrap(klass.prototype[functions[i]])
+ }
+}
+
+export const _safewrap_instance_methods = function (obj: Record): void {
+ for (const func in obj) {
+ if (_isFunction(obj[func])) {
+ obj[func] = _safewrap(obj[func])
+ }
+ }
+}
+
+export const _strip_empty_properties = function (p: Properties): Properties {
+ const ret: Properties = {}
+ _each(p, function (v, k) {
+ if (_isString(v) && v.length > 0) {
+ ret[k] = v
+ }
+ })
+ return ret
+}
+
+/**
+ * Deep copies an object.
+ * It handles cycles by replacing all references to them with `undefined`
+ * Also supports customizing native values
+ *
+ * @param value
+ * @param customizer
+ * @returns {{}|undefined|*}
+ */
+function deepCircularCopy = Record>(
+ value: T,
+ customizer?: (value: T[K], key?: K) => T[K]
+): T | undefined {
+ const COPY_IN_PROGRESS_SET = new Set()
+
+ function internalDeepCircularCopy(value: T, key?: string): T | undefined {
+ if (value !== Object(value)) return customizer ? customizer(value as any, key) : value // primitive value
+
+ if (COPY_IN_PROGRESS_SET.has(value)) return undefined
+ COPY_IN_PROGRESS_SET.add(value)
+ let result: T
+
+ if (_isArray(value)) {
+ result = [] as any as T
+ _eachArray(value, (it) => {
+ result.push(internalDeepCircularCopy(it))
+ })
+ } else {
+ result = {} as T
+ _each(value, (val, key) => {
+ if (!COPY_IN_PROGRESS_SET.has(val)) {
+ ;(result as any)[key] = internalDeepCircularCopy(val, key)
+ }
+ })
+ }
+ return result
+ }
+ return internalDeepCircularCopy(value)
+}
+
+const LONG_STRINGS_ALLOW_LIST = ['$performance_raw']
+
+export function _copyAndTruncateStrings = Record>(
+ object: T,
+ maxStringLength: number | null
+): T {
+ return deepCircularCopy(object, (value: any, key) => {
+ if (key && LONG_STRINGS_ALLOW_LIST.indexOf(key as string) > -1) {
+ return value
+ }
+ if (_isString(value) && !_isNull(maxStringLength)) {
+ return (value as string).slice(0, maxStringLength)
+ }
+ return value
+ }) as T
+}
+
+export function _base64Encode(data: null): null
+export function _base64Encode(data: undefined): undefined
+export function _base64Encode(data: string): string
+export function _base64Encode(data: string | null | undefined): string | null | undefined {
+ const b64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
+ let o1,
+ o2,
+ o3,
+ h1,
+ h2,
+ h3,
+ h4,
+ bits,
+ i = 0,
+ ac = 0,
+ enc = ''
+ const tmp_arr: string[] = []
+
+ if (!data) {
+ return data
+ }
+
+ data = _utf8Encode(data)
+
+ do {
+ // pack three octets into four hexets
+ o1 = data.charCodeAt(i++)
+ o2 = data.charCodeAt(i++)
+ o3 = data.charCodeAt(i++)
+
+ bits = (o1 << 16) | (o2 << 8) | o3
+
+ h1 = (bits >> 18) & 0x3f
+ h2 = (bits >> 12) & 0x3f
+ h3 = (bits >> 6) & 0x3f
+ h4 = bits & 0x3f
+
+ // use hexets to index into b64, and append result to encoded string
+ tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3) + b64.charAt(h4)
+ } while (i < data.length)
+
+ enc = tmp_arr.join('')
+
+ switch (data.length % 3) {
+ case 1:
+ enc = enc.slice(0, -2) + '=='
+ break
+ case 2:
+ enc = enc.slice(0, -1) + '='
+ break
+ }
+
+ return enc
+}
+
+export const _utf8Encode = function (string: string): string {
+ string = (string + '').replace(/\r\n/g, '\n').replace(/\r/g, '\n')
+
+ let utftext = '',
+ start,
+ end
+ let stringl = 0,
+ n
+
+ start = end = 0
+ stringl = string.length
+
+ for (n = 0; n < stringl; n++) {
+ const c1 = string.charCodeAt(n)
+ let enc = null
+
+ if (c1 < 128) {
+ end++
+ } else if (c1 > 127 && c1 < 2048) {
+ enc = String.fromCharCode((c1 >> 6) | 192, (c1 & 63) | 128)
+ } else {
+ enc = String.fromCharCode((c1 >> 12) | 224, ((c1 >> 6) & 63) | 128, (c1 & 63) | 128)
+ }
+ if (!_isNull(enc)) {
+ if (end > start) {
+ utftext += string.substring(start, end)
+ }
+ utftext += enc
+ start = end = n + 1
+ }
+ }
+
+ if (end > start) {
+ utftext += string.substring(start, string.length)
+ }
+
+ return utftext
+}
+
+export const DEFAULT_BLOCKED_UA_STRS = [
+ 'ahrefsbot',
+ 'applebot',
+ 'baiduspider',
+ 'bingbot',
+ 'bingpreview',
+ 'bot.htm',
+ 'bot.php',
+ 'crawler',
+ 'duckduckbot',
+ 'facebookexternal',
+ 'facebookcatalog',
+ 'gptbot',
+ 'hubspot',
+ 'linkedinbot',
+ 'mj12bot',
+ 'petalbot',
+ 'pinterest',
+ 'prerender',
+ 'rogerbot',
+ 'screaming frog',
+ 'semrushbot',
+ 'sitebulb',
+ 'twitterbot',
+ 'yahoo! slurp',
+ 'yandexbot',
+
+ // a whole bunch of goog-specific crawlers
+ // https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers
+ 'adsbot-google',
+ 'apis-google',
+ 'duplexweb-google',
+ 'feedfetcher-google',
+ 'google favicon',
+ 'google web preview',
+ 'google-read-aloud',
+ 'googlebot',
+ 'googleweblight',
+ 'mediapartners-google',
+ 'storebot-google',
+]
+
+// _.isBlockedUA()
+// This is to block various web spiders from executing our JS and
+// sending false capturing data
+export const _isBlockedUA = function (ua: string, customBlockedUserAgents: string[]): boolean {
+ if (!ua) {
+ return false
+ }
+ const uaLower = ua.toLowerCase()
+ return DEFAULT_BLOCKED_UA_STRS.concat(customBlockedUserAgents || []).some((blockedUA) => {
+ const blockedUaLower = blockedUA.toLowerCase()
+ if (uaLower.includes) {
+ return uaLower.includes(blockedUaLower)
+ } else {
+ // IE 11 :/
+ return uaLower.indexOf(blockedUaLower) !== -1
+ }
+ })
+}
+
+export const _register_event = (function () {
+ // written by Dean Edwards, 2005
+ // with input from Tino Zijdel - crisp@xs4all.nl
+ // with input from Carl Sverre - mail@carlsverre.com
+ // with input from PostHog
+ // http://dean.edwards.name/weblog/2005/10/add-event/
+ // https://gist.github.com/1930440
+
+ /**
+ * @param {Object} element
+ * @param {string} type
+ * @param {function(...*)} handler
+ * @param {boolean=} oldSchool
+ * @param {boolean=} useCapture
+ */
+ const register_event = function (
+ element: Element | Window | Document | Node,
+ type: string,
+ handler: EventHandler,
+ oldSchool?: boolean,
+ useCapture?: boolean
+ ) {
+ if (!element) {
+ logger.error('No valid element provided to register_event')
+ return
+ }
+
+ if (element.addEventListener && !oldSchool) {
+ element.addEventListener(type, handler, !!useCapture)
+ } else {
+ const ontype = 'on' + type
+ const old_handler = (element as any)[ontype] // can be undefined
+ ;(element as any)[ontype] = makeHandler(element, handler, old_handler)
+ }
+ }
+
+ function makeHandler(
+ element: Element | Window | Document | Node,
+ new_handler: EventHandler,
+ old_handlers: EventHandler
+ ) {
+ return function (event: Event): boolean | void {
+ event = event || fixEvent(window.event)
+
+ // this basically happens in firefox whenever another script
+ // overwrites the onload callback and doesn't pass the event
+ // object to previously defined callbacks. All the browsers
+ // that don't define window.event implement addEventListener
+ // so the dom_loaded handler will still be fired as usual.
+ if (!event) {
+ return undefined
+ }
+
+ let ret = true
+ let old_result: any
+
+ if (_isFunction(old_handlers)) {
+ old_result = old_handlers(event)
+ }
+ const new_result = new_handler.call(element, event)
+
+ if (false === old_result || false === new_result) {
+ ret = false
+ }
+
+ return ret
+ }
+ }
+
+ function fixEvent(event: Event | undefined): Event | undefined {
+ if (event) {
+ event.preventDefault = fixEvent.preventDefault
+ event.stopPropagation = fixEvent.stopPropagation
+ }
+ return event
+ }
+ fixEvent.preventDefault = function () {
+ ;(this as any as Event).returnValue = false
+ }
+ fixEvent.stopPropagation = function () {
+ ;(this as any as Event).cancelBubble = true
+ }
+
+ return register_event
+})()
+
+export function loadScript(scriptUrlToLoad: string, callback: (error?: string | Event, event?: Event) => void): void {
+ const addScript = () => {
+ const scriptTag = document.createElement('script')
+ scriptTag.type = 'text/javascript'
+ scriptTag.src = scriptUrlToLoad
+ scriptTag.onload = (event) => callback(undefined, event)
+ scriptTag.onerror = (error) => callback(error)
+
+ const scripts = document.querySelectorAll('body > script')
+ if (scripts.length > 0) {
+ scripts[0].parentNode?.insertBefore(scriptTag, scripts[0])
+ } else {
+ // In exceptional situations this call might load before the DOM is fully ready.
+ document.body.appendChild(scriptTag)
+ }
+ }
+
+ if (document.body) {
+ addScript()
+ } else {
+ document.addEventListener('DOMContentLoaded', addScript)
+ }
+}
+
+export function isCrossDomainCookie(documentLocation: Location | undefined) {
+ const hostname = documentLocation?.hostname
+
+ if (!_isString(hostname)) {
+ return false
+ }
+ // split and slice isn't a great way to match arbitrary domains,
+ // but it's good enough for ensuring we only match herokuapp.com when it is the TLD
+ // for the hostname
+ return hostname.split('.').slice(-2).join('.') !== 'herokuapp.com'
+}
diff --git a/src/utils/logger.ts b/src/utils/logger.ts
new file mode 100644
index 000000000..48a52f89e
--- /dev/null
+++ b/src/utils/logger.ts
@@ -0,0 +1,40 @@
+import Config from '../config'
+import { _isUndefined } from './type-utils'
+import { window } from './globals'
+
+const LOGGER_PREFIX = '[PostHog.js]'
+export const logger = {
+ _log: (level: 'log' | 'warn' | 'error', ...args: any[]) => {
+ if ((Config.DEBUG || (window as any).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(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}`)
+ },
+}
diff --git a/src/utils/request-utils.ts b/src/utils/request-utils.ts
new file mode 100644
index 000000000..d5193e14b
--- /dev/null
+++ b/src/utils/request-utils.ts
@@ -0,0 +1,67 @@
+import { _each, _isValidRegex } from './'
+
+import { _isArray, _isUndefined } from './type-utils'
+import { logger } from './logger'
+
+const localDomains = ['localhost', '127.0.0.1']
+
+export const _isUrlMatchingRegex = function (url: string, pattern: string): boolean {
+ if (!_isValidRegex(pattern)) return false
+ return new RegExp(pattern).test(url)
+}
+
+export const _HTTPBuildQuery = function (formdata: Record, arg_separator = '&'): string {
+ let use_val: string
+ let use_key: string
+ const tph_arr: string[] = []
+
+ _each(formdata, function (val, key) {
+ // the key might be literally the string undefined for e.g. if {undefined: 'something'}
+ if (_isUndefined(val) || _isUndefined(key) || key === 'undefined') {
+ return
+ }
+
+ use_val = encodeURIComponent(val.toString())
+ use_key = encodeURIComponent(key)
+ tph_arr[tph_arr.length] = use_key + '=' + use_val
+ })
+
+ return tph_arr.join(arg_separator)
+}
+
+export const _getQueryParam = function (url: string, param: string): string {
+ const withoutHash: string = url.split('#')[0] || ''
+ const queryParams: string = withoutHash.split('?')[1] || ''
+
+ const queryParts = queryParams.split('&')
+ let keyValuePair
+
+ for (let i = 0; i < queryParts.length; i++) {
+ const parts = queryParts[i].split('=')
+ if (parts[0] === param) {
+ keyValuePair = parts
+ break
+ }
+ }
+
+ if (!_isArray(keyValuePair) || keyValuePair.length < 2) {
+ return ''
+ } else {
+ let result = keyValuePair[1]
+ try {
+ result = decodeURIComponent(result)
+ } catch (err) {
+ logger.error('Skipping decoding for malformed query param: ' + result)
+ }
+ return result.replace(/\+/g, ' ')
+ }
+}
+
+export const _getHashParam = function (hash: string, param: string): string | null {
+ const matches = hash.match(new RegExp(param + '=([^&]*)'))
+ return matches ? matches[1] : null
+}
+
+export const isLocalhost = (): boolean => {
+ return localDomains.includes(location.hostname)
+}
diff --git a/src/utils/type-utils.ts b/src/utils/type-utils.ts
new file mode 100644
index 000000000..76110a843
--- /dev/null
+++ b/src/utils/type-utils.ts
@@ -0,0 +1,64 @@
+// eslint-disable-next-line posthog-js/no-direct-array-check
+const nativeIsArray = Array.isArray
+const ObjProto = Object.prototype
+export const hasOwnProperty = ObjProto.hasOwnProperty
+const toString = ObjProto.toString
+
+export const _isArray =
+ nativeIsArray ||
+ function (obj: any): obj is any[] {
+ return toString.call(obj) === '[object Array]'
+ }
+export const _isUint8Array = function (x: unknown): x is Uint8Array {
+ return toString.call(x) === '[object Uint8Array]'
+}
+// from a comment on http://dbj.org/dbj/?p=286
+// fails on only one very rare and deliberate custom object:
+// let bomb = { toString : undefined, valueOf: function(o) { return "function BOMBA!"; }};
+export const _isFunction = function (f: any): f is (...args: any[]) => any {
+ try {
+ // eslint-disable-next-line posthog-js/no-direct-function-check
+ return /^\s*\bfunction\b/.test(f)
+ } catch (x) {
+ return false
+ }
+}
+// Underscore Addons
+export const _isObject = function (x: unknown): x is Record {
+ // eslint-disable-next-line posthog-js/no-direct-object-check
+ return x === Object(x) && !_isArray(x)
+}
+export const _isEmptyObject = function (x: unknown): x is Record {
+ if (_isObject(x)) {
+ for (const key in x) {
+ if (hasOwnProperty.call(x, key)) {
+ return false
+ }
+ }
+ return true
+ }
+ return false
+}
+export const _isUndefined = function (x: unknown): x is undefined {
+ return x === void 0
+}
+export const _isString = function (x: unknown): x is string {
+ // eslint-disable-next-line posthog-js/no-direct-string-check
+ return toString.call(x) == '[object String]'
+}
+export const _isNull = function (x: unknown): x is null {
+ // eslint-disable-next-line posthog-js/no-direct-null-check
+ return x === null
+}
+export const _isDate = function (x: unknown): x is Date {
+ // eslint-disable-next-line posthog-js/no-direct-date-check
+ return toString.call(x) == '[object Date]'
+}
+export const _isNumber = function (x: unknown): x is number {
+ // eslint-disable-next-line posthog-js/no-direct-number-check
+ return toString.call(x) == '[object Number]'
+}
+export const _isBoolean = function (x: unknown): x is boolean {
+ // eslint-disable-next-line posthog-js/no-direct-boolean-check
+ return toString.call(x) === '[object Boolean]'
+}
diff --git a/src/uuidv7.ts b/src/uuidv7.ts
index ac5fce06b..c1d6e3bb9 100644
--- a/src/uuidv7.ts
+++ b/src/uuidv7.ts
@@ -9,6 +9,10 @@
*/
// polyfill for IE11
+import { window } from './utils/globals'
+
+import { _isNumber, _isUndefined } from './utils/type-utils'
+
if (!Math.trunc) {
Math.trunc = function (v) {
return v < 0 ? Math.ceil(v) : Math.floor(v)
@@ -18,7 +22,7 @@ if (!Math.trunc) {
// polyfill for IE11
if (!Number.isInteger) {
Number.isInteger = function (value) {
- return typeof value === 'number' && isFinite(value) && Math.floor(value) === value
+ return _isNumber(value) && isFinite(value) && Math.floor(value) === value
}
}
@@ -140,13 +144,13 @@ class V7Generator {
*/
generate(): UUID {
const value = this.generateOrAbort()
- if (value !== undefined) {
+ if (!_isUndefined(value)) {
return value
} else {
// reset state and resume
this.timestamp = 0
const valueAfterReset = this.generateOrAbort()
- if (valueAfterReset === undefined) {
+ if (_isUndefined(valueAfterReset)) {
throw new Error('Could not generate UUID after timestamp reset')
}
return valueAfterReset
@@ -203,6 +207,7 @@ declare const UUIDV7_DENY_WEAK_RNG: boolean
/** Stores `crypto.getRandomValues()` available in the environment. */
let getRandomValues: (buffer: T) => T = (buffer) => {
// fall back on Math.random() unless the flag is set to true
+ // TRICKY: don't use the _isUndefined method here as can't pass the reference
if (typeof UUIDV7_DENY_WEAK_RNG !== 'undefined' && UUIDV7_DENY_WEAK_RNG) {
throw new Error('no cryptographically strong RNG available')
}
@@ -214,7 +219,7 @@ let getRandomValues: (buffer: T) => T = (buf
}
// detect Web Crypto API
-if (typeof crypto !== 'undefined' && crypto.getRandomValues) {
+if (!_isUndefined(window.crypto) && crypto.getRandomValues) {
getRandomValues = (buffer) => crypto.getRandomValues(buffer)
}
diff --git a/yarn.lock b/yarn.lock
index c9121291d..8d766a887 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -24,13 +24,6 @@
dependencies:
"@babel/highlight" "^7.10.4"
-"@babel/code-frame@^7.12.11":
- version "7.12.11"
- resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.11.tgz#f4ad435aa263db935b8f10f2c552d23fb716a63f"
- integrity sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==
- dependencies:
- "@babel/highlight" "^7.10.4"
-
"@babel/code-frame@^7.12.13":
version "7.12.13"
resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.12.13.tgz#dcfc826beef65e75c50e21d3837d7d95798dd658"
@@ -59,6 +52,14 @@
dependencies:
"@babel/highlight" "^7.18.6"
+"@babel/code-frame@^7.22.13":
+ version "7.22.13"
+ resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.22.13.tgz#e3c1c099402598483b7a8c46a721d1038803755e"
+ integrity sha512-XktuhWlJ5g+3TJXc5upd9Ks1HutSArik6jf2eAjYFyIOf4ej3RN+184cZbzDvbPnuTJIUhPKKJE3cIsYTiAT3w==
+ dependencies:
+ "@babel/highlight" "^7.22.13"
+ chalk "^2.4.2"
+
"@babel/compat-data@^7.13.0", "@babel/compat-data@^7.13.8":
version "7.13.8"
resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.13.8.tgz#5b783b9808f15cef71547f1b691f34f8ff6003a6"
@@ -169,15 +170,6 @@
jsesc "^2.5.1"
source-map "^0.5.0"
-"@babel/generator@^7.12.11":
- version "7.12.11"
- resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.12.11.tgz#98a7df7b8c358c9a37ab07a24056853016aba3af"
- integrity sha512-Ggg6WPOJtSi8yYQvLVjG8F/TlpWDlKx0OpS4Kt+xMQPs5OaGYWy+v1A+1TvxI6sAMGZpKWWoAQ1DaeQbImlItA==
- dependencies:
- "@babel/types" "^7.12.11"
- jsesc "^2.5.1"
- source-map "^0.5.0"
-
"@babel/generator@^7.13.0":
version "7.13.9"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.13.9.tgz#3a7aa96f9efb8e2be42d38d80e2ceb4c64d8de39"
@@ -187,15 +179,6 @@
jsesc "^2.5.1"
source-map "^0.5.0"
-"@babel/generator@^7.16.0":
- version "7.16.0"
- resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.16.0.tgz#d40f3d1d5075e62d3500bccb67f3daa8a95265b2"
- integrity sha512-RR8hUCfRQn9j9RPKEVXo9LiwoxLPYn6hNZlvUOR8tSnaxlD0p0+la00ZP9/SnRt6HchKr+X0fO2r8vrETiJGew==
- dependencies:
- "@babel/types" "^7.16.0"
- jsesc "^2.5.1"
- source-map "^0.5.0"
-
"@babel/generator@^7.17.0", "@babel/generator@^7.7.2":
version "7.17.0"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.17.0.tgz#7bd890ba706cd86d3e2f727322346ffdbf98f65e"
@@ -205,15 +188,6 @@
jsesc "^2.5.1"
source-map "^0.5.0"
-"@babel/generator@^7.18.7":
- version "7.18.7"
- resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.7.tgz#2aa78da3c05aadfc82dbac16c99552fc802284bd"
- integrity sha512-shck+7VLlY72a2w9c3zYWuE1pwOKEiQHV7GTUbSnhyl5eu3i04t30tBY82ZRWrDfo3gkakCFtevExnxbkf2a3A==
- dependencies:
- "@babel/types" "^7.18.7"
- "@jridgewell/gen-mapping" "^0.3.2"
- jsesc "^2.5.1"
-
"@babel/generator@^7.18.9":
version "7.18.9"
resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.18.9.tgz#68337e9ea8044d6ddc690fb29acae39359cca0a5"
@@ -223,6 +197,16 @@
"@jridgewell/gen-mapping" "^0.3.2"
jsesc "^2.5.1"
+"@babel/generator@^7.23.0":
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.23.0.tgz#df5c386e2218be505b34837acbcb874d7a983420"
+ integrity sha512-lN85QRR+5IbYrMWM6Y4pE/noaQtg4pNiqeNGX60eqOfo6gtEj6uw/JagelB8vVztSd7R6M5n1+PQkDbHbBRU4g==
+ dependencies:
+ "@babel/types" "^7.23.0"
+ "@jridgewell/gen-mapping" "^0.3.2"
+ "@jridgewell/trace-mapping" "^0.3.17"
+ jsesc "^2.5.1"
+
"@babel/helper-annotate-as-pure@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.10.4.tgz#5bf0d495a3f757ac3bda48b5bf3b3ba309c72ba3"
@@ -422,6 +406,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz#0c0cee9b35d2ca190478756865bb3528422f51be"
integrity sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==
+"@babel/helper-environment-visitor@^7.22.20":
+ version "7.22.20"
+ resolved "https://registry.yarnpkg.com/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz#96159db61d34a29dba454c959f5ae4a649ba9167"
+ integrity sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==
+
"@babel/helper-explode-assignable-expression@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.10.4.tgz#40a1cd917bff1288f699a94a75b37a1a2dbd8c7c"
@@ -453,15 +442,6 @@
"@babel/template" "^7.10.4"
"@babel/types" "^7.10.4"
-"@babel/helper-function-name@^7.12.11":
- version "7.12.11"
- resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.11.tgz#1fd7738aee5dcf53c3ecff24f1da9c511ec47b42"
- integrity sha512-AtQKjtYNolKNi6nNNVLQ27CP6D9oFR6bq/HPYSizlzbp7uC1M59XJe8L+0uXjbIaZaUJF99ruHqVGiKXU/7ybA==
- dependencies:
- "@babel/helper-get-function-arity" "^7.12.10"
- "@babel/template" "^7.12.7"
- "@babel/types" "^7.12.11"
-
"@babel/helper-function-name@^7.12.13":
version "7.12.13"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.12.13.tgz#93ad656db3c3c2232559fd7b2c3dbdcbe0eb377a"
@@ -480,15 +460,6 @@
"@babel/template" "^7.16.0"
"@babel/types" "^7.16.0"
-"@babel/helper-function-name@^7.16.7":
- version "7.16.7"
- resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.16.7.tgz#f1ec51551fb1c8956bc8dd95f38523b6cf375f8f"
- integrity sha512-QfDfEnIUyyBSR3HtrtGECuZ6DAyCkYFp7GHl75vFtTnn6pjKeK0T1DB5lLkFvBea8MdaiUABx3osbgLyInoejA==
- dependencies:
- "@babel/helper-get-function-arity" "^7.16.7"
- "@babel/template" "^7.16.7"
- "@babel/types" "^7.16.7"
-
"@babel/helper-function-name@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.18.6.tgz#8334fecb0afba66e6d87a7e8c6bb7fed79926b83"
@@ -505,6 +476,14 @@
"@babel/template" "^7.18.6"
"@babel/types" "^7.18.9"
+"@babel/helper-function-name@^7.23.0":
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz#1f9a3cdbd5b2698a670c30d2735f9af95ed52759"
+ integrity sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==
+ dependencies:
+ "@babel/template" "^7.22.15"
+ "@babel/types" "^7.23.0"
+
"@babel/helper-get-function-arity@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.10.4.tgz#98c1cbea0e2332f33f9a4661b8ce1505b2c19ba2"
@@ -512,13 +491,6 @@
dependencies:
"@babel/types" "^7.10.4"
-"@babel/helper-get-function-arity@^7.12.10":
- version "7.12.10"
- resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.10.tgz#b158817a3165b5faa2047825dfa61970ddcc16cf"
- integrity sha512-mm0n5BPjR06wh9mPQaDdXWDoll/j5UpCAPl1x8fS71GHm7HA6Ua2V4ylG1Ju8lvcTOietbPNNPaSilKj+pj+Ag==
- dependencies:
- "@babel/types" "^7.12.10"
-
"@babel/helper-get-function-arity@^7.12.13":
version "7.12.13"
resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.12.13.tgz#bc63451d403a3b3082b97e1d8b3fe5bd4091e583"
@@ -533,13 +505,6 @@
dependencies:
"@babel/types" "^7.16.0"
-"@babel/helper-get-function-arity@^7.16.7":
- version "7.16.7"
- resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.16.7.tgz#ea08ac753117a669f1508ba06ebcc49156387419"
- integrity sha512-flc+RLSOBXzNzVhcLu6ujeHUrD6tANAOU5ojrRx/as+tbzf8+stUCj7+IfRRoAbEZqj/ahXEMsjhOhgeZsrnTw==
- dependencies:
- "@babel/types" "^7.16.7"
-
"@babel/helper-hoist-variables@^7.13.0":
version "7.13.0"
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.13.0.tgz#5d5882e855b5c5eda91e0cadc26c6e7a2c8593d8"
@@ -548,20 +513,6 @@
"@babel/traverse" "^7.13.0"
"@babel/types" "^7.13.0"
-"@babel/helper-hoist-variables@^7.16.0":
- version "7.16.0"
- resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.0.tgz#4c9023c2f1def7e28ff46fc1dbcd36a39beaa81a"
- integrity sha512-1AZlpazjUR0EQZQv3sgRNfM9mEVWPK3M6vlalczA+EECcPz3XPh6VplbErL5UoMpChhSck5wAJHthlj1bYpcmg==
- dependencies:
- "@babel/types" "^7.16.0"
-
-"@babel/helper-hoist-variables@^7.16.7":
- version "7.16.7"
- resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.16.7.tgz#86bcb19a77a509c7b77d0e22323ef588fa58c246"
- integrity sha512-m04d/0Op34H5v7pbZw6pSKP7weA6lsMvfiIAMeIvkY/R4xQtBSMFEigu9QTZ2qB/9l22vsxtM8a+Q8CzD255fg==
- dependencies:
- "@babel/types" "^7.16.7"
-
"@babel/helper-hoist-variables@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz#d4d2c8fb4baeaa5c68b99cc8245c56554f926678"
@@ -569,6 +520,13 @@
dependencies:
"@babel/types" "^7.18.6"
+"@babel/helper-hoist-variables@^7.22.5":
+ version "7.22.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz#c01a007dac05c085914e8fb652b339db50d823bb"
+ integrity sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==
+ dependencies:
+ "@babel/types" "^7.22.5"
+
"@babel/helper-member-expression-to-functions@^7.10.4":
version "7.11.0"
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.11.0.tgz#ae69c83d84ee82f4b42f96e2a09410935a8f26df"
@@ -914,13 +872,6 @@
dependencies:
"@babel/types" "^7.11.0"
-"@babel/helper-split-export-declaration@^7.12.11":
- version "7.12.11"
- resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.11.tgz#1b4cc424458643c47d37022223da33d76ea4603a"
- integrity sha512-LsIVN8j48gHgwzfocYUSkO/hjYAOJqlpJEc7tGXcIm4cubjVUf8LGW6eWRyxEu7gA25q02p0rQUWoCI33HNS5g==
- dependencies:
- "@babel/types" "^7.12.11"
-
"@babel/helper-split-export-declaration@^7.12.13":
version "7.12.13"
resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.12.13.tgz#e9430be00baf3e88b0e13e6f9d4eaf2136372b05"
@@ -949,6 +900,18 @@
dependencies:
"@babel/types" "^7.18.6"
+"@babel/helper-split-export-declaration@^7.22.6":
+ version "7.22.6"
+ resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz#322c61b7310c0997fe4c323955667f18fcefb91c"
+ integrity sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==
+ dependencies:
+ "@babel/types" "^7.22.5"
+
+"@babel/helper-string-parser@^7.22.5":
+ version "7.22.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz#533f36457a25814cf1df6488523ad547d784a99f"
+ integrity sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==
+
"@babel/helper-validator-identifier@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.10.4.tgz#a78c7a7251e01f616512d31b10adcf52ada5e0d2"
@@ -974,6 +937,11 @@
resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.18.6.tgz#9c97e30d31b2b8c72a1d08984f2ca9b574d7a076"
integrity sha512-MmetCkz9ej86nJQV+sFCxoGGrUbU3q02kgLciwkrt9QqEB7cP39oKEY0PakknEO0Gu20SskMRi+AYZ3b1TpN9g==
+"@babel/helper-validator-identifier@^7.22.20":
+ version "7.22.20"
+ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz#c4ae002c61d2879e724581d96665583dbc1dc0e0"
+ integrity sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==
+
"@babel/helper-validator-option@^7.12.17":
version "7.12.17"
resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.12.17.tgz#d1fbf012e1a79b7eebbfdc6d270baaf8d9eb9831"
@@ -1100,17 +1068,21 @@
chalk "^2.0.0"
js-tokens "^4.0.0"
-"@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.11.0", "@babel/parser@^7.11.1", "@babel/parser@^7.7.0":
+"@babel/highlight@^7.22.13":
+ version "7.22.20"
+ resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.22.20.tgz#4ca92b71d80554b01427815e06f2df965b9c1f54"
+ integrity sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==
+ dependencies:
+ "@babel/helper-validator-identifier" "^7.22.20"
+ chalk "^2.4.2"
+ js-tokens "^4.0.0"
+
+"@babel/parser@^7.1.0", "@babel/parser@^7.10.4", "@babel/parser@^7.11.1", "@babel/parser@^7.7.0":
version "7.11.3"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.11.3.tgz#9e1eae46738bcd08e23e867bab43e7b95299a8f9"
integrity sha512-REo8xv7+sDxkKvoxEywIdsNFiZLybwdI7hcT5uEPyQrSMB4YQ973BfC9OOrD/81MaIjh6UxdulIQXkjmiH3PcA==
-"@babel/parser@^7.12.11", "@babel/parser@^7.12.7":
- version "7.12.11"
- resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.12.11.tgz#9ce3595bcd74bc5c466905e86c535b8b25011e79"
- integrity sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==
-
-"@babel/parser@^7.12.13", "@babel/parser@^7.13.0", "@babel/parser@^7.13.4":
+"@babel/parser@^7.12.13", "@babel/parser@^7.13.4":
version "7.13.9"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.13.9.tgz#ca34cb95e1c2dd126863a84465ae8ef66114be99"
integrity sha512-nEUfRiARCcaVo3ny3ZQjURjHQZUo/JkEw7rLlSZy/psWGnvwXFtPcr6jb7Yb41DVW5LTe6KRq9LGleRNsg1Frw==
@@ -1120,12 +1092,12 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.17.0.tgz#f0ac33eddbe214e4105363bb17c3341c5ffcc43c"
integrity sha512-VKXSCQx5D8S04ej+Dqsr1CzYvvWgf20jIw2D+YhQCrIlr2UZGaDds23Y0xg75/skOxpLCRpUZvk/1EAVkGoDOw==
-"@babel/parser@^7.16.0", "@babel/parser@^7.16.3":
+"@babel/parser@^7.16.0":
version "7.16.4"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.16.4.tgz#d5f92f57cf2c74ffe9b37981c0e72fee7311372e"
integrity sha512-6V0qdPUaiVHH3RtZeLIsc+6pDhbYzHR8ogA8w+f+Wc77DuXto19g2QUwveINoS34Uw+W8/hQDGJCx+i4n7xcng==
-"@babel/parser@^7.18.6", "@babel/parser@^7.18.8":
+"@babel/parser@^7.18.6":
version "7.18.8"
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.8.tgz#822146080ac9c62dac0823bb3489622e0bc1cbdf"
integrity sha512-RSKRfYX20dyH+elbJK2uqAkVyucL+xXzhqlMD5/ZXx+dAAwpyB7HsvnHe/ZUGOF+xLr5Wx9/JoXVTj6BQE2/oA==
@@ -1135,6 +1107,11 @@
resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.18.9.tgz#f2dde0c682ccc264a9a8595efd030a5cc8fd2539"
integrity sha512-9uJveS9eY9DJ0t64YbIBZICtJy8a5QrDEVdiLCG97fVLpDTpGX7t8mMSb6OWw6Lrnjqj4O8zwjELX3dhoMgiBg==
+"@babel/parser@^7.22.15", "@babel/parser@^7.23.0":
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.23.0.tgz#da950e622420bf96ca0d0f2909cdddac3acd8719"
+ integrity sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==
+
"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.18.6.tgz#da5b8f9a580acdfbe53494dba45ea389fb09a4d2"
@@ -2453,15 +2430,6 @@
"@babel/parser" "^7.12.13"
"@babel/types" "^7.12.13"
-"@babel/template@^7.12.7":
- version "7.12.7"
- resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.12.7.tgz#c817233696018e39fbb6c491d2fb684e05ed43bc"
- integrity sha512-GkDzmHS6GV7ZeXfJZ0tLRBhZcMcY0/Lnb+eEbXDBfCAcZCjrZKe6p3J4we/D24O9Y8enxWAg1cWwof59yLh2ow==
- dependencies:
- "@babel/code-frame" "^7.10.4"
- "@babel/parser" "^7.12.7"
- "@babel/types" "^7.12.7"
-
"@babel/template@^7.16.0":
version "7.16.0"
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.16.0.tgz#d16a35ebf4cd74e202083356fab21dd89363ddd6"
@@ -2489,111 +2457,28 @@
"@babel/parser" "^7.18.6"
"@babel/types" "^7.18.6"
-"@babel/traverse@^7.10.4", "@babel/traverse@^7.11.0", "@babel/traverse@^7.7.0":
- version "7.11.0"
- resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.11.0.tgz#9b996ce1b98f53f7c3e4175115605d56ed07dd24"
- integrity sha512-ZB2V+LskoWKNpMq6E5UUCrjtDUh5IOTAyIl0dTjIEoXum/iKWkoIEKIRDnUucO6f+2FzNkE0oD4RLKoPIufDtg==
- dependencies:
- "@babel/code-frame" "^7.10.4"
- "@babel/generator" "^7.11.0"
- "@babel/helper-function-name" "^7.10.4"
- "@babel/helper-split-export-declaration" "^7.11.0"
- "@babel/parser" "^7.11.0"
- "@babel/types" "^7.11.0"
- debug "^4.1.0"
- globals "^11.1.0"
- lodash "^4.17.19"
-
-"@babel/traverse@^7.12.10":
- version "7.12.12"
- resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.12.12.tgz#d0cd87892704edd8da002d674bc811ce64743376"
- integrity sha512-s88i0X0lPy45RrLM8b9mz8RPH5FqO9G9p7ti59cToE44xFm1Q+Pjh5Gq4SXBbtb88X7Uy7pexeqRIQDDMNkL0w==
- dependencies:
- "@babel/code-frame" "^7.12.11"
- "@babel/generator" "^7.12.11"
- "@babel/helper-function-name" "^7.12.11"
- "@babel/helper-split-export-declaration" "^7.12.11"
- "@babel/parser" "^7.12.11"
- "@babel/types" "^7.12.12"
- debug "^4.1.0"
- globals "^11.1.0"
- lodash "^4.17.19"
-
-"@babel/traverse@^7.13.0":
- version "7.13.0"
- resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.13.0.tgz#6d95752475f86ee7ded06536de309a65fc8966cc"
- integrity sha512-xys5xi5JEhzC3RzEmSGrs/b3pJW/o87SypZ+G/PhaE7uqVQNv/jlmVIBXuoh5atqQ434LfXV+sf23Oxj0bchJQ==
- dependencies:
- "@babel/code-frame" "^7.12.13"
- "@babel/generator" "^7.13.0"
- "@babel/helper-function-name" "^7.12.13"
- "@babel/helper-split-export-declaration" "^7.12.13"
- "@babel/parser" "^7.13.0"
- "@babel/types" "^7.13.0"
- debug "^4.1.0"
- globals "^11.1.0"
- lodash "^4.17.19"
-
-"@babel/traverse@^7.16.0":
- version "7.16.3"
- resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.16.3.tgz#f63e8a938cc1b780f66d9ed3c54f532ca2d14787"
- integrity sha512-eolumr1vVMjqevCpwVO99yN/LoGL0EyHiLO5I043aYQvwOJ9eR5UsZSClHVCzfhBduMAsSzgA/6AyqPjNayJag==
- dependencies:
- "@babel/code-frame" "^7.16.0"
- "@babel/generator" "^7.16.0"
- "@babel/helper-function-name" "^7.16.0"
- "@babel/helper-hoist-variables" "^7.16.0"
- "@babel/helper-split-export-declaration" "^7.16.0"
- "@babel/parser" "^7.16.3"
- "@babel/types" "^7.16.0"
- debug "^4.1.0"
- globals "^11.1.0"
-
-"@babel/traverse@^7.16.7", "@babel/traverse@^7.17.0", "@babel/traverse@^7.7.2":
- version "7.17.0"
- resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.17.0.tgz#3143e5066796408ccc880a33ecd3184f3e75cd30"
- integrity sha512-fpFIXvqD6kC7c7PUNnZ0Z8cQXlarCLtCUpt2S1Dx7PjoRtCFffvOkHHSom+m5HIxMZn5bIBVb71lhabcmjEsqg==
- dependencies:
- "@babel/code-frame" "^7.16.7"
- "@babel/generator" "^7.17.0"
- "@babel/helper-environment-visitor" "^7.16.7"
- "@babel/helper-function-name" "^7.16.7"
- "@babel/helper-hoist-variables" "^7.16.7"
- "@babel/helper-split-export-declaration" "^7.16.7"
- "@babel/parser" "^7.17.0"
- "@babel/types" "^7.17.0"
- debug "^4.1.0"
- globals "^11.1.0"
-
-"@babel/traverse@^7.18.6":
- version "7.18.8"
- resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.8.tgz#f095e62ab46abf1da35e5a2011f43aee72d8d5b0"
- integrity sha512-UNg/AcSySJYR/+mIcJQDCv00T+AqRO7j/ZEJLzpaYtgM48rMg5MnkJgyNqkzo88+p4tfRvZJCEiwwfG6h4jkRg==
- dependencies:
- "@babel/code-frame" "^7.18.6"
- "@babel/generator" "^7.18.7"
- "@babel/helper-environment-visitor" "^7.18.6"
- "@babel/helper-function-name" "^7.18.6"
- "@babel/helper-hoist-variables" "^7.18.6"
- "@babel/helper-split-export-declaration" "^7.18.6"
- "@babel/parser" "^7.18.8"
- "@babel/types" "^7.18.8"
- debug "^4.1.0"
- globals "^11.1.0"
-
-"@babel/traverse@^7.18.9":
- version "7.18.9"
- resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.18.9.tgz#deeff3e8f1bad9786874cb2feda7a2d77a904f98"
- integrity sha512-LcPAnujXGwBgv3/WHv01pHtb2tihcyW1XuL9wd7jqh1Z8AQkTd+QVjMrMijrln0T7ED3UXLIy36P9Ao7W75rYg==
- dependencies:
- "@babel/code-frame" "^7.18.6"
- "@babel/generator" "^7.18.9"
- "@babel/helper-environment-visitor" "^7.18.9"
- "@babel/helper-function-name" "^7.18.9"
- "@babel/helper-hoist-variables" "^7.18.6"
- "@babel/helper-split-export-declaration" "^7.18.6"
- "@babel/parser" "^7.18.9"
- "@babel/types" "^7.18.9"
+"@babel/template@^7.22.15":
+ version "7.22.15"
+ resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.22.15.tgz#09576efc3830f0430f4548ef971dde1350ef2f38"
+ integrity sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==
+ dependencies:
+ "@babel/code-frame" "^7.22.13"
+ "@babel/parser" "^7.22.15"
+ "@babel/types" "^7.22.15"
+
+"@babel/traverse@^7.10.4", "@babel/traverse@^7.11.0", "@babel/traverse@^7.12.10", "@babel/traverse@^7.13.0", "@babel/traverse@^7.16.0", "@babel/traverse@^7.16.7", "@babel/traverse@^7.17.0", "@babel/traverse@^7.18.6", "@babel/traverse@^7.18.9", "@babel/traverse@^7.7.0", "@babel/traverse@^7.7.2":
+ version "7.23.2"
+ resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.23.2.tgz#329c7a06735e144a506bdb2cad0268b7f46f4ad8"
+ integrity sha512-azpe59SQ48qG6nu2CzcMLbxUudtN+dOM9kDbUqGq3HXUJRlo7i8fvPoxQUzYgLZ4cMVmuZgm8vvBpNeRhd6XSw==
+ dependencies:
+ "@babel/code-frame" "^7.22.13"
+ "@babel/generator" "^7.23.0"
+ "@babel/helper-environment-visitor" "^7.22.20"
+ "@babel/helper-function-name" "^7.23.0"
+ "@babel/helper-hoist-variables" "^7.22.5"
+ "@babel/helper-split-export-declaration" "^7.22.6"
+ "@babel/parser" "^7.23.0"
+ "@babel/types" "^7.23.0"
debug "^4.1.0"
globals "^11.1.0"
@@ -2606,7 +2491,7 @@
lodash "^4.17.19"
to-fast-properties "^2.0.0"
-"@babel/types@^7.12.1", "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.12", "@babel/types@^7.12.5", "@babel/types@^7.12.7":
+"@babel/types@^7.12.1", "@babel/types@^7.12.10", "@babel/types@^7.12.11", "@babel/types@^7.12.5", "@babel/types@^7.12.7":
version "7.12.12"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.12.12.tgz#4608a6ec313abbd87afa55004d373ad04a96c299"
integrity sha512-lnIX7piTxOH22xE7fDXDbSHg9MM1/6ORnafpJmov5rs0kX5g4BZxeXNJLXsMRiO0U5Rb8/FvMS6xlTnTHvxonQ==
@@ -2640,7 +2525,7 @@
"@babel/helper-validator-identifier" "^7.16.7"
to-fast-properties "^2.0.0"
-"@babel/types@^7.18.6", "@babel/types@^7.18.7", "@babel/types@^7.18.8":
+"@babel/types@^7.18.6":
version "7.18.8"
resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.18.8.tgz#c5af199951bf41ba4a6a9a6d0d8ad722b30cd42f"
integrity sha512-qwpdsmraq0aJ3osLJRApsc2ouSJCdnMeZwB0DhbtHAtRpZNZCdlbRnHIgcRKzdE1g0iOGg644fzjOBcdOz9cPw==
@@ -2656,6 +2541,15 @@
"@babel/helper-validator-identifier" "^7.18.6"
to-fast-properties "^2.0.0"
+"@babel/types@^7.22.15", "@babel/types@^7.22.5", "@babel/types@^7.23.0":
+ version "7.23.0"
+ resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.23.0.tgz#8c1f020c9df0e737e4e247c0619f58c68458aaeb"
+ integrity sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==
+ dependencies:
+ "@babel/helper-string-parser" "^7.22.5"
+ "@babel/helper-validator-identifier" "^7.22.20"
+ to-fast-properties "^2.0.0"
+
"@bcoe/v8-coverage@^0.2.3":
version "0.2.3"
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
@@ -3302,6 +3196,11 @@
resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.0.4.tgz#b876e3feefb9c8d3aa84014da28b5e52a0640d72"
integrity sha512-cz8HFjOFfUBtvN+NXYSFMHYRdxZMaEl0XypVrhzxBgadKIXhIkRd8aMeHhmF56Sl7SuS8OnUpQ73/k9LE4VnLg==
+"@jridgewell/resolve-uri@^3.1.0":
+ version "3.1.1"
+ resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz#c08679063f279615a3326583ba3a90d1d82cc721"
+ integrity sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==
+
"@jridgewell/set-array@^1.0.0", "@jridgewell/set-array@^1.0.1":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.1.2.tgz#7c6cf998d6d20b914c0a55a91ae928ff25965e72"
@@ -3320,6 +3219,11 @@
resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.10.tgz#baf57b4e2a690d4f38560171f91783656b7f8186"
integrity sha512-Ht8wIW5v165atIX1p+JvKR5ONzUyF4Ac8DZIQ5kZs9zrb6M8SJNXpx1zn04rn65VjBMygRoMXcyYwNK0fT7bEg==
+"@jridgewell/sourcemap-codec@^1.4.14":
+ version "1.4.15"
+ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz#d7c6e6755c78567a951e04ab52ef0fd26de59f32"
+ integrity sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==
+
"@jridgewell/trace-mapping@^0.3.0":
version "0.3.2"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.2.tgz#e051581782a770c30ba219634f2019241c5d3cde"
@@ -3328,6 +3232,14 @@
"@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"
+"@jridgewell/trace-mapping@^0.3.17":
+ version "0.3.20"
+ resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz#72e45707cf240fa6b081d0366f8265b0cd10197f"
+ integrity sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==
+ dependencies:
+ "@jridgewell/resolve-uri" "^3.1.0"
+ "@jridgewell/sourcemap-codec" "^1.4.14"
+
"@jridgewell/trace-mapping@^0.3.9":
version "0.3.14"
resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.14.tgz#b231a081d8f66796e475ad588a1ef473112701ed"
@@ -3614,6 +3526,19 @@
resolved "https://registry.yarnpkg.com/@types/error-stack-parser/-/error-stack-parser-1.3.18.tgz#e01c9f8c85ca83b610320c62258b0c9026ade0f7"
integrity sha1-4ByfjIXKg7YQMgxiJYsMkCat4Pc=
+"@types/eslint@^8.44.6":
+ version "8.44.6"
+ resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.44.6.tgz#60e564551966dd255f4c01c459f0b4fb87068603"
+ integrity sha512-P6bY56TVmX8y9J87jHNgQh43h6VVU+6H7oN7hgvivV81K2XY8qJZ5vqPy/HdUoVIelii2kChYVzQanlswPWVFw==
+ dependencies:
+ "@types/estree" "*"
+ "@types/json-schema" "*"
+
+"@types/estree@*":
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.3.tgz#2be19e759a3dd18c79f9f436bd7363556c1a73dd"
+ integrity sha512-CS2rOaoQ/eAgAfcTfq6amKG7bsN+EMcgGY4FAFQdvSj2y1ixvOZTUA9mOtCai7E1SYu283XNw7urKK30nP3wkQ==
+
"@types/estree@0.0.39":
version "0.0.39"
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f"
@@ -3671,6 +3596,11 @@
resolved "https://registry.yarnpkg.com/@types/js-levenshtein/-/js-levenshtein-1.1.1.tgz#ba05426a43f9e4e30b631941e0aa17bf0c890ed5"
integrity sha512-qC4bCqYGy1y/NP7dDVr7KJarn+PbX1nSpwA7JXdu0HxT3QYjO8MJ+cntENtHFVy2dRAyBV23OZ6MxsW1AM1L8g==
+"@types/json-schema@*":
+ version "7.0.14"
+ resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.14.tgz#74a97a5573980802f32c8e47b663530ab3b6b7d1"
+ integrity sha512-U3PUjAudAdJBeC2pgN8uTIKgxrb4nlDF3SF0++EldXQvQBGkpFZMSnwQiIoDU77tv45VgNkl/L4ouD+rEomujw==
+
"@types/json-schema@^7.0.12":
version "7.0.12"
resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.12.tgz#d70faba7039d5fca54c83c7dbab41051d2b6f6cb"
@@ -4847,7 +4777,7 @@ chalk@4.1.1:
ansi-styles "^4.1.0"
supports-color "^7.1.0"
-chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.0:
+chalk@^2.0.0, chalk@^2.3.0, chalk@^2.4.0, chalk@^2.4.2:
version "2.4.2"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424"
integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==
@@ -5822,6 +5752,9 @@ escodegen@^2.0.0:
optionalDependencies:
source-map "~0.6.1"
+"eslint-config-posthog-js@link:./eslint-rules":
+ version "0.0.0"
+
eslint-config-prettier@^8.5.0:
version "8.5.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.5.0.tgz#5a81680ec934beca02c7b1a61cf8ca34b66feab1"
@@ -5848,6 +5781,9 @@ eslint-plugin-jest@^27.2.3:
dependencies:
"@typescript-eslint/utils" "^5.10.0"
+"eslint-plugin-posthog-js@link:./eslint-rules":
+ version "0.0.0"
+
eslint-plugin-prettier@^4.2.1:
version "4.2.1"
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b"
@@ -9831,7 +9767,6 @@ posix-character-classes@^0.1.0:
"posthog-js@link:.":
version "0.0.0"
- uid ""
prelude-ls@^1.2.1:
version "1.2.1"