Skip to content
Merged
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -400,7 +400,7 @@ module.exports = [
import: createImport('init', 'experimentalUseDiagnosticsChannelInjection'),
ignore: [...builtinModules, ...nodePrefixedBuiltinModules],
gzip: true,
limit: '136 KB',
limit: '140 KB',
disablePlugins: ['@size-limit/esbuild'],
},
{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as Sentry from '@sentry/node';
import { loggingTransport } from '@sentry-internal/node-integration-tests';

// Configure graphql on the diagnostics-channel injection path: opt in, then pass the matching
// `diagnosticsChannelInjectionIntegrations()` entry explicitly so its options (here
// `ignoreResolveSpans: false`) apply. This explicit instance wins over the default one the opt-in swaps in.
const { graphqlIntegration } = Sentry.diagnosticsChannelInjectionIntegrations();
Sentry.experimentalUseDiagnosticsChannelInjection();

Sentry.init({
dsn: 'https://public@dsn.ingest.sentry.io/1337',
release: '1.0',
tracesSampleRate: 1.0,
integrations: [graphqlIntegration({ ignoreResolveSpans: false })],
transport: loggingTransport,
});
Original file line number Diff line number Diff line change
Expand Up @@ -49,4 +49,43 @@ describe('GraphQL/Apollo Tests > resolve spans', () => {
.completed();
});
});

// Same behavior on the diagnostics-channel path: passing the channel integration explicitly (see
// instrument-dc.mjs) must carry `ignoreResolveSpans: false` through — the explicit instance wins over
// the swapped-in default — and emit resolve spans with the orchestrion origin.
const EXPECTED_ORCHESTRION_TRANSACTION = {
transaction: 'Test Transaction (query)',
spans: expect.arrayContaining([
expect.objectContaining({
description: 'query',
origin: 'auto.graphql.diagnostic_channel',
data: expect.objectContaining({
'graphql.operation.type': 'query',
'graphql.document': '{hello}',
'sentry.origin': 'auto.graphql.diagnostic_channel',
}),
}),
expect.objectContaining({ description: 'graphql.parse' }),
expect.objectContaining({ description: 'graphql.validate' }),
expect.objectContaining({
description: 'graphql.resolve hello',
data: expect.objectContaining({
'graphql.field.name': 'hello',
'graphql.field.path': 'hello',
'graphql.field.type': 'String',
'graphql.parent.name': 'Query',
}),
}),
]),
};

createEsmAndCjsTests(__dirname, 'scenario-query.mjs', 'instrument-dc.mjs', (createTestRunner, test) => {
test('emits resolve spans via diagnostics-channel injection when configured explicitly', async () => {
await createTestRunner()
.expect({ transaction: EXPECTED_START_SERVER_TRANSACTION })
.expect({ transaction: EXPECTED_ORCHESTRION_TRANSACTION })
.start()
.completed();
});
});
});
Original file line number Diff line number Diff line change
@@ -1,11 +1,39 @@
import { afterAll, describe, expect } from 'vitest';
import { isOrchestrionEnabled } from '../../../utils';
import { cleanupChildProcesses, createEsmAndCjsTests } from '../../../utils/runner';

// Server start transaction (Apollo Server v5 no longer runs introspection query on start)
const EXPECTED_START_SERVER_TRANSACTION = {
transaction: 'Test Server Start',
};

// apollo uses graphql v16, so the default run instruments it via the vendored OTel patcher and the
// orchestrion run (auto-injected on CI) via the diagnostics-channel path. Both emit the same span
// name/status; the origin and the document attribute (`graphql.source` vs `graphql.document`) differ.
const orchestrion = isOrchestrionEnabled();
const ORIGIN = orchestrion ? 'auto.graphql.diagnostic_channel' : 'auto.graphql.otel.graphql';

function graphqlExecuteSpan(opts: {
description: string;
operationType: string;
operationName?: string;
document: unknown;
status?: string;
}): ReturnType<typeof expect.objectContaining> {
const { description, operationType, operationName, document, status = 'ok' } = opts;
return expect.objectContaining({
description,
status,
origin: ORIGIN,
data: expect.objectContaining({
'graphql.operation.type': operationType,
...(operationName ? { 'graphql.operation.name': operationName } : {}),
[orchestrion ? 'graphql.document' : 'graphql.source']: document,
'sentry.origin': ORIGIN,
}),
});
}

