@tracelog/lib 0.11.2 → 0.11.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.
- package/dist/public-api.cjs +4387 -0
- package/dist/public-api.cjs.map +1 -0
- package/dist/public-api.d.mts +913 -0
- package/dist/public-api.d.ts +913 -0
- package/dist/public-api.js +4348 -0
- package/dist/public-api.js.map +1 -0
- package/package.json +18 -18
- package/dist/browser/tracelog.esm.js +0 -2983
- package/dist/browser/tracelog.esm.js.map +0 -1
- package/dist/browser/tracelog.js +0 -8
- package/dist/browser/tracelog.js.map +0 -1
- package/dist/cjs/api.d.ts +0 -19
- package/dist/cjs/api.d.ts.map +0 -1
- package/dist/cjs/api.js +0 -183
- package/dist/cjs/api.js.map +0 -1
- package/dist/cjs/app.constants.d.ts +0 -80
- package/dist/cjs/app.constants.d.ts.map +0 -1
- package/dist/cjs/app.constants.js +0 -94
- package/dist/cjs/app.constants.js.map +0 -1
- package/dist/cjs/app.d.ts +0 -43
- package/dist/cjs/app.d.ts.map +0 -1
- package/dist/cjs/app.js +0 -165
- package/dist/cjs/app.js.map +0 -1
- package/dist/cjs/constants/config.constants.d.ts +0 -109
- package/dist/cjs/constants/config.constants.d.ts.map +0 -1
- package/dist/cjs/constants/config.constants.js +0 -216
- package/dist/cjs/constants/config.constants.js.map +0 -1
- package/dist/cjs/constants/error.constants.d.ts +0 -56
- package/dist/cjs/constants/error.constants.d.ts.map +0 -1
- package/dist/cjs/constants/error.constants.js +0 -89
- package/dist/cjs/constants/error.constants.js.map +0 -1
- package/dist/cjs/constants/index.d.ts +0 -5
- package/dist/cjs/constants/index.d.ts.map +0 -1
- package/dist/cjs/constants/index.js +0 -21
- package/dist/cjs/constants/index.js.map +0 -1
- package/dist/cjs/constants/performance.constants.d.ts +0 -29
- package/dist/cjs/constants/performance.constants.d.ts.map +0 -1
- package/dist/cjs/constants/performance.constants.js +0 -44
- package/dist/cjs/constants/performance.constants.js.map +0 -1
- package/dist/cjs/constants/storage.constants.d.ts +0 -11
- package/dist/cjs/constants/storage.constants.d.ts.map +0 -1
- package/dist/cjs/constants/storage.constants.js +0 -23
- package/dist/cjs/constants/storage.constants.js.map +0 -1
- package/dist/cjs/handlers/click.handler.d.ts +0 -36
- package/dist/cjs/handlers/click.handler.d.ts.map +0 -1
- package/dist/cjs/handlers/click.handler.js +0 -263
- package/dist/cjs/handlers/click.handler.js.map +0 -1
- package/dist/cjs/handlers/error.handler.d.ts +0 -28
- package/dist/cjs/handlers/error.handler.d.ts.map +0 -1
- package/dist/cjs/handlers/error.handler.js +0 -168
- package/dist/cjs/handlers/error.handler.js.map +0 -1
- package/dist/cjs/handlers/page-view.handler.d.ts +0 -17
- package/dist/cjs/handlers/page-view.handler.d.ts.map +0 -1
- package/dist/cjs/handlers/page-view.handler.js +0 -100
- package/dist/cjs/handlers/page-view.handler.js.map +0 -1
- package/dist/cjs/handlers/performance.handler.d.ts +0 -23
- package/dist/cjs/handlers/performance.handler.d.ts.map +0 -1
- package/dist/cjs/handlers/performance.handler.js +0 -274
- package/dist/cjs/handlers/performance.handler.js.map +0 -1
- package/dist/cjs/handlers/scroll.handler.d.ts +0 -40
- package/dist/cjs/handlers/scroll.handler.d.ts.map +0 -1
- package/dist/cjs/handlers/scroll.handler.js +0 -327
- package/dist/cjs/handlers/scroll.handler.js.map +0 -1
- package/dist/cjs/handlers/session.handler.d.ts +0 -16
- package/dist/cjs/handlers/session.handler.d.ts.map +0 -1
- package/dist/cjs/handlers/session.handler.js +0 -74
- package/dist/cjs/handlers/session.handler.js.map +0 -1
- package/dist/cjs/handlers/viewport.handler.d.ts +0 -44
- package/dist/cjs/handlers/viewport.handler.d.ts.map +0 -1
- package/dist/cjs/handlers/viewport.handler.js +0 -286
- package/dist/cjs/handlers/viewport.handler.js.map +0 -1
- package/dist/cjs/integrations/google-analytics.integration.d.ts +0 -18
- package/dist/cjs/integrations/google-analytics.integration.d.ts.map +0 -1
- package/dist/cjs/integrations/google-analytics.integration.js +0 -90
- package/dist/cjs/integrations/google-analytics.integration.js.map +0 -1
- package/dist/cjs/listeners/activity-listener-manager.d.ts +0 -9
- package/dist/cjs/listeners/activity-listener-manager.d.ts.map +0 -1
- package/dist/cjs/listeners/activity-listener-manager.js +0 -33
- package/dist/cjs/listeners/activity-listener-manager.js.map +0 -1
- package/dist/cjs/listeners/index.d.ts +0 -7
- package/dist/cjs/listeners/index.d.ts.map +0 -1
- package/dist/cjs/listeners/index.js +0 -15
- package/dist/cjs/listeners/index.js.map +0 -1
- package/dist/cjs/listeners/input-listener-managers.d.ts +0 -25
- package/dist/cjs/listeners/input-listener-managers.d.ts.map +0 -1
- package/dist/cjs/listeners/input-listener-managers.js +0 -50
- package/dist/cjs/listeners/input-listener-managers.js.map +0 -1
- package/dist/cjs/listeners/listeners.types.d.ts +0 -5
- package/dist/cjs/listeners/listeners.types.d.ts.map +0 -1
- package/dist/cjs/listeners/listeners.types.js +0 -3
- package/dist/cjs/listeners/listeners.types.js.map +0 -1
- package/dist/cjs/listeners/touch-listener-manager.d.ts +0 -9
- package/dist/cjs/listeners/touch-listener-manager.d.ts.map +0 -1
- package/dist/cjs/listeners/touch-listener-manager.js +0 -35
- package/dist/cjs/listeners/touch-listener-manager.js.map +0 -1
- package/dist/cjs/listeners/unload-listener-manager.d.ts +0 -9
- package/dist/cjs/listeners/unload-listener-manager.d.ts.map +0 -1
- package/dist/cjs/listeners/unload-listener-manager.js +0 -31
- package/dist/cjs/listeners/unload-listener-manager.js.map +0 -1
- package/dist/cjs/listeners/visibility-listener-manager.d.ts +0 -10
- package/dist/cjs/listeners/visibility-listener-manager.d.ts.map +0 -1
- package/dist/cjs/listeners/visibility-listener-manager.js +0 -48
- package/dist/cjs/listeners/visibility-listener-manager.js.map +0 -1
- package/dist/cjs/managers/event.manager.d.ts +0 -62
- package/dist/cjs/managers/event.manager.d.ts.map +0 -1
- package/dist/cjs/managers/event.manager.js +0 -508
- package/dist/cjs/managers/event.manager.js.map +0 -1
- package/dist/cjs/managers/sender.manager.d.ts +0 -32
- package/dist/cjs/managers/sender.manager.d.ts.map +0 -1
- package/dist/cjs/managers/sender.manager.js +0 -271
- package/dist/cjs/managers/sender.manager.js.map +0 -1
- package/dist/cjs/managers/session.manager.d.ts +0 -40
- package/dist/cjs/managers/session.manager.d.ts.map +0 -1
- package/dist/cjs/managers/session.manager.js +0 -282
- package/dist/cjs/managers/session.manager.js.map +0 -1
- package/dist/cjs/managers/state.manager.d.ts +0 -9
- package/dist/cjs/managers/state.manager.d.ts.map +0 -1
- package/dist/cjs/managers/state.manager.js +0 -27
- package/dist/cjs/managers/state.manager.js.map +0 -1
- package/dist/cjs/managers/storage.manager.d.ts +0 -60
- package/dist/cjs/managers/storage.manager.d.ts.map +0 -1
- package/dist/cjs/managers/storage.manager.js +0 -277
- package/dist/cjs/managers/storage.manager.js.map +0 -1
- package/dist/cjs/managers/user.manager.d.ts +0 -17
- package/dist/cjs/managers/user.manager.d.ts.map +0 -1
- package/dist/cjs/managers/user.manager.js +0 -31
- package/dist/cjs/managers/user.manager.js.map +0 -1
- package/dist/cjs/public-api.d.ts +0 -11
- package/dist/cjs/public-api.d.ts.map +0 -1
- package/dist/cjs/public-api.js +0 -32
- package/dist/cjs/public-api.js.map +0 -1
- package/dist/cjs/test-bridge.d.ts +0 -47
- package/dist/cjs/test-bridge.d.ts.map +0 -1
- package/dist/cjs/test-bridge.js +0 -165
- package/dist/cjs/test-bridge.js.map +0 -1
- package/dist/cjs/types/common.types.d.ts +0 -6
- package/dist/cjs/types/common.types.d.ts.map +0 -1
- package/dist/cjs/types/common.types.js +0 -3
- package/dist/cjs/types/common.types.js.map +0 -1
- package/dist/cjs/types/config.types.d.ts +0 -49
- package/dist/cjs/types/config.types.d.ts.map +0 -1
- package/dist/cjs/types/config.types.js +0 -9
- package/dist/cjs/types/config.types.js.map +0 -1
- package/dist/cjs/types/device.types.d.ts +0 -7
- package/dist/cjs/types/device.types.d.ts.map +0 -1
- package/dist/cjs/types/device.types.js +0 -11
- package/dist/cjs/types/device.types.js.map +0 -1
- package/dist/cjs/types/emitter.types.d.ts +0 -12
- package/dist/cjs/types/emitter.types.d.ts.map +0 -1
- package/dist/cjs/types/emitter.types.js +0 -9
- package/dist/cjs/types/emitter.types.js.map +0 -1
- package/dist/cjs/types/error.types.d.ts +0 -12
- package/dist/cjs/types/error.types.d.ts.map +0 -1
- package/dist/cjs/types/error.types.js +0 -23
- package/dist/cjs/types/error.types.js.map +0 -1
- package/dist/cjs/types/event.types.d.ts +0 -235
- package/dist/cjs/types/event.types.d.ts.map +0 -1
- package/dist/cjs/types/event.types.js +0 -48
- package/dist/cjs/types/event.types.js.map +0 -1
- package/dist/cjs/types/index.d.ts +0 -17
- package/dist/cjs/types/index.d.ts.map +0 -1
- package/dist/cjs/types/index.js +0 -33
- package/dist/cjs/types/index.js.map +0 -1
- package/dist/cjs/types/log.types.d.ts +0 -5
- package/dist/cjs/types/log.types.d.ts.map +0 -1
- package/dist/cjs/types/log.types.js +0 -3
- package/dist/cjs/types/log.types.js.map +0 -1
- package/dist/cjs/types/mode.types.d.ts +0 -7
- package/dist/cjs/types/mode.types.d.ts.map +0 -1
- package/dist/cjs/types/mode.types.js +0 -11
- package/dist/cjs/types/mode.types.js.map +0 -1
- package/dist/cjs/types/queue.types.d.ts +0 -19
- package/dist/cjs/types/queue.types.d.ts.map +0 -1
- package/dist/cjs/types/queue.types.js +0 -3
- package/dist/cjs/types/queue.types.js.map +0 -1
- package/dist/cjs/types/scroll.types.d.ts +0 -16
- package/dist/cjs/types/scroll.types.d.ts.map +0 -1
- package/dist/cjs/types/scroll.types.js +0 -12
- package/dist/cjs/types/scroll.types.js.map +0 -1
- package/dist/cjs/types/session.types.d.ts +0 -2
- package/dist/cjs/types/session.types.d.ts.map +0 -1
- package/dist/cjs/types/session.types.js +0 -3
- package/dist/cjs/types/session.types.js.map +0 -1
- package/dist/cjs/types/state.types.d.ts +0 -16
- package/dist/cjs/types/state.types.d.ts.map +0 -1
- package/dist/cjs/types/state.types.js +0 -3
- package/dist/cjs/types/state.types.js.map +0 -1
- package/dist/cjs/types/test-bridge.types.d.ts +0 -40
- package/dist/cjs/types/test-bridge.types.d.ts.map +0 -1
- package/dist/cjs/types/test-bridge.types.js +0 -3
- package/dist/cjs/types/test-bridge.types.js.map +0 -1
- package/dist/cjs/types/validation-error.types.d.ts +0 -44
- package/dist/cjs/types/validation-error.types.d.ts.map +0 -1
- package/dist/cjs/types/validation-error.types.js +0 -70
- package/dist/cjs/types/validation-error.types.js.map +0 -1
- package/dist/cjs/types/viewport.types.d.ts +0 -55
- package/dist/cjs/types/viewport.types.d.ts.map +0 -1
- package/dist/cjs/types/viewport.types.js +0 -3
- package/dist/cjs/types/viewport.types.js.map +0 -1
- package/dist/cjs/types/window.types.d.ts +0 -16
- package/dist/cjs/types/window.types.d.ts.map +0 -1
- package/dist/cjs/types/window.types.js +0 -3
- package/dist/cjs/types/window.types.js.map +0 -1
- package/dist/cjs/utils/browser/device-detector.utils.d.ts +0 -7
- package/dist/cjs/utils/browser/device-detector.utils.d.ts.map +0 -1
- package/dist/cjs/utils/browser/device-detector.utils.js +0 -50
- package/dist/cjs/utils/browser/device-detector.utils.js.map +0 -1
- package/dist/cjs/utils/browser/index.d.ts +0 -4
- package/dist/cjs/utils/browser/index.d.ts.map +0 -1
- package/dist/cjs/utils/browser/index.js +0 -20
- package/dist/cjs/utils/browser/index.js.map +0 -1
- package/dist/cjs/utils/browser/qa-mode.utils.d.ts +0 -14
- package/dist/cjs/utils/browser/qa-mode.utils.d.ts.map +0 -1
- package/dist/cjs/utils/browser/qa-mode.utils.js +0 -44
- package/dist/cjs/utils/browser/qa-mode.utils.js.map +0 -1
- package/dist/cjs/utils/browser/utm-params.utils.d.ts +0 -7
- package/dist/cjs/utils/browser/utm-params.utils.d.ts.map +0 -1
- package/dist/cjs/utils/browser/utm-params.utils.js +0 -23
- package/dist/cjs/utils/browser/utm-params.utils.js.map +0 -1
- package/dist/cjs/utils/data/index.d.ts +0 -2
- package/dist/cjs/utils/data/index.d.ts.map +0 -1
- package/dist/cjs/utils/data/index.js +0 -18
- package/dist/cjs/utils/data/index.js.map +0 -1
- package/dist/cjs/utils/data/uuid.utils.d.ts +0 -19
- package/dist/cjs/utils/data/uuid.utils.d.ts.map +0 -1
- package/dist/cjs/utils/data/uuid.utils.js +0 -57
- package/dist/cjs/utils/data/uuid.utils.js.map +0 -1
- package/dist/cjs/utils/emitter.utils.d.ts +0 -9
- package/dist/cjs/utils/emitter.utils.d.ts.map +0 -1
- package/dist/cjs/utils/emitter.utils.js +0 -36
- package/dist/cjs/utils/emitter.utils.js.map +0 -1
- package/dist/cjs/utils/index.d.ts +0 -8
- package/dist/cjs/utils/index.d.ts.map +0 -1
- package/dist/cjs/utils/index.js +0 -24
- package/dist/cjs/utils/index.js.map +0 -1
- package/dist/cjs/utils/logging.utils.d.ts +0 -22
- package/dist/cjs/utils/logging.utils.d.ts.map +0 -1
- package/dist/cjs/utils/logging.utils.js +0 -87
- package/dist/cjs/utils/logging.utils.js.map +0 -1
- package/dist/cjs/utils/network/index.d.ts +0 -2
- package/dist/cjs/utils/network/index.d.ts.map +0 -1
- package/dist/cjs/utils/network/index.js +0 -18
- package/dist/cjs/utils/network/index.js.map +0 -1
- package/dist/cjs/utils/network/url.utils.d.ts +0 -16
- package/dist/cjs/utils/network/url.utils.d.ts.map +0 -1
- package/dist/cjs/utils/network/url.utils.js +0 -92
- package/dist/cjs/utils/network/url.utils.js.map +0 -1
- package/dist/cjs/utils/security/index.d.ts +0 -2
- package/dist/cjs/utils/security/index.d.ts.map +0 -1
- package/dist/cjs/utils/security/index.js +0 -18
- package/dist/cjs/utils/security/index.js.map +0 -1
- package/dist/cjs/utils/security/sanitize.utils.d.ts +0 -14
- package/dist/cjs/utils/security/sanitize.utils.d.ts.map +0 -1
- package/dist/cjs/utils/security/sanitize.utils.js +0 -123
- package/dist/cjs/utils/security/sanitize.utils.js.map +0 -1
- package/dist/cjs/utils/validations/config-validations.utils.d.ts +0 -19
- package/dist/cjs/utils/validations/config-validations.utils.d.ts.map +0 -1
- package/dist/cjs/utils/validations/config-validations.utils.js +0 -236
- package/dist/cjs/utils/validations/config-validations.utils.js.map +0 -1
- package/dist/cjs/utils/validations/event-validations.utils.d.ts +0 -13
- package/dist/cjs/utils/validations/event-validations.utils.d.ts.map +0 -1
- package/dist/cjs/utils/validations/event-validations.utils.js +0 -37
- package/dist/cjs/utils/validations/event-validations.utils.js.map +0 -1
- package/dist/cjs/utils/validations/index.d.ts +0 -5
- package/dist/cjs/utils/validations/index.d.ts.map +0 -1
- package/dist/cjs/utils/validations/index.js +0 -21
- package/dist/cjs/utils/validations/index.js.map +0 -1
- package/dist/cjs/utils/validations/metadata-validations.utils.d.ts +0 -23
- package/dist/cjs/utils/validations/metadata-validations.utils.d.ts.map +0 -1
- package/dist/cjs/utils/validations/metadata-validations.utils.js +0 -153
- package/dist/cjs/utils/validations/metadata-validations.utils.js.map +0 -1
- package/dist/cjs/utils/validations/type-guards.utils.d.ts +0 -9
- package/dist/cjs/utils/validations/type-guards.utils.d.ts.map +0 -1
- package/dist/cjs/utils/validations/type-guards.utils.js +0 -90
- package/dist/cjs/utils/validations/type-guards.utils.js.map +0 -1
- package/dist/esm/api.d.ts +0 -19
- package/dist/esm/api.d.ts.map +0 -1
- package/dist/esm/api.js +0 -173
- package/dist/esm/api.js.map +0 -1
- package/dist/esm/app.constants.d.ts +0 -80
- package/dist/esm/app.constants.d.ts.map +0 -1
- package/dist/esm/app.constants.js +0 -82
- package/dist/esm/app.constants.js.map +0 -1
- package/dist/esm/app.d.ts +0 -43
- package/dist/esm/app.d.ts.map +0 -1
- package/dist/esm/app.js +0 -161
- package/dist/esm/app.js.map +0 -1
- package/dist/esm/constants/config.constants.d.ts +0 -109
- package/dist/esm/constants/config.constants.d.ts.map +0 -1
- package/dist/esm/constants/config.constants.js +0 -212
- package/dist/esm/constants/config.constants.js.map +0 -1
- package/dist/esm/constants/error.constants.d.ts +0 -56
- package/dist/esm/constants/error.constants.d.ts.map +0 -1
- package/dist/esm/constants/error.constants.js +0 -86
- package/dist/esm/constants/error.constants.js.map +0 -1
- package/dist/esm/constants/index.d.ts +0 -5
- package/dist/esm/constants/index.d.ts.map +0 -1
- package/dist/esm/constants/index.js +0 -5
- package/dist/esm/constants/index.js.map +0 -1
- package/dist/esm/constants/performance.constants.d.ts +0 -29
- package/dist/esm/constants/performance.constants.d.ts.map +0 -1
- package/dist/esm/constants/performance.constants.js +0 -41
- package/dist/esm/constants/performance.constants.js.map +0 -1
- package/dist/esm/constants/storage.constants.d.ts +0 -11
- package/dist/esm/constants/storage.constants.d.ts.map +0 -1
- package/dist/esm/constants/storage.constants.js +0 -13
- package/dist/esm/constants/storage.constants.js.map +0 -1
- package/dist/esm/handlers/click.handler.d.ts +0 -36
- package/dist/esm/handlers/click.handler.d.ts.map +0 -1
- package/dist/esm/handlers/click.handler.js +0 -259
- package/dist/esm/handlers/click.handler.js.map +0 -1
- package/dist/esm/handlers/error.handler.d.ts +0 -28
- package/dist/esm/handlers/error.handler.d.ts.map +0 -1
- package/dist/esm/handlers/error.handler.js +0 -164
- package/dist/esm/handlers/error.handler.js.map +0 -1
- package/dist/esm/handlers/page-view.handler.d.ts +0 -17
- package/dist/esm/handlers/page-view.handler.d.ts.map +0 -1
- package/dist/esm/handlers/page-view.handler.js +0 -96
- package/dist/esm/handlers/page-view.handler.js.map +0 -1
- package/dist/esm/handlers/performance.handler.d.ts +0 -23
- package/dist/esm/handlers/performance.handler.d.ts.map +0 -1
- package/dist/esm/handlers/performance.handler.js +0 -237
- package/dist/esm/handlers/performance.handler.js.map +0 -1
- package/dist/esm/handlers/scroll.handler.d.ts +0 -40
- package/dist/esm/handlers/scroll.handler.d.ts.map +0 -1
- package/dist/esm/handlers/scroll.handler.js +0 -323
- package/dist/esm/handlers/scroll.handler.js.map +0 -1
- package/dist/esm/handlers/session.handler.d.ts +0 -16
- package/dist/esm/handlers/session.handler.d.ts.map +0 -1
- package/dist/esm/handlers/session.handler.js +0 -70
- package/dist/esm/handlers/session.handler.js.map +0 -1
- package/dist/esm/handlers/viewport.handler.d.ts +0 -44
- package/dist/esm/handlers/viewport.handler.d.ts.map +0 -1
- package/dist/esm/handlers/viewport.handler.js +0 -282
- package/dist/esm/handlers/viewport.handler.js.map +0 -1
- package/dist/esm/integrations/google-analytics.integration.d.ts +0 -18
- package/dist/esm/integrations/google-analytics.integration.d.ts.map +0 -1
- package/dist/esm/integrations/google-analytics.integration.js +0 -86
- package/dist/esm/integrations/google-analytics.integration.js.map +0 -1
- package/dist/esm/listeners/activity-listener-manager.d.ts +0 -9
- package/dist/esm/listeners/activity-listener-manager.d.ts.map +0 -1
- package/dist/esm/listeners/activity-listener-manager.js +0 -29
- package/dist/esm/listeners/activity-listener-manager.js.map +0 -1
- package/dist/esm/listeners/index.d.ts +0 -7
- package/dist/esm/listeners/index.d.ts.map +0 -1
- package/dist/esm/listeners/index.js +0 -6
- package/dist/esm/listeners/index.js.map +0 -1
- package/dist/esm/listeners/input-listener-managers.d.ts +0 -25
- package/dist/esm/listeners/input-listener-managers.d.ts.map +0 -1
- package/dist/esm/listeners/input-listener-managers.js +0 -45
- package/dist/esm/listeners/input-listener-managers.js.map +0 -1
- package/dist/esm/listeners/listeners.types.d.ts +0 -5
- package/dist/esm/listeners/listeners.types.d.ts.map +0 -1
- package/dist/esm/listeners/listeners.types.js +0 -2
- package/dist/esm/listeners/listeners.types.js.map +0 -1
- package/dist/esm/listeners/touch-listener-manager.d.ts +0 -9
- package/dist/esm/listeners/touch-listener-manager.d.ts.map +0 -1
- package/dist/esm/listeners/touch-listener-manager.js +0 -31
- package/dist/esm/listeners/touch-listener-manager.js.map +0 -1
- package/dist/esm/listeners/unload-listener-manager.d.ts +0 -9
- package/dist/esm/listeners/unload-listener-manager.d.ts.map +0 -1
- package/dist/esm/listeners/unload-listener-manager.js +0 -27
- package/dist/esm/listeners/unload-listener-manager.js.map +0 -1
- package/dist/esm/listeners/visibility-listener-manager.d.ts +0 -10
- package/dist/esm/listeners/visibility-listener-manager.d.ts.map +0 -1
- package/dist/esm/listeners/visibility-listener-manager.js +0 -44
- package/dist/esm/listeners/visibility-listener-manager.js.map +0 -1
- package/dist/esm/managers/event.manager.d.ts +0 -62
- package/dist/esm/managers/event.manager.d.ts.map +0 -1
- package/dist/esm/managers/event.manager.js +0 -504
- package/dist/esm/managers/event.manager.js.map +0 -1
- package/dist/esm/managers/sender.manager.d.ts +0 -32
- package/dist/esm/managers/sender.manager.d.ts.map +0 -1
- package/dist/esm/managers/sender.manager.js +0 -267
- package/dist/esm/managers/sender.manager.js.map +0 -1
- package/dist/esm/managers/session.manager.d.ts +0 -40
- package/dist/esm/managers/session.manager.d.ts.map +0 -1
- package/dist/esm/managers/session.manager.js +0 -278
- package/dist/esm/managers/session.manager.js.map +0 -1
- package/dist/esm/managers/state.manager.d.ts +0 -9
- package/dist/esm/managers/state.manager.d.ts.map +0 -1
- package/dist/esm/managers/state.manager.js +0 -21
- package/dist/esm/managers/state.manager.js.map +0 -1
- package/dist/esm/managers/storage.manager.d.ts +0 -60
- package/dist/esm/managers/storage.manager.d.ts.map +0 -1
- package/dist/esm/managers/storage.manager.js +0 -273
- package/dist/esm/managers/storage.manager.js.map +0 -1
- package/dist/esm/managers/user.manager.d.ts +0 -17
- package/dist/esm/managers/user.manager.d.ts.map +0 -1
- package/dist/esm/managers/user.manager.js +0 -27
- package/dist/esm/managers/user.manager.js.map +0 -1
- package/dist/esm/public-api.d.ts +0 -11
- package/dist/esm/public-api.d.ts.map +0 -1
- package/dist/esm/public-api.js +0 -15
- package/dist/esm/public-api.js.map +0 -1
- package/dist/esm/test-bridge.d.ts +0 -47
- package/dist/esm/test-bridge.d.ts.map +0 -1
- package/dist/esm/test-bridge.js +0 -161
- package/dist/esm/test-bridge.js.map +0 -1
- package/dist/esm/types/common.types.d.ts +0 -6
- package/dist/esm/types/common.types.d.ts.map +0 -1
- package/dist/esm/types/common.types.js +0 -2
- package/dist/esm/types/common.types.js.map +0 -1
- package/dist/esm/types/config.types.d.ts +0 -49
- package/dist/esm/types/config.types.d.ts.map +0 -1
- package/dist/esm/types/config.types.js +0 -6
- package/dist/esm/types/config.types.js.map +0 -1
- package/dist/esm/types/device.types.d.ts +0 -7
- package/dist/esm/types/device.types.d.ts.map +0 -1
- package/dist/esm/types/device.types.js +0 -8
- package/dist/esm/types/device.types.js.map +0 -1
- package/dist/esm/types/emitter.types.d.ts +0 -12
- package/dist/esm/types/emitter.types.d.ts.map +0 -1
- package/dist/esm/types/emitter.types.js +0 -6
- package/dist/esm/types/emitter.types.js.map +0 -1
- package/dist/esm/types/error.types.d.ts +0 -12
- package/dist/esm/types/error.types.d.ts.map +0 -1
- package/dist/esm/types/error.types.js +0 -19
- package/dist/esm/types/error.types.js.map +0 -1
- package/dist/esm/types/event.types.d.ts +0 -235
- package/dist/esm/types/event.types.d.ts.map +0 -1
- package/dist/esm/types/event.types.js +0 -45
- package/dist/esm/types/event.types.js.map +0 -1
- package/dist/esm/types/index.d.ts +0 -17
- package/dist/esm/types/index.d.ts.map +0 -1
- package/dist/esm/types/index.js +0 -17
- package/dist/esm/types/index.js.map +0 -1
- package/dist/esm/types/log.types.d.ts +0 -5
- package/dist/esm/types/log.types.d.ts.map +0 -1
- package/dist/esm/types/log.types.js +0 -2
- package/dist/esm/types/log.types.js.map +0 -1
- package/dist/esm/types/mode.types.d.ts +0 -7
- package/dist/esm/types/mode.types.d.ts.map +0 -1
- package/dist/esm/types/mode.types.js +0 -8
- package/dist/esm/types/mode.types.js.map +0 -1
- package/dist/esm/types/queue.types.d.ts +0 -19
- package/dist/esm/types/queue.types.d.ts.map +0 -1
- package/dist/esm/types/queue.types.js +0 -2
- package/dist/esm/types/queue.types.js.map +0 -1
- package/dist/esm/types/scroll.types.d.ts +0 -16
- package/dist/esm/types/scroll.types.d.ts.map +0 -1
- package/dist/esm/types/scroll.types.js +0 -8
- package/dist/esm/types/scroll.types.js.map +0 -1
- package/dist/esm/types/session.types.d.ts +0 -2
- package/dist/esm/types/session.types.d.ts.map +0 -1
- package/dist/esm/types/session.types.js +0 -2
- package/dist/esm/types/session.types.js.map +0 -1
- package/dist/esm/types/state.types.d.ts +0 -16
- package/dist/esm/types/state.types.d.ts.map +0 -1
- package/dist/esm/types/state.types.js +0 -2
- package/dist/esm/types/state.types.js.map +0 -1
- package/dist/esm/types/test-bridge.types.d.ts +0 -40
- package/dist/esm/types/test-bridge.types.d.ts.map +0 -1
- package/dist/esm/types/test-bridge.types.js +0 -2
- package/dist/esm/types/test-bridge.types.js.map +0 -1
- package/dist/esm/types/validation-error.types.d.ts +0 -44
- package/dist/esm/types/validation-error.types.d.ts.map +0 -1
- package/dist/esm/types/validation-error.types.js +0 -61
- package/dist/esm/types/validation-error.types.js.map +0 -1
- package/dist/esm/types/viewport.types.d.ts +0 -55
- package/dist/esm/types/viewport.types.d.ts.map +0 -1
- package/dist/esm/types/viewport.types.js +0 -2
- package/dist/esm/types/viewport.types.js.map +0 -1
- package/dist/esm/types/window.types.d.ts +0 -16
- package/dist/esm/types/window.types.d.ts.map +0 -1
- package/dist/esm/types/window.types.js +0 -2
- package/dist/esm/types/window.types.js.map +0 -1
- package/dist/esm/utils/browser/device-detector.utils.d.ts +0 -7
- package/dist/esm/utils/browser/device-detector.utils.d.ts.map +0 -1
- package/dist/esm/utils/browser/device-detector.utils.js +0 -46
- package/dist/esm/utils/browser/device-detector.utils.js.map +0 -1
- package/dist/esm/utils/browser/index.d.ts +0 -4
- package/dist/esm/utils/browser/index.d.ts.map +0 -1
- package/dist/esm/utils/browser/index.js +0 -4
- package/dist/esm/utils/browser/index.js.map +0 -1
- package/dist/esm/utils/browser/qa-mode.utils.d.ts +0 -14
- package/dist/esm/utils/browser/qa-mode.utils.d.ts.map +0 -1
- package/dist/esm/utils/browser/qa-mode.utils.js +0 -40
- package/dist/esm/utils/browser/qa-mode.utils.js.map +0 -1
- package/dist/esm/utils/browser/utm-params.utils.d.ts +0 -7
- package/dist/esm/utils/browser/utm-params.utils.d.ts.map +0 -1
- package/dist/esm/utils/browser/utm-params.utils.js +0 -19
- package/dist/esm/utils/browser/utm-params.utils.js.map +0 -1
- package/dist/esm/utils/data/index.d.ts +0 -2
- package/dist/esm/utils/data/index.d.ts.map +0 -1
- package/dist/esm/utils/data/index.js +0 -2
- package/dist/esm/utils/data/index.js.map +0 -1
- package/dist/esm/utils/data/uuid.utils.d.ts +0 -19
- package/dist/esm/utils/data/uuid.utils.d.ts.map +0 -1
- package/dist/esm/utils/data/uuid.utils.js +0 -52
- package/dist/esm/utils/data/uuid.utils.js.map +0 -1
- package/dist/esm/utils/emitter.utils.d.ts +0 -9
- package/dist/esm/utils/emitter.utils.d.ts.map +0 -1
- package/dist/esm/utils/emitter.utils.js +0 -32
- package/dist/esm/utils/emitter.utils.js.map +0 -1
- package/dist/esm/utils/index.d.ts +0 -8
- package/dist/esm/utils/index.d.ts.map +0 -1
- package/dist/esm/utils/index.js +0 -8
- package/dist/esm/utils/index.js.map +0 -1
- package/dist/esm/utils/logging.utils.d.ts +0 -22
- package/dist/esm/utils/logging.utils.d.ts.map +0 -1
- package/dist/esm/utils/logging.utils.js +0 -82
- package/dist/esm/utils/logging.utils.js.map +0 -1
- package/dist/esm/utils/network/index.d.ts +0 -2
- package/dist/esm/utils/network/index.d.ts.map +0 -1
- package/dist/esm/utils/network/index.js +0 -2
- package/dist/esm/utils/network/index.js.map +0 -1
- package/dist/esm/utils/network/url.utils.d.ts +0 -16
- package/dist/esm/utils/network/url.utils.d.ts.map +0 -1
- package/dist/esm/utils/network/url.utils.js +0 -87
- package/dist/esm/utils/network/url.utils.js.map +0 -1
- package/dist/esm/utils/security/index.d.ts +0 -2
- package/dist/esm/utils/security/index.d.ts.map +0 -1
- package/dist/esm/utils/security/index.js +0 -2
- package/dist/esm/utils/security/index.js.map +0 -1
- package/dist/esm/utils/security/sanitize.utils.d.ts +0 -14
- package/dist/esm/utils/security/sanitize.utils.d.ts.map +0 -1
- package/dist/esm/utils/security/sanitize.utils.js +0 -118
- package/dist/esm/utils/security/sanitize.utils.js.map +0 -1
- package/dist/esm/utils/validations/config-validations.utils.d.ts +0 -19
- package/dist/esm/utils/validations/config-validations.utils.d.ts.map +0 -1
- package/dist/esm/utils/validations/config-validations.utils.js +0 -231
- package/dist/esm/utils/validations/config-validations.utils.js.map +0 -1
- package/dist/esm/utils/validations/event-validations.utils.d.ts +0 -13
- package/dist/esm/utils/validations/event-validations.utils.d.ts.map +0 -1
- package/dist/esm/utils/validations/event-validations.utils.js +0 -33
- package/dist/esm/utils/validations/event-validations.utils.js.map +0 -1
- package/dist/esm/utils/validations/index.d.ts +0 -5
- package/dist/esm/utils/validations/index.d.ts.map +0 -1
- package/dist/esm/utils/validations/index.js +0 -5
- package/dist/esm/utils/validations/index.js.map +0 -1
- package/dist/esm/utils/validations/metadata-validations.utils.d.ts +0 -23
- package/dist/esm/utils/validations/metadata-validations.utils.d.ts.map +0 -1
- package/dist/esm/utils/validations/metadata-validations.utils.js +0 -148
- package/dist/esm/utils/validations/metadata-validations.utils.js.map +0 -1
- package/dist/esm/utils/validations/type-guards.utils.d.ts +0 -9
- package/dist/esm/utils/validations/type-guards.utils.d.ts.map +0 -1
- package/dist/esm/utils/validations/type-guards.utils.js +0 -86
- package/dist/esm/utils/validations/type-guards.utils.js.map +0 -1
|
@@ -0,0 +1,4387 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// src/constants/config.constants.ts
|
|
4
|
+
var DEFAULT_SESSION_TIMEOUT = 15 * 60 * 1e3;
|
|
5
|
+
var DUPLICATE_EVENT_THRESHOLD_MS = 500;
|
|
6
|
+
var EVENT_SENT_INTERVAL_MS = 1e4;
|
|
7
|
+
var SCROLL_DEBOUNCE_TIME_MS = 250;
|
|
8
|
+
var DEFAULT_VISIBILITY_TIMEOUT_MS = 2e3;
|
|
9
|
+
var DEFAULT_PAGE_VIEW_THROTTLE_MS = 1e3;
|
|
10
|
+
var DEFAULT_CLICK_THROTTLE_MS = 300;
|
|
11
|
+
var DEFAULT_VIEWPORT_COOLDOWN_PERIOD = 6e4;
|
|
12
|
+
var DEFAULT_VIEWPORT_MAX_TRACKED_ELEMENTS = 100;
|
|
13
|
+
var VIEWPORT_MUTATION_DEBOUNCE_MS = 100;
|
|
14
|
+
var MAX_THROTTLE_CACHE_ENTRIES = 1e3;
|
|
15
|
+
var THROTTLE_ENTRY_TTL_MS = 3e5;
|
|
16
|
+
var THROTTLE_PRUNE_INTERVAL_MS = 3e4;
|
|
17
|
+
var EVENT_EXPIRY_HOURS = 2;
|
|
18
|
+
var MAX_EVENTS_QUEUE_LENGTH = 100;
|
|
19
|
+
var REQUEST_TIMEOUT_MS = 1e4;
|
|
20
|
+
var SIGNIFICANT_SCROLL_DELTA = 10;
|
|
21
|
+
var MIN_SCROLL_DEPTH_CHANGE = 5;
|
|
22
|
+
var SCROLL_MIN_EVENT_INTERVAL_MS = 500;
|
|
23
|
+
var MAX_SCROLL_EVENTS_PER_SESSION = 120;
|
|
24
|
+
var DEFAULT_SAMPLING_RATE = 1;
|
|
25
|
+
var RATE_LIMIT_WINDOW_MS = 1e3;
|
|
26
|
+
var MAX_EVENTS_PER_SECOND = 50;
|
|
27
|
+
var MAX_SAME_EVENT_PER_MINUTE = 60;
|
|
28
|
+
var PER_EVENT_RATE_LIMIT_WINDOW_MS = 6e4;
|
|
29
|
+
var MAX_EVENTS_PER_SESSION = 1e3;
|
|
30
|
+
var MAX_CLICKS_PER_SESSION = 500;
|
|
31
|
+
var MAX_PAGE_VIEWS_PER_SESSION = 100;
|
|
32
|
+
var MAX_CUSTOM_EVENTS_PER_SESSION = 500;
|
|
33
|
+
var MAX_VIEWPORT_EVENTS_PER_SESSION = 200;
|
|
34
|
+
var BATCH_SIZE_THRESHOLD = 50;
|
|
35
|
+
var MAX_PENDING_EVENTS_BUFFER = 100;
|
|
36
|
+
var MIN_SESSION_TIMEOUT_MS = 3e4;
|
|
37
|
+
var MAX_SESSION_TIMEOUT_MS = 864e5;
|
|
38
|
+
var MAX_CUSTOM_EVENT_NAME_LENGTH = 120;
|
|
39
|
+
var MAX_CUSTOM_EVENT_STRING_SIZE = 8 * 1024;
|
|
40
|
+
var MAX_CUSTOM_EVENT_KEYS = 10;
|
|
41
|
+
var MAX_CUSTOM_EVENT_ARRAY_SIZE = 10;
|
|
42
|
+
var MAX_NESTED_OBJECT_KEYS = 20;
|
|
43
|
+
var MAX_METADATA_NESTING_DEPTH = 1;
|
|
44
|
+
var MAX_TEXT_LENGTH = 255;
|
|
45
|
+
var MAX_STRING_LENGTH = 1e3;
|
|
46
|
+
var MAX_STRING_LENGTH_IN_ARRAY = 500;
|
|
47
|
+
var MAX_ARRAY_LENGTH = 100;
|
|
48
|
+
var MAX_OBJECT_DEPTH = 3;
|
|
49
|
+
var PRECISION_TWO_DECIMALS = 2;
|
|
50
|
+
var MAX_BEACON_PAYLOAD_SIZE = 64 * 1024;
|
|
51
|
+
var MAX_FINGERPRINTS = 1e3;
|
|
52
|
+
var FINGERPRINT_CLEANUP_MULTIPLIER = 10;
|
|
53
|
+
var MAX_FINGERPRINTS_HARD_LIMIT = 2e3;
|
|
54
|
+
var HTML_DATA_ATTR_PREFIX = "data-tlog";
|
|
55
|
+
var INTERACTIVE_SELECTORS = [
|
|
56
|
+
"button",
|
|
57
|
+
"a",
|
|
58
|
+
'input[type="button"]',
|
|
59
|
+
'input[type="submit"]',
|
|
60
|
+
'input[type="reset"]',
|
|
61
|
+
'input[type="checkbox"]',
|
|
62
|
+
'input[type="radio"]',
|
|
63
|
+
"select",
|
|
64
|
+
"textarea",
|
|
65
|
+
'[role="button"]',
|
|
66
|
+
'[role="link"]',
|
|
67
|
+
'[role="tab"]',
|
|
68
|
+
'[role="menuitem"]',
|
|
69
|
+
'[role="option"]',
|
|
70
|
+
'[role="checkbox"]',
|
|
71
|
+
'[role="radio"]',
|
|
72
|
+
'[role="switch"]',
|
|
73
|
+
"[routerLink]",
|
|
74
|
+
"[ng-click]",
|
|
75
|
+
"[data-action]",
|
|
76
|
+
"[data-click]",
|
|
77
|
+
"[data-navigate]",
|
|
78
|
+
"[data-toggle]",
|
|
79
|
+
"[onclick]",
|
|
80
|
+
".btn",
|
|
81
|
+
".button",
|
|
82
|
+
".clickable",
|
|
83
|
+
".nav-link",
|
|
84
|
+
".menu-item",
|
|
85
|
+
"[data-testid]",
|
|
86
|
+
'[tabindex="0"]'
|
|
87
|
+
];
|
|
88
|
+
var UTM_PARAMS = ["utm_source", "utm_medium", "utm_campaign", "utm_term", "utm_content"];
|
|
89
|
+
var DEFAULT_SENSITIVE_QUERY_PARAMS = [
|
|
90
|
+
"token",
|
|
91
|
+
"auth",
|
|
92
|
+
"key",
|
|
93
|
+
"session",
|
|
94
|
+
"reset",
|
|
95
|
+
"password",
|
|
96
|
+
"api_key",
|
|
97
|
+
"apikey",
|
|
98
|
+
"secret",
|
|
99
|
+
"access_token",
|
|
100
|
+
"refresh_token",
|
|
101
|
+
"verification",
|
|
102
|
+
"code",
|
|
103
|
+
"otp"
|
|
104
|
+
];
|
|
105
|
+
var INITIALIZATION_TIMEOUT_MS = 1e4;
|
|
106
|
+
var SCROLL_SUPPRESS_MULTIPLIER = 2;
|
|
107
|
+
var VALIDATION_MESSAGES = {
|
|
108
|
+
INVALID_SESSION_TIMEOUT: `Session timeout must be between ${MIN_SESSION_TIMEOUT_MS}ms (30 seconds) and ${MAX_SESSION_TIMEOUT_MS}ms (24 hours)`,
|
|
109
|
+
INVALID_SAMPLING_RATE: "Sampling rate must be between 0 and 1",
|
|
110
|
+
INVALID_ERROR_SAMPLING_RATE: "Error sampling must be between 0 and 1",
|
|
111
|
+
INVALID_TRACELOG_PROJECT_ID: "TraceLog project ID is required when integration is enabled",
|
|
112
|
+
INVALID_CUSTOM_API_URL: "Custom API URL is required when integration is enabled",
|
|
113
|
+
INVALID_GOOGLE_ANALYTICS_ID: "Google Analytics measurement ID is required when integration is enabled",
|
|
114
|
+
INVALID_GLOBAL_METADATA: "Global metadata must be an object",
|
|
115
|
+
INVALID_SENSITIVE_QUERY_PARAMS: "Sensitive query params must be an array of strings",
|
|
116
|
+
INVALID_PRIMARY_SCROLL_SELECTOR: "Primary scroll selector must be a non-empty string",
|
|
117
|
+
INVALID_PRIMARY_SCROLL_SELECTOR_SYNTAX: "Invalid CSS selector syntax for primaryScrollSelector",
|
|
118
|
+
INVALID_PAGE_VIEW_THROTTLE: "Page view throttle must be a non-negative number",
|
|
119
|
+
INVALID_CLICK_THROTTLE: "Click throttle must be a non-negative number",
|
|
120
|
+
INVALID_MAX_SAME_EVENT_PER_MINUTE: "Max same event per minute must be a positive number",
|
|
121
|
+
INVALID_VIEWPORT_CONFIG: "Viewport config must be an object",
|
|
122
|
+
INVALID_VIEWPORT_ELEMENTS: "Viewport elements must be a non-empty array",
|
|
123
|
+
INVALID_VIEWPORT_ELEMENT: "Each viewport element must have a valid selector string",
|
|
124
|
+
INVALID_VIEWPORT_ELEMENT_ID: "Viewport element id must be a non-empty string",
|
|
125
|
+
INVALID_VIEWPORT_ELEMENT_NAME: "Viewport element name must be a non-empty string",
|
|
126
|
+
INVALID_VIEWPORT_THRESHOLD: "Viewport threshold must be a number between 0 and 1",
|
|
127
|
+
INVALID_VIEWPORT_MIN_DWELL_TIME: "Viewport minDwellTime must be a non-negative number",
|
|
128
|
+
INVALID_VIEWPORT_COOLDOWN_PERIOD: "Viewport cooldownPeriod must be a non-negative number",
|
|
129
|
+
INVALID_VIEWPORT_MAX_TRACKED_ELEMENTS: "Viewport maxTrackedElements must be a positive number"
|
|
130
|
+
};
|
|
131
|
+
var XSS_PATTERNS = [
|
|
132
|
+
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi,
|
|
133
|
+
/javascript:/gi,
|
|
134
|
+
/on\w+\s*=/gi,
|
|
135
|
+
/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi,
|
|
136
|
+
/<embed\b[^>]*>/gi,
|
|
137
|
+
/<object\b[^<]*(?:(?!<\/object>)<[^<]*)*<\/object>/gi
|
|
138
|
+
];
|
|
139
|
+
|
|
140
|
+
// src/types/config.types.ts
|
|
141
|
+
var SpecialApiUrl = /* @__PURE__ */ ((SpecialApiUrl2) => {
|
|
142
|
+
SpecialApiUrl2["Localhost"] = "localhost:8080";
|
|
143
|
+
SpecialApiUrl2["Fail"] = "localhost:9999";
|
|
144
|
+
return SpecialApiUrl2;
|
|
145
|
+
})(SpecialApiUrl || {});
|
|
146
|
+
|
|
147
|
+
// src/types/device.types.ts
|
|
148
|
+
var DeviceType = /* @__PURE__ */ ((DeviceType2) => {
|
|
149
|
+
DeviceType2["Mobile"] = "mobile";
|
|
150
|
+
DeviceType2["Tablet"] = "tablet";
|
|
151
|
+
DeviceType2["Desktop"] = "desktop";
|
|
152
|
+
DeviceType2["Unknown"] = "unknown";
|
|
153
|
+
return DeviceType2;
|
|
154
|
+
})(DeviceType || {});
|
|
155
|
+
|
|
156
|
+
// src/types/emitter.types.ts
|
|
157
|
+
var EmitterEvent = /* @__PURE__ */ ((EmitterEvent2) => {
|
|
158
|
+
EmitterEvent2["EVENT"] = "event";
|
|
159
|
+
EmitterEvent2["QUEUE"] = "queue";
|
|
160
|
+
return EmitterEvent2;
|
|
161
|
+
})(EmitterEvent || {});
|
|
162
|
+
|
|
163
|
+
// src/types/error.types.ts
|
|
164
|
+
var PermanentError = class _PermanentError extends Error {
|
|
165
|
+
constructor(message, statusCode) {
|
|
166
|
+
super(message);
|
|
167
|
+
this.statusCode = statusCode;
|
|
168
|
+
this.name = "PermanentError";
|
|
169
|
+
if (Error.captureStackTrace) {
|
|
170
|
+
Error.captureStackTrace(this, _PermanentError);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// src/types/event.types.ts
|
|
176
|
+
var EventType = /* @__PURE__ */ ((EventType2) => {
|
|
177
|
+
EventType2["PAGE_VIEW"] = "page_view";
|
|
178
|
+
EventType2["CLICK"] = "click";
|
|
179
|
+
EventType2["SCROLL"] = "scroll";
|
|
180
|
+
EventType2["SESSION_START"] = "session_start";
|
|
181
|
+
EventType2["SESSION_END"] = "session_end";
|
|
182
|
+
EventType2["CUSTOM"] = "custom";
|
|
183
|
+
EventType2["WEB_VITALS"] = "web_vitals";
|
|
184
|
+
EventType2["ERROR"] = "error";
|
|
185
|
+
EventType2["VIEWPORT_VISIBLE"] = "viewport_visible";
|
|
186
|
+
return EventType2;
|
|
187
|
+
})(EventType || {});
|
|
188
|
+
var ScrollDirection = /* @__PURE__ */ ((ScrollDirection2) => {
|
|
189
|
+
ScrollDirection2["UP"] = "up";
|
|
190
|
+
ScrollDirection2["DOWN"] = "down";
|
|
191
|
+
return ScrollDirection2;
|
|
192
|
+
})(ScrollDirection || {});
|
|
193
|
+
var ErrorType = /* @__PURE__ */ ((ErrorType2) => {
|
|
194
|
+
ErrorType2["JS_ERROR"] = "js_error";
|
|
195
|
+
ErrorType2["PROMISE_REJECTION"] = "promise_rejection";
|
|
196
|
+
return ErrorType2;
|
|
197
|
+
})(ErrorType || {});
|
|
198
|
+
|
|
199
|
+
// src/types/mode.types.ts
|
|
200
|
+
var Mode = /* @__PURE__ */ ((Mode2) => {
|
|
201
|
+
Mode2["QA"] = "qa";
|
|
202
|
+
return Mode2;
|
|
203
|
+
})(Mode || {});
|
|
204
|
+
|
|
205
|
+
// src/types/scroll.types.ts
|
|
206
|
+
function isPrimaryScrollEvent(event2) {
|
|
207
|
+
return event2.type === "scroll" /* SCROLL */ && "scroll_data" in event2 && event2.scroll_data.is_primary === true;
|
|
208
|
+
}
|
|
209
|
+
function isSecondaryScrollEvent(event2) {
|
|
210
|
+
return event2.type === "scroll" /* SCROLL */ && "scroll_data" in event2 && event2.scroll_data.is_primary === false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/types/validation-error.types.ts
|
|
214
|
+
var TraceLogValidationError = class extends Error {
|
|
215
|
+
constructor(message, errorCode, layer) {
|
|
216
|
+
super(message);
|
|
217
|
+
this.errorCode = errorCode;
|
|
218
|
+
this.layer = layer;
|
|
219
|
+
this.name = this.constructor.name;
|
|
220
|
+
if (Error.captureStackTrace) {
|
|
221
|
+
Error.captureStackTrace(this, this.constructor);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
var AppConfigValidationError = class extends TraceLogValidationError {
|
|
226
|
+
constructor(message, layer = "config") {
|
|
227
|
+
super(message, "APP_CONFIG_INVALID", layer);
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
var SessionTimeoutValidationError = class extends TraceLogValidationError {
|
|
231
|
+
constructor(message, layer = "config") {
|
|
232
|
+
super(message, "SESSION_TIMEOUT_INVALID", layer);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
var SamplingRateValidationError = class extends TraceLogValidationError {
|
|
236
|
+
constructor(message, layer = "config") {
|
|
237
|
+
super(message, "SAMPLING_RATE_INVALID", layer);
|
|
238
|
+
}
|
|
239
|
+
};
|
|
240
|
+
var IntegrationValidationError = class extends TraceLogValidationError {
|
|
241
|
+
constructor(message, layer = "config") {
|
|
242
|
+
super(message, "INTEGRATION_INVALID", layer);
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
var InitializationTimeoutError = class extends TraceLogValidationError {
|
|
246
|
+
constructor(message, timeoutMs, layer = "runtime") {
|
|
247
|
+
super(message, "INITIALIZATION_TIMEOUT", layer);
|
|
248
|
+
this.timeoutMs = timeoutMs;
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// src/utils/logging.utils.ts
|
|
253
|
+
var formatLogMsg = (msg, error) => {
|
|
254
|
+
if (error) {
|
|
255
|
+
if (process.env.NODE_ENV !== "development" && error instanceof Error) {
|
|
256
|
+
const sanitizedMessage = error.message.replace(/\s+at\s+.*$/gm, "").replace(/\(.*?:\d+:\d+\)/g, "");
|
|
257
|
+
return `[TraceLog] ${msg}: ${sanitizedMessage}`;
|
|
258
|
+
}
|
|
259
|
+
return `[TraceLog] ${msg}: ${error instanceof Error ? error.message : "Unknown error"}`;
|
|
260
|
+
}
|
|
261
|
+
return `[TraceLog] ${msg}`;
|
|
262
|
+
};
|
|
263
|
+
var log = (type, msg, extra) => {
|
|
264
|
+
const { error, data, showToClient = false } = extra ?? {};
|
|
265
|
+
const formattedMsg = error ? formatLogMsg(msg, error) : `[TraceLog] ${msg}`;
|
|
266
|
+
const method = type === "error" ? "error" : type === "warn" ? "warn" : "log";
|
|
267
|
+
const isProduction = process.env.NODE_ENV !== "development";
|
|
268
|
+
if (isProduction) {
|
|
269
|
+
if (type === "debug") {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
if (type === "info" && !showToClient) {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (isProduction && data !== void 0) {
|
|
277
|
+
const sanitizedData = sanitizeLogData(data);
|
|
278
|
+
console[method](formattedMsg, sanitizedData);
|
|
279
|
+
} else if (data !== void 0) {
|
|
280
|
+
console[method](formattedMsg, data);
|
|
281
|
+
} else {
|
|
282
|
+
console[method](formattedMsg);
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
var sanitizeLogData = (data) => {
|
|
286
|
+
const sanitized = {};
|
|
287
|
+
const sensitiveKeys = ["token", "password", "secret", "key", "apikey", "api_key", "sessionid", "session_id"];
|
|
288
|
+
for (const [key, value] of Object.entries(data)) {
|
|
289
|
+
const lowerKey = key.toLowerCase();
|
|
290
|
+
if (sensitiveKeys.some((sensitiveKey) => lowerKey.includes(sensitiveKey))) {
|
|
291
|
+
sanitized[key] = "[REDACTED]";
|
|
292
|
+
} else {
|
|
293
|
+
sanitized[key] = value;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return sanitized;
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// src/utils/browser/device-detector.utils.ts
|
|
300
|
+
var coarsePointerQuery;
|
|
301
|
+
var noHoverQuery;
|
|
302
|
+
var initMediaQueries = () => {
|
|
303
|
+
if (typeof window !== "undefined" && !coarsePointerQuery) {
|
|
304
|
+
coarsePointerQuery = window.matchMedia("(pointer: coarse)");
|
|
305
|
+
noHoverQuery = window.matchMedia("(hover: none)");
|
|
306
|
+
}
|
|
307
|
+
};
|
|
308
|
+
var getDeviceType = () => {
|
|
309
|
+
try {
|
|
310
|
+
const nav = navigator;
|
|
311
|
+
if (nav.userAgentData && typeof nav.userAgentData.mobile === "boolean") {
|
|
312
|
+
if (nav.userAgentData.platform && /ipad|tablet/i.test(nav.userAgentData.platform)) {
|
|
313
|
+
return "tablet" /* Tablet */;
|
|
314
|
+
}
|
|
315
|
+
const result = nav.userAgentData.mobile ? "mobile" /* Mobile */ : "desktop" /* Desktop */;
|
|
316
|
+
return result;
|
|
317
|
+
}
|
|
318
|
+
initMediaQueries();
|
|
319
|
+
const width = window.innerWidth;
|
|
320
|
+
const hasCoarsePointer = coarsePointerQuery?.matches ?? false;
|
|
321
|
+
const hasNoHover = noHoverQuery?.matches ?? false;
|
|
322
|
+
const hasTouchSupport = "ontouchstart" in window || navigator.maxTouchPoints > 0;
|
|
323
|
+
const ua = navigator.userAgent.toLowerCase();
|
|
324
|
+
const isMobileUA = /mobile|android|iphone|ipod|blackberry|iemobile|opera mini/.test(ua);
|
|
325
|
+
const isTabletUA = /tablet|ipad|android(?!.*mobile)/.test(ua);
|
|
326
|
+
if (width <= 767 || isMobileUA && hasTouchSupport) {
|
|
327
|
+
return "mobile" /* Mobile */;
|
|
328
|
+
}
|
|
329
|
+
if (width >= 768 && width <= 1024 || isTabletUA || hasCoarsePointer && hasNoHover && hasTouchSupport) {
|
|
330
|
+
return "tablet" /* Tablet */;
|
|
331
|
+
}
|
|
332
|
+
return "desktop" /* Desktop */;
|
|
333
|
+
} catch (error) {
|
|
334
|
+
log("warn", "Device detection failed, defaulting to desktop", { error });
|
|
335
|
+
return "desktop" /* Desktop */;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
// src/constants/storage.constants.ts
|
|
340
|
+
var STORAGE_BASE_KEY = "tlog";
|
|
341
|
+
var QA_MODE_KEY = `${STORAGE_BASE_KEY}:qa_mode`;
|
|
342
|
+
var USER_ID_KEY = `${STORAGE_BASE_KEY}:uid`;
|
|
343
|
+
var QUEUE_KEY = (id) => id ? `${STORAGE_BASE_KEY}:${id}:queue` : `${STORAGE_BASE_KEY}:queue`;
|
|
344
|
+
var SESSION_STORAGE_KEY = (id) => id ? `${STORAGE_BASE_KEY}:${id}:session` : `${STORAGE_BASE_KEY}:session`;
|
|
345
|
+
var BROADCAST_CHANNEL_NAME = (id) => id ? `${STORAGE_BASE_KEY}:${id}:broadcast` : `${STORAGE_BASE_KEY}:broadcast`;
|
|
346
|
+
|
|
347
|
+
// src/constants/performance.constants.ts
|
|
348
|
+
var WEB_VITALS_THRESHOLDS = {
|
|
349
|
+
LCP: 4e3,
|
|
350
|
+
FCP: 1800,
|
|
351
|
+
CLS: 0.25,
|
|
352
|
+
INP: 200,
|
|
353
|
+
TTFB: 800,
|
|
354
|
+
LONG_TASK: 50
|
|
355
|
+
};
|
|
356
|
+
var LONG_TASK_THROTTLE_MS = 1e3;
|
|
357
|
+
var MAX_NAVIGATION_HISTORY = 50;
|
|
358
|
+
|
|
359
|
+
// src/constants/error.constants.ts
|
|
360
|
+
var PII_PATTERNS = [
|
|
361
|
+
// Email addresses
|
|
362
|
+
/\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/gi,
|
|
363
|
+
// US Phone numbers (various formats)
|
|
364
|
+
/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g,
|
|
365
|
+
// Credit card numbers (16 digits with optional separators)
|
|
366
|
+
/\b\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/g,
|
|
367
|
+
// IBAN (International Bank Account Number)
|
|
368
|
+
/\b[A-Z]{2}\d{2}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}[-\s]?\d{4}\b/gi,
|
|
369
|
+
// API keys/tokens (sk_test_, sk_live_, pk_test_, pk_live_, etc.)
|
|
370
|
+
/\b[sp]k_(test|live)_[a-zA-Z0-9]{10,}\b/gi,
|
|
371
|
+
// Bearer tokens (JWT-like patterns - matches complete and partial tokens)
|
|
372
|
+
/Bearer\s+[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)?(?:\.[A-Za-z0-9_-]+)?/gi,
|
|
373
|
+
// Passwords in connection strings (protocol://user:password@host)
|
|
374
|
+
/:\/\/[^:/]+:([^@]+)@/gi
|
|
375
|
+
];
|
|
376
|
+
var MAX_ERROR_MESSAGE_LENGTH = 500;
|
|
377
|
+
var ERROR_SUPPRESSION_WINDOW_MS = 5e3;
|
|
378
|
+
var MAX_TRACKED_ERRORS = 50;
|
|
379
|
+
var MAX_TRACKED_ERRORS_HARD_LIMIT = MAX_TRACKED_ERRORS * 2;
|
|
380
|
+
var DEFAULT_ERROR_SAMPLING_RATE = 1;
|
|
381
|
+
var ERROR_BURST_WINDOW_MS = 1e3;
|
|
382
|
+
var ERROR_BURST_THRESHOLD = 10;
|
|
383
|
+
var ERROR_BURST_BACKOFF_MS = 5e3;
|
|
384
|
+
var PERMANENT_ERROR_LOG_THROTTLE_MS = 6e4;
|
|
385
|
+
|
|
386
|
+
// src/utils/browser/qa-mode.utils.ts
|
|
387
|
+
var QA_MODE_PARAM = "tlog_mode";
|
|
388
|
+
var QA_MODE_VALUE = "qa";
|
|
389
|
+
var detectQaMode = () => {
|
|
390
|
+
const stored = sessionStorage.getItem(QA_MODE_KEY);
|
|
391
|
+
if (stored === "true") {
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
const params = new URLSearchParams(window.location.search);
|
|
395
|
+
const modeParam = params.get(QA_MODE_PARAM);
|
|
396
|
+
const isQaMode = modeParam === QA_MODE_VALUE;
|
|
397
|
+
if (isQaMode) {
|
|
398
|
+
sessionStorage.setItem(QA_MODE_KEY, "true");
|
|
399
|
+
params.delete(QA_MODE_PARAM);
|
|
400
|
+
const newSearch = params.toString();
|
|
401
|
+
const newUrl = `${window.location.pathname}${newSearch ? "?" + newSearch : ""}${window.location.hash}`;
|
|
402
|
+
try {
|
|
403
|
+
window.history.replaceState({}, "", newUrl);
|
|
404
|
+
} catch (error) {
|
|
405
|
+
log("warn", "History API not available, cannot replace URL", { error });
|
|
406
|
+
}
|
|
407
|
+
console.log(
|
|
408
|
+
"%c[TraceLog] QA Mode ACTIVE",
|
|
409
|
+
"background: #ff9800; color: white; font-weight: bold; padding: 2px 8px; border-radius: 3px;"
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
return isQaMode;
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
// src/utils/browser/utm-params.utils.ts
|
|
416
|
+
var getUTMParameters = () => {
|
|
417
|
+
const urlParams = new URLSearchParams(window.location.search);
|
|
418
|
+
const utmParams = {};
|
|
419
|
+
UTM_PARAMS.forEach((param) => {
|
|
420
|
+
const value = urlParams.get(param);
|
|
421
|
+
if (value) {
|
|
422
|
+
const key = param.split("utm_")[1];
|
|
423
|
+
utmParams[key] = value;
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
const result = Object.keys(utmParams).length ? utmParams : void 0;
|
|
427
|
+
return result;
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
// src/utils/data/uuid.utils.ts
|
|
431
|
+
var generateUUID = () => {
|
|
432
|
+
if (typeof crypto !== "undefined" && crypto.randomUUID) {
|
|
433
|
+
return crypto.randomUUID();
|
|
434
|
+
}
|
|
435
|
+
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
|
|
436
|
+
const r = Math.random() * 16 | 0;
|
|
437
|
+
const v = c === "x" ? r : r & 3 | 8;
|
|
438
|
+
return v.toString(16);
|
|
439
|
+
});
|
|
440
|
+
};
|
|
441
|
+
var generateEventId = () => {
|
|
442
|
+
const timestamp = Date.now();
|
|
443
|
+
let random = "";
|
|
444
|
+
try {
|
|
445
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
446
|
+
const bytes = crypto.getRandomValues(new Uint8Array(4));
|
|
447
|
+
if (bytes) {
|
|
448
|
+
random = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
} catch {
|
|
452
|
+
}
|
|
453
|
+
if (!random) {
|
|
454
|
+
random = Math.floor(Math.random() * 4294967295).toString(16).padStart(8, "0");
|
|
455
|
+
}
|
|
456
|
+
return `${timestamp}-${random}`;
|
|
457
|
+
};
|
|
458
|
+
|
|
459
|
+
// src/utils/network/url.utils.ts
|
|
460
|
+
var isValidUrl = (url, allowHttp = false) => {
|
|
461
|
+
try {
|
|
462
|
+
const parsed = new URL(url);
|
|
463
|
+
const isHttps = parsed.protocol === "https:";
|
|
464
|
+
const isHttp = parsed.protocol === "http:";
|
|
465
|
+
return isHttps || allowHttp && isHttp;
|
|
466
|
+
} catch {
|
|
467
|
+
return false;
|
|
468
|
+
}
|
|
469
|
+
};
|
|
470
|
+
var getCollectApiUrl = (config) => {
|
|
471
|
+
if (config.integrations?.tracelog?.projectId) {
|
|
472
|
+
try {
|
|
473
|
+
const url = new URL(window.location.href);
|
|
474
|
+
const host = url.hostname;
|
|
475
|
+
if (!host || typeof host !== "string") {
|
|
476
|
+
throw new Error("Invalid hostname");
|
|
477
|
+
}
|
|
478
|
+
const parts = host.split(".");
|
|
479
|
+
if (!parts || !Array.isArray(parts) || parts.length === 0 || parts.length === 1 && parts[0] === "") {
|
|
480
|
+
throw new Error("Invalid hostname structure");
|
|
481
|
+
}
|
|
482
|
+
const projectId = config.integrations.tracelog.projectId;
|
|
483
|
+
const cleanDomain = parts.slice(-2).join(".");
|
|
484
|
+
if (!cleanDomain) {
|
|
485
|
+
throw new Error("Invalid domain");
|
|
486
|
+
}
|
|
487
|
+
const collectApiUrl2 = `https://${projectId}.${cleanDomain}/collect`;
|
|
488
|
+
const isValid = isValidUrl(collectApiUrl2);
|
|
489
|
+
if (!isValid) {
|
|
490
|
+
throw new Error("Invalid URL");
|
|
491
|
+
}
|
|
492
|
+
return collectApiUrl2;
|
|
493
|
+
} catch (error) {
|
|
494
|
+
throw new Error(`Invalid URL configuration: ${error instanceof Error ? error.message : String(error)}`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
const collectApiUrl = config.integrations?.custom?.collectApiUrl;
|
|
498
|
+
if (collectApiUrl) {
|
|
499
|
+
const allowHttp = config.integrations?.custom?.allowHttp ?? false;
|
|
500
|
+
const isValid = isValidUrl(collectApiUrl, allowHttp);
|
|
501
|
+
if (!isValid) {
|
|
502
|
+
throw new Error("Invalid URL");
|
|
503
|
+
}
|
|
504
|
+
return collectApiUrl;
|
|
505
|
+
}
|
|
506
|
+
return "";
|
|
507
|
+
};
|
|
508
|
+
var normalizeUrl = (url, sensitiveQueryParams = []) => {
|
|
509
|
+
if (!url || typeof url !== "string") {
|
|
510
|
+
log("warn", "Invalid URL provided to normalizeUrl", { data: { url: String(url) } });
|
|
511
|
+
return url || "";
|
|
512
|
+
}
|
|
513
|
+
try {
|
|
514
|
+
const urlObject = new URL(url);
|
|
515
|
+
const searchParams = urlObject.searchParams;
|
|
516
|
+
const allSensitiveParams = [.../* @__PURE__ */ new Set([...DEFAULT_SENSITIVE_QUERY_PARAMS, ...sensitiveQueryParams])];
|
|
517
|
+
let hasChanged = false;
|
|
518
|
+
const removedParams = [];
|
|
519
|
+
allSensitiveParams.forEach((param) => {
|
|
520
|
+
if (searchParams.has(param)) {
|
|
521
|
+
searchParams.delete(param);
|
|
522
|
+
hasChanged = true;
|
|
523
|
+
removedParams.push(param);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
if (!hasChanged && url.includes("?")) {
|
|
527
|
+
return url;
|
|
528
|
+
}
|
|
529
|
+
urlObject.search = searchParams.toString();
|
|
530
|
+
const result = urlObject.toString();
|
|
531
|
+
return result;
|
|
532
|
+
} catch (error) {
|
|
533
|
+
const urlPreview = url && typeof url === "string" ? url.slice(0, 100) : String(url);
|
|
534
|
+
log("warn", "URL normalization failed, returning original", { error, data: { url: urlPreview } });
|
|
535
|
+
return url;
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
// src/utils/security/sanitize.utils.ts
|
|
540
|
+
var sanitizeString = (value) => {
|
|
541
|
+
if (!value || typeof value !== "string" || value.trim().length === 0) {
|
|
542
|
+
return "";
|
|
543
|
+
}
|
|
544
|
+
let sanitized = value;
|
|
545
|
+
if (value.length > MAX_STRING_LENGTH) {
|
|
546
|
+
sanitized = value.slice(0, Math.max(0, MAX_STRING_LENGTH));
|
|
547
|
+
}
|
|
548
|
+
let xssPatternMatches = 0;
|
|
549
|
+
for (const pattern of XSS_PATTERNS) {
|
|
550
|
+
const beforeReplace = sanitized;
|
|
551
|
+
sanitized = sanitized.replace(pattern, "");
|
|
552
|
+
if (beforeReplace !== sanitized) {
|
|
553
|
+
xssPatternMatches++;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
if (xssPatternMatches > 0) {
|
|
557
|
+
log("warn", "XSS patterns detected and removed", {
|
|
558
|
+
data: {
|
|
559
|
+
patternMatches: xssPatternMatches,
|
|
560
|
+
originalValue: value.slice(0, 100)
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
sanitized = sanitized.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'").replaceAll("/", "/");
|
|
565
|
+
const result = sanitized.trim();
|
|
566
|
+
return result;
|
|
567
|
+
};
|
|
568
|
+
var sanitizeValue = (value, depth = 0) => {
|
|
569
|
+
if (depth > MAX_OBJECT_DEPTH) {
|
|
570
|
+
return null;
|
|
571
|
+
}
|
|
572
|
+
if (value === null || value === void 0) {
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
if (typeof value === "string") {
|
|
576
|
+
return sanitizeString(value);
|
|
577
|
+
}
|
|
578
|
+
if (typeof value === "number") {
|
|
579
|
+
if (!Number.isFinite(value) || value < -Number.MAX_SAFE_INTEGER || value > Number.MAX_SAFE_INTEGER) {
|
|
580
|
+
return 0;
|
|
581
|
+
}
|
|
582
|
+
return value;
|
|
583
|
+
}
|
|
584
|
+
if (typeof value === "boolean") {
|
|
585
|
+
return value;
|
|
586
|
+
}
|
|
587
|
+
if (Array.isArray(value)) {
|
|
588
|
+
const limitedArray = value.slice(0, MAX_ARRAY_LENGTH);
|
|
589
|
+
const sanitizedArray = limitedArray.map((item) => sanitizeValue(item, depth + 1)).filter((item) => item !== null);
|
|
590
|
+
return sanitizedArray;
|
|
591
|
+
}
|
|
592
|
+
if (typeof value === "object") {
|
|
593
|
+
const sanitizedObject = {};
|
|
594
|
+
const entries = Object.entries(value);
|
|
595
|
+
const limitedEntries = entries.slice(0, MAX_NESTED_OBJECT_KEYS);
|
|
596
|
+
for (const [key, value_] of limitedEntries) {
|
|
597
|
+
const sanitizedKey = sanitizeString(key);
|
|
598
|
+
if (sanitizedKey) {
|
|
599
|
+
const sanitizedValue = sanitizeValue(value_, depth + 1);
|
|
600
|
+
if (sanitizedValue !== null) {
|
|
601
|
+
sanitizedObject[sanitizedKey] = sanitizedValue;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
return sanitizedObject;
|
|
606
|
+
}
|
|
607
|
+
return null;
|
|
608
|
+
};
|
|
609
|
+
var sanitizeMetadata = (metadata) => {
|
|
610
|
+
if (typeof metadata !== "object" || metadata === null) {
|
|
611
|
+
return {};
|
|
612
|
+
}
|
|
613
|
+
try {
|
|
614
|
+
const sanitized = sanitizeValue(metadata);
|
|
615
|
+
const result = typeof sanitized === "object" && sanitized !== null ? sanitized : {};
|
|
616
|
+
return result;
|
|
617
|
+
} catch (error) {
|
|
618
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
619
|
+
throw new Error(`[TraceLog] Metadata sanitization failed: ${errorMessage}`);
|
|
620
|
+
}
|
|
621
|
+
};
|
|
622
|
+
|
|
623
|
+
// src/utils/validations/config-validations.utils.ts
|
|
624
|
+
var validateAppConfig = (config) => {
|
|
625
|
+
if (config !== void 0 && (config === null || typeof config !== "object")) {
|
|
626
|
+
throw new AppConfigValidationError("Configuration must be an object", "config");
|
|
627
|
+
}
|
|
628
|
+
if (!config) {
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
if (config.sessionTimeout !== void 0) {
|
|
632
|
+
if (typeof config.sessionTimeout !== "number" || config.sessionTimeout < MIN_SESSION_TIMEOUT_MS || config.sessionTimeout > MAX_SESSION_TIMEOUT_MS) {
|
|
633
|
+
throw new SessionTimeoutValidationError(VALIDATION_MESSAGES.INVALID_SESSION_TIMEOUT, "config");
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
if (config.globalMetadata !== void 0) {
|
|
637
|
+
if (typeof config.globalMetadata !== "object" || config.globalMetadata === null) {
|
|
638
|
+
throw new AppConfigValidationError(VALIDATION_MESSAGES.INVALID_GLOBAL_METADATA, "config");
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
if (config.integrations) {
|
|
642
|
+
validateIntegrations(config.integrations);
|
|
643
|
+
}
|
|
644
|
+
if (config.sensitiveQueryParams !== void 0) {
|
|
645
|
+
if (!Array.isArray(config.sensitiveQueryParams)) {
|
|
646
|
+
throw new AppConfigValidationError(VALIDATION_MESSAGES.INVALID_SENSITIVE_QUERY_PARAMS, "config");
|
|
647
|
+
}
|
|
648
|
+
for (const param of config.sensitiveQueryParams) {
|
|
649
|
+
if (typeof param !== "string") {
|
|
650
|
+
throw new AppConfigValidationError("All sensitive query params must be strings", "config");
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
if (config.errorSampling !== void 0) {
|
|
655
|
+
if (typeof config.errorSampling !== "number" || config.errorSampling < 0 || config.errorSampling > 1) {
|
|
656
|
+
throw new SamplingRateValidationError(VALIDATION_MESSAGES.INVALID_ERROR_SAMPLING_RATE, "config");
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
if (config.samplingRate !== void 0) {
|
|
660
|
+
if (typeof config.samplingRate !== "number" || config.samplingRate < 0 || config.samplingRate > 1) {
|
|
661
|
+
throw new SamplingRateValidationError(VALIDATION_MESSAGES.INVALID_SAMPLING_RATE, "config");
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (config.primaryScrollSelector !== void 0) {
|
|
665
|
+
if (typeof config.primaryScrollSelector !== "string" || !config.primaryScrollSelector.trim()) {
|
|
666
|
+
throw new AppConfigValidationError(VALIDATION_MESSAGES.INVALID_PRIMARY_SCROLL_SELECTOR, "config");
|
|
667
|
+
}
|
|
668
|
+
if (config.primaryScrollSelector !== "window") {
|
|
669
|
+
try {
|
|
670
|
+
document.querySelector(config.primaryScrollSelector);
|
|
671
|
+
} catch {
|
|
672
|
+
throw new AppConfigValidationError(
|
|
673
|
+
`${VALIDATION_MESSAGES.INVALID_PRIMARY_SCROLL_SELECTOR_SYNTAX}: "${config.primaryScrollSelector}"`,
|
|
674
|
+
"config"
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
if (config.pageViewThrottleMs !== void 0) {
|
|
680
|
+
if (typeof config.pageViewThrottleMs !== "number" || config.pageViewThrottleMs < 0) {
|
|
681
|
+
throw new AppConfigValidationError(VALIDATION_MESSAGES.INVALID_PAGE_VIEW_THROTTLE, "config");
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (config.clickThrottleMs !== void 0) {
|
|
685
|
+
if (typeof config.clickThrottleMs !== "number" || config.clickThrottleMs < 0) {
|
|
686
|
+
throw new AppConfigValidationError(VALIDATION_MESSAGES.INVALID_CLICK_THROTTLE, "config");
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
if (config.maxSameEventPerMinute !== void 0) {
|
|
690
|
+
if (typeof config.maxSameEventPerMinute !== "number" || config.maxSameEventPerMinute <= 0) {
|
|
691
|
+
throw new AppConfigValidationError(VALIDATION_MESSAGES.INVALID_MAX_SAME_EVENT_PER_MINUTE, "config");
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
if (config.viewport !== void 0) {
|
|
695
|
+
validateViewportConfig(config.viewport);
|
|
696
|
+
}
|
|
697
|
+
};
|
|
698
|
+
var validateViewportConfig = (viewport) => {
|
|
699
|
+
if (typeof viewport !== "object" || viewport === null) {
|
|
700
|
+
throw new AppConfigValidationError(VALIDATION_MESSAGES.INVALID_VIEWPORT_CONFIG, "config");
|
|
701
|
+
}
|
|
702
|
+
if (!viewport.elements || !Array.isArray(viewport.elements)) {
|
|
703
|
+
throw new AppConfigValidationError(VALIDATION_MESSAGES.INVALID_VIEWPORT_ELEMENTS, "config");
|
|
704
|
+
}
|
|
705
|
+
if (viewport.elements.length === 0) {
|
|
706
|
+
throw new AppConfigValidationError(VALIDATION_MESSAGES.INVALID_VIEWPORT_ELEMENTS, "config");
|
|
707
|
+
}
|
|
708
|
+
const uniqueSelectors = /* @__PURE__ */ new Set();
|
|
709
|
+
for (const element of viewport.elements) {
|
|
710
|
+
if (!element.selector || typeof element.selector !== "string" || !element.selector.trim()) {
|
|
711
|
+
throw new AppConfigValidationError(VALIDATION_MESSAGES.INVALID_VIEWPORT_ELEMENT, "config");
|
|
712
|
+
}
|
|
713
|
+
const normalizedSelector = element.selector.trim();
|
|
714
|
+
if (uniqueSelectors.has(normalizedSelector)) {
|
|
715
|
+
throw new AppConfigValidationError(
|
|
716
|
+
`Duplicate viewport selector found: "${normalizedSelector}". Each selector should appear only once.`,
|
|
717
|
+
"config"
|
|
718
|
+
);
|
|
719
|
+
}
|
|
720
|
+
uniqueSelectors.add(normalizedSelector);
|
|
721
|
+
if (element.id !== void 0 && (typeof element.id !== "string" || !element.id.trim())) {
|
|
722
|
+
throw new AppConfigValidationError(VALIDATION_MESSAGES.INVALID_VIEWPORT_ELEMENT_ID, "config");
|
|
723
|
+
}
|
|
724
|
+
if (element.name !== void 0 && (typeof element.name !== "string" || !element.name.trim())) {
|
|
725
|
+
throw new AppConfigValidationError(VALIDATION_MESSAGES.INVALID_VIEWPORT_ELEMENT_NAME, "config");
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (viewport.threshold !== void 0) {
|
|
729
|
+
if (typeof viewport.threshold !== "number" || viewport.threshold < 0 || viewport.threshold > 1) {
|
|
730
|
+
throw new AppConfigValidationError(VALIDATION_MESSAGES.INVALID_VIEWPORT_THRESHOLD, "config");
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
if (viewport.minDwellTime !== void 0) {
|
|
734
|
+
if (typeof viewport.minDwellTime !== "number" || viewport.minDwellTime < 0) {
|
|
735
|
+
throw new AppConfigValidationError(VALIDATION_MESSAGES.INVALID_VIEWPORT_MIN_DWELL_TIME, "config");
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
if (viewport.cooldownPeriod !== void 0) {
|
|
739
|
+
if (typeof viewport.cooldownPeriod !== "number" || viewport.cooldownPeriod < 0) {
|
|
740
|
+
throw new AppConfigValidationError(VALIDATION_MESSAGES.INVALID_VIEWPORT_COOLDOWN_PERIOD, "config");
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (viewport.maxTrackedElements !== void 0) {
|
|
744
|
+
if (typeof viewport.maxTrackedElements !== "number" || viewport.maxTrackedElements <= 0) {
|
|
745
|
+
throw new AppConfigValidationError(VALIDATION_MESSAGES.INVALID_VIEWPORT_MAX_TRACKED_ELEMENTS, "config");
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
};
|
|
749
|
+
var validateIntegrations = (integrations) => {
|
|
750
|
+
if (!integrations) {
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (integrations.tracelog) {
|
|
754
|
+
if (!integrations.tracelog.projectId || typeof integrations.tracelog.projectId !== "string" || integrations.tracelog.projectId.trim() === "") {
|
|
755
|
+
throw new IntegrationValidationError(VALIDATION_MESSAGES.INVALID_TRACELOG_PROJECT_ID, "config");
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
if (integrations.custom) {
|
|
759
|
+
if (!integrations.custom.collectApiUrl || typeof integrations.custom.collectApiUrl !== "string" || integrations.custom.collectApiUrl.trim() === "") {
|
|
760
|
+
throw new IntegrationValidationError(VALIDATION_MESSAGES.INVALID_CUSTOM_API_URL, "config");
|
|
761
|
+
}
|
|
762
|
+
if (integrations.custom.allowHttp !== void 0 && typeof integrations.custom.allowHttp !== "boolean") {
|
|
763
|
+
throw new IntegrationValidationError("allowHttp must be a boolean", "config");
|
|
764
|
+
}
|
|
765
|
+
const collectApiUrl = integrations.custom.collectApiUrl.trim();
|
|
766
|
+
if (!collectApiUrl.startsWith("http://") && !collectApiUrl.startsWith("https://")) {
|
|
767
|
+
throw new IntegrationValidationError('Custom API URL must start with "http://" or "https://"', "config");
|
|
768
|
+
}
|
|
769
|
+
const allowHttp = integrations.custom.allowHttp ?? false;
|
|
770
|
+
if (!allowHttp && collectApiUrl.startsWith("http://")) {
|
|
771
|
+
throw new IntegrationValidationError(
|
|
772
|
+
"Custom API URL must use HTTPS in production. Set allowHttp: true in integration config to allow HTTP (not recommended)",
|
|
773
|
+
"config"
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
if (integrations.googleAnalytics) {
|
|
778
|
+
if (!integrations.googleAnalytics.measurementId || typeof integrations.googleAnalytics.measurementId !== "string" || integrations.googleAnalytics.measurementId.trim() === "") {
|
|
779
|
+
throw new IntegrationValidationError(VALIDATION_MESSAGES.INVALID_GOOGLE_ANALYTICS_ID, "config");
|
|
780
|
+
}
|
|
781
|
+
const measurementId = integrations.googleAnalytics.measurementId.trim();
|
|
782
|
+
if (!measurementId.match(/^(G-|UA-)/)) {
|
|
783
|
+
throw new IntegrationValidationError('Google Analytics measurement ID must start with "G-" or "UA-"', "config");
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
var validateAndNormalizeConfig = (config) => {
|
|
788
|
+
validateAppConfig(config);
|
|
789
|
+
const normalizedConfig = {
|
|
790
|
+
...config ?? {},
|
|
791
|
+
sessionTimeout: config?.sessionTimeout ?? DEFAULT_SESSION_TIMEOUT,
|
|
792
|
+
globalMetadata: config?.globalMetadata ?? {},
|
|
793
|
+
sensitiveQueryParams: config?.sensitiveQueryParams ?? [],
|
|
794
|
+
errorSampling: config?.errorSampling ?? DEFAULT_ERROR_SAMPLING_RATE,
|
|
795
|
+
samplingRate: config?.samplingRate ?? DEFAULT_SAMPLING_RATE,
|
|
796
|
+
pageViewThrottleMs: config?.pageViewThrottleMs ?? DEFAULT_PAGE_VIEW_THROTTLE_MS,
|
|
797
|
+
clickThrottleMs: config?.clickThrottleMs ?? DEFAULT_CLICK_THROTTLE_MS,
|
|
798
|
+
maxSameEventPerMinute: config?.maxSameEventPerMinute ?? MAX_SAME_EVENT_PER_MINUTE
|
|
799
|
+
};
|
|
800
|
+
if (normalizedConfig.integrations?.custom) {
|
|
801
|
+
normalizedConfig.integrations.custom = {
|
|
802
|
+
...normalizedConfig.integrations.custom,
|
|
803
|
+
allowHttp: normalizedConfig.integrations.custom.allowHttp ?? false
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
if (normalizedConfig.viewport) {
|
|
807
|
+
normalizedConfig.viewport = {
|
|
808
|
+
...normalizedConfig.viewport,
|
|
809
|
+
threshold: normalizedConfig.viewport.threshold ?? 0.5,
|
|
810
|
+
minDwellTime: normalizedConfig.viewport.minDwellTime ?? DEFAULT_VISIBILITY_TIMEOUT_MS,
|
|
811
|
+
cooldownPeriod: normalizedConfig.viewport.cooldownPeriod ?? DEFAULT_VIEWPORT_COOLDOWN_PERIOD,
|
|
812
|
+
maxTrackedElements: normalizedConfig.viewport.maxTrackedElements ?? DEFAULT_VIEWPORT_MAX_TRACKED_ELEMENTS
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
return normalizedConfig;
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
// src/utils/validations/type-guards.utils.ts
|
|
819
|
+
var isValidArrayItem = (item) => {
|
|
820
|
+
if (typeof item === "string") {
|
|
821
|
+
return true;
|
|
822
|
+
}
|
|
823
|
+
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
|
|
824
|
+
const entries = Object.entries(item);
|
|
825
|
+
if (entries.length > MAX_NESTED_OBJECT_KEYS) {
|
|
826
|
+
return false;
|
|
827
|
+
}
|
|
828
|
+
for (const [, value] of entries) {
|
|
829
|
+
if (value === null || value === void 0) {
|
|
830
|
+
continue;
|
|
831
|
+
}
|
|
832
|
+
const type = typeof value;
|
|
833
|
+
if (type !== "string" && type !== "number" && type !== "boolean") {
|
|
834
|
+
return false;
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return true;
|
|
838
|
+
}
|
|
839
|
+
return false;
|
|
840
|
+
};
|
|
841
|
+
var isOnlyPrimitiveFields = (object, depth = 0) => {
|
|
842
|
+
if (typeof object !== "object" || object === null) {
|
|
843
|
+
return false;
|
|
844
|
+
}
|
|
845
|
+
if (depth > MAX_METADATA_NESTING_DEPTH) {
|
|
846
|
+
return false;
|
|
847
|
+
}
|
|
848
|
+
for (const value of Object.values(object)) {
|
|
849
|
+
if (value === null || value === void 0) {
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
const type = typeof value;
|
|
853
|
+
if (type === "string" || type === "number" || type === "boolean") {
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
if (Array.isArray(value)) {
|
|
857
|
+
if (value.length === 0) {
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
const firstItem = value[0];
|
|
861
|
+
const isStringArray = typeof firstItem === "string";
|
|
862
|
+
if (isStringArray) {
|
|
863
|
+
if (!value.every((item) => typeof item === "string")) {
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
} else {
|
|
867
|
+
if (!value.every((item) => isValidArrayItem(item))) {
|
|
868
|
+
return false;
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
continue;
|
|
872
|
+
}
|
|
873
|
+
if (type === "object" && depth === 0) {
|
|
874
|
+
if (!isOnlyPrimitiveFields(value, depth + 1)) {
|
|
875
|
+
return false;
|
|
876
|
+
}
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
return false;
|
|
880
|
+
}
|
|
881
|
+
return true;
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
// src/utils/validations/metadata-validations.utils.ts
|
|
885
|
+
var isValidEventName = (eventName) => {
|
|
886
|
+
if (typeof eventName !== "string") {
|
|
887
|
+
return {
|
|
888
|
+
valid: false,
|
|
889
|
+
error: "Event name must be a string"
|
|
890
|
+
};
|
|
891
|
+
}
|
|
892
|
+
if (eventName.length === 0) {
|
|
893
|
+
return {
|
|
894
|
+
valid: false,
|
|
895
|
+
error: "Event name cannot be empty"
|
|
896
|
+
};
|
|
897
|
+
}
|
|
898
|
+
if (eventName.length > MAX_CUSTOM_EVENT_NAME_LENGTH) {
|
|
899
|
+
return {
|
|
900
|
+
valid: false,
|
|
901
|
+
error: `Event name is too long (max ${MAX_CUSTOM_EVENT_NAME_LENGTH} characters)`
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
if (eventName.includes("<") || eventName.includes(">") || eventName.includes("&")) {
|
|
905
|
+
return {
|
|
906
|
+
valid: false,
|
|
907
|
+
error: "Event name contains invalid characters"
|
|
908
|
+
};
|
|
909
|
+
}
|
|
910
|
+
const reservedWords = ["constructor", "prototype", "__proto__", "eval", "function", "var", "let", "const"];
|
|
911
|
+
if (reservedWords.includes(eventName.toLowerCase())) {
|
|
912
|
+
return {
|
|
913
|
+
valid: false,
|
|
914
|
+
error: "Event name cannot be a reserved word"
|
|
915
|
+
};
|
|
916
|
+
}
|
|
917
|
+
return { valid: true };
|
|
918
|
+
};
|
|
919
|
+
var validateSingleMetadata = (eventName, metadata, type) => {
|
|
920
|
+
const sanitizedMetadata = sanitizeMetadata(metadata);
|
|
921
|
+
const intro = `${type} "${eventName}" metadata error` ;
|
|
922
|
+
if (!isOnlyPrimitiveFields(sanitizedMetadata)) {
|
|
923
|
+
return {
|
|
924
|
+
valid: false,
|
|
925
|
+
error: `${intro}: object has invalid types. Valid types are string, number, boolean or string arrays.`
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
let jsonString;
|
|
929
|
+
try {
|
|
930
|
+
jsonString = JSON.stringify(sanitizedMetadata);
|
|
931
|
+
} catch {
|
|
932
|
+
return {
|
|
933
|
+
valid: false,
|
|
934
|
+
error: `${intro}: object contains circular references or cannot be serialized.`
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
if (jsonString.length > MAX_CUSTOM_EVENT_STRING_SIZE) {
|
|
938
|
+
return {
|
|
939
|
+
valid: false,
|
|
940
|
+
error: `${intro}: object is too large (max ${MAX_CUSTOM_EVENT_STRING_SIZE / 1024} KB).`
|
|
941
|
+
};
|
|
942
|
+
}
|
|
943
|
+
const keyCount = Object.keys(sanitizedMetadata).length;
|
|
944
|
+
if (keyCount > MAX_CUSTOM_EVENT_KEYS) {
|
|
945
|
+
return {
|
|
946
|
+
valid: false,
|
|
947
|
+
error: `${intro}: object has too many keys (max ${MAX_CUSTOM_EVENT_KEYS} keys).`
|
|
948
|
+
};
|
|
949
|
+
}
|
|
950
|
+
for (const [key, value] of Object.entries(sanitizedMetadata)) {
|
|
951
|
+
if (Array.isArray(value)) {
|
|
952
|
+
if (value.length > MAX_CUSTOM_EVENT_ARRAY_SIZE) {
|
|
953
|
+
return {
|
|
954
|
+
valid: false,
|
|
955
|
+
error: `${intro}: array property "${key}" is too large (max ${MAX_CUSTOM_EVENT_ARRAY_SIZE} items).`
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
for (const item of value) {
|
|
959
|
+
if (typeof item === "string" && item.length > MAX_STRING_LENGTH_IN_ARRAY) {
|
|
960
|
+
return {
|
|
961
|
+
valid: false,
|
|
962
|
+
error: `${intro}: array property "${key}" contains strings that are too long (max ${MAX_STRING_LENGTH_IN_ARRAY} characters).`
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
if (typeof value === "string" && value.length > MAX_STRING_LENGTH) {
|
|
968
|
+
return {
|
|
969
|
+
valid: false,
|
|
970
|
+
error: `${intro}: property "${key}" is too long (max ${MAX_STRING_LENGTH} characters).`
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
return {
|
|
975
|
+
valid: true,
|
|
976
|
+
sanitizedMetadata
|
|
977
|
+
};
|
|
978
|
+
};
|
|
979
|
+
var isValidMetadata = (eventName, metadata, type) => {
|
|
980
|
+
if (Array.isArray(metadata)) {
|
|
981
|
+
const sanitizedArray = [];
|
|
982
|
+
const intro = `${type} "${eventName}" metadata error` ;
|
|
983
|
+
for (let i = 0; i < metadata.length; i++) {
|
|
984
|
+
const item = metadata[i];
|
|
985
|
+
if (typeof item !== "object" || item === null || Array.isArray(item)) {
|
|
986
|
+
return {
|
|
987
|
+
valid: false,
|
|
988
|
+
error: `${intro}: array item at index ${i} must be an object.`
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
const itemValidation = validateSingleMetadata(eventName, item, type);
|
|
992
|
+
if (!itemValidation.valid) {
|
|
993
|
+
return {
|
|
994
|
+
valid: false,
|
|
995
|
+
error: `${intro}: array item at index ${i} is invalid: ${itemValidation.error}`
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
if (itemValidation.sanitizedMetadata) {
|
|
999
|
+
sanitizedArray.push(itemValidation.sanitizedMetadata);
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return {
|
|
1003
|
+
valid: true,
|
|
1004
|
+
sanitizedMetadata: sanitizedArray
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
return validateSingleMetadata(eventName, metadata, type);
|
|
1008
|
+
};
|
|
1009
|
+
|
|
1010
|
+
// src/utils/validations/event-validations.utils.ts
|
|
1011
|
+
var isEventValid = (eventName, metadata) => {
|
|
1012
|
+
const nameValidation = isValidEventName(eventName);
|
|
1013
|
+
if (!nameValidation.valid) {
|
|
1014
|
+
log("error", "Event name validation failed", {
|
|
1015
|
+
showToClient: true,
|
|
1016
|
+
data: { eventName, error: nameValidation.error }
|
|
1017
|
+
});
|
|
1018
|
+
return nameValidation;
|
|
1019
|
+
}
|
|
1020
|
+
if (!metadata) {
|
|
1021
|
+
return { valid: true };
|
|
1022
|
+
}
|
|
1023
|
+
const metadataValidation = isValidMetadata(eventName, metadata, "customEvent");
|
|
1024
|
+
if (!metadataValidation.valid) {
|
|
1025
|
+
log("error", "Event metadata validation failed", {
|
|
1026
|
+
showToClient: true,
|
|
1027
|
+
data: {
|
|
1028
|
+
eventName,
|
|
1029
|
+
error: metadataValidation.error
|
|
1030
|
+
}
|
|
1031
|
+
});
|
|
1032
|
+
}
|
|
1033
|
+
return metadataValidation;
|
|
1034
|
+
};
|
|
1035
|
+
|
|
1036
|
+
// src/utils/emitter.utils.ts
|
|
1037
|
+
var Emitter = class {
|
|
1038
|
+
listeners = /* @__PURE__ */ new Map();
|
|
1039
|
+
on(event2, callback) {
|
|
1040
|
+
if (!this.listeners.has(event2)) {
|
|
1041
|
+
this.listeners.set(event2, []);
|
|
1042
|
+
}
|
|
1043
|
+
this.listeners.get(event2).push(callback);
|
|
1044
|
+
}
|
|
1045
|
+
off(event2, callback) {
|
|
1046
|
+
const callbacks = this.listeners.get(event2);
|
|
1047
|
+
if (callbacks) {
|
|
1048
|
+
const index = callbacks.indexOf(callback);
|
|
1049
|
+
if (index > -1) {
|
|
1050
|
+
callbacks.splice(index, 1);
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
emit(event2, data) {
|
|
1055
|
+
const callbacks = this.listeners.get(event2);
|
|
1056
|
+
if (callbacks) {
|
|
1057
|
+
callbacks.forEach((callback) => {
|
|
1058
|
+
callback(data);
|
|
1059
|
+
});
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
removeAllListeners() {
|
|
1063
|
+
this.listeners.clear();
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
|
|
1067
|
+
// src/managers/state.manager.ts
|
|
1068
|
+
var globalState = {};
|
|
1069
|
+
var StateManager = class {
|
|
1070
|
+
get(key) {
|
|
1071
|
+
return globalState[key];
|
|
1072
|
+
}
|
|
1073
|
+
set(key, value) {
|
|
1074
|
+
globalState[key] = value;
|
|
1075
|
+
}
|
|
1076
|
+
getState() {
|
|
1077
|
+
return { ...globalState };
|
|
1078
|
+
}
|
|
1079
|
+
};
|
|
1080
|
+
|
|
1081
|
+
// src/managers/sender.manager.ts
|
|
1082
|
+
var SenderManager = class extends StateManager {
|
|
1083
|
+
storeManager;
|
|
1084
|
+
lastPermanentErrorLog = null;
|
|
1085
|
+
constructor(storeManager) {
|
|
1086
|
+
super();
|
|
1087
|
+
this.storeManager = storeManager;
|
|
1088
|
+
}
|
|
1089
|
+
getQueueStorageKey() {
|
|
1090
|
+
const userId = this.get("userId") || "anonymous";
|
|
1091
|
+
return QUEUE_KEY(userId);
|
|
1092
|
+
}
|
|
1093
|
+
sendEventsQueueSync(body) {
|
|
1094
|
+
if (this.shouldSkipSend()) {
|
|
1095
|
+
return true;
|
|
1096
|
+
}
|
|
1097
|
+
const config = this.get("config");
|
|
1098
|
+
if (config?.integrations?.custom?.collectApiUrl === "localhost:9999" /* Fail */) {
|
|
1099
|
+
log("warn", "Fail mode: simulating network failure (sync)", {
|
|
1100
|
+
data: { events: body.events.length }
|
|
1101
|
+
});
|
|
1102
|
+
return false;
|
|
1103
|
+
}
|
|
1104
|
+
return this.sendQueueSyncInternal(body);
|
|
1105
|
+
}
|
|
1106
|
+
async sendEventsQueue(body, callbacks) {
|
|
1107
|
+
try {
|
|
1108
|
+
const success = await this.send(body);
|
|
1109
|
+
if (success) {
|
|
1110
|
+
this.clearPersistedEvents();
|
|
1111
|
+
callbacks?.onSuccess?.(body.events.length, body.events, body);
|
|
1112
|
+
} else {
|
|
1113
|
+
this.persistEvents(body);
|
|
1114
|
+
callbacks?.onFailure?.();
|
|
1115
|
+
}
|
|
1116
|
+
return success;
|
|
1117
|
+
} catch (error) {
|
|
1118
|
+
if (error instanceof PermanentError) {
|
|
1119
|
+
this.logPermanentError("Permanent error, not retrying", error);
|
|
1120
|
+
this.clearPersistedEvents();
|
|
1121
|
+
callbacks?.onFailure?.();
|
|
1122
|
+
return false;
|
|
1123
|
+
}
|
|
1124
|
+
this.persistEvents(body);
|
|
1125
|
+
callbacks?.onFailure?.();
|
|
1126
|
+
return false;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
async recoverPersistedEvents(callbacks) {
|
|
1130
|
+
try {
|
|
1131
|
+
const persistedData = this.getPersistedData();
|
|
1132
|
+
if (!persistedData || !this.isDataRecent(persistedData) || persistedData.events.length === 0) {
|
|
1133
|
+
this.clearPersistedEvents();
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
const body = this.createRecoveryBody(persistedData);
|
|
1137
|
+
const success = await this.send(body);
|
|
1138
|
+
if (success) {
|
|
1139
|
+
this.clearPersistedEvents();
|
|
1140
|
+
callbacks?.onSuccess?.(persistedData.events.length, persistedData.events, body);
|
|
1141
|
+
} else {
|
|
1142
|
+
callbacks?.onFailure?.();
|
|
1143
|
+
}
|
|
1144
|
+
} catch (error) {
|
|
1145
|
+
if (error instanceof PermanentError) {
|
|
1146
|
+
this.logPermanentError("Permanent error during recovery, clearing persisted events", error);
|
|
1147
|
+
this.clearPersistedEvents();
|
|
1148
|
+
callbacks?.onFailure?.();
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
log("error", "Failed to recover persisted events", { error });
|
|
1152
|
+
}
|
|
1153
|
+
}
|
|
1154
|
+
stop() {
|
|
1155
|
+
}
|
|
1156
|
+
async send(body) {
|
|
1157
|
+
if (this.shouldSkipSend()) {
|
|
1158
|
+
return this.simulateSuccessfulSend();
|
|
1159
|
+
}
|
|
1160
|
+
const config = this.get("config");
|
|
1161
|
+
if (config?.integrations?.custom?.collectApiUrl === "localhost:9999" /* Fail */) {
|
|
1162
|
+
log("warn", "Fail mode: simulating network failure", {
|
|
1163
|
+
data: { events: body.events.length }
|
|
1164
|
+
});
|
|
1165
|
+
return false;
|
|
1166
|
+
}
|
|
1167
|
+
const { url, payload } = this.prepareRequest(body);
|
|
1168
|
+
try {
|
|
1169
|
+
const response = await this.sendWithTimeout(url, payload);
|
|
1170
|
+
return response.ok;
|
|
1171
|
+
} catch (error) {
|
|
1172
|
+
if (error instanceof PermanentError) {
|
|
1173
|
+
throw error;
|
|
1174
|
+
}
|
|
1175
|
+
log("error", "Send request failed", {
|
|
1176
|
+
error,
|
|
1177
|
+
data: {
|
|
1178
|
+
events: body.events.length,
|
|
1179
|
+
url: url.replace(/\/\/[^/]+/, "//[DOMAIN]")
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
return false;
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
async sendWithTimeout(url, payload) {
|
|
1186
|
+
const controller = new AbortController();
|
|
1187
|
+
const timeoutId = setTimeout(() => {
|
|
1188
|
+
controller.abort();
|
|
1189
|
+
}, REQUEST_TIMEOUT_MS);
|
|
1190
|
+
try {
|
|
1191
|
+
const response = await fetch(url, {
|
|
1192
|
+
method: "POST",
|
|
1193
|
+
body: payload,
|
|
1194
|
+
keepalive: true,
|
|
1195
|
+
credentials: "include",
|
|
1196
|
+
signal: controller.signal,
|
|
1197
|
+
headers: {
|
|
1198
|
+
"Content-Type": "application/json"
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
if (!response.ok) {
|
|
1202
|
+
const isPermanentError = response.status >= 400 && response.status < 500;
|
|
1203
|
+
if (isPermanentError) {
|
|
1204
|
+
throw new PermanentError(`HTTP ${response.status}: ${response.statusText}`, response.status);
|
|
1205
|
+
}
|
|
1206
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1207
|
+
}
|
|
1208
|
+
return response;
|
|
1209
|
+
} finally {
|
|
1210
|
+
clearTimeout(timeoutId);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
sendQueueSyncInternal(body) {
|
|
1214
|
+
const { url, payload } = this.prepareRequest(body);
|
|
1215
|
+
if (payload.length > MAX_BEACON_PAYLOAD_SIZE) {
|
|
1216
|
+
log("warn", "Payload exceeds sendBeacon limit, persisting for recovery", {
|
|
1217
|
+
data: {
|
|
1218
|
+
size: payload.length,
|
|
1219
|
+
limit: MAX_BEACON_PAYLOAD_SIZE,
|
|
1220
|
+
events: body.events.length
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
this.persistEvents(body);
|
|
1224
|
+
return false;
|
|
1225
|
+
}
|
|
1226
|
+
const blob = new Blob([payload], { type: "application/json" });
|
|
1227
|
+
if (!this.isSendBeaconAvailable()) {
|
|
1228
|
+
log("warn", "sendBeacon not available, persisting events for recovery");
|
|
1229
|
+
this.persistEvents(body);
|
|
1230
|
+
return false;
|
|
1231
|
+
}
|
|
1232
|
+
const accepted = navigator.sendBeacon(url, blob);
|
|
1233
|
+
if (!accepted) {
|
|
1234
|
+
log("warn", "sendBeacon rejected request, persisting events for recovery");
|
|
1235
|
+
this.persistEvents(body);
|
|
1236
|
+
}
|
|
1237
|
+
return accepted;
|
|
1238
|
+
}
|
|
1239
|
+
prepareRequest(body) {
|
|
1240
|
+
const enrichedBody = {
|
|
1241
|
+
...body,
|
|
1242
|
+
_metadata: {
|
|
1243
|
+
referer: typeof window !== "undefined" ? window.location.href : void 0,
|
|
1244
|
+
timestamp: Date.now()
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
return {
|
|
1248
|
+
url: this.get("collectApiUrl"),
|
|
1249
|
+
payload: JSON.stringify(enrichedBody)
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
getPersistedData() {
|
|
1253
|
+
try {
|
|
1254
|
+
const storageKey = this.getQueueStorageKey();
|
|
1255
|
+
const persistedDataString = this.storeManager.getItem(storageKey);
|
|
1256
|
+
if (persistedDataString) {
|
|
1257
|
+
return JSON.parse(persistedDataString);
|
|
1258
|
+
}
|
|
1259
|
+
} catch (error) {
|
|
1260
|
+
log("warn", "Failed to parse persisted data", { error });
|
|
1261
|
+
this.clearPersistedEvents();
|
|
1262
|
+
}
|
|
1263
|
+
return null;
|
|
1264
|
+
}
|
|
1265
|
+
isDataRecent(data) {
|
|
1266
|
+
if (!data.timestamp || typeof data.timestamp !== "number") {
|
|
1267
|
+
return false;
|
|
1268
|
+
}
|
|
1269
|
+
const ageInHours = (Date.now() - data.timestamp) / (1e3 * 60 * 60);
|
|
1270
|
+
return ageInHours < EVENT_EXPIRY_HOURS;
|
|
1271
|
+
}
|
|
1272
|
+
createRecoveryBody(data) {
|
|
1273
|
+
const { timestamp, ...queue } = data;
|
|
1274
|
+
return queue;
|
|
1275
|
+
}
|
|
1276
|
+
persistEvents(body) {
|
|
1277
|
+
try {
|
|
1278
|
+
const persistedData = {
|
|
1279
|
+
...body,
|
|
1280
|
+
timestamp: Date.now()
|
|
1281
|
+
};
|
|
1282
|
+
const storageKey = this.getQueueStorageKey();
|
|
1283
|
+
this.storeManager.setItem(storageKey, JSON.stringify(persistedData));
|
|
1284
|
+
return !!this.storeManager.getItem(storageKey);
|
|
1285
|
+
} catch (error) {
|
|
1286
|
+
log("warn", "Failed to persist events", { error });
|
|
1287
|
+
return false;
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
clearPersistedEvents() {
|
|
1291
|
+
try {
|
|
1292
|
+
const key = this.getQueueStorageKey();
|
|
1293
|
+
this.storeManager.removeItem(key);
|
|
1294
|
+
} catch (error) {
|
|
1295
|
+
log("warn", "Failed to clear persisted events", { error });
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
shouldSkipSend() {
|
|
1299
|
+
return !this.get("collectApiUrl");
|
|
1300
|
+
}
|
|
1301
|
+
async simulateSuccessfulSend() {
|
|
1302
|
+
const delay = Math.random() * 400 + 100;
|
|
1303
|
+
await new Promise((resolve) => setTimeout(resolve, delay));
|
|
1304
|
+
return true;
|
|
1305
|
+
}
|
|
1306
|
+
isSendBeaconAvailable() {
|
|
1307
|
+
return typeof navigator !== "undefined" && typeof navigator.sendBeacon === "function";
|
|
1308
|
+
}
|
|
1309
|
+
logPermanentError(context, error) {
|
|
1310
|
+
const now = Date.now();
|
|
1311
|
+
const shouldLog = !this.lastPermanentErrorLog || this.lastPermanentErrorLog.statusCode !== error.statusCode || now - this.lastPermanentErrorLog.timestamp >= PERMANENT_ERROR_LOG_THROTTLE_MS;
|
|
1312
|
+
if (shouldLog) {
|
|
1313
|
+
log("error", context, {
|
|
1314
|
+
data: { status: error.statusCode, message: error.message }
|
|
1315
|
+
});
|
|
1316
|
+
this.lastPermanentErrorLog = { statusCode: error.statusCode, timestamp: now };
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
};
|
|
1320
|
+
|
|
1321
|
+
// src/managers/event.manager.ts
|
|
1322
|
+
var EventManager = class extends StateManager {
|
|
1323
|
+
googleAnalytics;
|
|
1324
|
+
dataSender;
|
|
1325
|
+
emitter;
|
|
1326
|
+
eventsQueue = [];
|
|
1327
|
+
pendingEventsBuffer = [];
|
|
1328
|
+
recentEventFingerprints = /* @__PURE__ */ new Map();
|
|
1329
|
+
// Time-based deduplication cache
|
|
1330
|
+
sendIntervalId = null;
|
|
1331
|
+
rateLimitCounter = 0;
|
|
1332
|
+
rateLimitWindowStart = 0;
|
|
1333
|
+
perEventRateLimits = /* @__PURE__ */ new Map();
|
|
1334
|
+
sessionEventCounts = {
|
|
1335
|
+
total: 0,
|
|
1336
|
+
["click" /* CLICK */]: 0,
|
|
1337
|
+
["page_view" /* PAGE_VIEW */]: 0,
|
|
1338
|
+
["custom" /* CUSTOM */]: 0,
|
|
1339
|
+
["viewport_visible" /* VIEWPORT_VISIBLE */]: 0,
|
|
1340
|
+
["scroll" /* SCROLL */]: 0
|
|
1341
|
+
};
|
|
1342
|
+
lastSessionId = null;
|
|
1343
|
+
constructor(storeManager, googleAnalytics = null, emitter = null) {
|
|
1344
|
+
super();
|
|
1345
|
+
this.googleAnalytics = googleAnalytics;
|
|
1346
|
+
this.dataSender = new SenderManager(storeManager);
|
|
1347
|
+
this.emitter = emitter;
|
|
1348
|
+
}
|
|
1349
|
+
async recoverPersistedEvents() {
|
|
1350
|
+
await this.dataSender.recoverPersistedEvents({
|
|
1351
|
+
onSuccess: (_eventCount, recoveredEvents, body) => {
|
|
1352
|
+
if (recoveredEvents && recoveredEvents.length > 0) {
|
|
1353
|
+
const eventIds = recoveredEvents.map((e) => e.id);
|
|
1354
|
+
this.removeProcessedEvents(eventIds);
|
|
1355
|
+
if (body) {
|
|
1356
|
+
this.emitEventsQueue(body);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
},
|
|
1360
|
+
onFailure: () => {
|
|
1361
|
+
log("warn", "Failed to recover persisted events");
|
|
1362
|
+
}
|
|
1363
|
+
});
|
|
1364
|
+
}
|
|
1365
|
+
track({
|
|
1366
|
+
type,
|
|
1367
|
+
page_url,
|
|
1368
|
+
from_page_url,
|
|
1369
|
+
scroll_data,
|
|
1370
|
+
click_data,
|
|
1371
|
+
custom_event,
|
|
1372
|
+
web_vitals,
|
|
1373
|
+
error_data,
|
|
1374
|
+
session_end_reason,
|
|
1375
|
+
viewport_data
|
|
1376
|
+
}) {
|
|
1377
|
+
if (!type) {
|
|
1378
|
+
log("error", "Event type is required - event will be ignored");
|
|
1379
|
+
return;
|
|
1380
|
+
}
|
|
1381
|
+
const currentSessionId = this.get("sessionId");
|
|
1382
|
+
if (!currentSessionId) {
|
|
1383
|
+
if (this.pendingEventsBuffer.length >= MAX_PENDING_EVENTS_BUFFER) {
|
|
1384
|
+
this.pendingEventsBuffer.shift();
|
|
1385
|
+
log("warn", "Pending events buffer full - dropping oldest event", {
|
|
1386
|
+
data: { maxBufferSize: MAX_PENDING_EVENTS_BUFFER }
|
|
1387
|
+
});
|
|
1388
|
+
}
|
|
1389
|
+
this.pendingEventsBuffer.push({
|
|
1390
|
+
type,
|
|
1391
|
+
page_url,
|
|
1392
|
+
from_page_url,
|
|
1393
|
+
scroll_data,
|
|
1394
|
+
click_data,
|
|
1395
|
+
custom_event,
|
|
1396
|
+
web_vitals,
|
|
1397
|
+
error_data,
|
|
1398
|
+
session_end_reason,
|
|
1399
|
+
viewport_data
|
|
1400
|
+
});
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
if (this.lastSessionId !== currentSessionId) {
|
|
1404
|
+
this.lastSessionId = currentSessionId;
|
|
1405
|
+
this.sessionEventCounts = {
|
|
1406
|
+
total: 0,
|
|
1407
|
+
["click" /* CLICK */]: 0,
|
|
1408
|
+
["page_view" /* PAGE_VIEW */]: 0,
|
|
1409
|
+
["custom" /* CUSTOM */]: 0,
|
|
1410
|
+
["viewport_visible" /* VIEWPORT_VISIBLE */]: 0,
|
|
1411
|
+
["scroll" /* SCROLL */]: 0
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
const isCriticalEvent = type === "session_start" /* SESSION_START */ || type === "session_end" /* SESSION_END */;
|
|
1415
|
+
if (!isCriticalEvent && !this.checkRateLimit()) {
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1418
|
+
const eventType = type;
|
|
1419
|
+
if (!isCriticalEvent) {
|
|
1420
|
+
if (this.sessionEventCounts.total >= MAX_EVENTS_PER_SESSION) {
|
|
1421
|
+
log("warn", "Session event limit reached", {
|
|
1422
|
+
data: {
|
|
1423
|
+
type: eventType,
|
|
1424
|
+
total: this.sessionEventCounts.total,
|
|
1425
|
+
limit: MAX_EVENTS_PER_SESSION
|
|
1426
|
+
}
|
|
1427
|
+
});
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
const typeLimit = this.getTypeLimitForEvent(eventType);
|
|
1431
|
+
if (typeLimit) {
|
|
1432
|
+
const currentCount = this.sessionEventCounts[eventType];
|
|
1433
|
+
if (currentCount !== void 0 && currentCount >= typeLimit) {
|
|
1434
|
+
log("warn", "Session event type limit reached", {
|
|
1435
|
+
data: {
|
|
1436
|
+
type: eventType,
|
|
1437
|
+
count: currentCount,
|
|
1438
|
+
limit: typeLimit
|
|
1439
|
+
}
|
|
1440
|
+
});
|
|
1441
|
+
return;
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
if (eventType === "custom" /* CUSTOM */ && custom_event?.name) {
|
|
1446
|
+
const maxSameEventPerMinute = this.get("config")?.maxSameEventPerMinute ?? MAX_SAME_EVENT_PER_MINUTE;
|
|
1447
|
+
if (!this.checkPerEventRateLimit(custom_event.name, maxSameEventPerMinute)) {
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
const isSessionStart = eventType === "session_start" /* SESSION_START */;
|
|
1452
|
+
const currentPageUrl = page_url || this.get("pageUrl");
|
|
1453
|
+
const payload = this.buildEventPayload({
|
|
1454
|
+
type: eventType,
|
|
1455
|
+
page_url: currentPageUrl,
|
|
1456
|
+
from_page_url,
|
|
1457
|
+
scroll_data,
|
|
1458
|
+
click_data,
|
|
1459
|
+
custom_event,
|
|
1460
|
+
web_vitals,
|
|
1461
|
+
error_data,
|
|
1462
|
+
session_end_reason,
|
|
1463
|
+
viewport_data
|
|
1464
|
+
});
|
|
1465
|
+
if (!isCriticalEvent && !this.shouldSample()) {
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
if (isSessionStart) {
|
|
1469
|
+
const currentSessionId2 = this.get("sessionId");
|
|
1470
|
+
if (!currentSessionId2) {
|
|
1471
|
+
log("error", "Session start event requires sessionId - event will be ignored");
|
|
1472
|
+
return;
|
|
1473
|
+
}
|
|
1474
|
+
if (this.get("hasStartSession")) {
|
|
1475
|
+
log("warn", "Duplicate session_start detected", {
|
|
1476
|
+
data: { sessionId: currentSessionId2 }
|
|
1477
|
+
});
|
|
1478
|
+
return;
|
|
1479
|
+
}
|
|
1480
|
+
this.set("hasStartSession", true);
|
|
1481
|
+
}
|
|
1482
|
+
if (this.isDuplicateEvent(payload)) {
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
if (this.get("mode") === "qa" /* QA */ && eventType === "custom" /* CUSTOM */ && custom_event) {
|
|
1486
|
+
console.log("[TraceLog] Event", {
|
|
1487
|
+
name: custom_event.name,
|
|
1488
|
+
...custom_event.metadata && { metadata: custom_event.metadata }
|
|
1489
|
+
});
|
|
1490
|
+
this.emitEvent(payload);
|
|
1491
|
+
return;
|
|
1492
|
+
}
|
|
1493
|
+
this.addToQueue(payload);
|
|
1494
|
+
if (!isCriticalEvent) {
|
|
1495
|
+
this.sessionEventCounts.total++;
|
|
1496
|
+
if (this.sessionEventCounts[eventType] !== void 0) {
|
|
1497
|
+
this.sessionEventCounts[eventType]++;
|
|
1498
|
+
}
|
|
1499
|
+
}
|
|
1500
|
+
}
|
|
1501
|
+
stop() {
|
|
1502
|
+
if (this.sendIntervalId) {
|
|
1503
|
+
clearInterval(this.sendIntervalId);
|
|
1504
|
+
this.sendIntervalId = null;
|
|
1505
|
+
}
|
|
1506
|
+
this.eventsQueue = [];
|
|
1507
|
+
this.pendingEventsBuffer = [];
|
|
1508
|
+
this.recentEventFingerprints.clear();
|
|
1509
|
+
this.rateLimitCounter = 0;
|
|
1510
|
+
this.rateLimitWindowStart = 0;
|
|
1511
|
+
this.perEventRateLimits.clear();
|
|
1512
|
+
this.sessionEventCounts = {
|
|
1513
|
+
total: 0,
|
|
1514
|
+
["click" /* CLICK */]: 0,
|
|
1515
|
+
["page_view" /* PAGE_VIEW */]: 0,
|
|
1516
|
+
["custom" /* CUSTOM */]: 0,
|
|
1517
|
+
["viewport_visible" /* VIEWPORT_VISIBLE */]: 0,
|
|
1518
|
+
["scroll" /* SCROLL */]: 0
|
|
1519
|
+
};
|
|
1520
|
+
this.lastSessionId = null;
|
|
1521
|
+
this.dataSender.stop();
|
|
1522
|
+
}
|
|
1523
|
+
async flushImmediately() {
|
|
1524
|
+
return this.flushEvents(false);
|
|
1525
|
+
}
|
|
1526
|
+
flushImmediatelySync() {
|
|
1527
|
+
return this.flushEvents(true);
|
|
1528
|
+
}
|
|
1529
|
+
getQueueLength() {
|
|
1530
|
+
return this.eventsQueue.length;
|
|
1531
|
+
}
|
|
1532
|
+
flushPendingEvents() {
|
|
1533
|
+
if (this.pendingEventsBuffer.length === 0) {
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
const currentSessionId = this.get("sessionId");
|
|
1537
|
+
if (!currentSessionId) {
|
|
1538
|
+
log("warn", "Cannot flush pending events: session not initialized - keeping in buffer", {
|
|
1539
|
+
data: { bufferedEventCount: this.pendingEventsBuffer.length }
|
|
1540
|
+
});
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
const bufferedEvents = [...this.pendingEventsBuffer];
|
|
1544
|
+
this.pendingEventsBuffer = [];
|
|
1545
|
+
bufferedEvents.forEach((event2) => {
|
|
1546
|
+
this.track(event2);
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
clearSendInterval() {
|
|
1550
|
+
if (this.sendIntervalId) {
|
|
1551
|
+
clearInterval(this.sendIntervalId);
|
|
1552
|
+
this.sendIntervalId = null;
|
|
1553
|
+
}
|
|
1554
|
+
}
|
|
1555
|
+
flushEvents(isSync) {
|
|
1556
|
+
if (this.eventsQueue.length === 0) {
|
|
1557
|
+
return isSync ? true : Promise.resolve(true);
|
|
1558
|
+
}
|
|
1559
|
+
const body = this.buildEventsPayload();
|
|
1560
|
+
const eventsToSend = [...this.eventsQueue];
|
|
1561
|
+
const eventIds = eventsToSend.map((e) => e.id);
|
|
1562
|
+
if (isSync) {
|
|
1563
|
+
const success = this.dataSender.sendEventsQueueSync(body);
|
|
1564
|
+
if (success) {
|
|
1565
|
+
this.removeProcessedEvents(eventIds);
|
|
1566
|
+
this.clearSendInterval();
|
|
1567
|
+
this.emitEventsQueue(body);
|
|
1568
|
+
}
|
|
1569
|
+
return success;
|
|
1570
|
+
} else {
|
|
1571
|
+
return this.dataSender.sendEventsQueue(body, {
|
|
1572
|
+
onSuccess: () => {
|
|
1573
|
+
this.removeProcessedEvents(eventIds);
|
|
1574
|
+
this.clearSendInterval();
|
|
1575
|
+
this.emitEventsQueue(body);
|
|
1576
|
+
},
|
|
1577
|
+
onFailure: () => {
|
|
1578
|
+
log("warn", "Async flush failed", {
|
|
1579
|
+
data: { eventCount: eventsToSend.length }
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
async sendEventsQueue() {
|
|
1586
|
+
if (!this.get("sessionId") || this.eventsQueue.length === 0) {
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
const body = this.buildEventsPayload();
|
|
1590
|
+
const eventsToSend = [...this.eventsQueue];
|
|
1591
|
+
const eventIds = eventsToSend.map((e) => e.id);
|
|
1592
|
+
await this.dataSender.sendEventsQueue(body, {
|
|
1593
|
+
onSuccess: () => {
|
|
1594
|
+
this.removeProcessedEvents(eventIds);
|
|
1595
|
+
this.emitEventsQueue(body);
|
|
1596
|
+
},
|
|
1597
|
+
onFailure: () => {
|
|
1598
|
+
log("warn", "Events send failed, keeping in queue", {
|
|
1599
|
+
data: { eventCount: eventsToSend.length }
|
|
1600
|
+
});
|
|
1601
|
+
}
|
|
1602
|
+
});
|
|
1603
|
+
}
|
|
1604
|
+
buildEventsPayload() {
|
|
1605
|
+
const eventMap = /* @__PURE__ */ new Map();
|
|
1606
|
+
const order = [];
|
|
1607
|
+
for (const event2 of this.eventsQueue) {
|
|
1608
|
+
const signature = this.createEventSignature(event2);
|
|
1609
|
+
if (!eventMap.has(signature)) {
|
|
1610
|
+
order.push(signature);
|
|
1611
|
+
}
|
|
1612
|
+
eventMap.set(signature, event2);
|
|
1613
|
+
}
|
|
1614
|
+
const events = order.map((signature) => eventMap.get(signature)).filter((event2) => Boolean(event2)).sort((a, b) => a.timestamp - b.timestamp);
|
|
1615
|
+
return {
|
|
1616
|
+
user_id: this.get("userId"),
|
|
1617
|
+
session_id: this.get("sessionId"),
|
|
1618
|
+
device: this.get("device"),
|
|
1619
|
+
events,
|
|
1620
|
+
...this.get("config")?.globalMetadata && { global_metadata: this.get("config")?.globalMetadata }
|
|
1621
|
+
};
|
|
1622
|
+
}
|
|
1623
|
+
buildEventPayload(data) {
|
|
1624
|
+
const isSessionStart = data.type === "session_start" /* SESSION_START */;
|
|
1625
|
+
const currentPageUrl = data.page_url ?? this.get("pageUrl");
|
|
1626
|
+
const payload = {
|
|
1627
|
+
id: generateEventId(),
|
|
1628
|
+
type: data.type,
|
|
1629
|
+
page_url: currentPageUrl,
|
|
1630
|
+
timestamp: Date.now(),
|
|
1631
|
+
...isSessionStart && { referrer: document.referrer || "Direct" },
|
|
1632
|
+
...data.from_page_url && { from_page_url: data.from_page_url },
|
|
1633
|
+
...data.scroll_data && { scroll_data: data.scroll_data },
|
|
1634
|
+
...data.click_data && { click_data: data.click_data },
|
|
1635
|
+
...data.custom_event && { custom_event: data.custom_event },
|
|
1636
|
+
...data.web_vitals && { web_vitals: data.web_vitals },
|
|
1637
|
+
...data.error_data && { error_data: data.error_data },
|
|
1638
|
+
...data.session_end_reason && { session_end_reason: data.session_end_reason },
|
|
1639
|
+
...data.viewport_data && { viewport_data: data.viewport_data },
|
|
1640
|
+
...isSessionStart && getUTMParameters() && { utm: getUTMParameters() }
|
|
1641
|
+
};
|
|
1642
|
+
return payload;
|
|
1643
|
+
}
|
|
1644
|
+
/**
|
|
1645
|
+
* Checks if event is a duplicate using time-based cache
|
|
1646
|
+
* Tracks recent event fingerprints with timestamp-based cleanup
|
|
1647
|
+
*/
|
|
1648
|
+
isDuplicateEvent(event2) {
|
|
1649
|
+
const now = Date.now();
|
|
1650
|
+
const fingerprint = this.createEventFingerprint(event2);
|
|
1651
|
+
const lastSeen = this.recentEventFingerprints.get(fingerprint);
|
|
1652
|
+
if (lastSeen && now - lastSeen < DUPLICATE_EVENT_THRESHOLD_MS) {
|
|
1653
|
+
this.recentEventFingerprints.set(fingerprint, now);
|
|
1654
|
+
return true;
|
|
1655
|
+
}
|
|
1656
|
+
this.recentEventFingerprints.set(fingerprint, now);
|
|
1657
|
+
if (this.recentEventFingerprints.size > MAX_FINGERPRINTS) {
|
|
1658
|
+
this.pruneOldFingerprints();
|
|
1659
|
+
}
|
|
1660
|
+
if (this.recentEventFingerprints.size > MAX_FINGERPRINTS_HARD_LIMIT) {
|
|
1661
|
+
this.recentEventFingerprints.clear();
|
|
1662
|
+
this.recentEventFingerprints.set(fingerprint, now);
|
|
1663
|
+
log("warn", "Event fingerprint cache exceeded hard limit, cleared", {
|
|
1664
|
+
data: { hardLimit: MAX_FINGERPRINTS_HARD_LIMIT }
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
return false;
|
|
1668
|
+
}
|
|
1669
|
+
/**
|
|
1670
|
+
* Prunes old fingerprints from cache based on timestamp
|
|
1671
|
+
* Removes entries older than 10x the duplicate threshold (5 seconds)
|
|
1672
|
+
*/
|
|
1673
|
+
pruneOldFingerprints() {
|
|
1674
|
+
const now = Date.now();
|
|
1675
|
+
const cutoff = DUPLICATE_EVENT_THRESHOLD_MS * FINGERPRINT_CLEANUP_MULTIPLIER;
|
|
1676
|
+
for (const [fingerprint, timestamp] of this.recentEventFingerprints.entries()) {
|
|
1677
|
+
if (now - timestamp > cutoff) {
|
|
1678
|
+
this.recentEventFingerprints.delete(fingerprint);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
log("debug", "Pruned old event fingerprints", {
|
|
1682
|
+
data: {
|
|
1683
|
+
remaining: this.recentEventFingerprints.size,
|
|
1684
|
+
cutoffMs: cutoff
|
|
1685
|
+
}
|
|
1686
|
+
});
|
|
1687
|
+
}
|
|
1688
|
+
createEventFingerprint(event2) {
|
|
1689
|
+
let fingerprint = `${event2.type}_${event2.page_url}`;
|
|
1690
|
+
if (event2.click_data) {
|
|
1691
|
+
const x = Math.round((event2.click_data.x || 0) / 10) * 10;
|
|
1692
|
+
const y = Math.round((event2.click_data.y || 0) / 10) * 10;
|
|
1693
|
+
fingerprint += `_click_${x}_${y}`;
|
|
1694
|
+
}
|
|
1695
|
+
if (event2.scroll_data) {
|
|
1696
|
+
fingerprint += `_scroll_${event2.scroll_data.depth}_${event2.scroll_data.direction}`;
|
|
1697
|
+
}
|
|
1698
|
+
if (event2.custom_event) {
|
|
1699
|
+
fingerprint += `_custom_${event2.custom_event.name}`;
|
|
1700
|
+
}
|
|
1701
|
+
if (event2.web_vitals) {
|
|
1702
|
+
fingerprint += `_vitals_${event2.web_vitals.type}`;
|
|
1703
|
+
}
|
|
1704
|
+
if (event2.error_data) {
|
|
1705
|
+
fingerprint += `_error_${event2.error_data.type}_${event2.error_data.message}`;
|
|
1706
|
+
}
|
|
1707
|
+
return fingerprint;
|
|
1708
|
+
}
|
|
1709
|
+
createEventSignature(event2) {
|
|
1710
|
+
return this.createEventFingerprint(event2);
|
|
1711
|
+
}
|
|
1712
|
+
addToQueue(event2) {
|
|
1713
|
+
this.eventsQueue.push(event2);
|
|
1714
|
+
this.emitEvent(event2);
|
|
1715
|
+
if (this.eventsQueue.length > MAX_EVENTS_QUEUE_LENGTH) {
|
|
1716
|
+
const nonCriticalIndex = this.eventsQueue.findIndex(
|
|
1717
|
+
(e) => e.type !== "session_start" /* SESSION_START */ && e.type !== "session_end" /* SESSION_END */
|
|
1718
|
+
);
|
|
1719
|
+
const removedEvent = nonCriticalIndex >= 0 ? this.eventsQueue.splice(nonCriticalIndex, 1)[0] : this.eventsQueue.shift();
|
|
1720
|
+
log("warn", "Event queue overflow, oldest non-critical event removed", {
|
|
1721
|
+
data: {
|
|
1722
|
+
maxLength: MAX_EVENTS_QUEUE_LENGTH,
|
|
1723
|
+
currentLength: this.eventsQueue.length,
|
|
1724
|
+
removedEventType: removedEvent?.type,
|
|
1725
|
+
wasCritical: removedEvent?.type === "session_start" /* SESSION_START */ || removedEvent?.type === "session_end" /* SESSION_END */
|
|
1726
|
+
}
|
|
1727
|
+
});
|
|
1728
|
+
}
|
|
1729
|
+
if (!this.sendIntervalId) {
|
|
1730
|
+
this.startSendInterval();
|
|
1731
|
+
}
|
|
1732
|
+
if (this.eventsQueue.length >= BATCH_SIZE_THRESHOLD) {
|
|
1733
|
+
void this.sendEventsQueue();
|
|
1734
|
+
}
|
|
1735
|
+
this.handleGoogleAnalyticsIntegration(event2);
|
|
1736
|
+
}
|
|
1737
|
+
startSendInterval() {
|
|
1738
|
+
this.sendIntervalId = window.setInterval(() => {
|
|
1739
|
+
if (this.eventsQueue.length > 0) {
|
|
1740
|
+
void this.sendEventsQueue();
|
|
1741
|
+
}
|
|
1742
|
+
}, EVENT_SENT_INTERVAL_MS);
|
|
1743
|
+
}
|
|
1744
|
+
handleGoogleAnalyticsIntegration(event2) {
|
|
1745
|
+
if (this.googleAnalytics && event2.type === "custom" /* CUSTOM */ && event2.custom_event) {
|
|
1746
|
+
if (this.get("mode") === "qa" /* QA */) {
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
this.googleAnalytics.trackEvent(event2.custom_event.name, event2.custom_event.metadata ?? {});
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
shouldSample() {
|
|
1753
|
+
const samplingRate = this.get("config")?.samplingRate ?? 1;
|
|
1754
|
+
return Math.random() < samplingRate;
|
|
1755
|
+
}
|
|
1756
|
+
checkRateLimit() {
|
|
1757
|
+
const now = Date.now();
|
|
1758
|
+
if (now - this.rateLimitWindowStart > RATE_LIMIT_WINDOW_MS) {
|
|
1759
|
+
this.rateLimitCounter = 0;
|
|
1760
|
+
this.rateLimitWindowStart = now;
|
|
1761
|
+
}
|
|
1762
|
+
if (this.rateLimitCounter >= MAX_EVENTS_PER_SECOND) {
|
|
1763
|
+
return false;
|
|
1764
|
+
}
|
|
1765
|
+
this.rateLimitCounter++;
|
|
1766
|
+
return true;
|
|
1767
|
+
}
|
|
1768
|
+
/**
|
|
1769
|
+
* Checks per-event-name rate limiting to prevent infinite loops in user code
|
|
1770
|
+
* Tracks timestamps per event name and limits to maxSameEventPerMinute per minute
|
|
1771
|
+
*/
|
|
1772
|
+
checkPerEventRateLimit(eventName, maxSameEventPerMinute) {
|
|
1773
|
+
const now = Date.now();
|
|
1774
|
+
const timestamps = this.perEventRateLimits.get(eventName) ?? [];
|
|
1775
|
+
const validTimestamps = timestamps.filter((ts) => now - ts < PER_EVENT_RATE_LIMIT_WINDOW_MS);
|
|
1776
|
+
if (validTimestamps.length >= maxSameEventPerMinute) {
|
|
1777
|
+
log("warn", "Per-event rate limit exceeded for custom event", {
|
|
1778
|
+
data: {
|
|
1779
|
+
eventName,
|
|
1780
|
+
limit: maxSameEventPerMinute,
|
|
1781
|
+
window: `${PER_EVENT_RATE_LIMIT_WINDOW_MS / 1e3}s`
|
|
1782
|
+
}
|
|
1783
|
+
});
|
|
1784
|
+
return false;
|
|
1785
|
+
}
|
|
1786
|
+
validTimestamps.push(now);
|
|
1787
|
+
this.perEventRateLimits.set(eventName, validTimestamps);
|
|
1788
|
+
return true;
|
|
1789
|
+
}
|
|
1790
|
+
/**
|
|
1791
|
+
* Gets the per-session limit for a specific event type (Phase 3)
|
|
1792
|
+
*/
|
|
1793
|
+
getTypeLimitForEvent(type) {
|
|
1794
|
+
const limits = {
|
|
1795
|
+
["click" /* CLICK */]: MAX_CLICKS_PER_SESSION,
|
|
1796
|
+
["page_view" /* PAGE_VIEW */]: MAX_PAGE_VIEWS_PER_SESSION,
|
|
1797
|
+
["custom" /* CUSTOM */]: MAX_CUSTOM_EVENTS_PER_SESSION,
|
|
1798
|
+
["viewport_visible" /* VIEWPORT_VISIBLE */]: MAX_VIEWPORT_EVENTS_PER_SESSION,
|
|
1799
|
+
["scroll" /* SCROLL */]: MAX_SCROLL_EVENTS_PER_SESSION
|
|
1800
|
+
};
|
|
1801
|
+
return limits[type] ?? null;
|
|
1802
|
+
}
|
|
1803
|
+
removeProcessedEvents(eventIds) {
|
|
1804
|
+
const eventIdSet = new Set(eventIds);
|
|
1805
|
+
this.eventsQueue = this.eventsQueue.filter((event2) => {
|
|
1806
|
+
return !eventIdSet.has(event2.id);
|
|
1807
|
+
});
|
|
1808
|
+
}
|
|
1809
|
+
emitEvent(eventData) {
|
|
1810
|
+
if (this.emitter) {
|
|
1811
|
+
this.emitter.emit("event" /* EVENT */, eventData);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
emitEventsQueue(queue) {
|
|
1815
|
+
if (this.emitter) {
|
|
1816
|
+
this.emitter.emit("queue" /* QUEUE */, queue);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1819
|
+
};
|
|
1820
|
+
|
|
1821
|
+
// src/managers/user.manager.ts
|
|
1822
|
+
var UserManager = class {
|
|
1823
|
+
/**
|
|
1824
|
+
* Gets or creates a unique user ID for the given project.
|
|
1825
|
+
* The user ID is persisted in localStorage and reused across sessions.
|
|
1826
|
+
*
|
|
1827
|
+
* @param storageManager - Storage manager instance
|
|
1828
|
+
* @param projectId - Project identifier for namespacing
|
|
1829
|
+
* @returns Persistent unique user ID
|
|
1830
|
+
*/
|
|
1831
|
+
static getId(storageManager) {
|
|
1832
|
+
const storageKey = USER_ID_KEY;
|
|
1833
|
+
const storedUserId = storageManager.getItem(storageKey);
|
|
1834
|
+
if (storedUserId) {
|
|
1835
|
+
return storedUserId;
|
|
1836
|
+
}
|
|
1837
|
+
const newUserId = generateUUID();
|
|
1838
|
+
storageManager.setItem(storageKey, newUserId);
|
|
1839
|
+
return newUserId;
|
|
1840
|
+
}
|
|
1841
|
+
};
|
|
1842
|
+
|
|
1843
|
+
// src/managers/session.manager.ts
|
|
1844
|
+
var SessionManager = class extends StateManager {
|
|
1845
|
+
storageManager;
|
|
1846
|
+
eventManager;
|
|
1847
|
+
projectId;
|
|
1848
|
+
sessionTimeoutId = null;
|
|
1849
|
+
broadcastChannel = null;
|
|
1850
|
+
activityHandler = null;
|
|
1851
|
+
visibilityChangeHandler = null;
|
|
1852
|
+
beforeUnloadHandler = null;
|
|
1853
|
+
isTracking = false;
|
|
1854
|
+
constructor(storageManager, eventManager, projectId) {
|
|
1855
|
+
super();
|
|
1856
|
+
this.storageManager = storageManager;
|
|
1857
|
+
this.eventManager = eventManager;
|
|
1858
|
+
this.projectId = projectId;
|
|
1859
|
+
}
|
|
1860
|
+
initCrossTabSync() {
|
|
1861
|
+
if (typeof BroadcastChannel === "undefined") {
|
|
1862
|
+
log("warn", "BroadcastChannel not supported");
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1865
|
+
const projectId = this.getProjectId();
|
|
1866
|
+
this.broadcastChannel = new BroadcastChannel(BROADCAST_CHANNEL_NAME(projectId));
|
|
1867
|
+
this.broadcastChannel.onmessage = (event2) => {
|
|
1868
|
+
const { action, sessionId, timestamp, projectId: messageProjectId } = event2.data ?? {};
|
|
1869
|
+
if (messageProjectId !== projectId) {
|
|
1870
|
+
return;
|
|
1871
|
+
}
|
|
1872
|
+
if (action === "session_end") {
|
|
1873
|
+
this.resetSessionState();
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
if (sessionId && typeof timestamp === "number" && timestamp > Date.now() - 5e3) {
|
|
1877
|
+
this.set("sessionId", sessionId);
|
|
1878
|
+
this.set("hasStartSession", true);
|
|
1879
|
+
this.persistSession(sessionId, timestamp);
|
|
1880
|
+
if (this.isTracking) {
|
|
1881
|
+
this.setupSessionTimeout();
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
shareSession(sessionId) {
|
|
1887
|
+
if (this.broadcastChannel && typeof this.broadcastChannel.postMessage === "function") {
|
|
1888
|
+
this.broadcastChannel.postMessage({
|
|
1889
|
+
action: "session_start",
|
|
1890
|
+
projectId: this.getProjectId(),
|
|
1891
|
+
sessionId,
|
|
1892
|
+
timestamp: Date.now()
|
|
1893
|
+
});
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
broadcastSessionEnd(sessionId, reason) {
|
|
1897
|
+
if (!sessionId) {
|
|
1898
|
+
return;
|
|
1899
|
+
}
|
|
1900
|
+
if (this.broadcastChannel && typeof this.broadcastChannel.postMessage === "function") {
|
|
1901
|
+
try {
|
|
1902
|
+
this.broadcastChannel.postMessage({
|
|
1903
|
+
action: "session_end",
|
|
1904
|
+
projectId: this.getProjectId(),
|
|
1905
|
+
sessionId,
|
|
1906
|
+
reason,
|
|
1907
|
+
timestamp: Date.now()
|
|
1908
|
+
});
|
|
1909
|
+
} catch (error) {
|
|
1910
|
+
log("warn", "Failed to broadcast session end", { error, data: { sessionId, reason } });
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
cleanupCrossTabSync() {
|
|
1915
|
+
if (this.broadcastChannel) {
|
|
1916
|
+
if (typeof this.broadcastChannel.close === "function") {
|
|
1917
|
+
this.broadcastChannel.close();
|
|
1918
|
+
}
|
|
1919
|
+
this.broadcastChannel = null;
|
|
1920
|
+
}
|
|
1921
|
+
}
|
|
1922
|
+
recoverSession() {
|
|
1923
|
+
const storedSession = this.loadStoredSession();
|
|
1924
|
+
if (!storedSession) {
|
|
1925
|
+
return null;
|
|
1926
|
+
}
|
|
1927
|
+
const sessionTimeout = this.get("config")?.sessionTimeout ?? DEFAULT_SESSION_TIMEOUT;
|
|
1928
|
+
if (Date.now() - storedSession.lastActivity > sessionTimeout) {
|
|
1929
|
+
this.clearStoredSession();
|
|
1930
|
+
return null;
|
|
1931
|
+
}
|
|
1932
|
+
return storedSession.id;
|
|
1933
|
+
}
|
|
1934
|
+
persistSession(sessionId, lastActivity = Date.now()) {
|
|
1935
|
+
this.saveStoredSession({
|
|
1936
|
+
id: sessionId,
|
|
1937
|
+
lastActivity
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
clearStoredSession() {
|
|
1941
|
+
const storageKey = this.getSessionStorageKey();
|
|
1942
|
+
this.storageManager.removeItem(storageKey);
|
|
1943
|
+
}
|
|
1944
|
+
loadStoredSession() {
|
|
1945
|
+
const storageKey = this.getSessionStorageKey();
|
|
1946
|
+
const storedData = this.storageManager.getItem(storageKey);
|
|
1947
|
+
if (!storedData) {
|
|
1948
|
+
return null;
|
|
1949
|
+
}
|
|
1950
|
+
try {
|
|
1951
|
+
const parsed = JSON.parse(storedData);
|
|
1952
|
+
if (!parsed.id || typeof parsed.lastActivity !== "number") {
|
|
1953
|
+
return null;
|
|
1954
|
+
}
|
|
1955
|
+
return parsed;
|
|
1956
|
+
} catch {
|
|
1957
|
+
this.storageManager.removeItem(storageKey);
|
|
1958
|
+
return null;
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
saveStoredSession(session) {
|
|
1962
|
+
const storageKey = this.getSessionStorageKey();
|
|
1963
|
+
this.storageManager.setItem(storageKey, JSON.stringify(session));
|
|
1964
|
+
}
|
|
1965
|
+
getSessionStorageKey() {
|
|
1966
|
+
return SESSION_STORAGE_KEY(this.getProjectId());
|
|
1967
|
+
}
|
|
1968
|
+
getProjectId() {
|
|
1969
|
+
return this.projectId;
|
|
1970
|
+
}
|
|
1971
|
+
startTracking() {
|
|
1972
|
+
if (this.isTracking) {
|
|
1973
|
+
log("warn", "Session tracking already active");
|
|
1974
|
+
return;
|
|
1975
|
+
}
|
|
1976
|
+
const recoveredSessionId = this.recoverSession();
|
|
1977
|
+
const sessionId = recoveredSessionId ?? this.generateSessionId();
|
|
1978
|
+
const isRecovered = Boolean(recoveredSessionId);
|
|
1979
|
+
this.isTracking = true;
|
|
1980
|
+
try {
|
|
1981
|
+
this.set("sessionId", sessionId);
|
|
1982
|
+
this.persistSession(sessionId);
|
|
1983
|
+
if (!isRecovered) {
|
|
1984
|
+
this.eventManager.track({
|
|
1985
|
+
type: "session_start" /* SESSION_START */
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
this.initCrossTabSync();
|
|
1989
|
+
this.shareSession(sessionId);
|
|
1990
|
+
this.setupSessionTimeout();
|
|
1991
|
+
this.setupActivityListeners();
|
|
1992
|
+
this.setupLifecycleListeners();
|
|
1993
|
+
} catch (error) {
|
|
1994
|
+
this.isTracking = false;
|
|
1995
|
+
this.clearSessionTimeout();
|
|
1996
|
+
this.cleanupActivityListeners();
|
|
1997
|
+
this.cleanupLifecycleListeners();
|
|
1998
|
+
this.cleanupCrossTabSync();
|
|
1999
|
+
this.set("sessionId", null);
|
|
2000
|
+
throw error;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
generateSessionId() {
|
|
2004
|
+
return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
|
|
2005
|
+
}
|
|
2006
|
+
setupSessionTimeout() {
|
|
2007
|
+
this.clearSessionTimeout();
|
|
2008
|
+
const sessionTimeout = this.get("config")?.sessionTimeout ?? DEFAULT_SESSION_TIMEOUT;
|
|
2009
|
+
this.sessionTimeoutId = setTimeout(() => {
|
|
2010
|
+
this.endSession("inactivity");
|
|
2011
|
+
}, sessionTimeout);
|
|
2012
|
+
}
|
|
2013
|
+
resetSessionTimeout() {
|
|
2014
|
+
this.setupSessionTimeout();
|
|
2015
|
+
const sessionId = this.get("sessionId");
|
|
2016
|
+
if (sessionId) {
|
|
2017
|
+
this.persistSession(sessionId);
|
|
2018
|
+
}
|
|
2019
|
+
}
|
|
2020
|
+
clearSessionTimeout() {
|
|
2021
|
+
if (this.sessionTimeoutId) {
|
|
2022
|
+
clearTimeout(this.sessionTimeoutId);
|
|
2023
|
+
this.sessionTimeoutId = null;
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
setupActivityListeners() {
|
|
2027
|
+
this.activityHandler = () => {
|
|
2028
|
+
this.resetSessionTimeout();
|
|
2029
|
+
};
|
|
2030
|
+
document.addEventListener("click", this.activityHandler, { passive: true });
|
|
2031
|
+
document.addEventListener("keydown", this.activityHandler, { passive: true });
|
|
2032
|
+
document.addEventListener("scroll", this.activityHandler, { passive: true });
|
|
2033
|
+
}
|
|
2034
|
+
cleanupActivityListeners() {
|
|
2035
|
+
if (this.activityHandler) {
|
|
2036
|
+
document.removeEventListener("click", this.activityHandler);
|
|
2037
|
+
document.removeEventListener("keydown", this.activityHandler);
|
|
2038
|
+
document.removeEventListener("scroll", this.activityHandler);
|
|
2039
|
+
this.activityHandler = null;
|
|
2040
|
+
}
|
|
2041
|
+
}
|
|
2042
|
+
setupLifecycleListeners() {
|
|
2043
|
+
if (this.visibilityChangeHandler || this.beforeUnloadHandler) {
|
|
2044
|
+
return;
|
|
2045
|
+
}
|
|
2046
|
+
this.visibilityChangeHandler = () => {
|
|
2047
|
+
if (document.hidden) {
|
|
2048
|
+
this.clearSessionTimeout();
|
|
2049
|
+
} else {
|
|
2050
|
+
const sessionId = this.get("sessionId");
|
|
2051
|
+
if (sessionId) {
|
|
2052
|
+
this.setupSessionTimeout();
|
|
2053
|
+
}
|
|
2054
|
+
}
|
|
2055
|
+
};
|
|
2056
|
+
this.beforeUnloadHandler = () => {
|
|
2057
|
+
this.endSession("page_unload");
|
|
2058
|
+
};
|
|
2059
|
+
document.addEventListener("visibilitychange", this.visibilityChangeHandler);
|
|
2060
|
+
window.addEventListener("beforeunload", this.beforeUnloadHandler);
|
|
2061
|
+
}
|
|
2062
|
+
cleanupLifecycleListeners() {
|
|
2063
|
+
if (this.visibilityChangeHandler) {
|
|
2064
|
+
document.removeEventListener("visibilitychange", this.visibilityChangeHandler);
|
|
2065
|
+
this.visibilityChangeHandler = null;
|
|
2066
|
+
}
|
|
2067
|
+
if (this.beforeUnloadHandler) {
|
|
2068
|
+
window.removeEventListener("beforeunload", this.beforeUnloadHandler);
|
|
2069
|
+
this.beforeUnloadHandler = null;
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
endSession(reason) {
|
|
2073
|
+
const sessionId = this.get("sessionId");
|
|
2074
|
+
if (!sessionId) {
|
|
2075
|
+
log("warn", "endSession called without active session", { data: { reason } });
|
|
2076
|
+
this.resetSessionState(reason);
|
|
2077
|
+
return;
|
|
2078
|
+
}
|
|
2079
|
+
this.eventManager.track({
|
|
2080
|
+
type: "session_end" /* SESSION_END */,
|
|
2081
|
+
session_end_reason: reason
|
|
2082
|
+
});
|
|
2083
|
+
const flushResult = this.eventManager.flushImmediatelySync();
|
|
2084
|
+
if (!flushResult) {
|
|
2085
|
+
log("warn", "Sync flush failed during session end, events persisted for recovery", {
|
|
2086
|
+
data: { reason, sessionId }
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
this.broadcastSessionEnd(sessionId, reason);
|
|
2090
|
+
this.resetSessionState(reason);
|
|
2091
|
+
}
|
|
2092
|
+
resetSessionState(reason) {
|
|
2093
|
+
this.clearSessionTimeout();
|
|
2094
|
+
this.cleanupActivityListeners();
|
|
2095
|
+
this.cleanupLifecycleListeners();
|
|
2096
|
+
this.cleanupCrossTabSync();
|
|
2097
|
+
if (reason !== "page_unload") {
|
|
2098
|
+
this.clearStoredSession();
|
|
2099
|
+
}
|
|
2100
|
+
this.set("sessionId", null);
|
|
2101
|
+
this.set("hasStartSession", false);
|
|
2102
|
+
this.isTracking = false;
|
|
2103
|
+
}
|
|
2104
|
+
stopTracking() {
|
|
2105
|
+
this.endSession("manual_stop");
|
|
2106
|
+
}
|
|
2107
|
+
destroy() {
|
|
2108
|
+
this.clearSessionTimeout();
|
|
2109
|
+
this.cleanupActivityListeners();
|
|
2110
|
+
this.cleanupCrossTabSync();
|
|
2111
|
+
this.cleanupLifecycleListeners();
|
|
2112
|
+
this.isTracking = false;
|
|
2113
|
+
this.set("hasStartSession", false);
|
|
2114
|
+
}
|
|
2115
|
+
};
|
|
2116
|
+
|
|
2117
|
+
// src/handlers/session.handler.ts
|
|
2118
|
+
var SessionHandler = class extends StateManager {
|
|
2119
|
+
eventManager;
|
|
2120
|
+
storageManager;
|
|
2121
|
+
sessionManager = null;
|
|
2122
|
+
destroyed = false;
|
|
2123
|
+
constructor(storageManager, eventManager) {
|
|
2124
|
+
super();
|
|
2125
|
+
this.eventManager = eventManager;
|
|
2126
|
+
this.storageManager = storageManager;
|
|
2127
|
+
}
|
|
2128
|
+
startTracking() {
|
|
2129
|
+
if (this.isActive()) {
|
|
2130
|
+
return;
|
|
2131
|
+
}
|
|
2132
|
+
if (this.destroyed) {
|
|
2133
|
+
log("warn", "Cannot start tracking on destroyed handler");
|
|
2134
|
+
return;
|
|
2135
|
+
}
|
|
2136
|
+
const config = this.get("config");
|
|
2137
|
+
const projectId = config?.integrations?.tracelog?.projectId ?? config?.integrations?.custom?.collectApiUrl ?? "default";
|
|
2138
|
+
if (!projectId) {
|
|
2139
|
+
throw new Error("Cannot start session tracking: config not available");
|
|
2140
|
+
}
|
|
2141
|
+
try {
|
|
2142
|
+
this.sessionManager = new SessionManager(this.storageManager, this.eventManager, projectId);
|
|
2143
|
+
this.sessionManager.startTracking();
|
|
2144
|
+
this.eventManager.flushPendingEvents();
|
|
2145
|
+
} catch (error) {
|
|
2146
|
+
if (this.sessionManager) {
|
|
2147
|
+
try {
|
|
2148
|
+
this.sessionManager.destroy();
|
|
2149
|
+
} catch {
|
|
2150
|
+
}
|
|
2151
|
+
this.sessionManager = null;
|
|
2152
|
+
}
|
|
2153
|
+
log("error", "Failed to start session tracking", { error });
|
|
2154
|
+
throw error;
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
isActive() {
|
|
2158
|
+
return this.sessionManager !== null && !this.destroyed;
|
|
2159
|
+
}
|
|
2160
|
+
cleanupSessionManager() {
|
|
2161
|
+
if (this.sessionManager) {
|
|
2162
|
+
this.sessionManager.stopTracking();
|
|
2163
|
+
this.sessionManager.destroy();
|
|
2164
|
+
this.sessionManager = null;
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
stopTracking() {
|
|
2168
|
+
this.cleanupSessionManager();
|
|
2169
|
+
}
|
|
2170
|
+
destroy() {
|
|
2171
|
+
if (this.destroyed) {
|
|
2172
|
+
return;
|
|
2173
|
+
}
|
|
2174
|
+
if (this.sessionManager) {
|
|
2175
|
+
this.sessionManager.destroy();
|
|
2176
|
+
this.sessionManager = null;
|
|
2177
|
+
}
|
|
2178
|
+
this.destroyed = true;
|
|
2179
|
+
this.set("hasStartSession", false);
|
|
2180
|
+
}
|
|
2181
|
+
};
|
|
2182
|
+
|
|
2183
|
+
// src/handlers/page-view.handler.ts
|
|
2184
|
+
var PageViewHandler = class extends StateManager {
|
|
2185
|
+
eventManager;
|
|
2186
|
+
onTrack;
|
|
2187
|
+
originalPushState;
|
|
2188
|
+
originalReplaceState;
|
|
2189
|
+
lastPageViewTime = 0;
|
|
2190
|
+
constructor(eventManager, onTrack) {
|
|
2191
|
+
super();
|
|
2192
|
+
this.eventManager = eventManager;
|
|
2193
|
+
this.onTrack = onTrack;
|
|
2194
|
+
}
|
|
2195
|
+
startTracking() {
|
|
2196
|
+
this.trackInitialPageView();
|
|
2197
|
+
window.addEventListener("popstate", this.trackCurrentPage, true);
|
|
2198
|
+
window.addEventListener("hashchange", this.trackCurrentPage, true);
|
|
2199
|
+
this.patchHistory("pushState");
|
|
2200
|
+
this.patchHistory("replaceState");
|
|
2201
|
+
}
|
|
2202
|
+
stopTracking() {
|
|
2203
|
+
window.removeEventListener("popstate", this.trackCurrentPage, true);
|
|
2204
|
+
window.removeEventListener("hashchange", this.trackCurrentPage, true);
|
|
2205
|
+
if (this.originalPushState) {
|
|
2206
|
+
window.history.pushState = this.originalPushState;
|
|
2207
|
+
}
|
|
2208
|
+
if (this.originalReplaceState) {
|
|
2209
|
+
window.history.replaceState = this.originalReplaceState;
|
|
2210
|
+
}
|
|
2211
|
+
this.lastPageViewTime = 0;
|
|
2212
|
+
}
|
|
2213
|
+
patchHistory(method) {
|
|
2214
|
+
const original = window.history[method];
|
|
2215
|
+
if (method === "pushState" && !this.originalPushState) {
|
|
2216
|
+
this.originalPushState = original;
|
|
2217
|
+
} else if (method === "replaceState" && !this.originalReplaceState) {
|
|
2218
|
+
this.originalReplaceState = original;
|
|
2219
|
+
}
|
|
2220
|
+
window.history[method] = (...args) => {
|
|
2221
|
+
original.apply(window.history, args);
|
|
2222
|
+
this.trackCurrentPage();
|
|
2223
|
+
};
|
|
2224
|
+
}
|
|
2225
|
+
trackCurrentPage = () => {
|
|
2226
|
+
const rawUrl = window.location.href;
|
|
2227
|
+
const normalizedUrl = normalizeUrl(rawUrl, this.get("config").sensitiveQueryParams);
|
|
2228
|
+
if (this.get("pageUrl") === normalizedUrl) {
|
|
2229
|
+
return;
|
|
2230
|
+
}
|
|
2231
|
+
const now = Date.now();
|
|
2232
|
+
const throttleMs = this.get("config").pageViewThrottleMs ?? DEFAULT_PAGE_VIEW_THROTTLE_MS;
|
|
2233
|
+
if (now - this.lastPageViewTime < throttleMs) {
|
|
2234
|
+
return;
|
|
2235
|
+
}
|
|
2236
|
+
this.lastPageViewTime = now;
|
|
2237
|
+
this.onTrack();
|
|
2238
|
+
const fromUrl = this.get("pageUrl");
|
|
2239
|
+
this.set("pageUrl", normalizedUrl);
|
|
2240
|
+
const pageViewData = this.extractPageViewData();
|
|
2241
|
+
this.eventManager.track({
|
|
2242
|
+
type: "page_view" /* PAGE_VIEW */,
|
|
2243
|
+
page_url: this.get("pageUrl"),
|
|
2244
|
+
from_page_url: fromUrl,
|
|
2245
|
+
...pageViewData && { page_view: pageViewData }
|
|
2246
|
+
});
|
|
2247
|
+
};
|
|
2248
|
+
trackInitialPageView() {
|
|
2249
|
+
const normalizedUrl = normalizeUrl(window.location.href, this.get("config").sensitiveQueryParams);
|
|
2250
|
+
const pageViewData = this.extractPageViewData();
|
|
2251
|
+
this.lastPageViewTime = Date.now();
|
|
2252
|
+
this.eventManager.track({
|
|
2253
|
+
type: "page_view" /* PAGE_VIEW */,
|
|
2254
|
+
page_url: normalizedUrl,
|
|
2255
|
+
...pageViewData && { page_view: pageViewData }
|
|
2256
|
+
});
|
|
2257
|
+
this.onTrack();
|
|
2258
|
+
}
|
|
2259
|
+
extractPageViewData() {
|
|
2260
|
+
const { pathname, search, hash } = window.location;
|
|
2261
|
+
const { referrer } = document;
|
|
2262
|
+
const { title } = document;
|
|
2263
|
+
if (!referrer && !title && !pathname && !search && !hash) {
|
|
2264
|
+
return void 0;
|
|
2265
|
+
}
|
|
2266
|
+
const data = {
|
|
2267
|
+
...referrer && { referrer },
|
|
2268
|
+
...title && { title },
|
|
2269
|
+
...pathname && { pathname },
|
|
2270
|
+
...search && { search },
|
|
2271
|
+
...hash && { hash }
|
|
2272
|
+
};
|
|
2273
|
+
return data;
|
|
2274
|
+
}
|
|
2275
|
+
};
|
|
2276
|
+
|
|
2277
|
+
// src/handlers/click.handler.ts
|
|
2278
|
+
var ClickHandler = class extends StateManager {
|
|
2279
|
+
eventManager;
|
|
2280
|
+
lastClickTimes = /* @__PURE__ */ new Map();
|
|
2281
|
+
clickHandler;
|
|
2282
|
+
lastPruneTime = 0;
|
|
2283
|
+
constructor(eventManager) {
|
|
2284
|
+
super();
|
|
2285
|
+
this.eventManager = eventManager;
|
|
2286
|
+
}
|
|
2287
|
+
startTracking() {
|
|
2288
|
+
if (this.clickHandler) {
|
|
2289
|
+
return;
|
|
2290
|
+
}
|
|
2291
|
+
this.clickHandler = (event2) => {
|
|
2292
|
+
const mouseEvent = event2;
|
|
2293
|
+
const target = mouseEvent.target;
|
|
2294
|
+
const clickedElement = typeof HTMLElement !== "undefined" && target instanceof HTMLElement ? target : typeof HTMLElement !== "undefined" && target instanceof Node && target.parentElement instanceof HTMLElement ? target.parentElement : null;
|
|
2295
|
+
if (!clickedElement) {
|
|
2296
|
+
log("warn", "Click target not found or not an element");
|
|
2297
|
+
return;
|
|
2298
|
+
}
|
|
2299
|
+
if (this.shouldIgnoreElement(clickedElement)) {
|
|
2300
|
+
return;
|
|
2301
|
+
}
|
|
2302
|
+
const clickThrottleMs = this.get("config")?.clickThrottleMs ?? DEFAULT_CLICK_THROTTLE_MS;
|
|
2303
|
+
if (clickThrottleMs > 0 && !this.checkClickThrottle(clickedElement, clickThrottleMs)) {
|
|
2304
|
+
return;
|
|
2305
|
+
}
|
|
2306
|
+
const trackingElement = this.findTrackingElement(clickedElement);
|
|
2307
|
+
const relevantClickElement = this.getRelevantClickElement(clickedElement);
|
|
2308
|
+
const coordinates = this.calculateClickCoordinates(mouseEvent, clickedElement);
|
|
2309
|
+
if (trackingElement) {
|
|
2310
|
+
const trackingData = this.extractTrackingData(trackingElement);
|
|
2311
|
+
if (trackingData) {
|
|
2312
|
+
const attributeData = this.createCustomEventData(trackingData);
|
|
2313
|
+
this.eventManager.track({
|
|
2314
|
+
type: "custom" /* CUSTOM */,
|
|
2315
|
+
custom_event: {
|
|
2316
|
+
name: attributeData.name,
|
|
2317
|
+
...attributeData.value && { metadata: { value: attributeData.value } }
|
|
2318
|
+
}
|
|
2319
|
+
});
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
const clickData = this.generateClickData(clickedElement, relevantClickElement, coordinates);
|
|
2323
|
+
this.eventManager.track({
|
|
2324
|
+
type: "click" /* CLICK */,
|
|
2325
|
+
click_data: clickData
|
|
2326
|
+
});
|
|
2327
|
+
};
|
|
2328
|
+
window.addEventListener("click", this.clickHandler, true);
|
|
2329
|
+
}
|
|
2330
|
+
stopTracking() {
|
|
2331
|
+
if (this.clickHandler) {
|
|
2332
|
+
window.removeEventListener("click", this.clickHandler, true);
|
|
2333
|
+
this.clickHandler = void 0;
|
|
2334
|
+
}
|
|
2335
|
+
this.lastClickTimes.clear();
|
|
2336
|
+
this.lastPruneTime = 0;
|
|
2337
|
+
}
|
|
2338
|
+
shouldIgnoreElement(element) {
|
|
2339
|
+
if (element.hasAttribute(`${HTML_DATA_ATTR_PREFIX}-ignore`)) {
|
|
2340
|
+
return true;
|
|
2341
|
+
}
|
|
2342
|
+
const parent = element.closest(`[${HTML_DATA_ATTR_PREFIX}-ignore]`);
|
|
2343
|
+
return parent !== null;
|
|
2344
|
+
}
|
|
2345
|
+
/**
|
|
2346
|
+
* Checks per-element click throttling to prevent double-clicks and rapid spam
|
|
2347
|
+
* Returns true if the click should be tracked, false if throttled
|
|
2348
|
+
*/
|
|
2349
|
+
checkClickThrottle(element, throttleMs) {
|
|
2350
|
+
const signature = this.getElementSignature(element);
|
|
2351
|
+
const now = Date.now();
|
|
2352
|
+
this.pruneThrottleCache(now);
|
|
2353
|
+
const lastClickTime = this.lastClickTimes.get(signature);
|
|
2354
|
+
if (lastClickTime !== void 0 && now - lastClickTime < throttleMs) {
|
|
2355
|
+
log("debug", "ClickHandler: Click suppressed by throttle", {
|
|
2356
|
+
data: {
|
|
2357
|
+
signature,
|
|
2358
|
+
throttleRemaining: throttleMs - (now - lastClickTime)
|
|
2359
|
+
}
|
|
2360
|
+
});
|
|
2361
|
+
return false;
|
|
2362
|
+
}
|
|
2363
|
+
this.lastClickTimes.set(signature, now);
|
|
2364
|
+
return true;
|
|
2365
|
+
}
|
|
2366
|
+
/**
|
|
2367
|
+
* Prunes stale entries from the throttle cache to prevent memory leaks
|
|
2368
|
+
* Uses TTL-based eviction (5 minutes) and enforces max size limit
|
|
2369
|
+
* Called during checkClickThrottle with built-in rate limiting (every 30 seconds)
|
|
2370
|
+
*/
|
|
2371
|
+
pruneThrottleCache(now) {
|
|
2372
|
+
if (now - this.lastPruneTime < THROTTLE_PRUNE_INTERVAL_MS) {
|
|
2373
|
+
return;
|
|
2374
|
+
}
|
|
2375
|
+
this.lastPruneTime = now;
|
|
2376
|
+
const cutoff = now - THROTTLE_ENTRY_TTL_MS;
|
|
2377
|
+
for (const [key, timestamp] of this.lastClickTimes.entries()) {
|
|
2378
|
+
if (timestamp < cutoff) {
|
|
2379
|
+
this.lastClickTimes.delete(key);
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
if (this.lastClickTimes.size > MAX_THROTTLE_CACHE_ENTRIES) {
|
|
2383
|
+
const entries = Array.from(this.lastClickTimes.entries()).sort((a, b) => a[1] - b[1]);
|
|
2384
|
+
const excessCount = this.lastClickTimes.size - MAX_THROTTLE_CACHE_ENTRIES;
|
|
2385
|
+
const toDelete = entries.slice(0, excessCount);
|
|
2386
|
+
for (const [key] of toDelete) {
|
|
2387
|
+
this.lastClickTimes.delete(key);
|
|
2388
|
+
}
|
|
2389
|
+
log("debug", "ClickHandler: Pruned throttle cache", {
|
|
2390
|
+
data: {
|
|
2391
|
+
removed: toDelete.length,
|
|
2392
|
+
remaining: this.lastClickTimes.size
|
|
2393
|
+
}
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
/**
|
|
2398
|
+
* Creates a stable signature for an element to track throttling
|
|
2399
|
+
* Priority: id > data-testid > data-tlog-name > DOM path
|
|
2400
|
+
*/
|
|
2401
|
+
getElementSignature(element) {
|
|
2402
|
+
if (element.id) {
|
|
2403
|
+
return `#${element.id}`;
|
|
2404
|
+
}
|
|
2405
|
+
const testId = element.getAttribute("data-testid");
|
|
2406
|
+
if (testId) {
|
|
2407
|
+
return `[data-testid="${testId}"]`;
|
|
2408
|
+
}
|
|
2409
|
+
const tlogName = element.getAttribute(`${HTML_DATA_ATTR_PREFIX}-name`);
|
|
2410
|
+
if (tlogName) {
|
|
2411
|
+
return `[${HTML_DATA_ATTR_PREFIX}-name="${tlogName}"]`;
|
|
2412
|
+
}
|
|
2413
|
+
return this.getElementPath(element);
|
|
2414
|
+
}
|
|
2415
|
+
/**
|
|
2416
|
+
* Generates a DOM path for an element (e.g., "body>div>button")
|
|
2417
|
+
*/
|
|
2418
|
+
getElementPath(element) {
|
|
2419
|
+
const path = [];
|
|
2420
|
+
let current = element;
|
|
2421
|
+
while (current && current !== document.body) {
|
|
2422
|
+
let selector = current.tagName.toLowerCase();
|
|
2423
|
+
if (current.className) {
|
|
2424
|
+
const firstClass = current.className.split(" ")[0];
|
|
2425
|
+
if (firstClass) {
|
|
2426
|
+
selector += `.${firstClass}`;
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
path.unshift(selector);
|
|
2430
|
+
current = current.parentElement;
|
|
2431
|
+
}
|
|
2432
|
+
return path.join(">") || "unknown";
|
|
2433
|
+
}
|
|
2434
|
+
findTrackingElement(element) {
|
|
2435
|
+
if (element.hasAttribute(`${HTML_DATA_ATTR_PREFIX}-name`)) {
|
|
2436
|
+
return element;
|
|
2437
|
+
}
|
|
2438
|
+
const closest = element.closest(`[${HTML_DATA_ATTR_PREFIX}-name]`);
|
|
2439
|
+
return closest;
|
|
2440
|
+
}
|
|
2441
|
+
getRelevantClickElement(element) {
|
|
2442
|
+
for (const selector of INTERACTIVE_SELECTORS) {
|
|
2443
|
+
try {
|
|
2444
|
+
if (element.matches(selector)) {
|
|
2445
|
+
return element;
|
|
2446
|
+
}
|
|
2447
|
+
const parent = element.closest(selector);
|
|
2448
|
+
if (parent) {
|
|
2449
|
+
return parent;
|
|
2450
|
+
}
|
|
2451
|
+
} catch (error) {
|
|
2452
|
+
log("warn", "Invalid selector in element search", { error, data: { selector } });
|
|
2453
|
+
continue;
|
|
2454
|
+
}
|
|
2455
|
+
}
|
|
2456
|
+
return element;
|
|
2457
|
+
}
|
|
2458
|
+
clamp(value) {
|
|
2459
|
+
return Math.max(0, Math.min(1, Number(value.toFixed(3))));
|
|
2460
|
+
}
|
|
2461
|
+
calculateClickCoordinates(event2, element) {
|
|
2462
|
+
const rect = element.getBoundingClientRect();
|
|
2463
|
+
const x = event2.clientX;
|
|
2464
|
+
const y = event2.clientY;
|
|
2465
|
+
const relativeX = rect.width > 0 ? this.clamp((x - rect.left) / rect.width) : 0;
|
|
2466
|
+
const relativeY = rect.height > 0 ? this.clamp((y - rect.top) / rect.height) : 0;
|
|
2467
|
+
return { x, y, relativeX, relativeY };
|
|
2468
|
+
}
|
|
2469
|
+
extractTrackingData(trackingElement) {
|
|
2470
|
+
const name = trackingElement.getAttribute(`${HTML_DATA_ATTR_PREFIX}-name`);
|
|
2471
|
+
const value = trackingElement.getAttribute(`${HTML_DATA_ATTR_PREFIX}-value`);
|
|
2472
|
+
if (!name) {
|
|
2473
|
+
return void 0;
|
|
2474
|
+
}
|
|
2475
|
+
return {
|
|
2476
|
+
element: trackingElement,
|
|
2477
|
+
name,
|
|
2478
|
+
...value && { value }
|
|
2479
|
+
};
|
|
2480
|
+
}
|
|
2481
|
+
generateClickData(clickedElement, relevantElement, coordinates) {
|
|
2482
|
+
const { x, y, relativeX, relativeY } = coordinates;
|
|
2483
|
+
const text = this.getRelevantText(clickedElement, relevantElement);
|
|
2484
|
+
const attributes = this.extractElementAttributes(relevantElement);
|
|
2485
|
+
return {
|
|
2486
|
+
x,
|
|
2487
|
+
y,
|
|
2488
|
+
relativeX,
|
|
2489
|
+
relativeY,
|
|
2490
|
+
tag: relevantElement.tagName.toLowerCase(),
|
|
2491
|
+
...relevantElement.id && { id: relevantElement.id },
|
|
2492
|
+
...relevantElement.className && { class: relevantElement.className },
|
|
2493
|
+
...text && { text },
|
|
2494
|
+
...attributes.href && { href: attributes.href },
|
|
2495
|
+
...attributes.title && { title: attributes.title },
|
|
2496
|
+
...attributes.alt && { alt: attributes.alt },
|
|
2497
|
+
...attributes.role && { role: attributes.role },
|
|
2498
|
+
...attributes["aria-label"] && { ariaLabel: attributes["aria-label"] },
|
|
2499
|
+
...Object.keys(attributes).length > 0 && { dataAttributes: attributes }
|
|
2500
|
+
};
|
|
2501
|
+
}
|
|
2502
|
+
sanitizeText(text) {
|
|
2503
|
+
let sanitized = text;
|
|
2504
|
+
for (const pattern of PII_PATTERNS) {
|
|
2505
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
2506
|
+
sanitized = sanitized.replace(regex, "[REDACTED]");
|
|
2507
|
+
}
|
|
2508
|
+
return sanitized;
|
|
2509
|
+
}
|
|
2510
|
+
getRelevantText(clickedElement, relevantElement) {
|
|
2511
|
+
const clickedText = clickedElement.textContent?.trim() ?? "";
|
|
2512
|
+
const relevantText = relevantElement.textContent?.trim() ?? "";
|
|
2513
|
+
if (!clickedText && !relevantText) {
|
|
2514
|
+
return "";
|
|
2515
|
+
}
|
|
2516
|
+
let finalText = "";
|
|
2517
|
+
if (clickedText && clickedText.length <= MAX_TEXT_LENGTH) {
|
|
2518
|
+
finalText = clickedText;
|
|
2519
|
+
} else if (relevantText.length <= MAX_TEXT_LENGTH) {
|
|
2520
|
+
finalText = relevantText;
|
|
2521
|
+
} else {
|
|
2522
|
+
finalText = relevantText.slice(0, MAX_TEXT_LENGTH - 3) + "...";
|
|
2523
|
+
}
|
|
2524
|
+
return this.sanitizeText(finalText);
|
|
2525
|
+
}
|
|
2526
|
+
extractElementAttributes(element) {
|
|
2527
|
+
const commonAttributes = [
|
|
2528
|
+
"id",
|
|
2529
|
+
"class",
|
|
2530
|
+
"data-testid",
|
|
2531
|
+
"aria-label",
|
|
2532
|
+
"title",
|
|
2533
|
+
"href",
|
|
2534
|
+
"type",
|
|
2535
|
+
"name",
|
|
2536
|
+
"alt",
|
|
2537
|
+
"role"
|
|
2538
|
+
];
|
|
2539
|
+
const result = {};
|
|
2540
|
+
for (const attributeName of commonAttributes) {
|
|
2541
|
+
const value = element.getAttribute(attributeName);
|
|
2542
|
+
if (value) {
|
|
2543
|
+
result[attributeName] = value;
|
|
2544
|
+
}
|
|
2545
|
+
}
|
|
2546
|
+
return result;
|
|
2547
|
+
}
|
|
2548
|
+
createCustomEventData(trackingData) {
|
|
2549
|
+
return {
|
|
2550
|
+
name: trackingData.name,
|
|
2551
|
+
...trackingData.value && { value: trackingData.value }
|
|
2552
|
+
};
|
|
2553
|
+
}
|
|
2554
|
+
};
|
|
2555
|
+
|
|
2556
|
+
// src/handlers/scroll.handler.ts
|
|
2557
|
+
var ScrollHandler = class extends StateManager {
|
|
2558
|
+
eventManager;
|
|
2559
|
+
containers = [];
|
|
2560
|
+
limitWarningLogged = false;
|
|
2561
|
+
minDepthChange = MIN_SCROLL_DEPTH_CHANGE;
|
|
2562
|
+
minIntervalMs = SCROLL_MIN_EVENT_INTERVAL_MS;
|
|
2563
|
+
maxEventsPerSession = MAX_SCROLL_EVENTS_PER_SESSION;
|
|
2564
|
+
retryTimeoutId = null;
|
|
2565
|
+
constructor(eventManager) {
|
|
2566
|
+
super();
|
|
2567
|
+
this.eventManager = eventManager;
|
|
2568
|
+
}
|
|
2569
|
+
startTracking() {
|
|
2570
|
+
this.limitWarningLogged = false;
|
|
2571
|
+
this.applyConfigOverrides();
|
|
2572
|
+
this.set("scrollEventCount", 0);
|
|
2573
|
+
this.tryDetectScrollContainers(0);
|
|
2574
|
+
}
|
|
2575
|
+
stopTracking() {
|
|
2576
|
+
if (this.retryTimeoutId !== null) {
|
|
2577
|
+
clearTimeout(this.retryTimeoutId);
|
|
2578
|
+
this.retryTimeoutId = null;
|
|
2579
|
+
}
|
|
2580
|
+
for (const container of this.containers) {
|
|
2581
|
+
this.clearContainerTimer(container);
|
|
2582
|
+
if (container.element === window) {
|
|
2583
|
+
window.removeEventListener("scroll", container.listener);
|
|
2584
|
+
} else {
|
|
2585
|
+
container.element.removeEventListener("scroll", container.listener);
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
this.containers.length = 0;
|
|
2589
|
+
this.set("scrollEventCount", 0);
|
|
2590
|
+
this.limitWarningLogged = false;
|
|
2591
|
+
}
|
|
2592
|
+
tryDetectScrollContainers(attempt) {
|
|
2593
|
+
const elements = this.findScrollableElements();
|
|
2594
|
+
if (this.isWindowScrollable()) {
|
|
2595
|
+
this.setupScrollContainer(window, "window");
|
|
2596
|
+
}
|
|
2597
|
+
if (elements.length > 0) {
|
|
2598
|
+
for (const element of elements) {
|
|
2599
|
+
const selector = this.getElementSelector(element);
|
|
2600
|
+
this.setupScrollContainer(element, selector);
|
|
2601
|
+
}
|
|
2602
|
+
this.applyPrimaryScrollSelectorIfConfigured();
|
|
2603
|
+
return;
|
|
2604
|
+
}
|
|
2605
|
+
if (attempt < 5) {
|
|
2606
|
+
this.retryTimeoutId = window.setTimeout(() => {
|
|
2607
|
+
this.retryTimeoutId = null;
|
|
2608
|
+
this.tryDetectScrollContainers(attempt + 1);
|
|
2609
|
+
}, 200);
|
|
2610
|
+
return;
|
|
2611
|
+
}
|
|
2612
|
+
if (this.containers.length === 0) {
|
|
2613
|
+
this.setupScrollContainer(window, "window");
|
|
2614
|
+
}
|
|
2615
|
+
this.applyPrimaryScrollSelectorIfConfigured();
|
|
2616
|
+
}
|
|
2617
|
+
applyPrimaryScrollSelectorIfConfigured() {
|
|
2618
|
+
const config = this.get("config");
|
|
2619
|
+
if (config?.primaryScrollSelector) {
|
|
2620
|
+
this.applyPrimaryScrollSelector(config.primaryScrollSelector);
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
findScrollableElements() {
|
|
2624
|
+
if (!document.body) {
|
|
2625
|
+
return [];
|
|
2626
|
+
}
|
|
2627
|
+
const elements = [];
|
|
2628
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, {
|
|
2629
|
+
acceptNode: (node2) => {
|
|
2630
|
+
const element = node2;
|
|
2631
|
+
if (!element.isConnected || !element.offsetParent) {
|
|
2632
|
+
return NodeFilter.FILTER_SKIP;
|
|
2633
|
+
}
|
|
2634
|
+
const style = getComputedStyle(element);
|
|
2635
|
+
const hasVerticalScrollableStyle = style.overflowY === "auto" || style.overflowY === "scroll" || style.overflow === "auto" || style.overflow === "scroll";
|
|
2636
|
+
return hasVerticalScrollableStyle ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_SKIP;
|
|
2637
|
+
}
|
|
2638
|
+
});
|
|
2639
|
+
let node;
|
|
2640
|
+
while ((node = walker.nextNode()) && elements.length < 10) {
|
|
2641
|
+
const element = node;
|
|
2642
|
+
if (this.isElementScrollable(element)) {
|
|
2643
|
+
elements.push(element);
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
return elements;
|
|
2647
|
+
}
|
|
2648
|
+
getElementSelector(element) {
|
|
2649
|
+
if (element === window) {
|
|
2650
|
+
return "window";
|
|
2651
|
+
}
|
|
2652
|
+
const htmlElement = element;
|
|
2653
|
+
if (htmlElement.id) {
|
|
2654
|
+
return `#${htmlElement.id}`;
|
|
2655
|
+
}
|
|
2656
|
+
if (htmlElement.className && typeof htmlElement.className === "string") {
|
|
2657
|
+
const firstClass = htmlElement.className.split(" ").filter((c) => c.trim())[0];
|
|
2658
|
+
if (firstClass) {
|
|
2659
|
+
return `.${firstClass}`;
|
|
2660
|
+
}
|
|
2661
|
+
}
|
|
2662
|
+
return htmlElement.tagName.toLowerCase();
|
|
2663
|
+
}
|
|
2664
|
+
determineIfPrimary(element) {
|
|
2665
|
+
if (this.isWindowScrollable()) {
|
|
2666
|
+
return element === window;
|
|
2667
|
+
}
|
|
2668
|
+
return this.containers.length === 0;
|
|
2669
|
+
}
|
|
2670
|
+
setupScrollContainer(element, selector) {
|
|
2671
|
+
const alreadyTracking = this.containers.some((c) => c.element === element);
|
|
2672
|
+
if (alreadyTracking) {
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2675
|
+
if (element !== window && !this.isElementScrollable(element)) {
|
|
2676
|
+
return;
|
|
2677
|
+
}
|
|
2678
|
+
const initialScrollTop = this.getScrollTop(element);
|
|
2679
|
+
const initialDepth = this.calculateScrollDepth(
|
|
2680
|
+
initialScrollTop,
|
|
2681
|
+
this.getScrollHeight(element),
|
|
2682
|
+
this.getViewportHeight(element)
|
|
2683
|
+
);
|
|
2684
|
+
const isPrimary = this.determineIfPrimary(element);
|
|
2685
|
+
const container = {
|
|
2686
|
+
element,
|
|
2687
|
+
selector,
|
|
2688
|
+
isPrimary,
|
|
2689
|
+
lastScrollPos: initialScrollTop,
|
|
2690
|
+
lastDepth: initialDepth,
|
|
2691
|
+
lastDirection: "down" /* DOWN */,
|
|
2692
|
+
lastEventTime: 0,
|
|
2693
|
+
maxDepthReached: initialDepth,
|
|
2694
|
+
debounceTimer: null,
|
|
2695
|
+
listener: null
|
|
2696
|
+
// Will be assigned after handleScroll is defined
|
|
2697
|
+
};
|
|
2698
|
+
const handleScroll = () => {
|
|
2699
|
+
if (this.get("suppressNextScroll")) {
|
|
2700
|
+
return;
|
|
2701
|
+
}
|
|
2702
|
+
this.clearContainerTimer(container);
|
|
2703
|
+
container.debounceTimer = window.setTimeout(() => {
|
|
2704
|
+
const scrollData = this.calculateScrollData(container);
|
|
2705
|
+
if (scrollData) {
|
|
2706
|
+
const now = Date.now();
|
|
2707
|
+
this.processScrollEvent(container, scrollData, now);
|
|
2708
|
+
}
|
|
2709
|
+
container.debounceTimer = null;
|
|
2710
|
+
}, SCROLL_DEBOUNCE_TIME_MS);
|
|
2711
|
+
};
|
|
2712
|
+
container.listener = handleScroll;
|
|
2713
|
+
this.containers.push(container);
|
|
2714
|
+
if (element === window) {
|
|
2715
|
+
window.addEventListener("scroll", handleScroll, { passive: true });
|
|
2716
|
+
} else {
|
|
2717
|
+
element.addEventListener("scroll", handleScroll, { passive: true });
|
|
2718
|
+
}
|
|
2719
|
+
}
|
|
2720
|
+
processScrollEvent(container, scrollData, timestamp) {
|
|
2721
|
+
if (!this.shouldEmitScrollEvent(container, scrollData, timestamp)) {
|
|
2722
|
+
return;
|
|
2723
|
+
}
|
|
2724
|
+
container.lastEventTime = timestamp;
|
|
2725
|
+
container.lastDepth = scrollData.depth;
|
|
2726
|
+
container.lastDirection = scrollData.direction;
|
|
2727
|
+
const currentCount = this.get("scrollEventCount") ?? 0;
|
|
2728
|
+
this.set("scrollEventCount", currentCount + 1);
|
|
2729
|
+
this.eventManager.track({
|
|
2730
|
+
type: "scroll" /* SCROLL */,
|
|
2731
|
+
scroll_data: {
|
|
2732
|
+
...scrollData,
|
|
2733
|
+
container_selector: container.selector,
|
|
2734
|
+
is_primary: container.isPrimary
|
|
2735
|
+
}
|
|
2736
|
+
});
|
|
2737
|
+
}
|
|
2738
|
+
shouldEmitScrollEvent(container, scrollData, timestamp) {
|
|
2739
|
+
if (this.hasReachedSessionLimit()) {
|
|
2740
|
+
this.logLimitOnce();
|
|
2741
|
+
return false;
|
|
2742
|
+
}
|
|
2743
|
+
if (!this.hasElapsedMinimumInterval(container, timestamp)) {
|
|
2744
|
+
return false;
|
|
2745
|
+
}
|
|
2746
|
+
if (!this.hasSignificantDepthChange(container, scrollData.depth)) {
|
|
2747
|
+
return false;
|
|
2748
|
+
}
|
|
2749
|
+
return true;
|
|
2750
|
+
}
|
|
2751
|
+
hasReachedSessionLimit() {
|
|
2752
|
+
const currentCount = this.get("scrollEventCount") ?? 0;
|
|
2753
|
+
return currentCount >= this.maxEventsPerSession;
|
|
2754
|
+
}
|
|
2755
|
+
hasElapsedMinimumInterval(container, timestamp) {
|
|
2756
|
+
if (container.lastEventTime === 0) {
|
|
2757
|
+
return true;
|
|
2758
|
+
}
|
|
2759
|
+
return timestamp - container.lastEventTime >= this.minIntervalMs;
|
|
2760
|
+
}
|
|
2761
|
+
hasSignificantDepthChange(container, newDepth) {
|
|
2762
|
+
return Math.abs(newDepth - container.lastDepth) >= this.minDepthChange;
|
|
2763
|
+
}
|
|
2764
|
+
logLimitOnce() {
|
|
2765
|
+
if (this.limitWarningLogged) {
|
|
2766
|
+
return;
|
|
2767
|
+
}
|
|
2768
|
+
this.limitWarningLogged = true;
|
|
2769
|
+
log("warn", "Max scroll events per session reached", {
|
|
2770
|
+
data: { limit: this.maxEventsPerSession }
|
|
2771
|
+
});
|
|
2772
|
+
}
|
|
2773
|
+
applyConfigOverrides() {
|
|
2774
|
+
this.minDepthChange = MIN_SCROLL_DEPTH_CHANGE;
|
|
2775
|
+
this.minIntervalMs = SCROLL_MIN_EVENT_INTERVAL_MS;
|
|
2776
|
+
this.maxEventsPerSession = MAX_SCROLL_EVENTS_PER_SESSION;
|
|
2777
|
+
}
|
|
2778
|
+
isWindowScrollable() {
|
|
2779
|
+
return document.documentElement.scrollHeight > window.innerHeight;
|
|
2780
|
+
}
|
|
2781
|
+
clearContainerTimer(container) {
|
|
2782
|
+
if (container.debounceTimer !== null) {
|
|
2783
|
+
clearTimeout(container.debounceTimer);
|
|
2784
|
+
container.debounceTimer = null;
|
|
2785
|
+
}
|
|
2786
|
+
}
|
|
2787
|
+
getScrollDirection(current, previous) {
|
|
2788
|
+
return current > previous ? "down" /* DOWN */ : "up" /* UP */;
|
|
2789
|
+
}
|
|
2790
|
+
calculateScrollDepth(scrollTop, scrollHeight, viewportHeight) {
|
|
2791
|
+
if (scrollHeight <= viewportHeight) {
|
|
2792
|
+
return 0;
|
|
2793
|
+
}
|
|
2794
|
+
const maxScrollTop = scrollHeight - viewportHeight;
|
|
2795
|
+
return Math.min(100, Math.max(0, Math.floor(scrollTop / maxScrollTop * 100)));
|
|
2796
|
+
}
|
|
2797
|
+
calculateScrollData(container) {
|
|
2798
|
+
const { element, lastScrollPos, lastEventTime } = container;
|
|
2799
|
+
const scrollTop = this.getScrollTop(element);
|
|
2800
|
+
const now = Date.now();
|
|
2801
|
+
const positionDelta = Math.abs(scrollTop - lastScrollPos);
|
|
2802
|
+
if (positionDelta < SIGNIFICANT_SCROLL_DELTA) {
|
|
2803
|
+
return null;
|
|
2804
|
+
}
|
|
2805
|
+
if (element === window && !this.isWindowScrollable()) {
|
|
2806
|
+
return null;
|
|
2807
|
+
}
|
|
2808
|
+
const viewportHeight = this.getViewportHeight(element);
|
|
2809
|
+
const scrollHeight = this.getScrollHeight(element);
|
|
2810
|
+
const direction = this.getScrollDirection(scrollTop, lastScrollPos);
|
|
2811
|
+
const depth = this.calculateScrollDepth(scrollTop, scrollHeight, viewportHeight);
|
|
2812
|
+
const timeDelta = lastEventTime > 0 ? now - lastEventTime : 0;
|
|
2813
|
+
const velocity = timeDelta > 0 ? Math.round(positionDelta / timeDelta * 1e3) : 0;
|
|
2814
|
+
if (depth > container.maxDepthReached) {
|
|
2815
|
+
container.maxDepthReached = depth;
|
|
2816
|
+
}
|
|
2817
|
+
container.lastScrollPos = scrollTop;
|
|
2818
|
+
return {
|
|
2819
|
+
depth,
|
|
2820
|
+
direction,
|
|
2821
|
+
velocity,
|
|
2822
|
+
max_depth_reached: container.maxDepthReached
|
|
2823
|
+
};
|
|
2824
|
+
}
|
|
2825
|
+
getScrollTop(element) {
|
|
2826
|
+
return element === window ? window.scrollY : element.scrollTop;
|
|
2827
|
+
}
|
|
2828
|
+
getViewportHeight(element) {
|
|
2829
|
+
return element === window ? window.innerHeight : element.clientHeight;
|
|
2830
|
+
}
|
|
2831
|
+
getScrollHeight(element) {
|
|
2832
|
+
return element === window ? document.documentElement.scrollHeight : element.scrollHeight;
|
|
2833
|
+
}
|
|
2834
|
+
isElementScrollable(element) {
|
|
2835
|
+
const style = getComputedStyle(element);
|
|
2836
|
+
const hasVerticalScrollableOverflow = style.overflowY === "auto" || style.overflowY === "scroll" || style.overflow === "auto" || style.overflow === "scroll";
|
|
2837
|
+
const hasVerticalOverflowContent = element.scrollHeight > element.clientHeight;
|
|
2838
|
+
return hasVerticalScrollableOverflow && hasVerticalOverflowContent;
|
|
2839
|
+
}
|
|
2840
|
+
applyPrimaryScrollSelector(selector) {
|
|
2841
|
+
let targetElement;
|
|
2842
|
+
if (selector === "window") {
|
|
2843
|
+
targetElement = window;
|
|
2844
|
+
} else {
|
|
2845
|
+
const element = document.querySelector(selector);
|
|
2846
|
+
if (!(element instanceof HTMLElement)) {
|
|
2847
|
+
log("warn", `Selector "${selector}" did not match an HTMLElement`);
|
|
2848
|
+
return;
|
|
2849
|
+
}
|
|
2850
|
+
targetElement = element;
|
|
2851
|
+
}
|
|
2852
|
+
this.containers.forEach((container) => {
|
|
2853
|
+
this.updateContainerPrimary(container, container.element === targetElement);
|
|
2854
|
+
});
|
|
2855
|
+
const targetAlreadyTracked = this.containers.some((c) => c.element === targetElement);
|
|
2856
|
+
if (!targetAlreadyTracked && targetElement instanceof HTMLElement) {
|
|
2857
|
+
if (this.isElementScrollable(targetElement)) {
|
|
2858
|
+
this.setupScrollContainer(targetElement, selector);
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
updateContainerPrimary(container, isPrimary) {
|
|
2863
|
+
container.isPrimary = isPrimary;
|
|
2864
|
+
}
|
|
2865
|
+
};
|
|
2866
|
+
|
|
2867
|
+
// src/handlers/viewport.handler.ts
|
|
2868
|
+
var ViewportHandler = class extends StateManager {
|
|
2869
|
+
eventManager;
|
|
2870
|
+
trackedElements = /* @__PURE__ */ new Map();
|
|
2871
|
+
observer = null;
|
|
2872
|
+
mutationObserver = null;
|
|
2873
|
+
mutationDebounceTimer = null;
|
|
2874
|
+
config = null;
|
|
2875
|
+
constructor(eventManager) {
|
|
2876
|
+
super();
|
|
2877
|
+
this.eventManager = eventManager;
|
|
2878
|
+
}
|
|
2879
|
+
/**
|
|
2880
|
+
* Starts tracking viewport visibility for configured elements
|
|
2881
|
+
*/
|
|
2882
|
+
startTracking() {
|
|
2883
|
+
const config = this.get("config");
|
|
2884
|
+
this.config = config.viewport ?? null;
|
|
2885
|
+
if (!this.config?.elements || this.config.elements.length === 0) {
|
|
2886
|
+
return;
|
|
2887
|
+
}
|
|
2888
|
+
const threshold = this.config.threshold ?? 0.5;
|
|
2889
|
+
const minDwellTime = this.config.minDwellTime ?? 1e3;
|
|
2890
|
+
if (threshold < 0 || threshold > 1) {
|
|
2891
|
+
log("warn", "ViewportHandler: Invalid threshold, must be between 0 and 1");
|
|
2892
|
+
return;
|
|
2893
|
+
}
|
|
2894
|
+
if (minDwellTime < 0) {
|
|
2895
|
+
log("warn", "ViewportHandler: Invalid minDwellTime, must be non-negative");
|
|
2896
|
+
return;
|
|
2897
|
+
}
|
|
2898
|
+
if (typeof IntersectionObserver === "undefined") {
|
|
2899
|
+
log("warn", "ViewportHandler: IntersectionObserver not supported in this browser");
|
|
2900
|
+
return;
|
|
2901
|
+
}
|
|
2902
|
+
this.observer = new IntersectionObserver(this.handleIntersection, {
|
|
2903
|
+
threshold
|
|
2904
|
+
});
|
|
2905
|
+
this.observeElements();
|
|
2906
|
+
this.setupMutationObserver();
|
|
2907
|
+
}
|
|
2908
|
+
/**
|
|
2909
|
+
* Stops tracking and cleans up resources
|
|
2910
|
+
*/
|
|
2911
|
+
stopTracking() {
|
|
2912
|
+
if (this.observer) {
|
|
2913
|
+
this.observer.disconnect();
|
|
2914
|
+
this.observer = null;
|
|
2915
|
+
}
|
|
2916
|
+
if (this.mutationObserver) {
|
|
2917
|
+
this.mutationObserver.disconnect();
|
|
2918
|
+
this.mutationObserver = null;
|
|
2919
|
+
}
|
|
2920
|
+
if (this.mutationDebounceTimer !== null) {
|
|
2921
|
+
window.clearTimeout(this.mutationDebounceTimer);
|
|
2922
|
+
this.mutationDebounceTimer = null;
|
|
2923
|
+
}
|
|
2924
|
+
for (const tracked of this.trackedElements.values()) {
|
|
2925
|
+
if (tracked.timeoutId !== null) {
|
|
2926
|
+
window.clearTimeout(tracked.timeoutId);
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
this.trackedElements.clear();
|
|
2930
|
+
}
|
|
2931
|
+
/**
|
|
2932
|
+
* Query and observe all elements matching configured elements
|
|
2933
|
+
*/
|
|
2934
|
+
observeElements() {
|
|
2935
|
+
if (!this.config || !this.observer) return;
|
|
2936
|
+
const maxTrackedElements = this.config.maxTrackedElements ?? DEFAULT_VIEWPORT_MAX_TRACKED_ELEMENTS;
|
|
2937
|
+
let totalTracked = this.trackedElements.size;
|
|
2938
|
+
for (const elementConfig of this.config.elements) {
|
|
2939
|
+
try {
|
|
2940
|
+
const elements = document.querySelectorAll(elementConfig.selector);
|
|
2941
|
+
for (const element of Array.from(elements)) {
|
|
2942
|
+
if (totalTracked >= maxTrackedElements) {
|
|
2943
|
+
log("warn", "ViewportHandler: Maximum tracked elements reached", {
|
|
2944
|
+
data: {
|
|
2945
|
+
limit: maxTrackedElements,
|
|
2946
|
+
selector: elementConfig.selector,
|
|
2947
|
+
message: "Some elements will not be tracked. Consider more specific selectors."
|
|
2948
|
+
}
|
|
2949
|
+
});
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
if (element.hasAttribute(`${HTML_DATA_ATTR_PREFIX}-ignore`)) {
|
|
2953
|
+
continue;
|
|
2954
|
+
}
|
|
2955
|
+
if (this.trackedElements.has(element)) {
|
|
2956
|
+
continue;
|
|
2957
|
+
}
|
|
2958
|
+
this.trackedElements.set(element, {
|
|
2959
|
+
element,
|
|
2960
|
+
selector: elementConfig.selector,
|
|
2961
|
+
id: elementConfig.id,
|
|
2962
|
+
name: elementConfig.name,
|
|
2963
|
+
startTime: null,
|
|
2964
|
+
timeoutId: null,
|
|
2965
|
+
lastFiredTime: null
|
|
2966
|
+
});
|
|
2967
|
+
this.observer?.observe(element);
|
|
2968
|
+
totalTracked++;
|
|
2969
|
+
}
|
|
2970
|
+
} catch (error) {
|
|
2971
|
+
log("warn", `ViewportHandler: Invalid selector "${elementConfig.selector}"`, { error });
|
|
2972
|
+
}
|
|
2973
|
+
}
|
|
2974
|
+
log("debug", "ViewportHandler: Elements tracked", {
|
|
2975
|
+
data: { count: totalTracked, limit: maxTrackedElements }
|
|
2976
|
+
});
|
|
2977
|
+
}
|
|
2978
|
+
/**
|
|
2979
|
+
* Handles intersection events from IntersectionObserver
|
|
2980
|
+
*/
|
|
2981
|
+
handleIntersection = (entries) => {
|
|
2982
|
+
if (!this.config) return;
|
|
2983
|
+
const minDwellTime = this.config.minDwellTime ?? 1e3;
|
|
2984
|
+
for (const entry of entries) {
|
|
2985
|
+
const tracked = this.trackedElements.get(entry.target);
|
|
2986
|
+
if (!tracked) continue;
|
|
2987
|
+
if (entry.isIntersecting) {
|
|
2988
|
+
if (tracked.startTime === null) {
|
|
2989
|
+
tracked.startTime = performance.now();
|
|
2990
|
+
tracked.timeoutId = window.setTimeout(() => {
|
|
2991
|
+
const visibilityRatio = Math.round(entry.intersectionRatio * 100) / 100;
|
|
2992
|
+
this.fireViewportEvent(tracked, visibilityRatio);
|
|
2993
|
+
}, minDwellTime);
|
|
2994
|
+
}
|
|
2995
|
+
} else {
|
|
2996
|
+
if (tracked.startTime !== null) {
|
|
2997
|
+
if (tracked.timeoutId !== null) {
|
|
2998
|
+
window.clearTimeout(tracked.timeoutId);
|
|
2999
|
+
tracked.timeoutId = null;
|
|
3000
|
+
}
|
|
3001
|
+
tracked.startTime = null;
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
};
|
|
3006
|
+
/**
|
|
3007
|
+
* Fires a viewport visible event
|
|
3008
|
+
*/
|
|
3009
|
+
fireViewportEvent(tracked, visibilityRatio) {
|
|
3010
|
+
if (tracked.startTime === null) return;
|
|
3011
|
+
const dwellTime = Math.round(performance.now() - tracked.startTime);
|
|
3012
|
+
if (tracked.element.hasAttribute("data-tlog-ignore")) {
|
|
3013
|
+
return;
|
|
3014
|
+
}
|
|
3015
|
+
const cooldownPeriod = this.config?.cooldownPeriod ?? DEFAULT_VIEWPORT_COOLDOWN_PERIOD;
|
|
3016
|
+
const now = Date.now();
|
|
3017
|
+
if (tracked.lastFiredTime !== null && now - tracked.lastFiredTime < cooldownPeriod) {
|
|
3018
|
+
log("debug", "ViewportHandler: Event suppressed by cooldown period", {
|
|
3019
|
+
data: {
|
|
3020
|
+
selector: tracked.selector,
|
|
3021
|
+
cooldownRemaining: cooldownPeriod - (now - tracked.lastFiredTime)
|
|
3022
|
+
}
|
|
3023
|
+
});
|
|
3024
|
+
tracked.startTime = null;
|
|
3025
|
+
tracked.timeoutId = null;
|
|
3026
|
+
return;
|
|
3027
|
+
}
|
|
3028
|
+
const eventData = {
|
|
3029
|
+
selector: tracked.selector,
|
|
3030
|
+
dwellTime,
|
|
3031
|
+
visibilityRatio,
|
|
3032
|
+
...tracked.id !== void 0 && { id: tracked.id },
|
|
3033
|
+
...tracked.name !== void 0 && { name: tracked.name }
|
|
3034
|
+
};
|
|
3035
|
+
this.eventManager.track({
|
|
3036
|
+
type: "viewport_visible" /* VIEWPORT_VISIBLE */,
|
|
3037
|
+
viewport_data: eventData
|
|
3038
|
+
});
|
|
3039
|
+
tracked.startTime = null;
|
|
3040
|
+
tracked.timeoutId = null;
|
|
3041
|
+
tracked.lastFiredTime = now;
|
|
3042
|
+
}
|
|
3043
|
+
/**
|
|
3044
|
+
* Sets up MutationObserver to detect dynamically added elements
|
|
3045
|
+
*/
|
|
3046
|
+
setupMutationObserver() {
|
|
3047
|
+
if (!this.config || typeof MutationObserver === "undefined") {
|
|
3048
|
+
return;
|
|
3049
|
+
}
|
|
3050
|
+
if (!document.body) {
|
|
3051
|
+
log("warn", "ViewportHandler: document.body not available, skipping MutationObserver setup");
|
|
3052
|
+
return;
|
|
3053
|
+
}
|
|
3054
|
+
this.mutationObserver = new MutationObserver((mutations) => {
|
|
3055
|
+
let hasAddedNodes = false;
|
|
3056
|
+
for (const mutation of mutations) {
|
|
3057
|
+
if (mutation.type === "childList") {
|
|
3058
|
+
if (mutation.addedNodes.length > 0) {
|
|
3059
|
+
hasAddedNodes = true;
|
|
3060
|
+
}
|
|
3061
|
+
if (mutation.removedNodes.length > 0) {
|
|
3062
|
+
this.cleanupRemovedNodes(mutation.removedNodes);
|
|
3063
|
+
}
|
|
3064
|
+
}
|
|
3065
|
+
}
|
|
3066
|
+
if (hasAddedNodes) {
|
|
3067
|
+
if (this.mutationDebounceTimer !== null) {
|
|
3068
|
+
window.clearTimeout(this.mutationDebounceTimer);
|
|
3069
|
+
}
|
|
3070
|
+
this.mutationDebounceTimer = window.setTimeout(() => {
|
|
3071
|
+
this.observeElements();
|
|
3072
|
+
this.mutationDebounceTimer = null;
|
|
3073
|
+
}, VIEWPORT_MUTATION_DEBOUNCE_MS);
|
|
3074
|
+
}
|
|
3075
|
+
});
|
|
3076
|
+
this.mutationObserver.observe(document.body, {
|
|
3077
|
+
childList: true,
|
|
3078
|
+
subtree: true
|
|
3079
|
+
});
|
|
3080
|
+
}
|
|
3081
|
+
/**
|
|
3082
|
+
* Cleans up tracking for removed DOM nodes
|
|
3083
|
+
*/
|
|
3084
|
+
cleanupRemovedNodes(removedNodes) {
|
|
3085
|
+
removedNodes.forEach((node) => {
|
|
3086
|
+
if (node.nodeType !== 1) return;
|
|
3087
|
+
const element = node;
|
|
3088
|
+
const tracked = this.trackedElements.get(element);
|
|
3089
|
+
if (tracked) {
|
|
3090
|
+
if (tracked.timeoutId !== null) {
|
|
3091
|
+
window.clearTimeout(tracked.timeoutId);
|
|
3092
|
+
}
|
|
3093
|
+
this.observer?.unobserve(element);
|
|
3094
|
+
this.trackedElements.delete(element);
|
|
3095
|
+
}
|
|
3096
|
+
const descendants = Array.from(this.trackedElements.keys()).filter((el) => element.contains(el));
|
|
3097
|
+
descendants.forEach((el) => {
|
|
3098
|
+
const descendantTracked = this.trackedElements.get(el);
|
|
3099
|
+
if (descendantTracked && descendantTracked.timeoutId !== null) {
|
|
3100
|
+
window.clearTimeout(descendantTracked.timeoutId);
|
|
3101
|
+
}
|
|
3102
|
+
this.observer?.unobserve(el);
|
|
3103
|
+
this.trackedElements.delete(el);
|
|
3104
|
+
});
|
|
3105
|
+
});
|
|
3106
|
+
}
|
|
3107
|
+
};
|
|
3108
|
+
|
|
3109
|
+
// src/integrations/google-analytics.integration.ts
|
|
3110
|
+
var GoogleAnalyticsIntegration = class extends StateManager {
|
|
3111
|
+
isInitialized = false;
|
|
3112
|
+
async initialize() {
|
|
3113
|
+
if (this.isInitialized) {
|
|
3114
|
+
return;
|
|
3115
|
+
}
|
|
3116
|
+
const measurementId = this.get("config").integrations?.googleAnalytics?.measurementId;
|
|
3117
|
+
const userId = this.get("userId");
|
|
3118
|
+
if (!measurementId?.trim() || !userId?.trim()) {
|
|
3119
|
+
return;
|
|
3120
|
+
}
|
|
3121
|
+
try {
|
|
3122
|
+
if (this.isScriptAlreadyLoaded()) {
|
|
3123
|
+
this.isInitialized = true;
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
3126
|
+
await this.loadScript(measurementId);
|
|
3127
|
+
this.configureGtag(measurementId, userId);
|
|
3128
|
+
this.isInitialized = true;
|
|
3129
|
+
} catch (error) {
|
|
3130
|
+
log("error", "Google Analytics initialization failed", { error });
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
trackEvent(eventName, metadata) {
|
|
3134
|
+
if (!eventName?.trim() || !this.isInitialized || typeof window.gtag !== "function") {
|
|
3135
|
+
return;
|
|
3136
|
+
}
|
|
3137
|
+
try {
|
|
3138
|
+
const normalizedMetadata = Array.isArray(metadata) ? { items: metadata } : metadata;
|
|
3139
|
+
window.gtag("event", eventName, normalizedMetadata);
|
|
3140
|
+
} catch (error) {
|
|
3141
|
+
log("error", "Google Analytics event tracking failed", { error });
|
|
3142
|
+
}
|
|
3143
|
+
}
|
|
3144
|
+
cleanup() {
|
|
3145
|
+
this.isInitialized = false;
|
|
3146
|
+
const script = document.getElementById("tracelog-ga-script");
|
|
3147
|
+
if (script) {
|
|
3148
|
+
script.remove();
|
|
3149
|
+
}
|
|
3150
|
+
}
|
|
3151
|
+
isScriptAlreadyLoaded() {
|
|
3152
|
+
if (document.getElementById("tracelog-ga-script")) {
|
|
3153
|
+
return true;
|
|
3154
|
+
}
|
|
3155
|
+
const existingGAScript = document.querySelector('script[src*="googletagmanager.com/gtag/js"]');
|
|
3156
|
+
return !!existingGAScript;
|
|
3157
|
+
}
|
|
3158
|
+
async loadScript(measurementId) {
|
|
3159
|
+
return new Promise((resolve, reject) => {
|
|
3160
|
+
const script = document.createElement("script");
|
|
3161
|
+
script.id = "tracelog-ga-script";
|
|
3162
|
+
script.async = true;
|
|
3163
|
+
script.src = `https://www.googletagmanager.com/gtag/js?id=${measurementId}`;
|
|
3164
|
+
script.onload = () => {
|
|
3165
|
+
resolve();
|
|
3166
|
+
};
|
|
3167
|
+
script.onerror = () => {
|
|
3168
|
+
reject(new Error("Failed to load Google Analytics script"));
|
|
3169
|
+
};
|
|
3170
|
+
document.head.appendChild(script);
|
|
3171
|
+
});
|
|
3172
|
+
}
|
|
3173
|
+
configureGtag(measurementId, userId) {
|
|
3174
|
+
const gaScriptConfig = document.createElement("script");
|
|
3175
|
+
gaScriptConfig.innerHTML = `
|
|
3176
|
+
window.dataLayer = window.dataLayer || [];
|
|
3177
|
+
function gtag(){dataLayer.push(arguments);}
|
|
3178
|
+
gtag('js', new Date());
|
|
3179
|
+
gtag('config', '${measurementId}', {
|
|
3180
|
+
'user_id': '${userId}'
|
|
3181
|
+
});
|
|
3182
|
+
`;
|
|
3183
|
+
document.head.appendChild(gaScriptConfig);
|
|
3184
|
+
}
|
|
3185
|
+
};
|
|
3186
|
+
|
|
3187
|
+
// src/managers/storage.manager.ts
|
|
3188
|
+
var StorageManager = class {
|
|
3189
|
+
storage;
|
|
3190
|
+
sessionStorageRef;
|
|
3191
|
+
fallbackStorage = /* @__PURE__ */ new Map();
|
|
3192
|
+
fallbackSessionStorage = /* @__PURE__ */ new Map();
|
|
3193
|
+
hasQuotaExceededError = false;
|
|
3194
|
+
constructor() {
|
|
3195
|
+
this.storage = this.initializeStorage("localStorage");
|
|
3196
|
+
this.sessionStorageRef = this.initializeStorage("sessionStorage");
|
|
3197
|
+
if (!this.storage) {
|
|
3198
|
+
log("warn", "localStorage not available, using memory fallback");
|
|
3199
|
+
}
|
|
3200
|
+
if (!this.sessionStorageRef) {
|
|
3201
|
+
log("warn", "sessionStorage not available, using memory fallback");
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
3204
|
+
/**
|
|
3205
|
+
* Retrieves an item from storage
|
|
3206
|
+
*/
|
|
3207
|
+
getItem(key) {
|
|
3208
|
+
try {
|
|
3209
|
+
if (this.storage) {
|
|
3210
|
+
return this.storage.getItem(key);
|
|
3211
|
+
}
|
|
3212
|
+
return this.fallbackStorage.get(key) ?? null;
|
|
3213
|
+
} catch {
|
|
3214
|
+
return this.fallbackStorage.get(key) ?? null;
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
/**
|
|
3218
|
+
* Stores an item in storage
|
|
3219
|
+
*/
|
|
3220
|
+
setItem(key, value) {
|
|
3221
|
+
this.fallbackStorage.set(key, value);
|
|
3222
|
+
try {
|
|
3223
|
+
if (this.storage) {
|
|
3224
|
+
this.storage.setItem(key, value);
|
|
3225
|
+
return;
|
|
3226
|
+
}
|
|
3227
|
+
} catch (error) {
|
|
3228
|
+
if (error instanceof DOMException && error.name === "QuotaExceededError") {
|
|
3229
|
+
this.hasQuotaExceededError = true;
|
|
3230
|
+
log("warn", "localStorage quota exceeded, attempting cleanup", {
|
|
3231
|
+
data: { key, valueSize: value.length }
|
|
3232
|
+
});
|
|
3233
|
+
const cleanedUp = this.cleanupOldData();
|
|
3234
|
+
if (cleanedUp) {
|
|
3235
|
+
try {
|
|
3236
|
+
if (this.storage) {
|
|
3237
|
+
this.storage.setItem(key, value);
|
|
3238
|
+
return;
|
|
3239
|
+
}
|
|
3240
|
+
} catch (retryError) {
|
|
3241
|
+
log("error", "localStorage quota exceeded even after cleanup - data will not persist", {
|
|
3242
|
+
error: retryError,
|
|
3243
|
+
data: { key, valueSize: value.length }
|
|
3244
|
+
});
|
|
3245
|
+
}
|
|
3246
|
+
} else {
|
|
3247
|
+
log("error", "localStorage quota exceeded and no data to cleanup - data will not persist", {
|
|
3248
|
+
error,
|
|
3249
|
+
data: { key, valueSize: value.length }
|
|
3250
|
+
});
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
}
|
|
3255
|
+
/**
|
|
3256
|
+
* Removes an item from storage
|
|
3257
|
+
*/
|
|
3258
|
+
removeItem(key) {
|
|
3259
|
+
try {
|
|
3260
|
+
if (this.storage) {
|
|
3261
|
+
this.storage.removeItem(key);
|
|
3262
|
+
}
|
|
3263
|
+
} catch {
|
|
3264
|
+
}
|
|
3265
|
+
this.fallbackStorage.delete(key);
|
|
3266
|
+
}
|
|
3267
|
+
/**
|
|
3268
|
+
* Clears all TracLog-related items from storage
|
|
3269
|
+
*/
|
|
3270
|
+
clear() {
|
|
3271
|
+
if (!this.storage) {
|
|
3272
|
+
this.fallbackStorage.clear();
|
|
3273
|
+
return;
|
|
3274
|
+
}
|
|
3275
|
+
try {
|
|
3276
|
+
const keysToRemove = [];
|
|
3277
|
+
for (let i = 0; i < this.storage.length; i++) {
|
|
3278
|
+
const key = this.storage.key(i);
|
|
3279
|
+
if (key?.startsWith("tracelog_")) {
|
|
3280
|
+
keysToRemove.push(key);
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
keysToRemove.forEach((key) => {
|
|
3284
|
+
this.storage.removeItem(key);
|
|
3285
|
+
});
|
|
3286
|
+
this.fallbackStorage.clear();
|
|
3287
|
+
} catch (error) {
|
|
3288
|
+
log("error", "Failed to clear storage", { error });
|
|
3289
|
+
this.fallbackStorage.clear();
|
|
3290
|
+
}
|
|
3291
|
+
}
|
|
3292
|
+
/**
|
|
3293
|
+
* Checks if storage is available
|
|
3294
|
+
*/
|
|
3295
|
+
isAvailable() {
|
|
3296
|
+
return this.storage !== null;
|
|
3297
|
+
}
|
|
3298
|
+
/**
|
|
3299
|
+
* Checks if a QuotaExceededError has occurred
|
|
3300
|
+
* This indicates localStorage is full and data may not persist
|
|
3301
|
+
*/
|
|
3302
|
+
hasQuotaError() {
|
|
3303
|
+
return this.hasQuotaExceededError;
|
|
3304
|
+
}
|
|
3305
|
+
/**
|
|
3306
|
+
* Attempts to cleanup old TraceLog data from storage to free up space
|
|
3307
|
+
* Returns true if any data was removed, false otherwise
|
|
3308
|
+
*/
|
|
3309
|
+
cleanupOldData() {
|
|
3310
|
+
if (!this.storage) {
|
|
3311
|
+
return false;
|
|
3312
|
+
}
|
|
3313
|
+
try {
|
|
3314
|
+
const tracelogKeys = [];
|
|
3315
|
+
const persistedEventsKeys = [];
|
|
3316
|
+
for (let i = 0; i < this.storage.length; i++) {
|
|
3317
|
+
const key = this.storage.key(i);
|
|
3318
|
+
if (key?.startsWith("tracelog_")) {
|
|
3319
|
+
tracelogKeys.push(key);
|
|
3320
|
+
if (key.startsWith("tracelog_persisted_events_")) {
|
|
3321
|
+
persistedEventsKeys.push(key);
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
}
|
|
3325
|
+
if (persistedEventsKeys.length > 0) {
|
|
3326
|
+
persistedEventsKeys.forEach((key) => {
|
|
3327
|
+
try {
|
|
3328
|
+
this.storage.removeItem(key);
|
|
3329
|
+
} catch {
|
|
3330
|
+
}
|
|
3331
|
+
});
|
|
3332
|
+
return true;
|
|
3333
|
+
}
|
|
3334
|
+
const criticalPrefixes = ["tracelog_session_", "tracelog_user_id", "tracelog_device_id", "tracelog_config"];
|
|
3335
|
+
const nonCriticalKeys = tracelogKeys.filter((key) => {
|
|
3336
|
+
return !criticalPrefixes.some((prefix) => key.startsWith(prefix));
|
|
3337
|
+
});
|
|
3338
|
+
if (nonCriticalKeys.length > 0) {
|
|
3339
|
+
const keysToRemove = nonCriticalKeys.slice(0, 5);
|
|
3340
|
+
keysToRemove.forEach((key) => {
|
|
3341
|
+
try {
|
|
3342
|
+
this.storage.removeItem(key);
|
|
3343
|
+
} catch {
|
|
3344
|
+
}
|
|
3345
|
+
});
|
|
3346
|
+
return true;
|
|
3347
|
+
}
|
|
3348
|
+
return false;
|
|
3349
|
+
} catch (error) {
|
|
3350
|
+
log("error", "Failed to cleanup old data", { error });
|
|
3351
|
+
return false;
|
|
3352
|
+
}
|
|
3353
|
+
}
|
|
3354
|
+
/**
|
|
3355
|
+
* Initialize storage (localStorage or sessionStorage) with feature detection
|
|
3356
|
+
*/
|
|
3357
|
+
initializeStorage(type) {
|
|
3358
|
+
if (typeof window === "undefined") {
|
|
3359
|
+
return null;
|
|
3360
|
+
}
|
|
3361
|
+
try {
|
|
3362
|
+
const storage = type === "localStorage" ? window.localStorage : window.sessionStorage;
|
|
3363
|
+
const testKey = "__tracelog_test__";
|
|
3364
|
+
storage.setItem(testKey, "test");
|
|
3365
|
+
storage.removeItem(testKey);
|
|
3366
|
+
return storage;
|
|
3367
|
+
} catch {
|
|
3368
|
+
return null;
|
|
3369
|
+
}
|
|
3370
|
+
}
|
|
3371
|
+
/**
|
|
3372
|
+
* Retrieves an item from sessionStorage
|
|
3373
|
+
*/
|
|
3374
|
+
getSessionItem(key) {
|
|
3375
|
+
try {
|
|
3376
|
+
if (this.sessionStorageRef) {
|
|
3377
|
+
return this.sessionStorageRef.getItem(key);
|
|
3378
|
+
}
|
|
3379
|
+
return this.fallbackSessionStorage.get(key) ?? null;
|
|
3380
|
+
} catch {
|
|
3381
|
+
return this.fallbackSessionStorage.get(key) ?? null;
|
|
3382
|
+
}
|
|
3383
|
+
}
|
|
3384
|
+
/**
|
|
3385
|
+
* Stores an item in sessionStorage
|
|
3386
|
+
*/
|
|
3387
|
+
setSessionItem(key, value) {
|
|
3388
|
+
this.fallbackSessionStorage.set(key, value);
|
|
3389
|
+
try {
|
|
3390
|
+
if (this.sessionStorageRef) {
|
|
3391
|
+
this.sessionStorageRef.setItem(key, value);
|
|
3392
|
+
return;
|
|
3393
|
+
}
|
|
3394
|
+
} catch (error) {
|
|
3395
|
+
if (error instanceof DOMException && error.name === "QuotaExceededError") {
|
|
3396
|
+
log("error", "sessionStorage quota exceeded - data will not persist", {
|
|
3397
|
+
error,
|
|
3398
|
+
data: { key, valueSize: value.length }
|
|
3399
|
+
});
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
}
|
|
3403
|
+
/**
|
|
3404
|
+
* Removes an item from sessionStorage
|
|
3405
|
+
*/
|
|
3406
|
+
removeSessionItem(key) {
|
|
3407
|
+
try {
|
|
3408
|
+
if (this.sessionStorageRef) {
|
|
3409
|
+
this.sessionStorageRef.removeItem(key);
|
|
3410
|
+
}
|
|
3411
|
+
} catch {
|
|
3412
|
+
}
|
|
3413
|
+
this.fallbackSessionStorage.delete(key);
|
|
3414
|
+
}
|
|
3415
|
+
};
|
|
3416
|
+
|
|
3417
|
+
// src/handlers/performance.handler.ts
|
|
3418
|
+
var PerformanceHandler = class extends StateManager {
|
|
3419
|
+
eventManager;
|
|
3420
|
+
reportedByNav = /* @__PURE__ */ new Map();
|
|
3421
|
+
navigationHistory = [];
|
|
3422
|
+
// FIFO queue for tracking navigation order
|
|
3423
|
+
observers = [];
|
|
3424
|
+
vitalThresholds = WEB_VITALS_THRESHOLDS;
|
|
3425
|
+
lastLongTaskSentAt = 0;
|
|
3426
|
+
constructor(eventManager) {
|
|
3427
|
+
super();
|
|
3428
|
+
this.eventManager = eventManager;
|
|
3429
|
+
}
|
|
3430
|
+
async startTracking() {
|
|
3431
|
+
await this.initWebVitals();
|
|
3432
|
+
this.observeLongTasks();
|
|
3433
|
+
}
|
|
3434
|
+
stopTracking() {
|
|
3435
|
+
this.observers.forEach((obs, index) => {
|
|
3436
|
+
try {
|
|
3437
|
+
obs.disconnect();
|
|
3438
|
+
} catch (error) {
|
|
3439
|
+
log("warn", "Failed to disconnect performance observer", { error, data: { observerIndex: index } });
|
|
3440
|
+
}
|
|
3441
|
+
});
|
|
3442
|
+
this.observers.length = 0;
|
|
3443
|
+
this.reportedByNav.clear();
|
|
3444
|
+
this.navigationHistory.length = 0;
|
|
3445
|
+
}
|
|
3446
|
+
observeWebVitalsFallback() {
|
|
3447
|
+
this.reportTTFB();
|
|
3448
|
+
this.safeObserve(
|
|
3449
|
+
"largest-contentful-paint",
|
|
3450
|
+
(list) => {
|
|
3451
|
+
const entries = list.getEntries();
|
|
3452
|
+
const last = entries[entries.length - 1];
|
|
3453
|
+
if (!last) {
|
|
3454
|
+
return;
|
|
3455
|
+
}
|
|
3456
|
+
this.sendVital({ type: "LCP", value: Number(last.startTime.toFixed(PRECISION_TWO_DECIMALS)) });
|
|
3457
|
+
},
|
|
3458
|
+
{ type: "largest-contentful-paint", buffered: true },
|
|
3459
|
+
true
|
|
3460
|
+
);
|
|
3461
|
+
let clsValue = 0;
|
|
3462
|
+
let currentNavId = this.getNavigationId();
|
|
3463
|
+
this.safeObserve(
|
|
3464
|
+
"layout-shift",
|
|
3465
|
+
(list) => {
|
|
3466
|
+
const navId = this.getNavigationId();
|
|
3467
|
+
if (navId !== currentNavId) {
|
|
3468
|
+
clsValue = 0;
|
|
3469
|
+
currentNavId = navId;
|
|
3470
|
+
}
|
|
3471
|
+
const entries = list.getEntries();
|
|
3472
|
+
for (const entry of entries) {
|
|
3473
|
+
if (entry.hadRecentInput === true) {
|
|
3474
|
+
continue;
|
|
3475
|
+
}
|
|
3476
|
+
const value = typeof entry.value === "number" ? entry.value : 0;
|
|
3477
|
+
clsValue += value;
|
|
3478
|
+
}
|
|
3479
|
+
this.sendVital({ type: "CLS", value: Number(clsValue.toFixed(PRECISION_TWO_DECIMALS)) });
|
|
3480
|
+
},
|
|
3481
|
+
{ type: "layout-shift", buffered: true }
|
|
3482
|
+
);
|
|
3483
|
+
this.safeObserve(
|
|
3484
|
+
"paint",
|
|
3485
|
+
(list) => {
|
|
3486
|
+
for (const entry of list.getEntries()) {
|
|
3487
|
+
if (entry.name === "first-contentful-paint") {
|
|
3488
|
+
this.sendVital({ type: "FCP", value: Number(entry.startTime.toFixed(PRECISION_TWO_DECIMALS)) });
|
|
3489
|
+
}
|
|
3490
|
+
}
|
|
3491
|
+
},
|
|
3492
|
+
{ type: "paint", buffered: true },
|
|
3493
|
+
true
|
|
3494
|
+
);
|
|
3495
|
+
this.safeObserve(
|
|
3496
|
+
"event",
|
|
3497
|
+
(list) => {
|
|
3498
|
+
let worst = 0;
|
|
3499
|
+
const entries = list.getEntries();
|
|
3500
|
+
for (const entry of entries) {
|
|
3501
|
+
const dur = (entry.processingEnd ?? 0) - (entry.startTime ?? 0);
|
|
3502
|
+
worst = Math.max(worst, dur);
|
|
3503
|
+
}
|
|
3504
|
+
if (worst > 0) {
|
|
3505
|
+
this.sendVital({ type: "INP", value: Number(worst.toFixed(PRECISION_TWO_DECIMALS)) });
|
|
3506
|
+
}
|
|
3507
|
+
},
|
|
3508
|
+
{ type: "event", buffered: true }
|
|
3509
|
+
);
|
|
3510
|
+
}
|
|
3511
|
+
async initWebVitals() {
|
|
3512
|
+
try {
|
|
3513
|
+
const { onLCP, onCLS, onFCP, onTTFB, onINP } = await import('web-vitals');
|
|
3514
|
+
const report = (type) => (metric) => {
|
|
3515
|
+
const value = Number(metric.value.toFixed(PRECISION_TWO_DECIMALS));
|
|
3516
|
+
this.sendVital({ type, value });
|
|
3517
|
+
};
|
|
3518
|
+
onLCP(report("LCP"), { reportAllChanges: false });
|
|
3519
|
+
onCLS(report("CLS"), { reportAllChanges: false });
|
|
3520
|
+
onFCP(report("FCP"), { reportAllChanges: false });
|
|
3521
|
+
onTTFB(report("TTFB"), { reportAllChanges: false });
|
|
3522
|
+
onINP(report("INP"), { reportAllChanges: false });
|
|
3523
|
+
} catch (error) {
|
|
3524
|
+
log("warn", "Failed to load web-vitals library, using fallback", { error });
|
|
3525
|
+
this.observeWebVitalsFallback();
|
|
3526
|
+
}
|
|
3527
|
+
}
|
|
3528
|
+
reportTTFB() {
|
|
3529
|
+
try {
|
|
3530
|
+
const nav = performance.getEntriesByType("navigation")[0];
|
|
3531
|
+
if (!nav) {
|
|
3532
|
+
return;
|
|
3533
|
+
}
|
|
3534
|
+
const ttfb = nav.responseStart;
|
|
3535
|
+
if (typeof ttfb === "number" && Number.isFinite(ttfb)) {
|
|
3536
|
+
this.sendVital({ type: "TTFB", value: Number(ttfb.toFixed(PRECISION_TWO_DECIMALS)) });
|
|
3537
|
+
}
|
|
3538
|
+
} catch (error) {
|
|
3539
|
+
log("warn", "Failed to report TTFB", { error });
|
|
3540
|
+
}
|
|
3541
|
+
}
|
|
3542
|
+
observeLongTasks() {
|
|
3543
|
+
this.safeObserve(
|
|
3544
|
+
"longtask",
|
|
3545
|
+
(list) => {
|
|
3546
|
+
const entries = list.getEntries();
|
|
3547
|
+
for (const entry of entries) {
|
|
3548
|
+
const duration = Number(entry.duration.toFixed(PRECISION_TWO_DECIMALS));
|
|
3549
|
+
const now = Date.now();
|
|
3550
|
+
if (now - this.lastLongTaskSentAt >= LONG_TASK_THROTTLE_MS) {
|
|
3551
|
+
if (this.shouldSendVital("LONG_TASK", duration)) {
|
|
3552
|
+
this.trackWebVital("LONG_TASK", duration);
|
|
3553
|
+
}
|
|
3554
|
+
this.lastLongTaskSentAt = now;
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
},
|
|
3558
|
+
{ type: "longtask", buffered: true }
|
|
3559
|
+
);
|
|
3560
|
+
}
|
|
3561
|
+
sendVital(sample) {
|
|
3562
|
+
if (!this.shouldSendVital(sample.type, sample.value)) {
|
|
3563
|
+
return;
|
|
3564
|
+
}
|
|
3565
|
+
const navId = this.getNavigationId();
|
|
3566
|
+
if (navId) {
|
|
3567
|
+
const reportedForNav = this.reportedByNav.get(navId);
|
|
3568
|
+
const isDuplicate = reportedForNav?.has(sample.type);
|
|
3569
|
+
if (isDuplicate) {
|
|
3570
|
+
return;
|
|
3571
|
+
}
|
|
3572
|
+
if (!reportedForNav) {
|
|
3573
|
+
this.reportedByNav.set(navId, /* @__PURE__ */ new Set([sample.type]));
|
|
3574
|
+
this.navigationHistory.push(navId);
|
|
3575
|
+
if (this.navigationHistory.length > MAX_NAVIGATION_HISTORY) {
|
|
3576
|
+
const oldestNav = this.navigationHistory.shift();
|
|
3577
|
+
if (oldestNav) {
|
|
3578
|
+
this.reportedByNav.delete(oldestNav);
|
|
3579
|
+
}
|
|
3580
|
+
}
|
|
3581
|
+
} else {
|
|
3582
|
+
reportedForNav.add(sample.type);
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
this.trackWebVital(sample.type, sample.value);
|
|
3586
|
+
}
|
|
3587
|
+
trackWebVital(type, value) {
|
|
3588
|
+
if (!Number.isFinite(value)) {
|
|
3589
|
+
log("warn", "Invalid web vital value", { data: { type, value } });
|
|
3590
|
+
return;
|
|
3591
|
+
}
|
|
3592
|
+
this.eventManager.track({
|
|
3593
|
+
type: "web_vitals" /* WEB_VITALS */,
|
|
3594
|
+
web_vitals: {
|
|
3595
|
+
type,
|
|
3596
|
+
value
|
|
3597
|
+
}
|
|
3598
|
+
});
|
|
3599
|
+
}
|
|
3600
|
+
getNavigationId() {
|
|
3601
|
+
try {
|
|
3602
|
+
const nav = performance.getEntriesByType("navigation")[0];
|
|
3603
|
+
if (!nav) {
|
|
3604
|
+
return null;
|
|
3605
|
+
}
|
|
3606
|
+
const timestamp = nav.startTime || performance.now();
|
|
3607
|
+
const random = Math.random().toString(36).substr(2, 5);
|
|
3608
|
+
return `${timestamp.toFixed(2)}_${window.location.pathname}_${random}`;
|
|
3609
|
+
} catch (error) {
|
|
3610
|
+
log("warn", "Failed to get navigation ID", { error });
|
|
3611
|
+
return null;
|
|
3612
|
+
}
|
|
3613
|
+
}
|
|
3614
|
+
isObserverSupported(type) {
|
|
3615
|
+
if (typeof PerformanceObserver === "undefined") return false;
|
|
3616
|
+
const supported = PerformanceObserver.supportedEntryTypes;
|
|
3617
|
+
return !supported || supported.includes(type);
|
|
3618
|
+
}
|
|
3619
|
+
safeObserve(type, cb, options, once = false) {
|
|
3620
|
+
try {
|
|
3621
|
+
if (!this.isObserverSupported(type)) {
|
|
3622
|
+
return false;
|
|
3623
|
+
}
|
|
3624
|
+
const obs = new PerformanceObserver((list, observer) => {
|
|
3625
|
+
try {
|
|
3626
|
+
cb(list, observer);
|
|
3627
|
+
} catch (callbackError) {
|
|
3628
|
+
log("warn", "Observer callback failed", {
|
|
3629
|
+
error: callbackError,
|
|
3630
|
+
data: { type }
|
|
3631
|
+
});
|
|
3632
|
+
}
|
|
3633
|
+
if (once) {
|
|
3634
|
+
try {
|
|
3635
|
+
observer.disconnect();
|
|
3636
|
+
} catch {
|
|
3637
|
+
}
|
|
3638
|
+
}
|
|
3639
|
+
});
|
|
3640
|
+
obs.observe(options ?? { type, buffered: true });
|
|
3641
|
+
if (!once) {
|
|
3642
|
+
this.observers.push(obs);
|
|
3643
|
+
}
|
|
3644
|
+
return true;
|
|
3645
|
+
} catch (error) {
|
|
3646
|
+
log("warn", "Failed to create performance observer", {
|
|
3647
|
+
error,
|
|
3648
|
+
data: { type }
|
|
3649
|
+
});
|
|
3650
|
+
return false;
|
|
3651
|
+
}
|
|
3652
|
+
}
|
|
3653
|
+
shouldSendVital(type, value) {
|
|
3654
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
3655
|
+
log("warn", "Invalid web vital value", { data: { type, value } });
|
|
3656
|
+
return false;
|
|
3657
|
+
}
|
|
3658
|
+
const threshold = this.vitalThresholds[type];
|
|
3659
|
+
if (typeof threshold === "number" && value <= threshold) {
|
|
3660
|
+
return false;
|
|
3661
|
+
}
|
|
3662
|
+
return true;
|
|
3663
|
+
}
|
|
3664
|
+
};
|
|
3665
|
+
|
|
3666
|
+
// src/handlers/error.handler.ts
|
|
3667
|
+
var ErrorHandler = class extends StateManager {
|
|
3668
|
+
eventManager;
|
|
3669
|
+
recentErrors = /* @__PURE__ */ new Map();
|
|
3670
|
+
errorBurstCounter = 0;
|
|
3671
|
+
burstWindowStart = 0;
|
|
3672
|
+
burstBackoffUntil = 0;
|
|
3673
|
+
constructor(eventManager) {
|
|
3674
|
+
super();
|
|
3675
|
+
this.eventManager = eventManager;
|
|
3676
|
+
}
|
|
3677
|
+
startTracking() {
|
|
3678
|
+
window.addEventListener("error", this.handleError);
|
|
3679
|
+
window.addEventListener("unhandledrejection", this.handleRejection);
|
|
3680
|
+
}
|
|
3681
|
+
stopTracking() {
|
|
3682
|
+
window.removeEventListener("error", this.handleError);
|
|
3683
|
+
window.removeEventListener("unhandledrejection", this.handleRejection);
|
|
3684
|
+
this.recentErrors.clear();
|
|
3685
|
+
this.errorBurstCounter = 0;
|
|
3686
|
+
this.burstWindowStart = 0;
|
|
3687
|
+
this.burstBackoffUntil = 0;
|
|
3688
|
+
}
|
|
3689
|
+
/**
|
|
3690
|
+
* Checks sampling rate and burst detection (Phase 3)
|
|
3691
|
+
* Returns false if in cooldown period after burst detection
|
|
3692
|
+
*/
|
|
3693
|
+
shouldSample() {
|
|
3694
|
+
const now = Date.now();
|
|
3695
|
+
if (now < this.burstBackoffUntil) {
|
|
3696
|
+
return false;
|
|
3697
|
+
}
|
|
3698
|
+
if (now - this.burstWindowStart > ERROR_BURST_WINDOW_MS) {
|
|
3699
|
+
this.errorBurstCounter = 0;
|
|
3700
|
+
this.burstWindowStart = now;
|
|
3701
|
+
}
|
|
3702
|
+
this.errorBurstCounter++;
|
|
3703
|
+
if (this.errorBurstCounter > ERROR_BURST_THRESHOLD) {
|
|
3704
|
+
this.burstBackoffUntil = now + ERROR_BURST_BACKOFF_MS;
|
|
3705
|
+
log("warn", "Error burst detected - entering cooldown", {
|
|
3706
|
+
data: {
|
|
3707
|
+
errorsInWindow: this.errorBurstCounter,
|
|
3708
|
+
cooldownMs: ERROR_BURST_BACKOFF_MS
|
|
3709
|
+
}
|
|
3710
|
+
});
|
|
3711
|
+
return false;
|
|
3712
|
+
}
|
|
3713
|
+
const config = this.get("config");
|
|
3714
|
+
const samplingRate = config?.errorSampling ?? DEFAULT_ERROR_SAMPLING_RATE;
|
|
3715
|
+
return Math.random() < samplingRate;
|
|
3716
|
+
}
|
|
3717
|
+
handleError = (event2) => {
|
|
3718
|
+
if (!this.shouldSample()) {
|
|
3719
|
+
return;
|
|
3720
|
+
}
|
|
3721
|
+
const sanitizedMessage = this.sanitize(event2.message || "Unknown error");
|
|
3722
|
+
if (this.shouldSuppressError("js_error" /* JS_ERROR */, sanitizedMessage)) {
|
|
3723
|
+
return;
|
|
3724
|
+
}
|
|
3725
|
+
this.eventManager.track({
|
|
3726
|
+
type: "error" /* ERROR */,
|
|
3727
|
+
error_data: {
|
|
3728
|
+
type: "js_error" /* JS_ERROR */,
|
|
3729
|
+
message: sanitizedMessage,
|
|
3730
|
+
...event2.filename && { filename: event2.filename },
|
|
3731
|
+
...event2.lineno && { line: event2.lineno },
|
|
3732
|
+
...event2.colno && { column: event2.colno }
|
|
3733
|
+
}
|
|
3734
|
+
});
|
|
3735
|
+
};
|
|
3736
|
+
handleRejection = (event2) => {
|
|
3737
|
+
if (!this.shouldSample()) {
|
|
3738
|
+
return;
|
|
3739
|
+
}
|
|
3740
|
+
const message = this.extractRejectionMessage(event2.reason);
|
|
3741
|
+
const sanitizedMessage = this.sanitize(message);
|
|
3742
|
+
if (this.shouldSuppressError("promise_rejection" /* PROMISE_REJECTION */, sanitizedMessage)) {
|
|
3743
|
+
return;
|
|
3744
|
+
}
|
|
3745
|
+
this.eventManager.track({
|
|
3746
|
+
type: "error" /* ERROR */,
|
|
3747
|
+
error_data: {
|
|
3748
|
+
type: "promise_rejection" /* PROMISE_REJECTION */,
|
|
3749
|
+
message: sanitizedMessage
|
|
3750
|
+
}
|
|
3751
|
+
});
|
|
3752
|
+
};
|
|
3753
|
+
extractRejectionMessage(reason) {
|
|
3754
|
+
if (!reason) return "Unknown rejection";
|
|
3755
|
+
if (typeof reason === "string") return reason;
|
|
3756
|
+
if (reason instanceof Error) {
|
|
3757
|
+
return reason.stack ?? reason.message ?? reason.toString();
|
|
3758
|
+
}
|
|
3759
|
+
if (typeof reason === "object" && "message" in reason) {
|
|
3760
|
+
return String(reason.message);
|
|
3761
|
+
}
|
|
3762
|
+
try {
|
|
3763
|
+
return JSON.stringify(reason);
|
|
3764
|
+
} catch {
|
|
3765
|
+
return String(reason);
|
|
3766
|
+
}
|
|
3767
|
+
}
|
|
3768
|
+
sanitize(text) {
|
|
3769
|
+
let sanitized = text.length > MAX_ERROR_MESSAGE_LENGTH ? text.slice(0, MAX_ERROR_MESSAGE_LENGTH) + "..." : text;
|
|
3770
|
+
for (const pattern of PII_PATTERNS) {
|
|
3771
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
3772
|
+
sanitized = sanitized.replace(regex, "[REDACTED]");
|
|
3773
|
+
}
|
|
3774
|
+
return sanitized;
|
|
3775
|
+
}
|
|
3776
|
+
shouldSuppressError(type, message) {
|
|
3777
|
+
const now = Date.now();
|
|
3778
|
+
const key = `${type}:${message}`;
|
|
3779
|
+
const lastSeenAt = this.recentErrors.get(key);
|
|
3780
|
+
if (lastSeenAt && now - lastSeenAt < ERROR_SUPPRESSION_WINDOW_MS) {
|
|
3781
|
+
this.recentErrors.set(key, now);
|
|
3782
|
+
return true;
|
|
3783
|
+
}
|
|
3784
|
+
this.recentErrors.set(key, now);
|
|
3785
|
+
if (this.recentErrors.size > MAX_TRACKED_ERRORS_HARD_LIMIT) {
|
|
3786
|
+
this.recentErrors.clear();
|
|
3787
|
+
this.recentErrors.set(key, now);
|
|
3788
|
+
return false;
|
|
3789
|
+
}
|
|
3790
|
+
if (this.recentErrors.size > MAX_TRACKED_ERRORS) {
|
|
3791
|
+
this.pruneOldErrors();
|
|
3792
|
+
}
|
|
3793
|
+
return false;
|
|
3794
|
+
}
|
|
3795
|
+
pruneOldErrors() {
|
|
3796
|
+
const now = Date.now();
|
|
3797
|
+
for (const [key, timestamp] of this.recentErrors.entries()) {
|
|
3798
|
+
if (now - timestamp > ERROR_SUPPRESSION_WINDOW_MS) {
|
|
3799
|
+
this.recentErrors.delete(key);
|
|
3800
|
+
}
|
|
3801
|
+
}
|
|
3802
|
+
if (this.recentErrors.size <= MAX_TRACKED_ERRORS) {
|
|
3803
|
+
return;
|
|
3804
|
+
}
|
|
3805
|
+
const entries = Array.from(this.recentErrors.entries()).sort((a, b) => a[1] - b[1]);
|
|
3806
|
+
const excess = this.recentErrors.size - MAX_TRACKED_ERRORS;
|
|
3807
|
+
for (let index = 0; index < excess; index += 1) {
|
|
3808
|
+
const entry = entries[index];
|
|
3809
|
+
if (entry) {
|
|
3810
|
+
this.recentErrors.delete(entry[0]);
|
|
3811
|
+
}
|
|
3812
|
+
}
|
|
3813
|
+
}
|
|
3814
|
+
};
|
|
3815
|
+
|
|
3816
|
+
// src/app.ts
|
|
3817
|
+
var App = class extends StateManager {
|
|
3818
|
+
isInitialized = false;
|
|
3819
|
+
suppressNextScrollTimer = null;
|
|
3820
|
+
emitter = new Emitter();
|
|
3821
|
+
managers = {};
|
|
3822
|
+
handlers = {};
|
|
3823
|
+
integrations = {};
|
|
3824
|
+
get initialized() {
|
|
3825
|
+
return this.isInitialized;
|
|
3826
|
+
}
|
|
3827
|
+
async init(config = {}) {
|
|
3828
|
+
if (this.isInitialized) {
|
|
3829
|
+
return;
|
|
3830
|
+
}
|
|
3831
|
+
this.managers.storage = new StorageManager();
|
|
3832
|
+
try {
|
|
3833
|
+
this.setupState(config);
|
|
3834
|
+
await this.setupIntegrations();
|
|
3835
|
+
this.managers.event = new EventManager(this.managers.storage, this.integrations.googleAnalytics, this.emitter);
|
|
3836
|
+
this.initializeHandlers();
|
|
3837
|
+
await this.managers.event.recoverPersistedEvents().catch((error) => {
|
|
3838
|
+
log("warn", "Failed to recover persisted events", { error });
|
|
3839
|
+
});
|
|
3840
|
+
this.isInitialized = true;
|
|
3841
|
+
} catch (error) {
|
|
3842
|
+
this.destroy(true);
|
|
3843
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
3844
|
+
throw new Error(`[TraceLog] TraceLog initialization failed: ${errorMessage}`);
|
|
3845
|
+
}
|
|
3846
|
+
}
|
|
3847
|
+
sendCustomEvent(name, metadata) {
|
|
3848
|
+
if (!this.managers.event) {
|
|
3849
|
+
return;
|
|
3850
|
+
}
|
|
3851
|
+
const { valid, error, sanitizedMetadata } = isEventValid(name, metadata);
|
|
3852
|
+
if (!valid) {
|
|
3853
|
+
if (this.get("mode") === "qa" /* QA */) {
|
|
3854
|
+
throw new Error(`[TraceLog] Custom event "${name}" validation failed: ${error}`);
|
|
3855
|
+
}
|
|
3856
|
+
return;
|
|
3857
|
+
}
|
|
3858
|
+
this.managers.event.track({
|
|
3859
|
+
type: "custom" /* CUSTOM */,
|
|
3860
|
+
custom_event: {
|
|
3861
|
+
name,
|
|
3862
|
+
...sanitizedMetadata && { metadata: sanitizedMetadata }
|
|
3863
|
+
}
|
|
3864
|
+
});
|
|
3865
|
+
}
|
|
3866
|
+
on(event2, callback) {
|
|
3867
|
+
this.emitter.on(event2, callback);
|
|
3868
|
+
}
|
|
3869
|
+
off(event2, callback) {
|
|
3870
|
+
this.emitter.off(event2, callback);
|
|
3871
|
+
}
|
|
3872
|
+
destroy(force = false) {
|
|
3873
|
+
if (!this.isInitialized && !force) {
|
|
3874
|
+
return;
|
|
3875
|
+
}
|
|
3876
|
+
this.integrations.googleAnalytics?.cleanup();
|
|
3877
|
+
Object.values(this.handlers).filter(Boolean).forEach((handler) => {
|
|
3878
|
+
try {
|
|
3879
|
+
handler.stopTracking();
|
|
3880
|
+
} catch (error) {
|
|
3881
|
+
log("warn", "Failed to stop tracking", { error });
|
|
3882
|
+
}
|
|
3883
|
+
});
|
|
3884
|
+
if (this.suppressNextScrollTimer) {
|
|
3885
|
+
clearTimeout(this.suppressNextScrollTimer);
|
|
3886
|
+
this.suppressNextScrollTimer = null;
|
|
3887
|
+
}
|
|
3888
|
+
this.managers.event?.flushImmediatelySync();
|
|
3889
|
+
this.managers.event?.stop();
|
|
3890
|
+
this.emitter.removeAllListeners();
|
|
3891
|
+
this.set("hasStartSession", false);
|
|
3892
|
+
this.set("suppressNextScroll", false);
|
|
3893
|
+
this.set("sessionId", null);
|
|
3894
|
+
this.isInitialized = false;
|
|
3895
|
+
this.handlers = {};
|
|
3896
|
+
}
|
|
3897
|
+
setupState(config = {}) {
|
|
3898
|
+
this.set("config", config);
|
|
3899
|
+
const userId = UserManager.getId(this.managers.storage);
|
|
3900
|
+
this.set("userId", userId);
|
|
3901
|
+
const collectApiUrl = getCollectApiUrl(config);
|
|
3902
|
+
this.set("collectApiUrl", collectApiUrl);
|
|
3903
|
+
const device = getDeviceType();
|
|
3904
|
+
this.set("device", device);
|
|
3905
|
+
const pageUrl = normalizeUrl(window.location.href, config.sensitiveQueryParams);
|
|
3906
|
+
this.set("pageUrl", pageUrl);
|
|
3907
|
+
const mode = detectQaMode() ? "qa" /* QA */ : void 0;
|
|
3908
|
+
if (mode) {
|
|
3909
|
+
this.set("mode", mode);
|
|
3910
|
+
}
|
|
3911
|
+
}
|
|
3912
|
+
async setupIntegrations() {
|
|
3913
|
+
const config = this.get("config");
|
|
3914
|
+
const measurementId = config.integrations?.googleAnalytics?.measurementId;
|
|
3915
|
+
if (measurementId?.trim()) {
|
|
3916
|
+
try {
|
|
3917
|
+
this.integrations.googleAnalytics = new GoogleAnalyticsIntegration();
|
|
3918
|
+
await this.integrations.googleAnalytics.initialize();
|
|
3919
|
+
} catch {
|
|
3920
|
+
this.integrations.googleAnalytics = void 0;
|
|
3921
|
+
}
|
|
3922
|
+
}
|
|
3923
|
+
}
|
|
3924
|
+
initializeHandlers() {
|
|
3925
|
+
this.handlers.session = new SessionHandler(
|
|
3926
|
+
this.managers.storage,
|
|
3927
|
+
this.managers.event
|
|
3928
|
+
);
|
|
3929
|
+
this.handlers.session.startTracking();
|
|
3930
|
+
const onPageView = () => {
|
|
3931
|
+
this.set("suppressNextScroll", true);
|
|
3932
|
+
if (this.suppressNextScrollTimer) {
|
|
3933
|
+
clearTimeout(this.suppressNextScrollTimer);
|
|
3934
|
+
}
|
|
3935
|
+
this.suppressNextScrollTimer = window.setTimeout(() => {
|
|
3936
|
+
this.set("suppressNextScroll", false);
|
|
3937
|
+
}, SCROLL_DEBOUNCE_TIME_MS * SCROLL_SUPPRESS_MULTIPLIER);
|
|
3938
|
+
};
|
|
3939
|
+
this.handlers.pageView = new PageViewHandler(this.managers.event, onPageView);
|
|
3940
|
+
this.handlers.pageView.startTracking();
|
|
3941
|
+
this.handlers.click = new ClickHandler(this.managers.event);
|
|
3942
|
+
this.handlers.click.startTracking();
|
|
3943
|
+
this.handlers.scroll = new ScrollHandler(this.managers.event);
|
|
3944
|
+
this.handlers.scroll.startTracking();
|
|
3945
|
+
this.handlers.performance = new PerformanceHandler(this.managers.event);
|
|
3946
|
+
this.handlers.performance.startTracking().catch((error) => {
|
|
3947
|
+
log("warn", "Failed to start performance tracking", { error });
|
|
3948
|
+
});
|
|
3949
|
+
this.handlers.error = new ErrorHandler(this.managers.event);
|
|
3950
|
+
this.handlers.error.startTracking();
|
|
3951
|
+
if (this.get("config").viewport) {
|
|
3952
|
+
this.handlers.viewport = new ViewportHandler(this.managers.event);
|
|
3953
|
+
this.handlers.viewport.startTracking();
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
};
|
|
3957
|
+
|
|
3958
|
+
// src/test-bridge.ts
|
|
3959
|
+
var TestBridge = class extends App {
|
|
3960
|
+
_isInitializing;
|
|
3961
|
+
_isDestroying = false;
|
|
3962
|
+
constructor(isInitializing2, isDestroying2) {
|
|
3963
|
+
super();
|
|
3964
|
+
this._isInitializing = isInitializing2;
|
|
3965
|
+
this._isDestroying = isDestroying2;
|
|
3966
|
+
}
|
|
3967
|
+
async init(config) {
|
|
3968
|
+
if (process.env.NODE_ENV !== "development") {
|
|
3969
|
+
throw new Error("[TraceLog] TestBridge is only available in development mode");
|
|
3970
|
+
}
|
|
3971
|
+
if (!__setAppInstance) {
|
|
3972
|
+
throw new Error("[TraceLog] __setAppInstance is not available (production build?)");
|
|
3973
|
+
}
|
|
3974
|
+
try {
|
|
3975
|
+
__setAppInstance(this);
|
|
3976
|
+
} catch {
|
|
3977
|
+
throw new Error("[TraceLog] TestBridge cannot sync with existing tracelog instance. Call destroy() first.");
|
|
3978
|
+
}
|
|
3979
|
+
try {
|
|
3980
|
+
await super.init(config);
|
|
3981
|
+
} catch (error) {
|
|
3982
|
+
if (__setAppInstance) {
|
|
3983
|
+
__setAppInstance(null);
|
|
3984
|
+
}
|
|
3985
|
+
throw error;
|
|
3986
|
+
}
|
|
3987
|
+
}
|
|
3988
|
+
isInitializing() {
|
|
3989
|
+
return this._isInitializing;
|
|
3990
|
+
}
|
|
3991
|
+
sendCustomEvent(name, data) {
|
|
3992
|
+
if (!this.initialized) {
|
|
3993
|
+
return;
|
|
3994
|
+
}
|
|
3995
|
+
super.sendCustomEvent(name, data);
|
|
3996
|
+
}
|
|
3997
|
+
getSessionData() {
|
|
3998
|
+
return {
|
|
3999
|
+
id: this.get("sessionId"),
|
|
4000
|
+
isActive: !!this.get("sessionId"),
|
|
4001
|
+
timeout: this.get("config")?.sessionTimeout ?? 15 * 60 * 1e3
|
|
4002
|
+
};
|
|
4003
|
+
}
|
|
4004
|
+
setSessionTimeout(timeout) {
|
|
4005
|
+
const config = this.get("config");
|
|
4006
|
+
if (config) {
|
|
4007
|
+
this.set("config", { ...config, sessionTimeout: timeout });
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
getQueueLength() {
|
|
4011
|
+
return this.managers.event?.getQueueLength() ?? 0;
|
|
4012
|
+
}
|
|
4013
|
+
forceInitLock(enabled = true) {
|
|
4014
|
+
this._isInitializing = enabled;
|
|
4015
|
+
}
|
|
4016
|
+
simulatePersistedEvents(events) {
|
|
4017
|
+
const storageManager = this.managers?.storage;
|
|
4018
|
+
if (!storageManager) {
|
|
4019
|
+
throw new Error("Storage manager not available");
|
|
4020
|
+
}
|
|
4021
|
+
const config = this.get("config");
|
|
4022
|
+
const projectId = config?.integrations?.tracelog?.projectId ?? config?.integrations?.custom?.collectApiUrl ?? "test";
|
|
4023
|
+
const userId = this.get("userId");
|
|
4024
|
+
const sessionId = this.get("sessionId");
|
|
4025
|
+
if (!projectId || !userId) {
|
|
4026
|
+
throw new Error("Project ID or User ID not available. Initialize TraceLog first.");
|
|
4027
|
+
}
|
|
4028
|
+
const persistedData = {
|
|
4029
|
+
userId,
|
|
4030
|
+
sessionId: sessionId || `test-session-${Date.now()}`,
|
|
4031
|
+
device: "desktop",
|
|
4032
|
+
events,
|
|
4033
|
+
timestamp: Date.now()
|
|
4034
|
+
};
|
|
4035
|
+
const storageKey = `${STORAGE_BASE_KEY}:${projectId}:queue:${userId}`;
|
|
4036
|
+
storageManager.setItem(storageKey, JSON.stringify(persistedData));
|
|
4037
|
+
}
|
|
4038
|
+
get(key) {
|
|
4039
|
+
return super.get(key);
|
|
4040
|
+
}
|
|
4041
|
+
// Manager accessors
|
|
4042
|
+
getStorageManager() {
|
|
4043
|
+
return this.safeAccess(this.managers?.storage);
|
|
4044
|
+
}
|
|
4045
|
+
getEventManager() {
|
|
4046
|
+
return this.safeAccess(this.managers?.event);
|
|
4047
|
+
}
|
|
4048
|
+
// Handler accessors
|
|
4049
|
+
getSessionHandler() {
|
|
4050
|
+
return this.safeAccess(this.handlers?.session);
|
|
4051
|
+
}
|
|
4052
|
+
getPageViewHandler() {
|
|
4053
|
+
return this.safeAccess(this.handlers?.pageView);
|
|
4054
|
+
}
|
|
4055
|
+
getClickHandler() {
|
|
4056
|
+
return this.safeAccess(this.handlers?.click);
|
|
4057
|
+
}
|
|
4058
|
+
getScrollHandler() {
|
|
4059
|
+
return this.safeAccess(this.handlers?.scroll);
|
|
4060
|
+
}
|
|
4061
|
+
getPerformanceHandler() {
|
|
4062
|
+
return this.safeAccess(this.handlers?.performance);
|
|
4063
|
+
}
|
|
4064
|
+
getErrorHandler() {
|
|
4065
|
+
return this.safeAccess(this.handlers?.error);
|
|
4066
|
+
}
|
|
4067
|
+
// Integration accessors
|
|
4068
|
+
getGoogleAnalytics() {
|
|
4069
|
+
return this.safeAccess(this.integrations?.googleAnalytics);
|
|
4070
|
+
}
|
|
4071
|
+
destroy(force = false) {
|
|
4072
|
+
if (!this.initialized && !force) {
|
|
4073
|
+
return;
|
|
4074
|
+
}
|
|
4075
|
+
this.ensureNotDestroying();
|
|
4076
|
+
this._isDestroying = true;
|
|
4077
|
+
try {
|
|
4078
|
+
super.destroy(force);
|
|
4079
|
+
if (__setAppInstance) {
|
|
4080
|
+
__setAppInstance(null);
|
|
4081
|
+
}
|
|
4082
|
+
} finally {
|
|
4083
|
+
this._isDestroying = false;
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
/**
|
|
4087
|
+
* Helper to safely access managers/handlers and convert undefined to null
|
|
4088
|
+
*/
|
|
4089
|
+
safeAccess(value) {
|
|
4090
|
+
return value ?? null;
|
|
4091
|
+
}
|
|
4092
|
+
/**
|
|
4093
|
+
* Ensures destroy operation is not in progress, throws if it is
|
|
4094
|
+
*/
|
|
4095
|
+
ensureNotDestroying() {
|
|
4096
|
+
if (this._isDestroying) {
|
|
4097
|
+
throw new Error("Destroy operation already in progress");
|
|
4098
|
+
}
|
|
4099
|
+
}
|
|
4100
|
+
};
|
|
4101
|
+
|
|
4102
|
+
// src/api.ts
|
|
4103
|
+
var pendingListeners = [];
|
|
4104
|
+
var app = null;
|
|
4105
|
+
var isInitializing = false;
|
|
4106
|
+
var isDestroying = false;
|
|
4107
|
+
var init = async (config) => {
|
|
4108
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
4109
|
+
return;
|
|
4110
|
+
}
|
|
4111
|
+
if (window.__traceLogDisabled) {
|
|
4112
|
+
return;
|
|
4113
|
+
}
|
|
4114
|
+
if (app) {
|
|
4115
|
+
return;
|
|
4116
|
+
}
|
|
4117
|
+
if (isInitializing) {
|
|
4118
|
+
return;
|
|
4119
|
+
}
|
|
4120
|
+
isInitializing = true;
|
|
4121
|
+
try {
|
|
4122
|
+
const validatedConfig = validateAndNormalizeConfig(config ?? {});
|
|
4123
|
+
const instance = new App();
|
|
4124
|
+
try {
|
|
4125
|
+
pendingListeners.forEach(({ event: event2, callback }) => {
|
|
4126
|
+
instance.on(event2, callback);
|
|
4127
|
+
});
|
|
4128
|
+
pendingListeners.length = 0;
|
|
4129
|
+
const initPromise = instance.init(validatedConfig);
|
|
4130
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
4131
|
+
setTimeout(() => {
|
|
4132
|
+
reject(new Error(`[TraceLog] Initialization timeout after ${INITIALIZATION_TIMEOUT_MS}ms`));
|
|
4133
|
+
}, INITIALIZATION_TIMEOUT_MS);
|
|
4134
|
+
});
|
|
4135
|
+
await Promise.race([initPromise, timeoutPromise]);
|
|
4136
|
+
app = instance;
|
|
4137
|
+
} catch (error) {
|
|
4138
|
+
try {
|
|
4139
|
+
instance.destroy(true);
|
|
4140
|
+
} catch (cleanupError) {
|
|
4141
|
+
log("error", "Failed to cleanup partially initialized app", { error: cleanupError });
|
|
4142
|
+
}
|
|
4143
|
+
throw error;
|
|
4144
|
+
}
|
|
4145
|
+
} catch (error) {
|
|
4146
|
+
app = null;
|
|
4147
|
+
throw error;
|
|
4148
|
+
} finally {
|
|
4149
|
+
isInitializing = false;
|
|
4150
|
+
}
|
|
4151
|
+
};
|
|
4152
|
+
var event = (name, metadata) => {
|
|
4153
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
4154
|
+
return;
|
|
4155
|
+
}
|
|
4156
|
+
if (!app) {
|
|
4157
|
+
throw new Error("[TraceLog] TraceLog not initialized. Please call init() first.");
|
|
4158
|
+
}
|
|
4159
|
+
if (isDestroying) {
|
|
4160
|
+
throw new Error("[TraceLog] Cannot send events while TraceLog is being destroyed");
|
|
4161
|
+
}
|
|
4162
|
+
app.sendCustomEvent(name, metadata);
|
|
4163
|
+
};
|
|
4164
|
+
var on = (event2, callback) => {
|
|
4165
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
4166
|
+
return;
|
|
4167
|
+
}
|
|
4168
|
+
if (!app || isInitializing) {
|
|
4169
|
+
pendingListeners.push({ event: event2, callback });
|
|
4170
|
+
return;
|
|
4171
|
+
}
|
|
4172
|
+
app.on(event2, callback);
|
|
4173
|
+
};
|
|
4174
|
+
var off = (event2, callback) => {
|
|
4175
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
4176
|
+
return;
|
|
4177
|
+
}
|
|
4178
|
+
if (!app) {
|
|
4179
|
+
const index = pendingListeners.findIndex((l) => l.event === event2 && l.callback === callback);
|
|
4180
|
+
if (index !== -1) {
|
|
4181
|
+
pendingListeners.splice(index, 1);
|
|
4182
|
+
}
|
|
4183
|
+
return;
|
|
4184
|
+
}
|
|
4185
|
+
app.off(event2, callback);
|
|
4186
|
+
};
|
|
4187
|
+
var isInitialized = () => {
|
|
4188
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
4189
|
+
return false;
|
|
4190
|
+
}
|
|
4191
|
+
return app !== null;
|
|
4192
|
+
};
|
|
4193
|
+
var destroy = () => {
|
|
4194
|
+
if (typeof window === "undefined" || typeof document === "undefined") {
|
|
4195
|
+
return;
|
|
4196
|
+
}
|
|
4197
|
+
if (isDestroying) {
|
|
4198
|
+
throw new Error("[TraceLog] Destroy operation already in progress");
|
|
4199
|
+
}
|
|
4200
|
+
if (!app) {
|
|
4201
|
+
throw new Error("[TraceLog] App not initialized");
|
|
4202
|
+
}
|
|
4203
|
+
isDestroying = true;
|
|
4204
|
+
try {
|
|
4205
|
+
app.destroy();
|
|
4206
|
+
app = null;
|
|
4207
|
+
isInitializing = false;
|
|
4208
|
+
pendingListeners.length = 0;
|
|
4209
|
+
if (process.env.NODE_ENV === "development" && typeof window !== "undefined" && window.__traceLogBridge) {
|
|
4210
|
+
window.__traceLogBridge = void 0;
|
|
4211
|
+
}
|
|
4212
|
+
} catch (error) {
|
|
4213
|
+
app = null;
|
|
4214
|
+
isInitializing = false;
|
|
4215
|
+
pendingListeners.length = 0;
|
|
4216
|
+
log("warn", "Error during destroy, forced cleanup completed", { error });
|
|
4217
|
+
} finally {
|
|
4218
|
+
isDestroying = false;
|
|
4219
|
+
}
|
|
4220
|
+
};
|
|
4221
|
+
var __setAppInstance = (instance) => {
|
|
4222
|
+
if (process.env.NODE_ENV !== "development") {
|
|
4223
|
+
return;
|
|
4224
|
+
}
|
|
4225
|
+
if (instance !== null) {
|
|
4226
|
+
const hasRequiredMethods = typeof instance === "object" && "init" in instance && "destroy" in instance && "on" in instance && "off" in instance;
|
|
4227
|
+
if (!hasRequiredMethods) {
|
|
4228
|
+
throw new Error("[TraceLog] Invalid app instance type");
|
|
4229
|
+
}
|
|
4230
|
+
}
|
|
4231
|
+
if (app !== null && instance !== null && app !== instance) {
|
|
4232
|
+
throw new Error("[TraceLog] Cannot overwrite existing app instance. Call destroy() first.");
|
|
4233
|
+
}
|
|
4234
|
+
app = instance;
|
|
4235
|
+
};
|
|
4236
|
+
if (process.env.NODE_ENV === "development" && typeof window !== "undefined" && typeof document !== "undefined") {
|
|
4237
|
+
const injectTestingBridge = () => {
|
|
4238
|
+
window.__traceLogBridge = new TestBridge(isInitializing, isDestroying);
|
|
4239
|
+
};
|
|
4240
|
+
if (document.readyState === "loading") {
|
|
4241
|
+
document.addEventListener("DOMContentLoaded", injectTestingBridge);
|
|
4242
|
+
} else {
|
|
4243
|
+
injectTestingBridge();
|
|
4244
|
+
}
|
|
4245
|
+
}
|
|
4246
|
+
|
|
4247
|
+
// src/app.constants.ts
|
|
4248
|
+
var PERFORMANCE_CONFIG = {
|
|
4249
|
+
WEB_VITALS_THRESHOLDS
|
|
4250
|
+
// Business thresholds for performance analysis
|
|
4251
|
+
};
|
|
4252
|
+
var DATA_PROTECTION = {
|
|
4253
|
+
PII_PATTERNS
|
|
4254
|
+
// Patterns for sensitive data protection
|
|
4255
|
+
};
|
|
4256
|
+
var ENGAGEMENT_THRESHOLDS = {
|
|
4257
|
+
LOW_ACTIVITY_EVENT_COUNT: 50,
|
|
4258
|
+
HIGH_ACTIVITY_EVENT_COUNT: 1e3,
|
|
4259
|
+
MIN_EVENTS_FOR_DYNAMIC_CALCULATION: 100,
|
|
4260
|
+
MIN_EVENTS_FOR_TREND_ANALYSIS: 30,
|
|
4261
|
+
BOUNCE_RATE_SESSION_THRESHOLD: 1,
|
|
4262
|
+
// Sessions with 1 page view = bounce
|
|
4263
|
+
MIN_ENGAGED_SESSION_DURATION_MS: 30 * 1e3,
|
|
4264
|
+
MIN_SCROLL_DEPTH_ENGAGEMENT: 25
|
|
4265
|
+
// 25% scroll depth for engagement
|
|
4266
|
+
};
|
|
4267
|
+
var SESSION_ANALYTICS = {
|
|
4268
|
+
INACTIVITY_TIMEOUT_MS: 30 * 60 * 1e3,
|
|
4269
|
+
// 30min for analytics (vs 15min client)
|
|
4270
|
+
SHORT_SESSION_THRESHOLD_MS: 30 * 1e3,
|
|
4271
|
+
MEDIUM_SESSION_THRESHOLD_MS: 5 * 60 * 1e3,
|
|
4272
|
+
LONG_SESSION_THRESHOLD_MS: 30 * 60 * 1e3,
|
|
4273
|
+
MAX_REALISTIC_SESSION_DURATION_MS: 8 * 60 * 60 * 1e3
|
|
4274
|
+
// Filter outliers
|
|
4275
|
+
};
|
|
4276
|
+
var DEVICE_ANALYTICS = {
|
|
4277
|
+
MOBILE_MAX_WIDTH: 768,
|
|
4278
|
+
TABLET_MAX_WIDTH: 1024,
|
|
4279
|
+
MOBILE_PERFORMANCE_FACTOR: 1.5,
|
|
4280
|
+
// Mobile typically 1.5x slower
|
|
4281
|
+
TABLET_PERFORMANCE_FACTOR: 1.2
|
|
4282
|
+
};
|
|
4283
|
+
var CONTENT_ANALYTICS = {
|
|
4284
|
+
MIN_TEXT_LENGTH_FOR_ANALYSIS: 10,
|
|
4285
|
+
MIN_CLICKS_FOR_HOT_ELEMENT: 10,
|
|
4286
|
+
// Popular element threshold
|
|
4287
|
+
MIN_SCROLL_COMPLETION_PERCENT: 80,
|
|
4288
|
+
// Page consumption threshold
|
|
4289
|
+
MIN_TIME_ON_PAGE_FOR_READ_MS: 15 * 1e3
|
|
4290
|
+
};
|
|
4291
|
+
var INSIGHT_THRESHOLDS = {
|
|
4292
|
+
SIGNIFICANT_CHANGE_PERCENT: 20,
|
|
4293
|
+
MAJOR_CHANGE_PERCENT: 50,
|
|
4294
|
+
MIN_EVENTS_FOR_INSIGHT: 100,
|
|
4295
|
+
MIN_SESSIONS_FOR_INSIGHT: 10,
|
|
4296
|
+
MIN_CORRELATION_STRENGTH: 0.7,
|
|
4297
|
+
// Strong correlation threshold
|
|
4298
|
+
LOW_ERROR_RATE_PERCENT: 1,
|
|
4299
|
+
HIGH_ERROR_RATE_PERCENT: 5,
|
|
4300
|
+
CRITICAL_ERROR_RATE_PERCENT: 10
|
|
4301
|
+
};
|
|
4302
|
+
var TEMPORAL_ANALYSIS = {
|
|
4303
|
+
SHORT_TERM_TREND_HOURS: 24,
|
|
4304
|
+
MEDIUM_TERM_TREND_DAYS: 7,
|
|
4305
|
+
LONG_TERM_TREND_DAYS: 30,
|
|
4306
|
+
MIN_DATA_POINTS_FOR_TREND: 5,
|
|
4307
|
+
WEEKLY_PATTERN_MIN_WEEKS: 4,
|
|
4308
|
+
DAILY_PATTERN_MIN_DAYS: 14
|
|
4309
|
+
};
|
|
4310
|
+
var SEGMENTATION_ANALYTICS = {
|
|
4311
|
+
MIN_SEGMENT_SIZE: 10,
|
|
4312
|
+
MIN_COHORT_SIZE: 5,
|
|
4313
|
+
COHORT_ANALYSIS_DAYS: [1, 3, 7, 14, 30],
|
|
4314
|
+
MIN_FUNNEL_EVENTS: 20
|
|
4315
|
+
};
|
|
4316
|
+
var ANALYTICS_QUERY_LIMITS = {
|
|
4317
|
+
DEFAULT_EVENTS_LIMIT: 5,
|
|
4318
|
+
DEFAULT_SESSIONS_LIMIT: 5,
|
|
4319
|
+
DEFAULT_PAGES_LIMIT: 5,
|
|
4320
|
+
MAX_EVENTS_FOR_DEEP_ANALYSIS: 1e4,
|
|
4321
|
+
MAX_TIME_RANGE_DAYS: 365,
|
|
4322
|
+
ANALYTICS_BATCH_SIZE: 1e3
|
|
4323
|
+
// For historical analysis
|
|
4324
|
+
};
|
|
4325
|
+
var ANOMALY_DETECTION = {
|
|
4326
|
+
ANOMALY_THRESHOLD_SIGMA: 2.5,
|
|
4327
|
+
STRONG_ANOMALY_THRESHOLD_SIGMA: 3,
|
|
4328
|
+
TRAFFIC_DROP_ALERT_PERCENT: -30,
|
|
4329
|
+
TRAFFIC_SPIKE_ALERT_PERCENT: 200,
|
|
4330
|
+
MIN_BASELINE_DAYS: 7,
|
|
4331
|
+
MIN_EVENTS_FOR_ANOMALY_DETECTION: 50
|
|
4332
|
+
};
|
|
4333
|
+
var SPECIAL_PAGE_URLS = {
|
|
4334
|
+
PAGE_URL_EXCLUDED: "excluded",
|
|
4335
|
+
PAGE_URL_UNKNOWN: "unknown"
|
|
4336
|
+
};
|
|
4337
|
+
|
|
4338
|
+
// src/public-api.ts
|
|
4339
|
+
var tracelog = {
|
|
4340
|
+
init,
|
|
4341
|
+
event,
|
|
4342
|
+
on,
|
|
4343
|
+
off,
|
|
4344
|
+
isInitialized,
|
|
4345
|
+
destroy
|
|
4346
|
+
};
|
|
4347
|
+
|
|
4348
|
+
exports.ANALYTICS_QUERY_LIMITS = ANALYTICS_QUERY_LIMITS;
|
|
4349
|
+
exports.ANOMALY_DETECTION = ANOMALY_DETECTION;
|
|
4350
|
+
exports.AppConfigValidationError = AppConfigValidationError;
|
|
4351
|
+
exports.CONTENT_ANALYTICS = CONTENT_ANALYTICS;
|
|
4352
|
+
exports.DATA_PROTECTION = DATA_PROTECTION;
|
|
4353
|
+
exports.DEVICE_ANALYTICS = DEVICE_ANALYTICS;
|
|
4354
|
+
exports.DeviceType = DeviceType;
|
|
4355
|
+
exports.ENGAGEMENT_THRESHOLDS = ENGAGEMENT_THRESHOLDS;
|
|
4356
|
+
exports.EmitterEvent = EmitterEvent;
|
|
4357
|
+
exports.ErrorType = ErrorType;
|
|
4358
|
+
exports.EventType = EventType;
|
|
4359
|
+
exports.INSIGHT_THRESHOLDS = INSIGHT_THRESHOLDS;
|
|
4360
|
+
exports.InitializationTimeoutError = InitializationTimeoutError;
|
|
4361
|
+
exports.IntegrationValidationError = IntegrationValidationError;
|
|
4362
|
+
exports.MAX_ARRAY_LENGTH = MAX_ARRAY_LENGTH;
|
|
4363
|
+
exports.MAX_CUSTOM_EVENT_ARRAY_SIZE = MAX_CUSTOM_EVENT_ARRAY_SIZE;
|
|
4364
|
+
exports.MAX_CUSTOM_EVENT_KEYS = MAX_CUSTOM_EVENT_KEYS;
|
|
4365
|
+
exports.MAX_CUSTOM_EVENT_NAME_LENGTH = MAX_CUSTOM_EVENT_NAME_LENGTH;
|
|
4366
|
+
exports.MAX_CUSTOM_EVENT_STRING_SIZE = MAX_CUSTOM_EVENT_STRING_SIZE;
|
|
4367
|
+
exports.MAX_METADATA_NESTING_DEPTH = MAX_METADATA_NESTING_DEPTH;
|
|
4368
|
+
exports.MAX_NESTED_OBJECT_KEYS = MAX_NESTED_OBJECT_KEYS;
|
|
4369
|
+
exports.MAX_STRING_LENGTH = MAX_STRING_LENGTH;
|
|
4370
|
+
exports.MAX_STRING_LENGTH_IN_ARRAY = MAX_STRING_LENGTH_IN_ARRAY;
|
|
4371
|
+
exports.Mode = Mode;
|
|
4372
|
+
exports.PERFORMANCE_CONFIG = PERFORMANCE_CONFIG;
|
|
4373
|
+
exports.PermanentError = PermanentError;
|
|
4374
|
+
exports.SEGMENTATION_ANALYTICS = SEGMENTATION_ANALYTICS;
|
|
4375
|
+
exports.SESSION_ANALYTICS = SESSION_ANALYTICS;
|
|
4376
|
+
exports.SPECIAL_PAGE_URLS = SPECIAL_PAGE_URLS;
|
|
4377
|
+
exports.SamplingRateValidationError = SamplingRateValidationError;
|
|
4378
|
+
exports.ScrollDirection = ScrollDirection;
|
|
4379
|
+
exports.SessionTimeoutValidationError = SessionTimeoutValidationError;
|
|
4380
|
+
exports.SpecialApiUrl = SpecialApiUrl;
|
|
4381
|
+
exports.TEMPORAL_ANALYSIS = TEMPORAL_ANALYSIS;
|
|
4382
|
+
exports.TraceLogValidationError = TraceLogValidationError;
|
|
4383
|
+
exports.isPrimaryScrollEvent = isPrimaryScrollEvent;
|
|
4384
|
+
exports.isSecondaryScrollEvent = isSecondaryScrollEvent;
|
|
4385
|
+
exports.tracelog = tracelog;
|
|
4386
|
+
//# sourceMappingURL=public-api.cjs.map
|
|
4387
|
+
//# sourceMappingURL=public-api.cjs.map
|