Repository: PostHog/posthog-js
Pull request #1937: feat: suppression matching
Author: login: daibhin
Labels: name: stale
Created: 2025-05-06T18:26:32Z | Merged: 2025-05-19T12:22:56Z

## Changes

Add suppression rules check before capturing exceptions

## Checklist
- [ ] Tests for new code (see [advice on the tests we use](https://github.com/PostHog/posthog-js#tiers-of-testing))
- [ ] Accounted for the impact of any changes across different browsers
- [ ] Accounted for backwards compatibility of any changes (no breaking changes in posthog-js!)
- [ ] Took care not to unnecessarily increase the bundle size




Diff:
diff --git a/src/__tests__/posthog-exceptions.test.ts b/src/__tests__/posthog-exceptions.test.ts
new file mode 100644
index 0000000000..601f08001d
--- /dev/null
+++ b/src/__tests__/posthog-exceptions.test.ts
@@ -0,0 +1,120 @@
+import { ERROR_TRACKING_SUPPRESSION_RULES } from '../constants'
+import { PostHog } from '../posthog-core'
+import { PostHogExceptions } from '../posthog-exceptions'
+import { PostHogPersistence } from '../posthog-persistence'
+import {
+    DecideResponse,
+    ErrorTrackingSuppressionRule,
+    ErrorTrackingSuppressionRuleValue,
+    PostHogConfig,
+    Property,
+} from '../types'
+
+function createSuppressionRule(
+    type: 'AND' | 'OR' = 'OR',
+    values: ErrorTrackingSuppressionRuleValue[] = [
+        {
+            key: '$exception_types',
+            value: ['TypeError', 'ReactError'],
+            operator: 'exact',
+            type: 'error_tracking_issue_property',
+        },
+        {
+            key: '$exception_messages',
+            value: 'ReactMinified',
+            operator: 'icontains',
+            type: 'error_tracking_issue_property',
+        },
+    ]
+): ErrorTrackingSuppressionRule {
+    return { type, values }
+}
+
+describe('PostHogExceptions', () => {
+    const captureMock = jest.fn()
+    let posthog: PostHog
+    let exceptions: PostHogExceptions
+    let config: PostHogConfig
+
+    beforeEach(() => {
+        config = { persistence: 'memory' } as unknown as PostHogConfig
+
+        const postHogPersistence = new PostHogPersistence(config)
+        postHogPersistence.clear()
+
+        // TODO: we really need to make this a real posthog instance :cry:
+        posthog = {
+            get_property: (property_key: string): Property | undefined => {
+                return postHogPersistence?.props[property_key]
+            },
+            config: config,
+            capture: captureMock,
+            persistence: postHogPersistence,
+        } as Partial<PostHog> as PostHog
+
+        // defaults
+        posthog.persistence?.register({
+            [ERROR_TRACKING_SUPPRESSION_RULES]: [],
+        })
+
+        exceptions = new PostHogExceptions(posthog)
+    })
+
+    afterEach(() => {
+        captureMock.mockClear()
+    })
+
+    describe('onRemoteConfig', () => {
+        it('persists the suppression rules', () => {
+            const suppressionRule = createSuppressionRule()
+            const decideResponse: Partial<DecideResponse> = { errorTracking: { suppressionRules: [suppressionRule] } }
+            exceptions.onRemoteConfig(decideResponse as DecideResponse)
+            expect(exceptions['_suppressionRules']).toEqual([suppressionRule])
+        })
+    })
+
+    describe('sendExceptionEvent', () => {
+        it('captures the event when no suppression rules are provided', () => {
+            exceptions.sendExceptionEvent({ custom_property: true })
+            expect(captureMock).toBeCalledWith('$exception', { custom_property: true }, expect.anything())
+        })
+
+        test.each([
+            ['TypeError', 'This is a type error'],
+            ['GenericError', 'This is a message that contains a ReactMinified error'],
+        ])('drops the event if a suppression rule matches', (type, value) => {
+            const suppressionRule = createSuppressionRule('OR')
+            exceptions.onRemoteConfig({ errorTracking: { suppressionRules: [suppressionRule] } } as DecideResponse)
+            exceptions.sendExceptionEvent({ $exception_list: [{ type, value }] })
+            expect(captureMock).not.toBeCalled()
+        })
+
+        it('captures an exception if no $exception_list property exists', () => {
+            const suppressionRule = createSuppressionRule('AND')
+            exceptions.onRemoteConfig({ errorTracking: { suppressionRules: [suppressionRule] } } as DecideResponse)
+            exceptions.sendExceptionEvent({ custom_property: true })
+            expect(captureMock).toBeCalled()
+        })
+
+        it('captures an exception if all rule conditions do not match', () => {
+            const suppressionRule = createSuppressionRule('AND')
+            exceptions.onRemoteConfig({ errorTracking: { suppressionRules: [suppressionRule] } } as DecideResponse)
+            exceptions.sendExceptionEvent({ $exception_list: [{ type: 'TypeError', value: 'This is a type error' }] })
+            expect(captureMock).toBeCalled()
+        })
+
+        it('captures an exception if there are no targets on the rule', () => {
+            const suppressionRule = createSuppressionRule('OR', [
+                {
+                    key: '$exception_types',
+                    value: [],
+                    operator: 'exact',
+                    type: 'error_tracking_issue_property',
+                },
+            ])
+            exceptions.onRemoteConfig({ errorTracking: { suppressionRules: [suppressionRule] } } as DecideResponse)
+            exceptions.sendExceptionEvent({ $exception_list: [{ type: 'TypeError', value: 'This is a type error' }] })
+            expect(captureMock).toBeCalled()
+        })
+    })
+})
diff --git a/src/__tests__/utils.test.ts b/src/__tests__/utils.test.ts
index e481798d91..1ba2d4cb91 100644
--- a/src/__tests__/utils.test.ts
+++ b/src/__tests__/utils.test.ts
@@ -12,7 +12,7 @@ import { isLikelyBot, DEFAULT_BLOCKED_UA_STRS, isBlockedUA, NavigatorUAData } fr
 import { expect } from '@jest/globals'
 
 import { _base64Encode } from '../utils/encode-utils'
-import { getPersonPropertiesHash } from '../utils/person-property-utils'
+import { getPersonPropertiesHash, propertyComparisons } from '../utils/property-utils'
 import { detectDeviceType } from '../utils/user-agent-utils'
 import { getEventProperties } from '../utils/event-utils'
 
@@ -413,4 +413,51 @@ describe('utils', () => {
             expect(hash).toContain('New York')
         })
     })