describe('GraphQL/Apollo Tests', () => {
afterAll(() => {
cleanupChildProcesses();
Expand All @@ -15,16 +43,7 @@ describe('GraphQL/Apollo Tests', () => {
const EXPECTED_TRANSACTION = {
transaction: 'Test Transaction (query)',
spans: expect.arrayContaining([
expect.objectContaining({
data: {
'graphql.operation.type': 'query',
'graphql.source': '{hello}',
'sentry.origin': 'auto.graphql.otel.graphql',
},
description: 'query',
status: 'ok',
origin: 'auto.graphql.otel.graphql',
}),
graphqlExecuteSpan({ description: 'query', operationType: 'query', document: '{hello}' }),
]),
};

Expand All @@ -50,16 +69,11 @@ describe('GraphQL/Apollo Tests', () => {
const EXPECTED_TRANSACTION = {
transaction: 'Test Transaction (mutation Mutation)',
spans: expect.arrayContaining([
expect.objectContaining({
data: {
'graphql.operation.name': 'Mutation',
'graphql.operation.type': 'mutation',
'graphql.source': 'mutation Mutation($email: String) {\n login(email: $email)\n}',
'sentry.origin': 'auto.graphql.otel.graphql',
},
graphqlExecuteSpan({
description: 'mutation Mutation',
status: 'ok',
origin: 'auto.graphql.otel.graphql',
operationType: 'mutation',
operationName: 'Mutation',
document: 'mutation Mutation($email: String) {\n login(email: $email)\n}',
}),
]),
};
Expand All @@ -86,16 +100,11 @@ describe('GraphQL/Apollo Tests', () => {
const EXPECTED_TRANSACTION = {
transaction: 'Test Transaction (mutation)',
spans: expect.arrayContaining([
expect.objectContaining({
// The inline email literal must be redacted to `"*"`, so the raw value never reaches the span.
graphqlExecuteSpan({
description: 'mutation',
status: 'ok',
origin: 'auto.graphql.otel.graphql',
data: expect.objectContaining({
'graphql.operation.type': 'mutation',
// The inline email literal must be redacted to `"*"`, so the raw value can never reach `graphql.source`.
'graphql.source': expect.stringContaining('login(email: "*")'),
'sentry.origin': 'auto.graphql.otel.graphql',
}),
operationType: 'mutation',
document: expect.stringContaining('login(email: "*")'),
}),
]),
};
Expand All @@ -105,7 +114,7 @@ describe('GraphQL/Apollo Tests', () => {
'scenario-redaction.mjs',
'instrument.mjs',
(createTestRunner, test) => {
test('redacts inline literal values from graphql.source.', async () => {
test('redacts inline literal values from the graphql document.', async () => {
await createTestRunner()
.expect({ transaction: EXPECTED_START_SERVER_TRANSACTION })
.expect({ transaction: EXPECTED_TRANSACTION })
Expand All @@ -122,16 +131,12 @@ describe('GraphQL/Apollo Tests', () => {
const EXPECTED_TRANSACTION = {
transaction: 'Test Transaction (mutation Mutation)',
spans: expect.arrayContaining([
expect.objectContaining({
data: {
'graphql.operation.name': 'Mutation',
'graphql.operation.type': 'mutation',
'graphql.source': 'mutation Mutation($email: String) {\n login(email: $email)\n}',
'sentry.origin': 'auto.graphql.otel.graphql',
},
graphqlExecuteSpan({
description: 'mutation Mutation',
operationType: 'mutation',
operationName: 'Mutation',
document: 'mutation Mutation($email: String) {\n login(email: $email)\n}',
status: 'internal_error',
origin: 'auto.graphql.otel.graphql',
}),
]),
};
Expand Down
Original file line number Diff line number Diff line change
@@ -1,152 +1,26 @@
/*
* Simplified types inlined from the `graphql` package.
* Only includes members accessed by this instrumentation.
*/

export type PromiseOrValue<T> = T | Promise<T>;

export type Maybe<T> = null | undefined | T;

export interface Location {
start: number;
end: number;
startToken: Token;
source: Source;
[key: string]: any;
}

export interface Token {
kind: string;
start: number;
end: number;
line: number;
column: number;
value: string;
prev: Token | null;
next: Token | null;
[key: string]: any;
}

export interface Source {
body: string;
name: string;
locationOffset: Location;
[key: string]: any;
}

export interface DocumentNode {
kind: string;
definitions: ReadonlyArray<DefinitionNode>;
loc?: Location;
[key: string]: any;
}

export interface DefinitionNode {
kind: string;
loc?: Location;
[key: string]: any;
}

export interface OperationDefinitionNode extends DefinitionNode {
operation: string;
name?: { kind: string; value: string; loc?: Location };
[key: string]: any;
}

export interface ParseOptions {
noLocation?: boolean;
[key: string]: any;
}

export interface ExecutionArgs {
schema: GraphQLSchema;
document: DocumentNode;
rootValue?: any;
contextValue?: any;
variableValues?: Maybe<{ [key: string]: any }>;
operationName?: Maybe<string>;
fieldResolver?: Maybe<GraphQLFieldResolver<any, any>>;
typeResolver?: Maybe<GraphQLTypeResolver<any, any>>;
[key: string]: any;
}

export interface ExecutionResult {
errors?: ReadonlyArray<GraphQLError>;
data?: Record<string, any> | null;
[key: string]: any;
}

export interface GraphQLError {
message: string;
locations?: ReadonlyArray<{ line: number; column: number }>;
path?: ReadonlyArray<string | number>;
[key: string]: any;
}

export interface GraphQLSchema {
getQueryType(): GraphQLObjectType | undefined | null;
getMutationType(): GraphQLObjectType | undefined | null;
[key: string]: any;
}

export interface GraphQLObjectType {
name: string;
getFields(): { [key: string]: GraphQLField };
[key: string]: any;
}

export interface GraphQLField {
name: string;
type: GraphQLOutputType;
resolve?: GraphQLFieldResolver<any, any>;
[key: string]: any;
}

export type GraphQLOutputType = GraphQLNamedOutputType | GraphQLWrappingType;

interface GraphQLNamedOutputType {
name: string;
[key: string]: any;
}

interface GraphQLWrappingType {
ofType: GraphQLOutputType;
[key: string]: any;
}

export interface GraphQLUnionType {
name: string;
getTypes(): ReadonlyArray<GraphQLObjectType>;
[key: string]: any;
}

export type GraphQLType = GraphQLOutputType | GraphQLUnionType;

export type GraphQLFieldResolver<TSource, TContext, TArgs = any> = (
source: TSource,
args: TArgs,
context: TContext,
info: GraphQLResolveInfo,
) => any;

export type GraphQLTypeResolver<TSource, TContext> = (
value: TSource,
context: TContext,
info: GraphQLResolveInfo,
abstractType: any,
) => any;

export interface GraphQLResolveInfo {
fieldName: string;
fieldNodes: ReadonlyArray<{ kind: string; loc?: Location; [key: string]: any }>;
returnType: { toString(): string; [key: string]: any };
parentType: { name: string; [key: string]: any };
path: any;
[key: string]: any;
}

export type ValidationRule = any;

export interface TypeInfo {
[key: string]: any;
}
// Single source of truth for the structural graphql types, shared with the orchestrion graphql
// integration in `@sentry/server-utils` so the two can't drift.
export type {
DefinitionNode,
DocumentNode,
ExecutionArgs,
ExecutionResult,
GraphQLError,
GraphQLFieldResolver,
GraphQLObjectType,
GraphQLOutputType,
GraphQLResolveInfo,
GraphQLSchema,
GraphQLType,
GraphQLTypeResolver,
GraphQLUnionType,
Location,
Maybe,
OperationDefinitionNode,
ParseOptions,
PromiseOrValue,
Source,
Token,
TypeInfo,
ValidationRule,
} from '@sentry/server-utils/orchestrion';
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,7 @@ export class GraphQLInstrumentation extends InstrumentationBase<GraphQLInstrumen
});
}

private _createExecuteSpan(operation: DefinitionNode | undefined, processedArgs: ExecutionArgs): Span {
private _createExecuteSpan(operation: DefinitionNode | undefined, processedArgs: OtelExecutionArgs): Span {
const span = startInactiveSpan({
name: SpanNames.EXECUTE,
attributes: { [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: ORIGIN },
Expand Down
Loading
Loading