Repository: getsentry/sentry-javascript (2025-07-18T10:07:52Z)

Issue:
Add `ignoreSpans` option
### Description

We have the recurring request to be able to ignore certain spans. We removed the option to use `beforeSendSpan` to allow dropping spans, because it can break metric extrapolation when using non-deterministic code to drop spans.

The replacement for this has been to configure specific integrations to drop spans. While this works, it has a few problems:

1. It can be hard to know where/how to configure integrations. Option names can differ, and you may have to configure a whole bunch of integrations to make this work as you want.
2. Some integrations do not have options to drop stuff. While we can add things in general, esp. with OpenTelemetry it can be hard because we require upstream changes.

In order to help alleviate this, we should add a `ignoreSpans` option that can be used to filter spans at creation time:

```ts
type IgnoreSpansFilter = 
{
  name: string | RegExp,
  op: string | RegExp,
} | 
{
  name: string | RegExp
} | 
{
  op: string | RegExp
}

interface Options {
  // ...
  ignoreSpans: (string | RegExp | IgnoreSpansFilter)[]
}
```

This will match against the span name **at creation time**, and make the span non-recording if it matches.

Important notes:
* On browser, this should work reasonably well, as we emit all the spans and they _usually_ have the correct span name at span creation time. Important exception: pageload spans often get updated, they may not have the correct span name at creation time.
* On node, we need to infer the name at creation time, which is a bit more expensive, resource wise. This is an opt-in option though, we just need to make it clear there may be a performance overhead with this. Also, in node span names can often be updated later, this also needs to be documented well.
* We need to document well that ignoring a span like this will also drop child spans of this span. So this should ideally mostly be used for leaf nodes.

This function will be run in the place where we also run `beforeSendSpan`, so after processing. If possible, we will re-stitch parent-child relationships of spans that are dropped there.


Discussion:
I think the span name alone might be a bit too generic in some cases. For example, let's say I want to filter out all `http.client` `GET` spans and I define a regex like this: `ignoreSpans: [/GET .*/]`. Now, if I throw this into the server-side config, it will also cause all `http.server` spans to be ignored because they also happen to start with `GET`. Meaning, we wouldn't emit any span server-side anymore. This might cause unintentional behaviour. 

Sure, we can say that this is on users, to figure out a regex that's smart enough but I'd argue for this specific case, it's actually quite hard to do so. Even if I further refine the regex to `ignoreSpans: [/GET api\/v0\/.*/]`, I would still likely ignore the `http.server` counterparts unintentionally. 

I agree though that we probably want an API like this in some way. One idea I'd have is to also include `op` in the filter, and for ease of use make it an optional property:

```ts
type IgnoreSpansFilter = 
{
  name: string | RegExp,
  op: string | RegExp,
} | 
{
  name: string | RegExp
} | 
{
  op: string | RegExp
}

{
  ignoreSpans: (string | RegExp | IgnoreSpansFilter)[]
}
```

A type like this would also allow for ignoring 

- all spans matching a specific name (generic, as before)
- all spans matching a specific op (generic, new)
- all spans matching a specific name and op combination (more specific)

This won't solve the too generic filterinrg by only specifying a `string|RegExp`, but by setting the name/op combination, users have a way to narrow things down significantly. 

AFAIK, we infer name and op at the same time in Node, so the same resource and performance concerns would exist as with the intially proposed API. WDYT?
I second your type proposal as it would be a bit too generic to just add a string/regex. However, in terms of simplicity, I would change the type to this:

```ts
{
  ignoreSpans: IgnoreSpansFilter[]
}
```

So a single string/regex is not allowed, and it gets more obvious if you are filtering by op or name.
makes sense to me! Your example is very reasonable, and op should be OK to check, overhead wise 👍 
ref: https://develop.sentry.dev/sdk/telemetry/spans/filtering/#filter-with-ignorespans --> we should also update if we adjust things here, possibly.
desperately need this!
@arielweinberger expect this to be released in the next days. I just merged the PR. (A GH bot will comment under this issue once it is released).
## A PR closing this issue has just been released 🚀

This issue was referenced by PR #17078, which was included in the [10.2.0 release](https://github.com/getsentry/sentry-javascript/releases/tag/10.2.0).