+
+    describe('propertyComparisons', () => {
+        const exactCases: [string[], string[], boolean][] = [
+            [[], ['one'], false], // no targets
+            [['one', 'two', 'three'], [], false], // no values
+            [['one', 'two', 'three'], ['one'], true], // one match
+            [['one', 'two', 'three'], ['one', 'does not match'], true], // one match, one not matching
+            [['one', 'two', 'three'], ['one', 'two'], true], // multiple matches
+            [['one', 'two', 'three'], ['no', 'matches'], false], // no matches
+        ]
+        test.each(exactCases)('exact', (targets, values, expected) => {
+            expect(propertyComparisons['exact'](targets, values)).toBe(expected)
+        })
+        test.each(exactCases)('is_not', (targets, values, expected) => {
+            expect(propertyComparisons['is_not'](targets, values)).toBe(!expected)
+        })
+
+        const regexCases: [string[], string[], boolean][] = [
+            [[], ['one'], false], // no targets
+            [['^one$', '^two$', '^three$'], [], false], // no values
+            [['^one$', '^two$', '^three$'], ['one'], true], // one match
+            [['^one$', '^two$', '^three$'], ['one', 'does not match'], true], // one match, one not matching
+            [['^one$', '^two$', '^three$'], ['one', 'two'], true], // multiple matches
+            [['^one$', '^two$', '^three$'], ['no', 'matches'], false], // no matches
+            [['^one$', '^two$', '^three$'], ['ones'], false], // partial match
+        ]
+        test.each(regexCases)('regex', (targets, values, expected) => {
+            expect(propertyComparisons['regex'](targets, values)).toBe(expected)
+        })
+        test.each(regexCases)('not_regex', (targets, values, expected) => {
+            expect(propertyComparisons['not_regex'](targets, values)).toBe(!expected)
+        })
+
+        const icontainsCases: [string[], string[], boolean][] = [
+            [[], ['one'], false], // no targets
+            [['one', 'two', 'three'], [], false], // no values
+            [['one', 'two', 'three'], ['one'], true], // full match
+            [['one', 'two', 'three'], ['no', 'matches'], false], // no matches
+            [['one', 'two', 'three'], ['A message with one matching'], true], // partial match
+        ]
+        test.each(icontainsCases)('icontains', (targets, values, expected) => {
+            expect(propertyComparisons['icontains'](targets, values)).toBe(expected)
+        })
+        test.each(icontainsCases)('not_icontains', (targets, values, expected) => {
+            expect(propertyComparisons['not_icontains'](targets, values)).toBe(!expected)
+        })
+    })
 })
diff --git a/src/constants.ts b/src/constants.ts
index 1e1cc9ed18..2b28f0abcd 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -13,6 +13,7 @@ export const EVENT_TIMERS_KEY = '__timers'
 export const AUTOCAPTURE_DISABLED_SERVER_SIDE = '$autocapture_disabled_server_side'
 export const HEATMAPS_ENABLED_SERVER_SIDE = '$heatmaps_enabled_server_side'
 export const EXCEPTION_CAPTURE_ENABLED_SERVER_SIDE = '$exception_capture_enabled_server_side'
+export const ERROR_TRACKING_SUPPRESSION_RULES = '$error_tracking_suppression_rules'
 export const WEB_VITALS_ENABLED_SERVER_SIDE = '$web_vitals_enabled_server_side'
 export const DEAD_CLICKS_ENABLED_SERVER_SIDE = '$dead_clicks_enabled_server_side'
 export const WEB_VITALS_ALLOWED_METRICS = '$web_vitals_allowed_metrics'
@@ -70,6 +71,7 @@ export const PERSISTENCE_RESERVED_PROPERTIES = [
     HEATMAPS_ENABLED_SERVER_SIDE,
     SESSION_ID,
     ENABLED_FEATURE_FLAGS,
+    ERROR_TRACKING_SUPPRESSION_RULES,
     USER_STATE,
     PERSISTENCE_EARLY_ACCESS_FEATURES,
     PERSISTENCE_FEATURE_FLAG_DETAILS,
diff --git a/src/extensions/surveys/surveys-extension-utils.tsx b/src/extensions/surveys/surveys-extension-utils.tsx
index 460f697642..3364bb86bb 100644
--- a/src/extensions/surveys/surveys-extension-utils.tsx
+++ b/src/extensions/surveys/surveys-extension-utils.tsx
@@ -12,9 +12,9 @@ import { document as _document, window as _window, userAgent } from '../../utils
 import { SURVEY_LOGGER as logger, SURVEY_IN_PROGRESS_PREFIX, SURVEY_SEEN_PREFIX } from '../../utils/survey-utils'
 import { isNullish, isArray } from '../../utils/type-utils'
 
-import { SurveyMatchType } from '../../posthog-surveys-types'
-import { isMatchingRegex } from '../../utils/regex-utils'
 import { detectDeviceType } from '../../utils/user-agent-utils'
+import { propertyComparisons } from '../../utils/property-utils'
+import { PropertyMatchType } from '../../types'
 import { prepareStylesheet } from '../utils/stylesheet-loader'
 // We cast the types here which is dangerous but protected by the top level generateSurveys call
 const window = _window as Window & typeof globalThis
@@ -566,16 +566,7 @@ export const renderChildrenAsTextOrHtml = ({ component, children, renderAsHtml,
           })
 }
 
-const surveyValidationMap: Record<SurveyMatchType, (targets: string[], value: string) => boolean> = {
-    icontains: (targets, value) => targets.some((target) => value.toLowerCase().includes(target.toLowerCase())),
-    not_icontains: (targets, value) => targets.every((target) => !value.toLowerCase().includes(target.toLowerCase())),
-    regex: (targets, value) => targets.some((target) => isMatchingRegex(value, target)),
-    not_regex: (targets, value) => targets.every((target) => !isMatchingRegex(value, target)),
-    exact: (targets, value) => targets.some((target) => value === target),
-    is_not: (targets, value) => targets.every((target) => value !== target),
-}
-
-function defaultMatchType(matchType?: SurveyMatchType): SurveyMatchType {
+function defaultMatchType(matchType?: PropertyMatchType): PropertyMatchType {
     return matchType ?? 'icontains'
 }
 
@@ -590,7 +581,8 @@ export function doesSurveyUrlMatch(survey: Pick<Survey, 'conditions'>): boolean
         return false
     }
     const targets = [survey.conditions.url]
