@leanbase.com/js 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,162 @@
1
+ export const DEFAULT_BLOCKED_UA_STRS = [
2
+ // Random assortment of bots
3
+ 'amazonbot',
4
+ 'amazonproductbot',
5
+ 'app.hypefactors.com', // Buck, but "buck" is too short to be safe to block (https://app.hypefactors.com/media-monitoring/about.htm)
6
+ 'applebot',
7
+ 'archive.org_bot',
8
+ 'awariobot',
9
+ 'backlinksextendedbot',
10
+ 'baiduspider',
11
+ 'bingbot',
12
+ 'bingpreview',
13
+ 'chrome-lighthouse',
14
+ 'dataforseobot',
15
+ 'deepscan',
16
+ 'duckduckbot',
17
+ 'facebookexternal',
18
+ 'facebookcatalog',
19
+ 'http://yandex.com/bots',
20
+ 'hubspot',
21
+ 'ia_archiver',
22
+ 'leikibot',
23
+ 'linkedinbot',
24
+ 'meta-externalagent',
25
+ 'mj12bot',
26
+ 'msnbot',
27
+ 'nessus',
28
+ 'petalbot',
29
+ 'pinterest',
30
+ 'prerender',
31
+ 'rogerbot',
32
+ 'screaming frog',
33
+ 'sebot-wa',
34
+ 'sitebulb',
35
+ 'slackbot',
36
+ 'slurp',
37
+ 'trendictionbot',
38
+ 'turnitin',
39
+ 'twitterbot',
40
+ 'vercel-screenshot',
41
+ 'vercelbot',
42
+ 'yahoo! slurp',
43
+ 'yandexbot',
44
+ 'zoombot',
45
+
46
+ // Bot-like words, maybe we should block `bot` entirely?
47
+ 'bot.htm',
48
+ 'bot.php',
49
+ '(bot;',
50
+ 'bot/',
51
+ 'crawler',
52
+
53
+ // Ahrefs: https://ahrefs.com/seo/glossary/ahrefsbot
54
+ 'ahrefsbot',
55
+ 'ahrefssiteaudit',
56
+
57
+ // Semrush bots: https://www.semrush.com/bot/
58
+ 'semrushbot',
59
+ 'siteauditbot',
60
+ 'splitsignalbot',
61
+
62
+ // AI Crawlers
63
+ 'gptbot',
64
+ 'oai-searchbot',
65
+ 'chatgpt-user',
66
+ 'perplexitybot',
67
+
68
+ // Uptime-like stuff
69
+ 'better uptime bot',
70
+ 'sentryuptimebot',
71
+ 'uptimerobot',
72
+
73
+ // headless browsers
74
+ 'headlesschrome',
75
+ 'cypress',
76
+ // we don't block electron here, as many customers use posthog-js in electron apps
77
+
78
+ // a whole bunch of goog-specific crawlers
79
+ // https://developers.google.com/search/docs/advanced/crawling/overview-google-crawlers
80
+ 'google-hoteladsverifier',
81
+ 'adsbot-google',
82
+ 'apis-google',
83
+ 'duplexweb-google',
84
+ 'feedfetcher-google',
85
+ 'google favicon',
86
+ 'google web preview',
87
+ 'google-read-aloud',
88
+ 'googlebot',
89
+ 'googleother',
90
+ 'google-cloudvertexbot',
91
+ 'googleweblight',
92
+ 'mediapartners-google',
93
+ 'storebot-google',
94
+ 'google-inspectiontool',
95
+ 'bytespider',
96
+ ]
97
+
98
+ /**
99
+ * Block various web spiders from executing our JS and sending false capturing data
100
+ */
101
+ export const isBlockedUA = function (ua: string, customBlockedUserAgents: string[]): boolean {
102
+ if (!ua) {
103
+ return false
104
+ }
105
+
106
+ const uaLower = ua.toLowerCase()
107
+ return DEFAULT_BLOCKED_UA_STRS.concat(customBlockedUserAgents || []).some((blockedUA) => {
108
+ const blockedUaLower = blockedUA.toLowerCase()
109
+
110
+ // can't use includes because IE 11 :/
111
+ return uaLower.indexOf(blockedUaLower) !== -1
112
+ })
113
+ }
114
+
115
+ // There's more in the type, but this is all we use. It's currently experimental, see
116
+ // https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgentData
117
+ // if you're reading this in the future, when it's no longer experimental, please remove this type and use an official one.
118
+ // Be extremely defensive here to ensure backwards and *forwards* compatibility, and remove this defensiveness in the
119
+ // future when it is safe to do so.
120
+ export interface NavigatorUAData {
121
+ brands?: {
122
+ brand: string
123
+ version: string
124
+ }[]
125
+ }
126
+ declare global {
127
+ interface Navigator {
128
+ userAgentData?: NavigatorUAData
129
+ }
130
+ }
131
+
132
+ export const isLikelyBot = function (navigator: Navigator | undefined, customBlockedUserAgents: string[]): boolean {
133
+ if (!navigator) {
134
+ return false
135
+ }
136
+ const ua = navigator.userAgent
137
+ if (ua) {
138
+ if (isBlockedUA(ua, customBlockedUserAgents)) {
139
+ return true
140
+ }
141
+ }
142
+ try {
143
+ // eslint-disable-next-line compat/compat
144
+ const uaData = navigator?.userAgentData as NavigatorUAData
145
+ if (uaData?.brands && uaData.brands.some((brandObj) => isBlockedUA(brandObj?.brand, customBlockedUserAgents))) {
146
+ return true
147
+ }
148
+ } catch {
149
+ // ignore the error, we were using experimental browser features
150
+ }
151
+
152
+ return !!navigator.webdriver
153
+
154
+ // There's some more enhancements we could make in this area, e.g. it's possible to check if Chrome dev tools are
155
+ // open, which will detect some bots that are trying to mask themselves and might get past the checks above.
156
+ // However, this would give false positives for actual humans who have dev tools open.
157
+
158
+ // We could also use the data in navigator.userAgentData.getHighEntropyValues() to detect bots, but we should wait
159
+ // until this stops being experimental. The MDN docs imply that this might eventually require user permission.
160
+ // See https://developer.mozilla.org/en-US/docs/Web/API/NavigatorUAData/getHighEntropyValues
161
+ // It would be very bad if posthog-js caused a permission prompt to appear on every page load.
162
+ }
@@ -0,0 +1,50 @@
1
+ import { TOOLBAR_CONTAINER_CLASS, TOOLBAR_ID } from '../constants'
2
+
3
+ export function isElementInToolbar(el: EventTarget | null): boolean {
4
+ if (el instanceof Element) {
5
+ // closest isn't available in IE11, but we'll polyfill when bundling
6
+ return el.id === TOOLBAR_ID || !!el.closest?.('.' + TOOLBAR_CONTAINER_CLASS)
7
+ }
8
+ return false
9
+ }
10
+
11
+ /*
12
+ * Check whether an element has nodeType Node.ELEMENT_NODE
13
+ * @param {Element} el - element to check
14
+ * @returns {boolean} whether el is of the correct nodeType
15
+ */
16
+ export function isElementNode(el: Node | Element | undefined | null): el is Element {
17
+ return !!el && el.nodeType === 1 // Node.ELEMENT_NODE - use integer constant for browser portability
18
+ }
19
+
20
+ /*
21
+ * Check whether an element is of a given tag type.
22
+ * Due to potential reference discrepancies (such as the webcomponents.js polyfill),
23
+ * we want to match tagNames instead of specific references because something like
24
+ * element === document.body won't always work because element might not be a native
25
+ * element.
26
+ * @param {Element} el - element to check
27
+ * @param {string} tag - tag name (e.g., "div")
28
+ * @returns {boolean} whether el is of the given tag type
29
+ */
30
+ export function isTag(el: Element | undefined | null, tag: string): el is HTMLElement {
31
+ return !!el && !!el.tagName && el.tagName.toLowerCase() === tag.toLowerCase()
32
+ }
33
+
34
+ /*
35
+ * Check whether an element has nodeType Node.TEXT_NODE
36
+ * @param {Element} el - element to check
37
+ * @returns {boolean} whether el is of the correct nodeType
38
+ */
39
+ export function isTextNode(el: Element | undefined | null): el is HTMLElement {
40
+ return !!el && el.nodeType === 3 // Node.TEXT_NODE - use integer constant for browser portability
41
+ }
42
+
43
+ /*
44
+ * Check whether an element has nodeType Node.DOCUMENT_FRAGMENT_NODE
45
+ * @param {Element} el - element to check
46
+ * @returns {boolean} whether el is of the correct nodeType
47
+ */
48
+ export function isDocumentFragment(el: Element | ParentNode | undefined | null): el is DocumentFragment {
49
+ return !!el && el.nodeType === 11 // Node.DOCUMENT_FRAGMENT_NODE - use integer constant for browser portability
50
+ }
@@ -0,0 +1,304 @@
1
+ import { convertToURL, getQueryParam, maskQueryParams } from './request-utils'
2
+ import { isNull, stripLeadingDollar } from '@posthog/core'
3
+ import { each, extend, extendArray, stripEmptyProperties } from './index'
4
+ import { document, location, userAgent, window } from './'
5
+ import { detectBrowser, detectBrowserVersion, detectDevice, detectDeviceType, detectOS } from './user-agent-utils'
6
+ import { cookieStore } from '../storage'
7
+ import Config from '../config'
8
+ import { Properties } from '../types'
9
+
10
+ const URL_REGEX_PREFIX = 'https?://(.*)'
11
+
12
+ // CAMPAIGN_PARAMS and EVENT_TO_PERSON_PROPERTIES should be kept in sync with
13
+ // https://github.com/PostHog/posthog/blob/master/plugin-server/src/utils/db/utils.ts#L60
14
+
15
+ // The list of campaign parameters that could be considered personal data under e.g. GDPR.
16
+ // These can be masked in URLs and properties before being sent to posthog.
17
+ export const PERSONAL_DATA_CAMPAIGN_PARAMS = [
18
+ 'gclid', // google ads
19
+ 'gclsrc', // google ads 360
20
+ 'dclid', // google display ads
21
+ 'gbraid', // google ads, web to app
22
+ 'wbraid', // google ads, app to web
23
+ 'fbclid', // facebook
24
+ 'msclkid', // microsoft
25
+ 'twclid', // twitter
26
+ 'li_fat_id', // linkedin
27
+ 'igshid', // instagram
28
+ 'ttclid', // tiktok
29
+ 'rdt_cid', // reddit
30
+ 'epik', // pinterest
31
+ 'qclid', // quora
32
+ 'sccid', // snapchat
33
+ 'irclid', // impact
34
+ '_kx', // klaviyo
35
+ ]
36
+
37
+ export const CAMPAIGN_PARAMS = extendArray(
38
+ [
39
+ 'utm_source',
40
+ 'utm_medium',
41
+ 'utm_campaign',
42
+ 'utm_content',
43
+ 'utm_term',
44
+ 'gad_source', // google ads source
45
+ 'mc_cid', // mailchimp campaign id
46
+ ],
47
+ PERSONAL_DATA_CAMPAIGN_PARAMS
48
+ )
49
+
50
+ export const EVENT_TO_PERSON_PROPERTIES = [
51
+ // mobile params
52
+ '$app_build',
53
+ '$app_name',
54
+ '$app_namespace',
55
+ '$app_version',
56
+ // web params
57
+ '$browser',
58
+ '$browser_version',
59
+ '$device_type',
60
+ '$current_url',
61
+ '$pathname',
62
+ '$os',
63
+ '$os_name', // $os_name is a special case, it's treated as an alias of $os!
64
+ '$os_version',
65
+ '$referring_domain',
66
+ '$referrer',
67
+ '$screen_height',
68
+ '$screen_width',
69
+ '$viewport_height',
70
+ '$viewport_width',
71
+ '$raw_user_agent',
72
+ ]
73
+
74
+ export const MASKED = '<masked>'
75
+
76
+ // Campaign params that can be read from the cookie store
77
+ export const COOKIE_CAMPAIGN_PARAMS = [
78
+ 'li_fat_id', // linkedin
79
+ ]
80
+
81
+ export function getCampaignParams(
82
+ customTrackedParams?: string[],
83
+ maskPersonalDataProperties?: boolean,
84
+ customPersonalDataProperties?: string[] | undefined
85
+ ): Record<string, string> {
86
+ if (!document) {
87
+ return {}
88
+ }
89
+
90
+ const paramsToMask = maskPersonalDataProperties
91
+ ? extendArray([], PERSONAL_DATA_CAMPAIGN_PARAMS, customPersonalDataProperties || [])
92
+ : []
93
+
94
+ // Initially get campaign params from the URL
95
+ const urlCampaignParams = _getCampaignParamsFromUrl(
96
+ maskQueryParams(document.URL, paramsToMask, MASKED),
97
+ customTrackedParams
98
+ )
99
+
100
+ // But we can also get some of them from the cookie store
101
+ // For example: https://learn.microsoft.com/en-us/linkedin/marketing/conversions/enabling-first-party-cookies?view=li-lms-2025-05#reading-li_fat_id-from-cookies
102
+ const cookieCampaignParams = _getCampaignParamsFromCookie()
103
+
104
+ // Prefer the values found in the urlCampaignParams if possible
105
+ // `extend` will override the values if found in the second argument
106
+ return extend(cookieCampaignParams, urlCampaignParams)
107
+ }
108
+
109
+ function _getCampaignParamsFromUrl(url: string, customParams?: string[]): Record<string, string> {
110
+ const campaign_keywords = CAMPAIGN_PARAMS.concat(customParams || [])
111
+
112
+ const params: Record<string, any> = {}
113
+ each(campaign_keywords, function (kwkey) {
114
+ const kw = getQueryParam(url, kwkey)
115
+ params[kwkey] = kw ? kw : null
116
+ })
117
+
118
+ return params
119
+ }
120
+
121
+ function _getCampaignParamsFromCookie(): Record<string, string> {
122
+ const params: Record<string, any> = {}
123
+ each(COOKIE_CAMPAIGN_PARAMS, function (kwkey) {
124
+ const kw = cookieStore._get(kwkey)
125
+ params[kwkey] = kw ? kw : null
126
+ })
127
+
128
+ return params
129
+ }
130
+
131
+ function _getSearchEngine(referrer: string): string | null {
132
+ if (!referrer) {
133
+ return null
134
+ } else {
135
+ if (referrer.search(URL_REGEX_PREFIX + 'google.([^/?]*)') === 0) {
136
+ return 'google'
137
+ } else if (referrer.search(URL_REGEX_PREFIX + 'bing.com') === 0) {
138
+ return 'bing'
139
+ } else if (referrer.search(URL_REGEX_PREFIX + 'yahoo.com') === 0) {
140
+ return 'yahoo'
141
+ } else if (referrer.search(URL_REGEX_PREFIX + 'duckduckgo.com') === 0) {
142
+ return 'duckduckgo'
143
+ } else {
144
+ return null
145
+ }
146
+ }
147
+ }
148
+
149
+ function _getSearchInfoFromReferrer(referrer: string): Record<string, any> {
150
+ const search = _getSearchEngine(referrer)
151
+ const param = search != 'yahoo' ? 'q' : 'p'
152
+ const ret: Record<string, any> = {}
153
+
154
+ if (!isNull(search)) {
155
+ ret['$search_engine'] = search
156
+
157
+ const keyword = document ? getQueryParam(document.referrer, param) : ''
158
+ if (keyword.length) {
159
+ ret['ph_keyword'] = keyword
160
+ }
161
+ }
162
+
163
+ return ret
164
+ }
165
+
166
+ export function getSearchInfo(): Record<string, any> {
167
+ const referrer = document?.referrer
168
+ if (!referrer) {
169
+ return {}
170
+ }
171
+ return _getSearchInfoFromReferrer(referrer)
172
+ }
173
+
174
+ export function getBrowserLanguage(): string | undefined {
175
+ return (
176
+ navigator.language || // Any modern browser
177
+ (navigator as Record<string, any>).userLanguage // IE11
178
+ )
179
+ }
180
+
181
+ export function getBrowserLanguagePrefix(): string | undefined {
182
+ const lang = getBrowserLanguage()
183
+ return typeof lang === 'string' ? lang.split('-')[0] : undefined
184
+ }
185
+
186
+ export function getReferrer(): string {
187
+ return document?.referrer || '$direct'
188
+ }
189
+
190
+ export function getReferringDomain(): string {
191
+ if (!document?.referrer) {
192
+ return '$direct'
193
+ }
194
+ return convertToURL(document.referrer)?.host || '$direct'
195
+ }
196
+
197
+ export function getReferrerInfo(): Record<string, any> {
198
+ return {
199
+ $referrer: getReferrer(),
200
+ $referring_domain: getReferringDomain(),
201
+ }
202
+ }
203
+
204
+ export function getPersonInfo(maskPersonalDataProperties?: boolean, customPersonalDataProperties?: string[]) {
205
+ const paramsToMask = maskPersonalDataProperties
206
+ ? extendArray([], PERSONAL_DATA_CAMPAIGN_PARAMS, customPersonalDataProperties || [])
207
+ : []
208
+ const url = location?.href.substring(0, 1000)
209
+ // we're being a bit more economical with bytes here because this is stored in the cookie
210
+ return {
211
+ r: getReferrer().substring(0, 1000),
212
+ u: url ? maskQueryParams(url, paramsToMask, MASKED) : undefined,
213
+ }
214
+ }
215
+
216
+ export function getPersonPropsFromInfo(info: Record<string, any>): Record<string, any> {
217
+ const { r: referrer, u: url } = info
218
+ const referring_domain =
219
+ referrer == null ? undefined : referrer == '$direct' ? '$direct' : convertToURL(referrer)?.host
220
+
221
+ const props: Record<string, string | undefined> = {
222
+ $referrer: referrer,
223
+ $referring_domain: referring_domain,
224
+ }
225
+ if (url) {
226
+ props['$current_url'] = url
227
+ const location = convertToURL(url)
228
+ props['$host'] = location?.host
229
+ props['$pathname'] = location?.pathname
230
+ const campaignParams = _getCampaignParamsFromUrl(url)
231
+ extend(props, campaignParams)
232
+ }
233
+ if (referrer) {
234
+ const searchInfo = _getSearchInfoFromReferrer(referrer)
235
+ extend(props, searchInfo)
236
+ }
237
+ return props
238
+ }
239
+
240
+ export function getInitialPersonPropsFromInfo(info: Record<string, any>): Record<string, any> {
241
+ const personProps = getPersonPropsFromInfo(info)
242
+ const props: Record<string, any> = {}
243
+ each(personProps, function (val: any, key: string) {
244
+ props[`$initial_${stripLeadingDollar(key)}`] = val
245
+ })
246
+ return props
247
+ }
248
+
249
+ export function getTimezone(): string | undefined {
250
+ try {
251
+ return Intl.DateTimeFormat().resolvedOptions().timeZone
252
+ } catch {
253
+ return undefined
254
+ }
255
+ }
256
+
257
+ export function getTimezoneOffset(): number | undefined {
258
+ try {
259
+ return new Date().getTimezoneOffset()
260
+ } catch {
261
+ return undefined
262
+ }
263
+ }
264
+
265
+ export function getEventProperties(
266
+ maskPersonalDataProperties?: boolean,
267
+ customPersonalDataProperties?: string[]
268
+ ): Properties {
269
+ if (!userAgent) {
270
+ return {}
271
+ }
272
+ const paramsToMask = maskPersonalDataProperties
273
+ ? extendArray([], PERSONAL_DATA_CAMPAIGN_PARAMS, customPersonalDataProperties || [])
274
+ : []
275
+ const [os_name, os_version] = detectOS(userAgent)
276
+ return extend(
277
+ stripEmptyProperties({
278
+ $os: os_name,
279
+ $os_version: os_version,
280
+ $browser: detectBrowser(userAgent, navigator.vendor),
281
+ $device: detectDevice(userAgent),
282
+ $device_type: detectDeviceType(userAgent),
283
+ $timezone: getTimezone(),
284
+ $timezone_offset: getTimezoneOffset(),
285
+ }),
286
+ {
287
+ $current_url: maskQueryParams(location?.href, paramsToMask, MASKED),
288
+ $host: location?.host,
289
+ $pathname: location?.pathname,
290
+ $raw_user_agent: userAgent.length > 1000 ? userAgent.substring(0, 997) + '...' : userAgent,
291
+ $browser_version: detectBrowserVersion(userAgent, navigator.vendor),
292
+ $browser_language: getBrowserLanguage(),
293
+ $browser_language_prefix: getBrowserLanguagePrefix(),
294
+ $screen_height: window?.screen.height,
295
+ $screen_width: window?.screen.width,
296
+ $viewport_height: window?.innerHeight,
297
+ $viewport_width: window?.innerWidth,
298
+ $lib: 'web',
299
+ $lib_version: Config.LIB_VERSION,
300
+ $insert_id: Math.random().toString(36).substring(2, 10) + Math.random().toString(36).substring(2, 10),
301
+ $time: Date.now() / 1000, // epoch time in seconds
302
+ }
303
+ )
304
+ }