diff --git a/packages/core/src/scope.ts b/packages/core/src/scope.ts index 3fba03feab94..986efd49d7ed 100644 --- a/packages/core/src/scope.ts +++ b/packages/core/src/scope.ts @@ -22,7 +22,14 @@ import type { SeverityLevel, User, } from '@sentry/types'; -import { dateTimestampInSeconds, generatePropagationContext, isPlainObject, logger, uuid4 } from '@sentry/utils'; +import { + LRUMap, + dateTimestampInSeconds, + generatePropagationContext, + isPlainObject, + logger, + uuid4, +} from '@sentry/utils'; import { updateSession } from './session'; import { _getSpanForScope, _setSpanForScope } from './utils/spanOnScope'; @@ -99,7 +106,7 @@ class ScopeClass implements ScopeInterface { protected _lastEventId?: string; /** LRU cache of flags last evaluated by a feature flag provider. Used by FF integrations. */ - protected _flagBuffer: FeatureFlag[]; + protected _flagBuffer: LRUMap; /** Max size of the flagBuffer */ protected _flagBufferSize: number; // TODO: make const? @@ -509,36 +516,6 @@ class ScopeClass implements ScopeInterface { return this._propagationContext; } - /** - * @inheritDoc - */ - public getFlags(): FeatureFlag[] { - return this._flagBuffer || []; - } - - /** - * @inheritDoc - */ - public insertFlag(name: string, value: boolean): void { - // Check if the flag is already in the buffer - const index = this._flagBuffer.findIndex(f => f.flag === name); - - if (index !== -1) { - // Delete flag if it is in the buffer - this._flagBuffer.splice(index, 1); - } else if (this._flagBuffer.length === this._flagBufferSize) { - // If at capacity, we need to remove the earliest flag (pop from front) - // This will only happen if not a duplicate flag - this._flagBuffer.shift(); - } - - // Push the flag to the end of the queue - this._flagBuffer.push({ - flag: name, - result: value, - }); - } - /** * @inheritDoc */ diff --git a/packages/features/.eslintignore b/packages/features/.eslintignore new file mode 100644 index 000000000000..b38db2f296ff --- /dev/null +++ b/packages/features/.eslintignore @@ -0,0 +1,2 @@ +node_modules/ +build/ diff --git a/packages/features/.eslintrc.js b/packages/features/.eslintrc.js new file mode 100644 index 000000000000..dc39a10b354b --- /dev/null +++ b/packages/features/.eslintrc.js @@ -0,0 +1,39 @@ +// Note: All paths are relative to the directory in which eslint is being run, rather than the directory where this file +// lives + +// ESLint config docs: https://eslint.org/docs/user-guide/configuring/ + +module.exports = { + extends: ['../../.eslintrc.js'], + overrides: [ + { + files: ['src/**/*.ts'], + }, + { + files: ['test.setup.ts', 'vitest.config.ts'], + parserOptions: { + project: ['tsconfig.test.json'], + }, + rules: { + 'no-console': 'off', + }, + }, + { + files: ['test/**/*.ts'], + + rules: { + // most of these errors come from `new Promise(process.nextTick)` + '@typescript-eslint/unbound-method': 'off', + // TODO: decide if we want to enable this again after the migration + // We can take the freedom to be a bit more lenient with tests + '@typescript-eslint/no-floating-promises': 'off', + }, + }, + { + files: ['src/types/deprecated.ts'], + rules: { + '@typescript-eslint/naming-convention': 'off', + }, + }, + ], +}; diff --git a/packages/features/.gitignore b/packages/features/.gitignore new file mode 100644 index 000000000000..363d3467c6fa --- /dev/null +++ b/packages/features/.gitignore @@ -0,0 +1,4 @@ +node_modules +/*.tgz +.eslintcache +build diff --git a/packages/features/LICENSE b/packages/features/LICENSE new file mode 100644 index 000000000000..6bfafc44539c --- /dev/null +++ b/packages/features/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023-2024 Functional Software, Inc. dba Sentry + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies +of the Software, and to permit persons to whom the Software is furnished to do +so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/features/README.md b/packages/features/README.md new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/features/package.json b/packages/features/package.json new file mode 100644 index 000000000000..c788d003cc96 --- /dev/null +++ b/packages/features/package.json @@ -0,0 +1,73 @@ +{ + "name": "@sentry-internal/features", + "version": "8.35.0", + "description": "Sentry SDK integration for shared feature flag internals", + "repository": "git://github.com/getsentry/sentry-javascript.git", + "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/features", + "author": "Sentry", + "license": "MIT", + "engines": { + "node": ">=14.18" + }, + "files": [ + "/build/npm" + ], + "main": "build/npm/cjs/index.js", + "module": "build/npm/esm/index.js", + "types": "build/npm/types/index.d.ts", + "exports": { + "./package.json": "./package.json", + ".": { + "import": { + "types": "./build/npm/types/index.d.ts", + "default": "./build/npm/esm/index.js" + }, + "require": { + "types": "./build/npm/types/index.d.ts", + "default": "./build/npm/cjs/index.js" + } + } + }, + "typesVersions": { + "<4.9": { + "build/npm/types/index.d.ts": [ + "build/npm/types-ts3.8/index.d.ts" + ] + } + }, + "publishConfig": { + "access": "public" + }, + "dependencies": { + "@sentry/browser": "^8.35.0", + "@sentry/core": "8.35.0", + "@sentry/types": "8.35.0", + "@sentry/utils": "8.35.0" + }, + "scripts": { + "build": "run-p build:transpile build:types build:bundle", + "build:transpile": "rollup -c rollup.npm.config.mjs", + "build:bundle": "rollup -c rollup.bundle.config.mjs", + "build:dev": "run-p build:transpile build:types", + "build:types": "run-s build:types:core build:types:downlevel", + "build:types:core": "tsc -p tsconfig.types.json", + "build:types:downlevel": "yarn downlevel-dts build/npm/types build/npm/types-ts3.8 --to ts3.8 && yarn node ./scripts/shim-preact-export.js", + "build:watch": "run-p build:transpile:watch build:bundle:watch build:types:watch", + "build:dev:watch": "run-p build:transpile:watch build:types:watch", + "build:transpile:watch": "yarn build:transpile --watch", + "build:bundle:watch": "yarn build:bundle --watch", + "build:types:watch": "tsc -p tsconfig.types.json --watch", + "build:tarball": "npm pack", + "circularDepCheck": "madge --circular src/index.ts", + "clean": "rimraf build sentry-internal-features-*.tgz", + "fix": "eslint . --format stylish --fix", + "lint": "eslint . --format stylish", + "test": "jest", + "test:watch": "jest --watch", + "yalc:publish": "yalc publish --push --sig" + }, + "volta": { + "extends": "../../package.json" + }, + "sideEffects": false +} diff --git a/packages/features/rollup.bundle.config.mjs b/packages/features/rollup.bundle.config.mjs new file mode 100644 index 000000000000..4b77b625220e --- /dev/null +++ b/packages/features/rollup.bundle.config.mjs @@ -0,0 +1,12 @@ +import { makeBaseBundleConfig, makeBundleConfigVariants } from '@sentry-internal/rollup-utils'; + +const baseBundleConfig = makeBaseBundleConfig({ + bundleType: 'addon', + entrypoints: ['src/index.ts'], + licenseTitle: '@sentry-internal/features', + outputFileBase: () => 'bundles/features', +}); + +const builds = makeBundleConfigVariants(baseBundleConfig); + +export default builds; diff --git a/packages/features/rollup.npm.config.mjs b/packages/features/rollup.npm.config.mjs new file mode 100644 index 000000000000..3b4431fa6829 --- /dev/null +++ b/packages/features/rollup.npm.config.mjs @@ -0,0 +1,19 @@ +import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils'; + +export default makeNPMConfigVariants( + makeBaseNPMConfig({ + hasBundles: true, + packageSpecificConfig: { + output: { + // set exports to 'named' or 'auto' so that rollup doesn't warn + exports: 'named', + // set preserveModules to false because for Replay we actually want + // to bundle everything into one file. + preserveModules: + process.env.SENTRY_BUILD_PRESERVE_MODULES === undefined + ? false + : Boolean(process.env.SENTRY_BUILD_PRESERVE_MODULES), + }, + }, + }), +); diff --git a/packages/features/scripts/shim-preact-export.js b/packages/features/scripts/shim-preact-export.js new file mode 100644 index 000000000000..bd74e4da0a05 --- /dev/null +++ b/packages/features/scripts/shim-preact-export.js @@ -0,0 +1,75 @@ +// preact does not support more modern TypeScript versions, which breaks our users that depend on older +// TypeScript versions. To fix this, we shim the types from preact to be any and remove the dependency on preact +// for types directly. This script is meant to be run after the build/npm/types-ts3.8 directory is created. + +// Path: build/npm/types-ts3.8/global.d.ts + +const fs = require('fs'); +const path = require('path'); + +/** + * This regex looks for preact imports we can replace and shim out. + * + * Example: + * import { ComponentChildren, VNode } from 'preact'; + */ +const preactImportRegex = /import\s*{\s*([\w\s,]+)\s*}\s*from\s*'preact'\s*;?/; + +function walk(dir) { + const files = fs.readdirSync(dir); + files.forEach(file => { + const filePath = path.join(dir, file); + const stat = fs.lstatSync(filePath); + if (stat.isDirectory()) { + walk(filePath); + } else { + if (filePath.endsWith('.d.ts')) { + const content = fs.readFileSync(filePath, 'utf8'); + const capture = preactImportRegex.exec(content); + if (capture) { + const groups = capture[1].split(',').map(s => s.trim()); + + // This generates a shim snippet to replace the type imports from preact + // It generates a snippet based on the capture groups of preactImportRegex. + // + // Example: + // + // import type { ComponentChildren, VNode } from 'preact'; + // becomes + // type ComponentChildren: any; + // type VNode: any; + const snippet = groups.reduce((acc, curr) => { + const searchableValue = curr.includes(' as ') ? curr.split(' as ')[1] : curr; + + // look to see if imported as value, then we have to use declare const + if (content.includes(`typeof ${searchableValue}`)) { + return `${acc}declare const ${searchableValue}: any;\n`; + } + + // look to see if generic type like Foo + if (content.includes(`${searchableValue}<`)) { + return `${acc}type ${searchableValue} = any;\n`; + } + + // otherwise we can just leave as type + return `${acc}type ${searchableValue} = any;\n`; + }, ''); + + // we then can remove the import from preact + const newContent = content.replace(preactImportRegex, '// replaced import from preact'); + + // and write the new content to the file + fs.writeFileSync(filePath, snippet + newContent, 'utf8'); + } + } + } + }); +} + +function run() { + // recurse through build/npm/types-ts3.8 directory + const dir = path.join('build', 'npm', 'types-ts3.8'); + walk(dir); +} + +run(); diff --git a/packages/features/src/core/integration.ts b/packages/features/src/core/integration.ts new file mode 100644 index 000000000000..8f3625038fe2 --- /dev/null +++ b/packages/features/src/core/integration.ts @@ -0,0 +1,14 @@ +/* eslint-disable @sentry-internal/sdk/no-class-field-initializers */ + +import * as Sentry from '@sentry/browser'; +import type { IntegrationFn } from '@sentry/types'; +import type { FeatureFlagOptions } from '../types'; + +/** + * Sentry integration for reading and modifying internal feature flag state. + */ +export const featureFlagIntegration = ((_options: FeatureFlagOptions) => { + return { + name: 'launchdarkly', + }; +}) satisfies IntegrationFn; diff --git a/packages/features/src/index.ts b/packages/features/src/index.ts new file mode 100644 index 000000000000..4c4f15d2ee24 --- /dev/null +++ b/packages/features/src/index.ts @@ -0,0 +1,4 @@ +// This file is used as entry point to generate the npm package and CDN bundles. + +export type { FeatureFlagOptions } from './types'; +export { featureFlagIntegration } from './core/integration'; diff --git a/packages/features/src/types.ts b/packages/features/src/types.ts new file mode 100644 index 000000000000..254501d1c967 --- /dev/null +++ b/packages/features/src/types.ts @@ -0,0 +1 @@ +export type FeatureFlagOptions = Record; diff --git a/packages/features/test.setup.ts b/packages/features/test.setup.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/packages/features/tsconfig.json b/packages/features/tsconfig.json new file mode 100644 index 000000000000..cd1b8207ea06 --- /dev/null +++ b/packages/features/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "lib": ["DOM", "ES2018"], + "module": "esnext" + }, + "include": ["src/**/*.ts"] +} diff --git a/packages/features/tsconfig.test.json b/packages/features/tsconfig.test.json new file mode 100644 index 000000000000..bb7130d948c0 --- /dev/null +++ b/packages/features/tsconfig.test.json @@ -0,0 +1,15 @@ +{ + "extends": "./tsconfig.json", + + "include": ["test/**/*.ts", "vitest.config.ts", "test.setup.ts"], + + "compilerOptions": { + "types": ["node"], + "esModuleInterop": true, + "allowJs": true, + "noImplicitAny": true, + "noImplicitThis": false, + "strictNullChecks": true, + "strictPropertyInitialization": false + } +} diff --git a/packages/features/tsconfig.types.json b/packages/features/tsconfig.types.json new file mode 100644 index 000000000000..374fd9bc9364 --- /dev/null +++ b/packages/features/tsconfig.types.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + + "compilerOptions": { + "declaration": true, + "declarationMap": true, + "emitDeclarationOnly": true, + "outDir": "build/npm/types" + } +} diff --git a/packages/features/vitest.config.ts b/packages/features/vitest.config.ts new file mode 100644 index 000000000000..976d9c37074d --- /dev/null +++ b/packages/features/vitest.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from 'vitest/config'; + +import baseConfig from '../../vite/vite.config'; + +export default defineConfig({ + ...baseConfig, + test: { + ...baseConfig.test, + setupFiles: ['./test.setup.ts'], + reporters: ['default'], + }, +}); diff --git a/packages/launchdarkly/package.json b/packages/launchdarkly/package.json index 8cd3530d3f1e..e235a507b33f 100644 --- a/packages/launchdarkly/package.json +++ b/packages/launchdarkly/package.json @@ -1,7 +1,7 @@ { - "name": "@sentry-internal/launchdarkly", + "name": "@sentry/launchdarkly", "version": "8.35.0", - "description": "Sentry SDK integration for user launchdarkly", + "description": "Sentry SDK integration for LaunchDarkly feature flagging", "repository": "git://github.com/getsentry/sentry-javascript.git", "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/launchdarkly", "author": "Sentry", @@ -60,7 +60,7 @@ "build:types:watch": "tsc -p tsconfig.types.json --watch", "build:tarball": "npm pack", "circularDepCheck": "madge --circular src/index.ts", - "clean": "rimraf build sentry-internal-launchdarkly-*.tgz", + "clean": "rimraf build sentry-launchdarkly-*.tgz", "fix": "eslint . --format stylish --fix", "lint": "eslint . --format stylish", "test": "jest", diff --git a/packages/launchdarkly/rollup.bundle.config.mjs b/packages/launchdarkly/rollup.bundle.config.mjs index cc29fadbc093..f79e2cd63d7e 100644 --- a/packages/launchdarkly/rollup.bundle.config.mjs +++ b/packages/launchdarkly/rollup.bundle.config.mjs @@ -3,7 +3,7 @@ import { makeBaseBundleConfig, makeBundleConfigVariants } from '@sentry-internal const baseBundleConfig = makeBaseBundleConfig({ bundleType: 'addon', entrypoints: ['src/index.ts'], - licenseTitle: '@sentry-internal/launchdarkly', + licenseTitle: '@sentry/launchdarkly', outputFileBase: () => 'bundles/launchdarkly', }); diff --git a/packages/launchdarkly/src/core/integration.ts b/packages/launchdarkly/src/core/integration.ts index 90c9a9194b34..0f1ee88a2945 100644 --- a/packages/launchdarkly/src/core/integration.ts +++ b/packages/launchdarkly/src/core/integration.ts @@ -3,7 +3,6 @@ import * as Sentry from '@sentry/browser'; import type { Client as SentryClient, Event, EventHint, IntegrationFn } from '@sentry/types'; import type { LDContext, LDEvaluationDetail, LDInspectionFlagUsedHandler } from 'launchdarkly-js-client-sdk'; -import type { LaunchDarklyOptions } from './types'; /** * Sentry integration for capturing feature flags from LaunchDarkly. diff --git a/packages/launchdarkly/src/index.ts b/packages/launchdarkly/src/index.ts index 8952a4bc1264..d741c00e1ce1 100644 --- a/packages/launchdarkly/src/index.ts +++ b/packages/launchdarkly/src/index.ts @@ -1,6 +1,3 @@ // This file is used as entry point to generate the npm package and CDN bundles. - export { launchDarklyIntegration } from './core/integration'; - -// export type { -// } from './types'; +export type { LaunchDarklyOptions } from './types'; diff --git a/packages/launchdarkly/src/types.ts b/packages/launchdarkly/src/types.ts new file mode 100644 index 000000000000..efd77687ea65 --- /dev/null +++ b/packages/launchdarkly/src/types.ts @@ -0,0 +1 @@ +export type LaunchDarklyOptions = Record; diff --git a/packages/launchdarkly/src/types/integration.ts b/packages/launchdarkly/src/types/integration.ts deleted file mode 100644 index 6516bc7a8ff7..000000000000 --- a/packages/launchdarkly/src/types/integration.ts +++ /dev/null @@ -1 +0,0 @@ -export type LaunchDarklyOptions = Record; //TODO: diff --git a/packages/launchdarkly/test.setup.ts b/packages/launchdarkly/test.setup.ts index 05a762e60d50..e69de29bb2d1 100644 --- a/packages/launchdarkly/test.setup.ts +++ b/packages/launchdarkly/test.setup.ts @@ -1,255 +0,0 @@ -import { printDiffOrStringify } from 'jest-matcher-utils'; -import { vi } from 'vitest'; -import type { Mocked, MockedFunction } from 'vitest'; - -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -import { getClient } from '@sentry/core'; -import type { ReplayRecordingData, Transport } from '@sentry/types'; -import * as SentryUtils from '@sentry/utils'; - -import type { ReplayContainer, Session } from './src/types'; - -type MockTransport = MockedFunction; - -vi.spyOn(SentryUtils, 'isBrowser').mockImplementation(() => true); - -type EnvelopeHeader = { - event_id: string; - sent_at: string; - sdk: { - name: string; - version?: string; - }; -}; - -type ReplayEventHeader = { type: 'replay_event' }; -type ReplayEventPayload = Record; -type RecordingHeader = { type: 'replay_recording'; length: number }; -type RecordingPayloadHeader = Record; -type SentReplayExpected = { - envelopeHeader?: EnvelopeHeader; - replayEventHeader?: ReplayEventHeader; - replayEventPayload?: ReplayEventPayload; - recordingHeader?: RecordingHeader; - recordingPayloadHeader?: RecordingPayloadHeader; - recordingData?: ReplayRecordingData; -}; - -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -const toHaveSameSession = function (received: Mocked, expected: undefined | Session) { - const pass = this.equals(received.session?.id, expected?.id) as boolean; - - const options = { - isNot: this.isNot, - promise: this.promise, - }; - - return { - pass, - message: () => - `${this.utils.matcherHint('toHaveSameSession', undefined, undefined, options)}\n\n${printDiffOrStringify( - expected, - received.session, - 'Expected', - 'Received', - )}`, - }; -}; - -type Result = { - passed: boolean; - key: string; - expectedVal: SentReplayExpected[keyof SentReplayExpected]; - actualVal: SentReplayExpected[keyof SentReplayExpected]; -}; -type Call = [ - EnvelopeHeader, - [ - [ReplayEventHeader | undefined, ReplayEventPayload | undefined], - [RecordingHeader | undefined, RecordingPayloadHeader | undefined], - ], -]; -type CheckCallForSentReplayResult = { pass: boolean; call: Call | undefined; results: Result[] }; - -function checkCallForSentReplay( - call: Call | undefined, - expected?: SentReplayExpected | { sample: SentReplayExpected; inverse: boolean }, -): CheckCallForSentReplayResult { - const envelopeHeader = call?.[0]; - const envelopeItems = call?.[1] || [[], []]; - const [[replayEventHeader, replayEventPayload], [recordingHeader, recordingPayload] = []] = envelopeItems; - - // @ts-expect-error recordingPayload is always a string in our tests - const [recordingPayloadHeader, recordingData] = recordingPayload?.split('\n') || []; - - const actualObj: Required = { - // @ts-expect-error Custom envelope - envelopeHeader: envelopeHeader, - // @ts-expect-error Custom envelope - replayEventHeader: replayEventHeader, - // @ts-expect-error Custom envelope - replayEventPayload: replayEventPayload, - // @ts-expect-error Custom envelope - recordingHeader: recordingHeader, - recordingPayloadHeader: recordingPayloadHeader && JSON.parse(recordingPayloadHeader), - recordingData, - }; - - const isObjectContaining = expected && 'sample' in expected && 'inverse' in expected; - const expectedObj = isObjectContaining - ? (expected as { sample: SentReplayExpected }).sample - : (expected as SentReplayExpected); - - if (isObjectContaining) { - // eslint-disable-next-line no-console - console.warn('`expect.objectContaining` is unnecessary when using the `toHaveSentReplay` matcher'); - } - - const results = expected - ? Object.keys(expectedObj) - .map(key => { - const actualVal = actualObj[key as keyof SentReplayExpected]; - const expectedVal = expectedObj[key as keyof SentReplayExpected]; - const passed = !expectedVal || this.equals(actualVal, expectedVal); - - return { passed, key, expectedVal, actualVal }; - }) - .filter(({ passed }) => !passed) - : []; - - const pass = Boolean(call && (!expected || results.length === 0)); - - return { - pass, - call, - results, - }; -} - -/** - * Only want calls that send replay events, i.e. ignore error events - */ -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function getReplayCalls(calls: any[][][]): any[][][] { - return calls - .map(call => { - const arg = call[0]; - if (arg.length !== 2) { - return []; - } - - if (!arg[1][0].find(({ type }: { type: string }) => ['replay_event', 'replay_recording'].includes(type))) { - return []; - } - - return [arg]; - }) - .filter(Boolean); -} - -/** - * Checks all calls to `fetch` and ensures a replay was uploaded by - * checking the `fetch()` request's body. - */ -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -const toHaveSentReplay = function ( - _received: Mocked, - expected?: SentReplayExpected | { sample: SentReplayExpected; inverse: boolean }, -) { - const { calls } = (getClient()?.getTransport()?.send as MockTransport).mock; - - let result: CheckCallForSentReplayResult; - - const expectedKeysLength = expected - ? ('sample' in expected ? Object.keys(expected.sample) : Object.keys(expected)).length - : 0; - - const replayCalls = getReplayCalls(calls); - - for (const currentCall of replayCalls) { - result = checkCallForSentReplay.call(this, currentCall[0], expected); - if (result.pass) { - break; - } - - // stop on the first call where any of the expected obj passes - if (result.results.length < expectedKeysLength) { - break; - } - } - - // @ts-expect-error use before assigned - const { results, call, pass } = result; - - const options = { - isNot: this.isNot, - promise: this.promise, - }; - - return { - pass, - message: () => - !call - ? pass - ? 'Expected Replay to not have been sent, but a request was attempted' - : 'Expected Replay to have been sent, but a request was not attempted' - : `${this.utils.matcherHint('toHaveSentReplay', undefined, undefined, options)}\n\n${results - .map(({ key, expectedVal, actualVal }: Result) => - printDiffOrStringify(expectedVal, actualVal, `Expected (key: ${key})`, `Received (key: ${key})`), - ) - .join('\n')}`, - }; -}; - -/** - * Checks the last call to `fetch` and ensures a replay was uploaded by - * checking the `fetch()` request's body. - */ -// eslint-disable-next-line @typescript-eslint/explicit-function-return-type -const toHaveLastSentReplay = function ( - _received: Mocked, - expected?: SentReplayExpected | { sample: SentReplayExpected; inverse: boolean }, -) { - const { calls } = (getClient()?.getTransport()?.send as MockTransport).mock; - const replayCalls = getReplayCalls(calls); - - const lastCall = replayCalls[calls.length - 1]?.[0]; - - const { results, call, pass } = checkCallForSentReplay.call(this, lastCall, expected); - - const options = { - isNot: this.isNot, - promise: this.promise, - }; - - return { - pass, - message: () => - !call - ? pass - ? 'Expected Replay to not have been sent, but a request was attempted' - : 'Expected Replay to have last been sent, but a request was not attempted' - : `${this.utils.matcherHint('toHaveSentReplay', undefined, undefined, options)}\n\n${results - .map(({ key, expectedVal, actualVal }: Result) => - printDiffOrStringify(expectedVal, actualVal, `Expected (key: ${key})`, `Received (key: ${key})`), - ) - .join('\n')}`, - }; -}; - -expect.extend({ - toHaveSameSession, - toHaveSentReplay, - toHaveLastSentReplay, -}); - -interface CustomMatchers { - toHaveSentReplay(expected?: SentReplayExpected): R; - toHaveLastSentReplay(expected?: SentReplayExpected): R; - toHaveSameSession(expected: undefined | Session): R; -} - -declare module 'vitest' { - type Assertion = CustomMatchers; - type AsymmetricMatchersContaining = CustomMatchers; -} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 0e173b0986f5..bf187a15edb5 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -174,4 +174,3 @@ export type { export type { ParameterizedString } from './parameterize'; export type { ContinuousProfiler, ProfilingIntegration, Profiler } from './profiling'; export type { ViewHierarchyData, ViewHierarchyWindow } from './view-hierarchy'; -export type { FeatureFlag } from './flags';