Patch:
diff --git a/.size-limit.js b/.size-limit.js
index a46d905fe89f..d53eaae56712 100644
--- a/.size-limit.js
+++ b/.size-limit.js
@@ -233,7 +233,7 @@ module.exports = [
     import: createImport('init'),
     ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
     gzip: true,
-    limit: '146 KB',
+    limit: '147 KB',
   },
   {
     name: '@sentry/node - without tracing',
diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts
index 8b03392106d9..3f1105de5c15 100644
--- a/packages/core/src/client.ts
+++ b/packages/core/src/client.ts
@@ -42,6 +42,7 @@ import { merge } from './utils/merge';
 import { checkOrSetAlreadyCaught, uuid4 } from './utils/misc';
 import { parseSampleRate } from './utils/parseSampleRate';
 import { prepareEvent } from './utils/prepareEvent';
+import { reparentChildSpans, shouldIgnoreSpan } from './utils/should-ignore-span';
 import { getActiveSpan, showSpanDropWarning, spanToTraceContext } from './utils/spanUtils';
 import { rejectedSyncPromise, resolvedSyncPromise, SyncPromise } from './utils/syncpromise';
 import { convertSpanJsonToTransactionEvent, convertTransactionEventToSpanJson } from './utils/transactionEvent';
@@ -1281,7 +1282,7 @@ function processBeforeSend(
   event: Event,
   hint: EventHint,
 ): PromiseLike<Event | null> | Event | null {
-  const { beforeSend, beforeSendTransaction, beforeSendSpan } = options;
+  const { beforeSend, beforeSendTransaction, beforeSendSpan, ignoreSpans } = options;
   let processedEvent = event;
 
   if (isErrorEvent(processedEvent) && beforeSend) {
@@ -1289,28 +1290,59 @@ function processBeforeSend(
   }
 
   if (isTransactionEvent(processedEvent)) {
-    if (beforeSendSpan) {
-      // process root span
-      const processedRootSpanJson = beforeSendSpan(convertTransactionEventToSpanJson(processedEvent));
-      if (!processedRootSpanJson) {
-        showSpanDropWarning();
-      } else {
-        // update event with processed root span values
-        processedEvent = merge(event, convertSpanJsonToTransactionEvent(processedRootSpanJson));
+    // Avoid processing if we don't have to
+    if (beforeSendSpan || ignoreSpans) {
+      // 1. Process root span
+      const rootSpanJson = convertTransactionEventToSpanJson(processedEvent);
+
+      // 1.1 If the root span should be ignored, drop the whole transaction
+      if (ignoreSpans?.length && shouldIgnoreSpan(rootSpanJson, ignoreSpans)) {
+        // dropping the whole transaction!
+        return null;
       }
 
-      // process child spans
+      // 1.2 If a `beforeSendSpan` callback is defined, process the root span
+      if (beforeSendSpan) {
+        const processedRootSpanJson = beforeSendSpan(rootSpanJson);
+        if (!processedRootSpanJson) {
+          showSpanDropWarning();
+        } else {
+          // update event with processed root span values
+          processedEvent = merge(event, convertSpanJsonToTransactionEvent(processedRootSpanJson));
+        }
+      }
+
+      // 2. Process child spans
       if (processedEvent.spans) {
         const processedSpans: SpanJSON[] = [];
-        for (const span of processedEvent.spans) {
-          const processedSpan = beforeSendSpan(span);
-          if (!processedSpan) {
-            showSpanDropWarning();
-            processedSpans.push(span);
+
+        const initialSpans = processedEvent.spans;
+
+        for (const span of initialSpans) {
+          // 2.a If the child span should be ignored, reparent it to the root span
+          if (ignoreSpans?.length && shouldIgnoreSpan(span, ignoreSpans)) {
+            reparentChildSpans(initialSpans, span);
+            continue;
+          }
+
+          // 2.b If a `beforeSendSpan` callback is defined, process the child span
+          if (beforeSendSpan) {
+            const processedSpan = beforeSendSpan(span);
+            if (!processedSpan) {
+              showSpanDropWarning();
+              processedSpans.push(span);
+            } else {
+              processedSpans.push(processedSpan);
+            }
           } else {
-            processedSpans.push(processedSpan);
+            processedSpans.push(span);
           }
         }
+
+        const droppedSpans = processedEvent.spans.length - processedSpans.length;
+        if (droppedSpans) {
+          client.recordDroppedEvent('before_send', 'span', droppedSpans);
+        }
         processedEvent.spans = processedSpans;
       }
     }
diff --git a/packages/core/src/envelope.ts b/packages/core/src/envelope.ts
index dc094d218812..6ae8fa718638 100644
--- a/packages/core/src/envelope.ts
+++ b/packages/core/src/envelope.ts
@@ -26,6 +26,7 @@ import {
   getSdkMetadataForEnvelopeHeader,
 } from './utils/envelope';
 import { uuid4 } from './utils/misc';
+import { shouldIgnoreSpan } from './utils/should-ignore-span';
 import { showSpanDropWarning, spanToJSON } from './utils/spanUtils';
 
 /**
@@ -122,7 +123,17 @@ export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client?
     ...(!!tunnel && dsn && { dsn: dsnToString(dsn) }),
   };
 
-  const beforeSendSpan = client?.getOptions().beforeSendSpan;
+  const { beforeSendSpan, ignoreSpans } = client?.getOptions() || {};
+
+  const filteredSpans = ignoreSpans?.length
+    ? spans.filter(span => !shouldIgnoreSpan(spanToJSON(span), ignoreSpans))
+    : spans;
+  const droppedSpans = spans.length - filteredSpans.length;
+
+  if (droppedSpans) {
+    client?.recordDroppedEvent('before_send', 'span', droppedSpans);
+  }
+
   const convertToSpanJSON = beforeSendSpan
     ? (span: SentrySpan) => {
         const spanJson = spanToJSON(span);
@@ -138,7 +149,7 @@ export function createSpanEnvelope(spans: [SentrySpan, ...SentrySpan[]], client?
     : spanToJSON;
 
   const items: SpanItem[] = [];
-  for (const span of spans) {
+  for (const span of filteredSpans) {
     const spanJson = convertToSpanJSON(span);
     if (spanJson) {
       items.push(createSpanEnvelopeItem(spanJson));
diff --git a/packages/core/src/types-hoist/options.ts b/packages/core/src/types-hoist/options.ts
index ce0851f940a5..142313b76c25 100644
--- a/packages/core/src/types-hoist/options.ts
+++ b/packages/core/src/types-hoist/options.ts
@@ -10,6 +10,32 @@ import type { StackLineParser, StackParser } from './stacktrace';
 import type { TracePropagationTargets } from './tracing';
 import type { BaseTransportOptions, Transport } from './transport';
 
+/**
+ * A filter object for ignoring spans.
+ * At least one of the properties (`op` or `name`) must be set.
+ */
+type IgnoreSpanFilter =
+  | {
+      /**
+       * Spans with a name matching this pattern will be ignored.
+       */
+      name: string | RegExp;
+      /**
+       * Spans with an op matching this pattern will be ignored.
+       */
+      op?: string | RegExp;
+    }
+  | {
+      /**
+       * Spans with a name matching this pattern will be ignored.
+       */
+      name?: string | RegExp;
+      /**
+       * Spans with an op matching this pattern will be ignored.
+       */
+      op: string | RegExp;
+    };
+
 export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOptions> {
   /**
    * Enable debug functionality in the SDK itself. If `debug` is set to `true` the SDK will attempt
@@ -208,6 +234,16 @@ export interface ClientOptions<TO extends BaseTransportOptions = BaseTransportOp
    */
   ignoreTransactions?: Array<string | RegExp>;
 
+  /**
+   * A list of span names or patterns to ignore.
+   *
+   * If you specify a pattern {@link IgnoreSpanFilter}, at least one
+   * of the properties (`op` or `name`) must be set.
+   *
+   * @default []
+   */
+  ignoreSpans?: (string | RegExp | IgnoreSpanFilter)[];
+
   /**
    * A URL to an envelope tunnel endpoint. An envelope tunnel is an HTTP endpoint
    * that accepts Sentry envelopes for forwarding. This can be used to force data
diff --git a/packages/core/src/utils/should-ignore-span.ts b/packages/core/src/utils/should-ignore-span.ts
new file mode 100644
index 000000000000..53aa109a18dc
--- /dev/null
+++ b/packages/core/src/utils/should-ignore-span.ts
@@ -0,0 +1,66 @@
+import type { ClientOptions } from '../types-hoist/options';
+import type { SpanJSON } from '../types-hoist/span';
+import { isMatchingPattern } from './string';
+
+/**
+ * Check if a span should be ignored based on the ignoreSpans configuration.
+ */
+export function shouldIgnoreSpan(
+  span: Pick<SpanJSON, 'description' | 'op'>,
+  ignoreSpans: Required<ClientOptions>['ignoreSpans'],
+): boolean {
+  if (!ignoreSpans?.length || !span.description) {
+    return false;
+  }
+
+  for (const pattern of ignoreSpans) {
+    if (isStringOrRegExp(pattern)) {
+      if (isMatchingPattern(span.description, pattern)) {
+        return true;
+      }
+      continue;
+    }
+
+    if (!pattern.name && !pattern.op) {
+      continue;
+    }
+
+    const nameMatches = pattern.name ? isMatchingPattern(span.description, pattern.name) : true;
+    const opMatches = pattern.op ? span.op && isMatchingPattern(span.op, pattern.op) : true;
+
+    // This check here is only correct because we can guarantee that we ran `isMatchingPattern`
+    // for at least one of `nameMatches` and `opMatches`. So in contrary to how this looks,
+    // not both op and name actually have to match. This is the most efficient way to check
+    // for all combinations of name and op patterns.
+    if (nameMatches && opMatches) {
+      return true;
+    }
+  }
+
+  return false;
+}
+
+/**
+ * Takes a list of spans, and a span that was dropped, and re-parents the child spans of the dropped span to the parent of the dropped span, if possible.
+ * This mutates the spans array in place!
+ */
+export function reparentChildSpans(spans: SpanJSON[], dropSpan: SpanJSON): void {
+  const droppedSpanParentId = dropSpan.parent_span_id;
+  const droppedSpanId = dropSpan.span_id;
+
+  // This should generally not happen, as we do not apply this on root spans
+  // but to be safe, we just bail in this case
+  if (!droppedSpanParentId) {
+    return;
+  }
+
+  for (const span of spans) {
+    if (span.parent_span_id === droppedSpanId) {
+      span.parent_span_id = droppedSpanParentId;
+    }
+  }
+}
+
+function isStringOrRegExp(value: unknown): value is string | RegExp {
+  return typeof value === 'string' || value instanceof RegExp;
+}


Test patch:
diff --git a/dev-packages/e2e-tests/test-applications/webpack-4/build.mjs b/dev-packages/e2e-tests/test-applications/webpack-4/build.mjs
index 0818243ad9ee..32d475aac75c 100644
--- a/dev-packages/e2e-tests/test-applications/webpack-4/build.mjs
+++ b/dev-packages/e2e-tests/test-applications/webpack-4/build.mjs
@@ -17,6 +17,11 @@ webpack(
       minimize: true,
       minimizer: [new TerserPlugin()],
     },
+    performance: {
+      hints: false,
+      maxEntrypointSize: 512000,
+      maxAssetSize: 512000
+  },
     plugins: [new HtmlWebpackPlugin(), new webpack.EnvironmentPlugin(['E2E_TEST_DSN'])],
     mode: 'production',
     // webpack 4 does not support ES2020 features out of the box, so we need to transpile them
diff --git a/packages/core/test/lib/client.test.ts b/packages/core/test/lib/client.test.ts
index 4b1c7a378114..3cae5ed65787 100644
--- a/packages/core/test/lib/client.test.ts
+++ b/packages/core/test/lib/client.test.ts
@@ -1046,6 +1046,178 @@ describe('Client', () => {
       expect(capturedEvent.transaction).toEqual(transaction.transaction);
     });
 
+    test('uses `ignoreSpans` to drop root spans', () => {
+      const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, ignoreSpans: ['root span'] });
+      const client = new TestClient(options);
+
+      const captureExceptionSpy = vi.spyOn(client, 'captureException');
+      const loggerLogSpy = vi.spyOn(debugLoggerModule.debug, 'log');
+
+      const transaction: Event = {
+        transaction: 'root span',
+        type: 'transaction',
+        spans: [
+          {
+            description: 'first span',
+            span_id: '9e15bf99fbe4bc80',
+            start_timestamp: 1591603196.637835,
+            trace_id: '86f39e84263a4de99c326acab3bfe3bd',
+            data: {},
+          },
+          {
+            description: 'second span',
+            span_id: 'aa554c1f506b0783',
+            start_timestamp: 1591603196.637835,
+            trace_id: '86f39e84263a4de99c326acab3bfe3bd',
+            data: {},
+          },
+        ],
+      };
+      client.captureEvent(transaction);
+
+      expect(TestClient.instance!.event).toBeUndefined();
+      // This proves that the reason the event didn't send/didn't get set on the test client is not because there was an
+      // error, but because the event processor returned `null`
+      expect(captureExceptionSpy).not.toBeCalled();
+      expect(loggerLogSpy).toBeCalledWith('before send for type `transaction` returned `null`, will not send event.');
+    });
+
+    test('uses `ignoreSpans` to drop child spans', () => {
+      const options = getDefaultTestClientOptions({ dsn: PUBLIC_DSN, ignoreSpans: ['first span'] });
+      const client = new TestClient(options);
+      const recordDroppedEventSpy = vi.spyOn(client, 'recordDroppedEvent');
+
+      const transaction: Event = {
+        contexts: {
+          trace: {
+            span_id: 'root-span-id',
+            trace_id: '86f39e84263a4de99c326acab3bfe3bd',
+          },
+        },
+        transaction: 'root span',
+        type: 'transaction',
+        spans: [
+          {
+            description: 'first span',
+            span_id: '9e15bf99fbe4bc80',
+            parent_span_id: 'root-span-id',
+            start_timestamp: 1591603196.637835,
+            trace_id: '86f39e84263a4de99c326acab3bfe3bd',
+            data: {},
+          },
+          {
+            description: 'second span',
+            span_id: 'aa554c1f506b0783',
+            parent_span_id: 'root-span-id',
+            start_timestamp: 1591603196.637835,
+            trace_id: '86f39e84263a4de99c326acab3bfe3bd',
+            data: {},
+          },
+          {
+            description: 'third span',
+            span_id: 'aa554c1f506b0784',
+            parent_span_id: '9e15bf99fbe4bc80',
+            start_timestamp: 1591603196.637835,
+            trace_id: '86f39e84263a4de99c326acab3bfe3bd',
+            data: {},
+          },
+        ],
+      };
+      client.captureEvent(transaction);
+
+      const capturedEvent = TestClient.instance!.event!;
+      expect(capturedEvent.spans).toEqual([
+        {
+          description: 'second span',
+          span_id: 'aa554c1f506b0783',
+          parent_span_id: 'root-span-id',
+          start_timestamp: 1591603196.637835,
+          trace_id: '86f39e84263a4de99c326acab3bfe3bd',
+          data: {},
+        },
+        {
+          description: 'third span',
+          span_id: 'aa554c1f506b0784',
+          parent_span_id: 'root-span-id',
+          start_timestamp: 1591603196.637835,
+          trace_id: '86f39e84263a4de99c326acab3bfe3bd',
+          data: {},
+        },
+      ]);
+      expect(recordDroppedEventSpy).toBeCalledWith('before_send', 'span', 1);
+    });
+
+    test('uses complex `ignoreSpans` to drop child spans', () => {
+      const options = getDefaultTestClientOptions({
+        dsn: PUBLIC_DSN,
+        ignoreSpans: [
+          {
+            name: 'first span',
+          },
+          {
+            name: 'span',
+            op: 'op1',
+          },
+        ],
+      });
+      const client = new TestClient(options);
+      const recordDroppedEventSpy = vi.spyOn(client, 'recordDroppedEvent');
+
+      const transaction: Event = {
+        contexts: {
+          trace: {
+            span_id: 'root-span-id',
+            trace_id: '86f39e84263a4de99c326acab3bfe3bd',
+          },
+        },
+        transaction: 'root span',
+        type: 'transaction',
+        spans: [
+          {
+            description: 'first span',
+            span_id: '9e15bf99fbe4bc80',
+            parent_span_id: 'root-span-id',
+            start_timestamp: 1591603196.637835,
+            trace_id: '86f39e84263a4de99c326acab3bfe3bd',
+            data: {},
+          },
+          {
+            description: 'second span',
+            op: 'op1',
+            span_id: 'aa554c1f506b0783',
+            parent_span_id: 'root-span-id',
+            start_timestamp: 1591603196.637835,
+            trace_id: '86f39e84263a4de99c326acab3bfe3bd',
+            data: {},
+          },
+          {
+            description: 'third span',
+            op: 'other op',
+            span_id: 'aa554c1f506b0784',
+            parent_span_id: '9e15bf99fbe4bc80',
+            start_timestamp: 1591603196.637835,
+            trace_id: '86f39e84263a4de99c326acab3bfe3bd',
+            data: {},
+          },
+        ],
+      };
+      client.captureEvent(transaction);
+
+      const capturedEvent = TestClient.instance!.event!;
+      expect(capturedEvent.spans).toEqual([
+        {
+          description: 'third span',
+          op: 'other op',
+          span_id: 'aa554c1f506b0784',
+          parent_span_id: 'root-span-id',
+          start_timestamp: 1591603196.637835,
+          trace_id: '86f39e84263a4de99c326acab3bfe3bd',
+          data: {},
+        },
+      ]);
+      expect(recordDroppedEventSpy).toBeCalledWith('before_send', 'span', 2);
+    });
+
     test('does not modify existing contexts for root span in `beforeSendSpan`', () => {
       const beforeSendSpan = vi.fn((span: SpanJSON) => {
         return {
diff --git a/packages/core/test/lib/utils/should-ignore-span.test.ts b/packages/core/test/lib/utils/should-ignore-span.test.ts
new file mode 100644
index 000000000000..92dc0a1435ee
--- /dev/null
+++ b/packages/core/test/lib/utils/should-ignore-span.test.ts
@@ -0,0 +1,124 @@
+import { describe, expect, it } from 'vitest';
+import type { ClientOptions, SpanJSON } from '../../../src';
+import { reparentChildSpans, shouldIgnoreSpan } from '../../../src/utils/should-ignore-span';
+
+describe('shouldIgnoreSpan', () => {
+  it('should not ignore spans with empty ignoreSpans', () => {
+    const span = { description: 'test', op: 'test' };
+    const ignoreSpans = [] as Required<ClientOptions>['ignoreSpans'];
+    expect(shouldIgnoreSpan(span, ignoreSpans)).toBe(false);
+  });
+
+  describe('string patterns', () => {
+    it.each([
+      ['test', 'test', true],
+      ['test', 'test2', false],
+      ['test2', 'test', true],
+    ])('should ignore spans with description %s & ignoreSpans=%s', (description, ignoreSpansPattern, expected) => {
+      const span = { description, op: 'default' };
+      const ignoreSpans = [ignoreSpansPattern];
+      expect(shouldIgnoreSpan(span, ignoreSpans)).toBe(expected);
+    });
+  });
+
+  describe('regex patterns', () => {
+    it.each([
+      ['test', /test/, true],
+      ['test', /test2/, false],
+      ['test2', /test/, true],
+    ])('should ignore spans with description %s & ignoreSpans=%s', (description, ignoreSpansPattern, expected) => {
+      const span = { description, op: 'default' };
+      const ignoreSpans = [ignoreSpansPattern];
+      expect(shouldIgnoreSpan(span, ignoreSpans)).toBe(expected);
+    });
+  });
+
+  describe('complex patterns', () => {
+    it.each([
+      [{ name: 'test' }, true],
+      [{ name: 'test2' }, false],
+      [{ op: 'test' }, true],
+      [{ op: 'test2' }, false],
+      [{ name: 'test', op: 'test' }, true],
+      [{ name: 'test2', op: 'test' }, false],
+      [{ name: 'test', op: 'test2' }, false],
+      [{ name: 'test2', op: 'test2' }, false],
+      [{ name: 'test', op: 'test2' }, false],
+    ])('should ignore spans with description %s & ignoreSpans=%s', (ignoreSpansPattern, expected) => {
+      const span = { description: 'test span name', op: 'test span op' };
+      const ignoreSpans = [ignoreSpansPattern];
+      expect(shouldIgnoreSpan(span, ignoreSpans)).toBe(expected);
+    });
+  });
+
+  it('works with multiple patterns', () => {
+    const ignoreSpans = ['test', /test2/, { op: 'test2' }];
+
+    // All of these are ignored because the name matches
+    const span1 = { description: 'test span name', op: 'test span op' };
+    const span2 = { description: 'test span name2', op: 'test span op2' };
+    const span3 = { description: 'test span name3', op: 'test span op3' };
+    const span4 = { description: 'test span name4', op: 'test span op4' };
+
+    expect(shouldIgnoreSpan(span1, ignoreSpans)).toBe(true);
+    expect(shouldIgnoreSpan(span2, ignoreSpans)).toBe(true);
+    expect(shouldIgnoreSpan(span3, ignoreSpans)).toBe(true);
+    expect(shouldIgnoreSpan(span4, ignoreSpans)).toBe(true);
+
+    // All of these are ignored because the op matches
+    const span5 = { description: 'custom 1', op: 'test2' };
+    const span6 = { description: 'custom 2', op: 'test2' };
+    const span7 = { description: 'custom 3', op: 'test2' };
+    const span8 = { description: 'custom 4', op: 'test2' };
+
+    expect(shouldIgnoreSpan(span5, ignoreSpans)).toBe(true);
+    expect(shouldIgnoreSpan(span6, ignoreSpans)).toBe(true);
+    expect(shouldIgnoreSpan(span7, ignoreSpans)).toBe(true);
+    expect(shouldIgnoreSpan(span8, ignoreSpans)).toBe(true);
+
+    // None of these are ignored because the name and op don't match
+    const span9 = { description: 'custom 5', op: 'test' };
+    const span10 = { description: 'custom 6', op: 'test' };
+    const span11 = { description: 'custom 7', op: 'test' };
+    const span12 = { description: 'custom 8', op: 'test' };
+
+    expect(shouldIgnoreSpan(span9, ignoreSpans)).toBe(false);
+    expect(shouldIgnoreSpan(span10, ignoreSpans)).toBe(false);
+    expect(shouldIgnoreSpan(span11, ignoreSpans)).toBe(false);
+    expect(shouldIgnoreSpan(span12, ignoreSpans)).toBe(false);
+  });
+});
+
+describe('reparentChildSpans', () => {
+  it('should ignore dropped root spans', () => {
+    const span1 = { span_id: '1' } as SpanJSON;
+    const span2 = { span_id: '2', parent_span_id: '1' } as SpanJSON;
+    const span3 = { span_id: '3', parent_span_id: '2' } as SpanJSON;
+
+    const spans = [span1, span2, span3];
+
+    reparentChildSpans(spans, span1);
+
+    expect(spans).toEqual([span1, span2, span3]);
+    expect(span1.parent_span_id).toBeUndefined();
+    expect(span2.parent_span_id).toBe('1');
+    expect(span3.parent_span_id).toBe('2');
+  });
+
+  it('should reparent child spans of the dropped span', () => {
+    const span1 = { span_id: '1' } as SpanJSON;
+    const span2 = { span_id: '2', parent_span_id: '1' } as SpanJSON;
+    const span3 = { span_id: '3', parent_span_id: '2' } as SpanJSON;
+    const span4 = { span_id: '4', parent_span_id: '3' } as SpanJSON;
+
+    const spans = [span1, span2, span3, span4];
+
+    reparentChildSpans(spans, span2);
+
+    expect(spans).toEqual([span1, span2, span3, span4]);
+    expect(span1.parent_span_id).toBeUndefined();
+    expect(span2.parent_span_id).toBe('1');
+    expect(span3.parent_span_id).toBe('1');
+    expect(span4.parent_span_id).toBe('3');
+  });
+});