-    return surveyValidationMap[defaultMatchType(survey.conditions?.urlMatchType)](targets, href)
+    const matchType = defaultMatchType(survey.conditions?.urlMatchType)
+    return propertyComparisons[matchType](targets, [href])
 }
 
 export function doesSurveyDeviceTypesMatch(survey: Survey): boolean {
@@ -603,9 +595,9 @@ export function doesSurveyDeviceTypesMatch(survey: Survey): boolean {
     }
 
     const deviceType = detectDeviceType(userAgent)
-    return surveyValidationMap[defaultMatchType(survey.conditions?.deviceTypesMatchType)](
+    return propertyComparisons[defaultMatchType(survey.conditions?.deviceTypesMatchType)](
         survey.conditions.deviceTypes,
-        deviceType
+        [deviceType]
     )
 }
 
diff --git a/src/posthog-core.ts b/src/posthog-core.ts
index 0f97cebfc5..9041c8bdc7 100644
--- a/src/posthog-core.ts
+++ b/src/posthog-core.ts
@@ -70,7 +70,7 @@ import { isLikelyBot } from './utils/blocked-uas'
 import { getEventProperties } from './utils/event-utils'
 import { assignableWindow, document, location, navigator, userAgent, window } from './utils/globals'
 import { logger } from './utils/logger'
-import { getPersonPropertiesHash } from './utils/person-property-utils'
+import { getPersonPropertiesHash } from './utils/property-utils'
 import { RequestRouter, RequestRouterRegion } from './utils/request-router'
 import { SimpleEventEmitter } from './utils/simple-event-emitter'
 import { includes, isDistinctIdStringLike } from './utils/string-utils'
@@ -627,6 +627,7 @@ export class PostHog {
         this.surveys.onRemoteConfig(config)
         this.webVitalsAutocapture?.onRemoteConfig(config)
         this.exceptionObserver?.onRemoteConfig(config)
+        this.exceptions.onRemoteConfig(config)
         this.deadClicksAutocapture?.onRemoteConfig(config)
     }
 
diff --git a/src/posthog-exceptions.ts b/src/posthog-exceptions.ts
index b3fa151450..2a088abe81 100644
--- a/src/posthog-exceptions.ts
+++ b/src/posthog-exceptions.ts
@@ -1,16 +1,77 @@
+import { ERROR_TRACKING_SUPPRESSION_RULES } from './constants'
 import { PostHog } from './posthog-core'
-import { Properties } from './types'
+import { ErrorTrackingSuppressionRule, Properties, RemoteConfig } from './types'
+import { createLogger } from './utils/logger'
+import { propertyComparisons } from './utils/property-utils'
+import { isArray, isString } from './utils/type-utils'
+
+const logger = createLogger('[Error tracking]')
 
 export class PostHogExceptions {
-    constructor(private readonly _instance: PostHog) {}
+    private readonly _instance: PostHog
+    private _suppressionRules: ErrorTrackingSuppressionRule[] = []
+
+    constructor(instance: PostHog) {
+        this._instance = instance
+        this._suppressionRules = this._instance.persistence?.get_property(ERROR_TRACKING_SUPPRESSION_RULES) ?? []
+    }
+
+    onRemoteConfig(response: RemoteConfig) {
+        const suppressionRules = response.errorTracking?.suppressionRules ?? []
+
+        // store this in-memory in case persistence is disabled
+        this._suppressionRules = suppressionRules
+
+        if (this._instance.persistence) {
+            this._instance.persistence.register({
+                [ERROR_TRACKING_SUPPRESSION_RULES]: this._suppressionRules,
+            })
+        }
+    }
 
-    /**
-     * :TRICKY: Make sure we batch these requests
-     */
     sendExceptionEvent(properties: Properties) {
+        if (this._matchesSuppressionRule(properties)) {
+            logger.info('Skipping exception capture because a suppression rule matched')
+            return
+        }
+
         this._instance.capture('$exception', properties, {
             _noTruncate: true,
             _batchKey: 'exceptionEvent',
         })
     }
+
+    private _matchesSuppressionRule(properties: Properties): boolean {
+        const exceptionList = properties.$exception_list
+
+        if (!exceptionList || !isArray(exceptionList) || exceptionList.length === 0) {
+            return false
+        }
+
+        const exceptionValues = exceptionList.reduce(
+            (acc, { type, value }) => {
+                if (isString(type) && type.length > 0) {
+                    acc['$exception_types'].push(type)
+                }
+                if (isString(value) && value.length > 0) {
+                    acc['$exception_messages'].push(value)
+                }
+                return acc
+            },
+            {
+                $exception_types: [],
+                $exception_messages: [],
+            }
+        )
+
+        return this._suppressionRules.some((rule) => {
+            const results = rule.values.map((v) => {
+                const compare = propertyComparisons[v.operator]
+                const targets = isArray(v.value) ? v.value : [v.value]
+                const values = exceptionValues[v.key]
+                return targets.length > 0 ? compare(targets, values) : false
+            })
+            return rule.type === 'OR' ? results.some(Boolean) : results.every(Boolean)
+        })
+    }
 }
diff --git a/src/posthog-surveys-types.ts b/src/posthog-surveys-types.ts
index fdedb51890..051db4f5e4 100644
--- a/src/posthog-surveys-types.ts
+++ b/src/posthog-surveys-types.ts
@@ -4,6 +4,8 @@
  * See https://github.com/PostHog/posthog-js/issues/698
  */
 
+import type { PropertyMatchType } from './types'
+
 export enum SurveyWidgetType {
     Button = 'button',
     Tab = 'tab',
@@ -133,8 +135,6 @@ interface SpecificQuestionBranching {
 
 export type SurveyCallback = (surveys: Survey[], context?: { isLoaded: boolean; error?: string }) => void
 
-export type SurveyMatchType = 'regex' | 'not_regex' | 'exact' | 'is_not' | 'icontains' | 'not_icontains'
-
 export interface SurveyElement {
     text?: string
     $el_text?: string
@@ -181,7 +181,7 @@ export interface Survey {
         url?: string
         selector?: string
         seenSurveyWaitPeriodInDays?: number
-        urlMatchType?: SurveyMatchType
+        urlMatchType?: PropertyMatchType
         events: {
             repeatedActivation?: boolean
             values: {
@@ -192,7 +192,7 @@ export interface Survey {
             values: SurveyActionType[]
         } | null
         deviceTypes?: string[]
-        deviceTypesMatchType?: SurveyMatchType
+        deviceTypesMatchType?: PropertyMatchType
     } | null
     start_date: string | null
     end_date: string | null
diff --git a/src/types.ts b/src/types.ts
index e3c2e6dc2d..e7068c9d6f 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -1289,6 +1289,14 @@ export interface RemoteConfig {
      */
     elementsChainAsString?: boolean
 
+    /**
+     * Error tracking configuration options
+     */
+    errorTracking?: {
+        autocaptureExceptions?: boolean
+        suppressionRules?: ErrorTrackingSuppressionRule[]
+    }
+
     /**
      * This is currently in development and may have breaking changes without a major version bump
      */
@@ -1643,3 +1651,17 @@ export interface SessionRecordingUrlTrigger {
     url: string
     matching: 'regex'
 }
+
+export type PropertyMatchType = 'regex' | 'not_regex' | 'exact' | 'is_not' | 'icontains' | 'not_icontains'
+
+export interface ErrorTrackingSuppressionRule {
+    type: 'AND' | 'OR'
+    values: ErrorTrackingSuppressionRuleValue[]
+}
+
+export interface ErrorTrackingSuppressionRuleValue {
+    key: '$exception_types' | '$exception_messages'
+    operator: PropertyMatchType
+    value: string | string[]
+    type: string
+}
diff --git a/src/utils/person-property-utils.ts b/src/utils/person-property-utils.ts
deleted file mode 100644
index 43be01b4bc..0000000000
--- a/src/utils/person-property-utils.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { jsonStringify } from '../request'
-import type { Properties } from '../types'
-
-export function getPersonPropertiesHash(
-    distinct_id: string,
-    userPropertiesToSet?: Properties,
-    userPropertiesToSetOnce?: Properties
-): string {
-    return jsonStringify({ distinct_id, userPropertiesToSet, userPropertiesToSetOnce })
-}
diff --git a/src/utils/property-utils.ts b/src/utils/property-utils.ts
new file mode 100644
index 0000000000..c318943b61
--- /dev/null
+++ b/src/utils/property-utils.ts
@@ -0,0 +1,24 @@
+import { jsonStringify } from '../request'
+import type { Properties, PropertyMatchType } from '../types'
+import { isMatchingRegex } from './regex-utils'
+
+export function getPersonPropertiesHash(
+    distinct_id: string,
+    userPropertiesToSet?: Properties,
+    userPropertiesToSetOnce?: Properties
+): string {
+    return jsonStringify({ distinct_id, userPropertiesToSet, userPropertiesToSetOnce })
+}
+
+export const propertyComparisons: Record<PropertyMatchType, (targets: string[], values: string[]) => boolean> = {
+    exact: (targets, values) => values.some((value) => targets.some((target) => value === target)),
+    is_not: (targets, values) => values.every((value) => targets.every((target) => value !== target)),
+    regex: (targets, values) => values.some((value) => targets.some((target) => isMatchingRegex(value, target))),
+    not_regex: (targets, values) => values.every((value) => targets.every((target) => !isMatchingRegex(value, target))),
+    icontains: (targets, values) =>
+        values.map(toLowerCase).some((value) => targets.map(toLowerCase).some((target) => value.includes(target))),
+    not_icontains: (targets, values) =>
+        values.map(toLowerCase).every((value) => targets.map(toLowerCase).every((target) => !value.includes(target))),
+}
+
+const toLowerCase = (v: string): string => v.toLowerCase()
diff --git a/terser-mangled-names.json b/terser-mangled-names.json
index f2c930a8e4..4730293859 100644
--- a/terser-mangled-names.json
+++ b/terser-mangled-names.json
@@ -157,6 +157,7 @@
         "_matchUTMConditions",
         "_matchUrlConditions",
         "_matchers",
+        "_matchesSuppressionRule",
         "_matchesTestVariant",
         "_maxAllowedValue",
         "_minimumDuration",
@@ -275,6 +276,7 @@
         "_storage",
         "_storageKey",
         "_storedConsent",
+        "_suppressionRules",
         "_surveyCallbacks",
         "_surveyInFocus",
         "_surveyManager",


Reviews:
user: login: greptile-apps[bot]
body: 
## PR Summary

This PR adds exception suppression rules to the PostHog JavaScript library, allowing filtering of exceptions before capture based on configurable matching criteria.

- Added `propertyComparisons` in `src/utils/property-utils.ts` to handle string matching with support for exact, regex, and case-insensitive contains operations
- Added `ERROR_TRACKING_SUPPRESSION_RULES` constant and related types for configuring exception filtering
- Consolidated property matching by replacing survey-specific validation with shared utility in `src/extensions/surveys/surveys-extension-utils.tsx`
- Added `onRemoteConfig` handling in `posthog-exceptions.ts` to load suppression rules from remote configuration
- Moved `getPersonPropertiesHash` from `person-property-utils.ts` to `property-utils.ts` for better code organization

<sub>10 file(s) reviewed, 10 comment(s)</sub>
<sub>[Edit PR Review Bot Settings](https://app.greptile.com/review/github) | [Greptile](https://greptile.com?utm_source=greptile_expert&utm_medium=github&utm_campaign=code_reviews&utm_content=posthog-js_1937)</sub>
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T09:01:14Z
user: login: oliverb123
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-19T11:45:56Z

Review comments:
diff_hunk: @@ -13,6 +13,7 @@ export const EVENT_TIMERS_KEY = '__timers'
 export const AUTOCAPTURE_DISABLED_SERVER_SIDE = '$autocapture_disabled_server_side'
 export const HEATMAPS_ENABLED_SERVER_SIDE = '$heatmaps_enabled_server_side'
 export const EXCEPTION_CAPTURE_ENABLED_SERVER_SIDE = '$exception_capture_enabled_server_side'
+export const ERROR_TRACKING_SUPPRESSION_RULES = '$error_tracking_suppression_rules'
path: src/constants.ts
user: login: greptile-apps[bot]
body: logic: This constant should be added to PERSISTENCE_RESERVED_PROPERTIES array since it's a property that shouldn't be automatically included in events
created_at: 2025-05-07T08:59:45Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,120 @@
+import { ERROR_TRACKING_SUPPRESSION_RULES } from '../constants'
+import { PostHog } from '../posthog-core'
+import { PostHogExceptions } from '../posthog-exceptions'
+import { PostHogPersistence } from '../posthog-persistence'
+import {
+    DecideResponse,
+    ErrorTrackingSuppressionRule,
+    ErrorTrackingSuppressionRuleValue,
+    PostHogConfig,
+    Property,
+} from '../types'
+
+function createSuppressionRule(
+    type: 'AND' | 'OR' = 'OR',
+    values: ErrorTrackingSuppressionRuleValue[] = [
+        {
+            key: '$exception_types',
+            value: ['TypeError', 'ReactError'],
+            operator: 'exact',
+            type: 'error_tracking_issue_property',
+        },
+        {
+            key: '$exception_messages',
+            value: 'ReactMinified',
+            operator: 'icontains',
+            type: 'error_tracking_issue_property',
+        },
+    ]
+): ErrorTrackingSuppressionRule {
+    return { type, values }
+}
+
+describe('PostHogExceptions', () => {
+    const captureMock = jest.fn()
+    let posthog: PostHog
+    let exceptions: PostHogExceptions
+    let config: PostHogConfig
+
+    beforeEach(() => {
+        config = { persistence: 'memory' } as unknown as PostHogConfig
+
+        const postHogPersistence = new PostHogPersistence(config)
+        postHogPersistence.clear()
+
+        // TODO: we really need to make this a real posthog instance :cry:
+        posthog = {
+            get_property: (property_key: string): Property | undefined => {
+                return postHogPersistence?.props[property_key]
+            },
+            config: config,
+            capture: captureMock,
+            persistence: postHogPersistence,
+        } as Partial<PostHog> as PostHog
path: src/__tests__/posthog-exceptions.test.ts
user: login: greptile-apps[bot]
body: logic: Mock implementation is incomplete - missing critical PostHog instance methods that may affect suppression rule behavior
created_at: 2025-05-07T08:59:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,120 @@
+import { ERROR_TRACKING_SUPPRESSION_RULES } from '../constants'
+import { PostHog } from '../posthog-core'
+import { PostHogExceptions } from '../posthog-exceptions'
+import { PostHogPersistence } from '../posthog-persistence'
+import {
+    DecideResponse,
+    ErrorTrackingSuppressionRule,
+    ErrorTrackingSuppressionRuleValue,
+    PostHogConfig,
+    Property,
+} from '../types'
+
+function createSuppressionRule(
+    type: 'AND' | 'OR' = 'OR',
+    values: ErrorTrackingSuppressionRuleValue[] = [
+        {
+            key: '$exception_types',
+            value: ['TypeError', 'ReactError'],
+            operator: 'exact',
+            type: 'error_tracking_issue_property',
+        },
+        {
+            key: '$exception_messages',
+            value: 'ReactMinified',
+            operator: 'icontains',
+            type: 'error_tracking_issue_property',
+        },
+    ]
+): ErrorTrackingSuppressionRule {
+    return { type, values }
+}
+
+describe('PostHogExceptions', () => {
+    const captureMock = jest.fn()
+    let posthog: PostHog
+    let exceptions: PostHogExceptions
+    let config: PostHogConfig
+
+    beforeEach(() => {
+        config = { persistence: 'memory' } as unknown as PostHogConfig
+
+        const postHogPersistence = new PostHogPersistence(config)
+        postHogPersistence.clear()
+
+        // TODO: we really need to make this a real posthog instance :cry:
+        posthog = {
+            get_property: (property_key: string): Property | undefined => {
+                return postHogPersistence?.props[property_key]
+            },
+            config: config,
+            capture: captureMock,
+            persistence: postHogPersistence,
+        } as Partial<PostHog> as PostHog
+
+        // defaults
+        posthog.persistence?.register({
+            [ERROR_TRACKING_SUPPRESSION_RULES]: [],
+        })
+
+        exceptions = new PostHogExceptions(posthog)
+    })
+
+    afterEach(() => {
+        captureMock.mockClear()
+    })
+
+    describe('onRemoteConfig', () => {
+        it('persists the suppression rules', () => {
+            const suppressionRule = createSuppressionRule()
+            const decideResponse: Partial<DecideResponse> = { errorTracking: { suppressionRules: [suppressionRule] } }
+            exceptions.onRemoteConfig(decideResponse as DecideResponse)
+            expect(exceptions['_suppressionRules']).toEqual([suppressionRule])
+        })
+    })
+
+    describe('sendExceptionEvent', () => {
+        it('captures the event when no suppression rules are provided', () => {
+            exceptions.sendExceptionEvent({ custom_property: true })
+            expect(captureMock).toBeCalledWith('$exception', { custom_property: true }, expect.anything())
+        })
+
+        test.each([
+            ['TypeError', 'This is a type error'],
+            ['GenericError', 'This is a message that contains a ReactMinified error'],
+        ])('drops the event if a suppression rule matches', (type, value) => {
+            const suppressionRule = createSuppressionRule('OR')
+            exceptions.onRemoteConfig({ errorTracking: { suppressionRules: [suppressionRule] } } as DecideResponse)
+            exceptions.sendExceptionEvent({ $exception_list: [{ type, value }] })
+            expect(captureMock).not.toBeCalled()
+        })
+
+        it('captures an exception if no $exception_list property exists', () => {
+            const suppressionRule = createSuppressionRule('AND')
+            exceptions.onRemoteConfig({ errorTracking: { suppressionRules: [suppressionRule] } } as DecideResponse)
+            exceptions.sendExceptionEvent({ custom_property: true })
+            expect(captureMock).toBeCalled()
+        })
+
+        it('captures an exception if all rule conditions do not match', () => {
+            const suppressionRule = createSuppressionRule('AND')
+            exceptions.onRemoteConfig({ errorTracking: { suppressionRules: [suppressionRule] } } as DecideResponse)
+            exceptions.sendExceptionEvent({ $exception_list: [{ type: 'TypeError', value: 'This is a type error' }] })
+            expect(captureMock).toBeCalled()
+        })
path: src/__tests__/posthog-exceptions.test.ts
user: login: greptile-apps[bot]
body: logic: Test description suggests testing 'AND' logic but the test data will still match one of the default rules (TypeError). This test is not actually verifying the intended behavior

```suggestion
        it('captures an exception if all rule conditions do not match', () => {
            const suppressionRule = createSuppressionRule('AND')
            exceptions.onRemoteConfig({ errorTracking: { suppressionRules: [suppressionRule] } } as DecideResponse)
            exceptions.sendExceptionEvent({ $exception_list: [{ type: 'OtherError', value: 'Some other error' }] })
            expect(captureMock).toBeCalled()
        })
```
created_at: 2025-05-07T08:59:51Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,120 @@
+import { ERROR_TRACKING_SUPPRESSION_RULES } from '../constants'
+import { PostHog } from '../posthog-core'
+import { PostHogExceptions } from '../posthog-exceptions'
+import { PostHogPersistence } from '../posthog-persistence'
+import {
+    DecideResponse,
+    ErrorTrackingSuppressionRule,
+    ErrorTrackingSuppressionRuleValue,
+    PostHogConfig,
+    Property,
+} from '../types'
+
+function createSuppressionRule(
+    type: 'AND' | 'OR' = 'OR',
+    values: ErrorTrackingSuppressionRuleValue[] = [
+        {
+            key: '$exception_types',
+            value: ['TypeError', 'ReactError'],
+            operator: 'exact',
+            type: 'error_tracking_issue_property',
+        },
+        {
+            key: '$exception_messages',
+            value: 'ReactMinified',
+            operator: 'icontains',
+            type: 'error_tracking_issue_property',
+        },
+    ]
+): ErrorTrackingSuppressionRule {
+    return { type, values }
+}
+
+describe('PostHogExceptions', () => {
+    const captureMock = jest.fn()
+    let posthog: PostHog
+    let exceptions: PostHogExceptions
+    let config: PostHogConfig
+
+    beforeEach(() => {
+        config = { persistence: 'memory' } as unknown as PostHogConfig
+
+        const postHogPersistence = new PostHogPersistence(config)
+        postHogPersistence.clear()
+
+        // TODO: we really need to make this a real posthog instance :cry:
+        posthog = {
+            get_property: (property_key: string): Property | undefined => {
+                return postHogPersistence?.props[property_key]
+            },
+            config: config,
+            capture: captureMock,
+            persistence: postHogPersistence,
+        } as Partial<PostHog> as PostHog
+
+        // defaults
+        posthog.persistence?.register({
+            [ERROR_TRACKING_SUPPRESSION_RULES]: [],
+        })
+
+        exceptions = new PostHogExceptions(posthog)
+    })
+
+    afterEach(() => {
+        captureMock.mockClear()
+    })
+
+    describe('onRemoteConfig', () => {
+        it('persists the suppression rules', () => {
+            const suppressionRule = createSuppressionRule()
+            const decideResponse: Partial<DecideResponse> = { errorTracking: { suppressionRules: [suppressionRule] } }
+            exceptions.onRemoteConfig(decideResponse as DecideResponse)
+            expect(exceptions['_suppressionRules']).toEqual([suppressionRule])
+        })
+    })
+
+    describe('sendExceptionEvent', () => {
+        it('captures the event when no suppression rules are provided', () => {
+            exceptions.sendExceptionEvent({ custom_property: true })
+            expect(captureMock).toBeCalledWith('$exception', { custom_property: true }, expect.anything())
+        })
+
+        test.each([
+            ['TypeError', 'This is a type error'],
+            ['GenericError', 'This is a message that contains a ReactMinified error'],
+        ])('drops the event if a suppression rule matches', (type, value) => {
+            const suppressionRule = createSuppressionRule('OR')
+            exceptions.onRemoteConfig({ errorTracking: { suppressionRules: [suppressionRule] } } as DecideResponse)
+            exceptions.sendExceptionEvent({ $exception_list: [{ type, value }] })
+            expect(captureMock).not.toBeCalled()
+        })
+
+        it('captures an exception if no $exception_list property exists', () => {
+            const suppressionRule = createSuppressionRule('AND')
+            exceptions.onRemoteConfig({ errorTracking: { suppressionRules: [suppressionRule] } } as DecideResponse)
+            exceptions.sendExceptionEvent({ custom_property: true })
+            expect(captureMock).toBeCalled()
+        })
+
+        it('captures an exception if all rule conditions do not match', () => {
+            const suppressionRule = createSuppressionRule('AND')
+            exceptions.onRemoteConfig({ errorTracking: { suppressionRules: [suppressionRule] } } as DecideResponse)
+            exceptions.sendExceptionEvent({ $exception_list: [{ type: 'TypeError', value: 'This is a type error' }] })
+            expect(captureMock).toBeCalled()
+        })
+
+        it('captures an exception if there are no targets on the rule', () => {
+            const suppressionRule = createSuppressionRule('OR', [
+                {
+                    key: '$exception_types',
+                    value: [],
+                    operator: 'exact',
+                    type: 'error_tracking_issue_property',
+                },
+            ])
+            exceptions.onRemoteConfig({ errorTracking: { suppressionRules: [suppressionRule] } } as DecideResponse)
+            exceptions.sendExceptionEvent({ custom_property: true })
+            expect(captureMock).toBeCalled()
+        })
path: src/__tests__/posthog-exceptions.test.ts
user: login: greptile-apps[bot]
body: logic: Test sends event without $exception_list but claims to test empty targets. Should include $exception_list to properly test empty target behavior

```suggestion
        it('captures an exception if there are no targets on the rule', () => {
            const suppressionRule = createSuppressionRule('OR', [
                {
                    key: '$exception_types',
                    value: [],
                    operator: 'exact',
                    type: 'error_tracking_issue_property',
                },
            ])
            exceptions.onRemoteConfig({ errorTracking: { suppressionRules: [suppressionRule] } } as DecideResponse)
            exceptions.sendExceptionEvent({ $exception_list: [{ type: 'SomeError', value: 'Some error' }], custom_property: true })
            expect(captureMock).toBeCalled()
        })
```
created_at: 2025-05-07T08:59:52Z
author_association: CONTRIBUTOR
diff_hunk: @@ -413,4 +413,51 @@ describe('utils', () => {
             expect(hash).toContain('New York')
         })
     })
+
+    describe('propertyComparisons', () => {
+        const exactCases: [string[], string[], boolean][] = [
+            [[], ['one'], false], // no targets
+            [['one', 'two', 'three'], [], false], // no values
+            [['one', 'two', 'three'], ['one'], true], // one match
+            [['one', 'two', 'three'], ['one', 'does not match'], true], // one match, one not matching
+            [['one', 'two', 'three'], ['one', 'two'], true], // multiple matches
+            [['one', 'two', 'three'], ['no', 'matches'], false], // no matches
+        ]
path: src/__tests__/utils.test.ts
user: login: greptile-apps[bot]
body: logic: Test cases don't verify behavior when targets or values are null/undefined, which could occur in production
created_at: 2025-05-07T09:00:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -413,4 +413,51 @@ describe('utils', () => {
             expect(hash).toContain('New York')
         })
     })
+
+    describe('propertyComparisons', () => {
+        const exactCases: [string[], string[], boolean][] = [
+            [[], ['one'], false], // no targets
+            [['one', 'two', 'three'], [], false], // no values
+            [['one', 'two', 'three'], ['one'], true], // one match
+            [['one', 'two', 'three'], ['one', 'does not match'], true], // one match, one not matching
+            [['one', 'two', 'three'], ['one', 'two'], true], // multiple matches
+            [['one', 'two', 'three'], ['no', 'matches'], false], // no matches
+        ]
+        test.each(exactCases)('exact', (targets, values, expected) => {
+            expect(propertyComparisons['exact'](targets, values)).toBe(expected)
+        })
+        test.each(exactCases)('is_not', (targets, values, expected) => {
+            expect(propertyComparisons['is_not'](targets, values)).toBe(!expected)
+        })
+
+        const regexCases: [string[], string[], boolean][] = [
+            [[], ['one'], false], // no targets
+            [['^one$', '^two$', '^three$'], [], false], // no values
+            [['^one$', '^two$', '^three$'], ['one'], true], // one match
+            [['^one$', '^two$', '^three$'], ['one', 'does not match'], true], // one match, one not matching
+            [['^one$', '^two$', '^three$'], ['one', 'two'], true], // multiple matches
+            [['^one$', '^two$', '^three$'], ['no', 'matches'], false], // no matches
+            [['^one$', '^two$', '^three$'], ['ones'], false], // partial match
+        ]
path: src/__tests__/utils.test.ts
user: login: greptile-apps[bot]
body: logic: Test cases don't verify behavior with invalid regex patterns that could throw exceptions
created_at: 2025-05-07T09:00:03Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,16 +1,66 @@
+import { ERROR_TRACKING_SUPPRESSION_RULES } from './constants'
 import { PostHog } from './posthog-core'
-import { Properties } from './types'
+import { ErrorTrackingSuppressionRule, Properties, RemoteConfig } from './types'
+import { createLogger } from './utils/logger'
+import { propertyComparisons } from './utils/property-utils'
+import { isArray } from './utils/type-utils'
+
+const logger = createLogger('[Error tracking]')
 
 export class PostHogExceptions {
-    constructor(private readonly _instance: PostHog) {}
+    private readonly _instance: PostHog
+    private _suppressionRules: ErrorTrackingSuppressionRule[] = []
+
+    constructor(instance: PostHog) {
+        this._instance = instance
+        this._suppressionRules = this._instance.persistence?.get_property(ERROR_TRACKING_SUPPRESSION_RULES) ?? []
+    }
+
+    onRemoteConfig(response: RemoteConfig) {
+        const suppressionRules = response.errorTracking?.suppressionRules ?? []
+
+        // store this in-memory in case persistence is disabled
+        this._suppressionRules = suppressionRules
+
+        if (this._instance.persistence) {
+            this._instance.persistence.register({
+                [ERROR_TRACKING_SUPPRESSION_RULES]: this._suppressionRules,
+            })
+        }
+    }
 
-    /**
-     * :TRICKY: Make sure we batch these requests
-     */
     sendExceptionEvent(properties: Properties) {
+        if (this._matchesSuppressionRule(properties)) {
+            logger.info('Skipping exception capture because a suppression rule matched')
+            return
+        }
+
         this._instance.capture('$exception', properties, {
             _noTruncate: true,
             _batchKey: 'exceptionEvent',
         })
     }
+
+    private _matchesSuppressionRule(properties: Properties): boolean {
+        const exceptionList = properties.$exception_list
+
+        if (!exceptionList || !isArray(exceptionList) || exceptionList.length === 0) {
+            return false
+        }
+
+        const exceptionValues = {
+            $exception_types: exceptionList.map((x) => x.type),
+            $exception_messages: exceptionList.map((x) => x.value),
+        }
+
+        return this._suppressionRules.some((rule) => {
+            const results = rule.values.map((v) => {
+                const compare = propertyComparisons[v.operator]
path: src/posthog-exceptions.ts
user: login: greptile-apps[bot]
body: logic: No null check on propertyComparisons[v.operator]. If an invalid operator is provided, this will crash.

```suggestion
                const compare = propertyComparisons[v.operator] ?? (() => false)
```
created_at: 2025-05-07T09:00:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,16 +1,66 @@
+import { ERROR_TRACKING_SUPPRESSION_RULES } from './constants'
 import { PostHog } from './posthog-core'
-import { Properties } from './types'
+import { ErrorTrackingSuppressionRule, Properties, RemoteConfig } from './types'
+import { createLogger } from './utils/logger'
+import { propertyComparisons } from './utils/property-utils'
+import { isArray } from './utils/type-utils'
+
+const logger = createLogger('[Error tracking]')
 
 export class PostHogExceptions {
-    constructor(private readonly _instance: PostHog) {}
+    private readonly _instance: PostHog
+    private _suppressionRules: ErrorTrackingSuppressionRule[] = []
+
+    constructor(instance: PostHog) {
+        this._instance = instance
+        this._suppressionRules = this._instance.persistence?.get_property(ERROR_TRACKING_SUPPRESSION_RULES) ?? []
+    }
+
+    onRemoteConfig(response: RemoteConfig) {
+        const suppressionRules = response.errorTracking?.suppressionRules ?? []
+
+        // store this in-memory in case persistence is disabled
+        this._suppressionRules = suppressionRules
+
+        if (this._instance.persistence) {
+            this._instance.persistence.register({
+                [ERROR_TRACKING_SUPPRESSION_RULES]: this._suppressionRules,
+            })
+        }
+    }
 
-    /**
-     * :TRICKY: Make sure we batch these requests
-     */
     sendExceptionEvent(properties: Properties) {
+        if (this._matchesSuppressionRule(properties)) {
+            logger.info('Skipping exception capture because a suppression rule matched')
+            return
+        }
+
         this._instance.capture('$exception', properties, {
             _noTruncate: true,
             _batchKey: 'exceptionEvent',
         })
     }
+
+    private _matchesSuppressionRule(properties: Properties): boolean {
+        const exceptionList = properties.$exception_list
+
+        if (!exceptionList || !isArray(exceptionList) || exceptionList.length === 0) {
+            return false
+        }
+
+        const exceptionValues = {
+            $exception_types: exceptionList.map((x) => x.type),
+            $exception_messages: exceptionList.map((x) => x.value),
+        }
+
+        return this._suppressionRules.some((rule) => {
+            const results = rule.values.map((v) => {
+                const compare = propertyComparisons[v.operator]
+                const targets = isArray(v.value) ? v.value : [v.value]
+                const values = exceptionValues[v.key]
path: src/posthog-exceptions.ts
user: login: greptile-apps[bot]
body: logic: exceptionValues[v.key] could be undefined if key is not $exception_types or $exception_messages. Add type safety or validation.

```suggestion
                const values = v.key in exceptionValues ? exceptionValues[v.key as keyof typeof exceptionValues] : []
```
created_at: 2025-05-07T09:00:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,16 +1,66 @@
+import { ERROR_TRACKING_SUPPRESSION_RULES } from './constants'
 import { PostHog } from './posthog-core'
-import { Properties } from './types'
+import { ErrorTrackingSuppressionRule, Properties, RemoteConfig } from './types'
+import { createLogger } from './utils/logger'
+import { propertyComparisons } from './utils/property-utils'
+import { isArray } from './utils/type-utils'
+
+const logger = createLogger('[Error tracking]')
 
 export class PostHogExceptions {
-    constructor(private readonly _instance: PostHog) {}
+    private readonly _instance: PostHog
+    private _suppressionRules: ErrorTrackingSuppressionRule[] = []
+
+    constructor(instance: PostHog) {
+        this._instance = instance
+        this._suppressionRules = this._instance.persistence?.get_property(ERROR_TRACKING_SUPPRESSION_RULES) ?? []
+    }
+
+    onRemoteConfig(response: RemoteConfig) {
+        const suppressionRules = response.errorTracking?.suppressionRules ?? []
+
+        // store this in-memory in case persistence is disabled
+        this._suppressionRules = suppressionRules
+
+        if (this._instance.persistence) {
+            this._instance.persistence.register({
+                [ERROR_TRACKING_SUPPRESSION_RULES]: this._suppressionRules,
+            })
+        }
+    }
 
-    /**
-     * :TRICKY: Make sure we batch these requests
-     */
     sendExceptionEvent(properties: Properties) {
+        if (this._matchesSuppressionRule(properties)) {
+            logger.info('Skipping exception capture because a suppression rule matched')
+            return
+        }
+
         this._instance.capture('$exception', properties, {
             _noTruncate: true,
             _batchKey: 'exceptionEvent',
         })
     }
+
+    private _matchesSuppressionRule(properties: Properties): boolean {
+        const exceptionList = properties.$exception_list
+
+        if (!exceptionList || !isArray(exceptionList) || exceptionList.length === 0) {
+            return false
+        }
+
+        const exceptionValues = {
+            $exception_types: exceptionList.map((x) => x.type),
+            $exception_messages: exceptionList.map((x) => x.value),
+        }
path: src/posthog-exceptions.ts
user: login: greptile-apps[bot]
body: logic: Map operations could throw if exceptionList items don't have expected shape. Add type checking for x.type and x.value.
created_at: 2025-05-07T09:00:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,22 @@
+import { jsonStringify } from '../request'
+import type { Properties, PropertyMatchType } from '../types'
+import { isMatchingRegex } from './regex-utils'
+
+export function getPersonPropertiesHash(
+    distinct_id: string,
+    userPropertiesToSet?: Properties,
+    userPropertiesToSetOnce?: Properties
+): string {
+    return jsonStringify({ distinct_id, userPropertiesToSet, userPropertiesToSetOnce })
+}
+
+export const propertyComparisons: Record<PropertyMatchType, (targets: string[], values: string[]) => boolean> = {
+    exact: (targets, values) => values.some((value) => targets.some((target) => value === target)),
+    is_not: (targets, values) => values.every((value) => targets.every((target) => value !== target)),
+    regex: (targets, values) => values.some((value) => targets.some((target) => isMatchingRegex(value, target))),
+    not_regex: (targets, values) => values.every((value) => targets.every((target) => !isMatchingRegex(value, target))),
+    icontains: (targets, values) =>
+        values.some((value) => targets.some((target) => value.toLowerCase().includes(target.toLowerCase()))),
+    not_icontains: (targets, values) =>
+        values.every((value) => targets.every((target) => !value.toLowerCase().includes(target.toLowerCase()))),
path: src/utils/property-utils.ts
user: login: greptile-apps[bot]
body: style: toLowerCase() called multiple times on same strings. Consider caching the lowercase values
created_at: 2025-05-07T09:00:55Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-05-06T18:26:37Z
body: [vc]: #C1cBq7yGVpJcEhGT4dlCFDaCctz6CUrLZM5DNvzxAlk=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJwb3N0aG9nLWpzIiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3Bvc3QtaG9nL3Bvc3Rob2ctanMvRGI1WjlCVXJpQmNBaHQ3WHlycmFZMlF5WDY1ZSIsInByZXZpZXdVcmwiOiJwb3N0aG9nLWpzLWdpdC1kbi1mZWF0LXN1cHByZXNzaW9uLW1hdGNoaW5nLXBvc3QtaG9nLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiREVQTE9ZRUQiLCJyb290RGlyZWN0b3J5IjoicGxheWdyb3VuZC9uZXh0anMifV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Updated (UTC) |
| :--- | :----- | :------ | :------ |
| **posthog-js** | ✅ Ready ([Inspect](https://vercel.com/post-hog/posthog-js/Db5Z9BUriBcAht7XyrraY2QyX65e)) | [Visit Preview](https://posthog-js-git-dn-feat-suppression-matching-post-hog.vercel.app) | May 19, 2025 10:11am |


author_association: NONE
user: login: github-actions[bot]
created_at: 2025-05-06T18:29:20Z
body: [Download bundle size visualization](https://github.com/PostHog/posthog-js/actions/runs/15110159245/artifacts/3150555505)
author_association: CONTRIBUTOR
user: login: github-actions[bot]
created_at: 2025-05-06T18:34:03Z
body: **Size Change:** +12.6 kB (+0.37%) 

**Total Size:** 3.4 MB

| Filename | Size | Change |
| :--- | :---: | :---: |
| `dist/all-external-dependencies.js` | 215 kB | +115 B (+0.05%) |
| `dist/array.full.es5.js` | 296 kB | +1.36 kB (+0.46%) |
| `dist/array.full.js` | 361 kB | +1.06 kB (+0.29%) |
| `dist/array.full.no-external.js` | 360 kB | +1.06 kB (+0.3%) |
| `dist/array.js` | 166 kB | +1.34 kB (+0.81%) |
| `dist/array.no-external.js` | 164 kB | +1.34 kB (+0.82%) |
| `dist/main.js` | 167 kB | +1.34 kB (+0.81%) |
| `dist/module.full.js` | 361 kB | +1.06 kB (+0.29%) |
| `dist/module.full.no-external.js` | 360 kB | +1.06 kB (+0.3%) |
| `dist/module.js` | 166 kB | +1.34 kB (+0.81%) |
| `dist/module.no-external.js` | 165 kB | +1.34 kB (+0.82%) |
| `dist/surveys-preview.js` | 66.4 kB | +128 B (+0.19%) |
| `dist/surveys.js` | 73.5 kB | +115 B (+0.16%) |

<details><summary>ℹ️ <strong>View Unchanged</strong></summary>

| Filename | Size |
| :--- | :---: |
| `dist/customizations.full.js` | 11 kB |
| `dist/dead-clicks-autocapture.js` | 12.7 kB |
| `dist/exception-autocapture.js` | 9.63 kB |
| `dist/external-scripts-loader.js` | 2.63 kB |
| `dist/posthog-recorder.js` | 207 kB |
| `dist/recorder-v2.js` | 113 kB |
| `dist/recorder.js` | 113 kB |
| `dist/tracing-headers.js` | 1.64 kB |
| `dist/web-vitals.js` | 10.4 kB |

</details>



<a href="https://github.com/preactjs/compressed-size-action"><sub>compressed-size-action</sub></a>
author_association: CONTRIBUTOR
user: login: posthog-bot
created_at: 2025-05-15T09:35:02Z
body: This PR hasn't seen activity in a week! Should it be merged, closed, or further worked on? If you want to keep it open, post a comment or remove the `stale` label – otherwise this will be closed in another week.
author_association: CONTRIBUTOR