Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 84 additions & 0 deletions packages/rum-core/src/domain/resource/graphql.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,90 @@ describe('GraphQL detection and metadata extraction', () => {
)
expect(result).toBeUndefined()
})

it('should use operationType from body when query field is absent', () => {
const requestBody = JSON.stringify({
operationName: 'CreateUser',
operationType: 'mutation',
variables: { name: 'Alice' },
})

const result = extractGraphQlRequestMetadata({ method: 'POST', url: '/graphql', requestBody }, false)

expect(result).toEqual({
operationType: 'mutation',
operationName: 'CreateUser',
variables: '{"name":"Alice"}',
payload: undefined,
})
})

it('should prefer operationType derived from query field over explicit operationType in body', () => {
const requestBody = JSON.stringify({
query: 'query GetUser { user { id } }',
operationName: 'GetUser',
operationType: 'mutation',
})

const result = extractGraphQlRequestMetadata({ method: 'POST', url: '/graphql', requestBody }, false)

expect(result?.operationType).toBe('query')
})

it('should fall back to operationType from body when query has no parseable operation type', () => {
const requestBody = JSON.stringify({
query: '{ user { id } }',
operationName: 'GetUser',
operationType: 'query',
})

const result = extractGraphQlRequestMetadata({ method: 'POST', url: '/graphql', requestBody }, false)

expect(result?.operationType).toBe('query')
})

it('should ignore unknown operationType value from body', () => {
const requestBody = JSON.stringify({
operationName: 'DoSomething',
operationType: 'foobar',
})

const result = extractGraphQlRequestMetadata({ method: 'POST', url: '/graphql', requestBody }, false)

expect(result?.operationType).toBeUndefined()
})

it('should ignore mixed-case operationType value from body', () => {
const requestBody = JSON.stringify({
operationName: 'DoSomething',
operationType: 'Mutation',
})

const result = extractGraphQlRequestMetadata({ method: 'POST', url: '/graphql', requestBody }, false)

expect(result?.operationType).toBeUndefined()
})

it('should use operationType from URL params for GET requests when query param is absent', () => {
const url = 'http://example.com/graphql?operationName=CreateUser&operationType=mutation'

const result = extractGraphQlRequestMetadata({ method: 'GET', url, requestBody: undefined }, false)

expect(result).toEqual({
operationType: 'mutation',
operationName: 'CreateUser',
variables: undefined,
payload: undefined,
})
})

it('should prefer operationType derived from query URL param over explicit operationType URL param for GET requests', () => {
const url = 'http://example.com/graphql?query=mutation%20CreateUser%20%7B%20createUser%20%7D&operationType=query'

const result = extractGraphQlRequestMetadata({ method: 'GET', url, requestBody: undefined }, false)

expect(result?.operationType).toBe('mutation')
})
})

describe('request payload truncation', () => {
Expand Down
11 changes: 11 additions & 0 deletions packages/rum-core/src/domain/resource/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ interface RawGraphQlMetadata {
query?: string
operationName?: string
variables?: string
operationType?: string
}

export interface GraphQlError {
Expand Down Expand Up @@ -108,6 +109,7 @@ function extractFromBody(requestBody: unknown): RawGraphQlMetadata | undefined {
query?: string
operationName?: string
variables?: unknown
operationType?: string
}>(requestBody)

if (!graphqlBody) {
Expand All @@ -118,6 +120,7 @@ function extractFromBody(requestBody: unknown): RawGraphQlMetadata | undefined {
query: graphqlBody.query,
operationName: graphqlBody.operationName,
variables: graphqlBody.variables ? JSON.stringify(graphqlBody.variables) : undefined,
operationType: graphqlBody.operationType,
}
}

Expand All @@ -130,6 +133,7 @@ function extractFromUrlQueryParams(url: string): RawGraphQlMetadata {
query: searchParams.get('query') || undefined,
operationName: searchParams.get('operationName') || undefined,
variables,
operationType: searchParams.get('operationType') || undefined,
}
}

Expand All @@ -146,6 +150,13 @@ function sanitizeGraphQlMetadata(rawMetadata: RawGraphQlMetadata, trackPayload:
}
}

if (!operationType) {
const bodyType = rawMetadata.operationType
if (bodyType === 'query' || bodyType === 'mutation' || bodyType === 'subscription') {
operationType = bodyType
}
}

if (rawMetadata.variables) {
variables = rawMetadata.variables
}
Expand Down
81 changes: 81 additions & 0 deletions test/e2e/scenario/rum/graphql.scenario.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,4 +188,85 @@ test.describe('GraphQL tracking', () => {
payload: undefined,
})
})

createTest('use operationType from POST body when query field is absent')
.withRum(buildGraphQlConfig())
.run(async ({ intakeRegistry, flushEvents, page }) => {
await page.evaluate(() =>
window.fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
operationName: 'CreateUser',
operationType: 'mutation',
variables: { name: 'Alice' },
}),
})
)

await flushEvents()
const resourceEvent = intakeRegistry.rumResourceEvents.find((event) => event.resource.url.includes('/graphql'))!
expect(resourceEvent).toBeDefined()
expect(resourceEvent.resource.graphql).toEqual({
operationType: 'mutation',
operationName: 'CreateUser',
variables: '{"name":"Alice"}',
payload: undefined,
})
})

createTest('query field operationType takes precedence over explicit operationType in POST body')
.withRum(buildGraphQlConfig())
.run(async ({ intakeRegistry, flushEvents, page }) => {
await page.evaluate(() =>
window.fetch('/graphql', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
query: 'query GetUser { user { id } }',
operationName: 'GetUser',
operationType: 'mutation',
}),
})
)

await flushEvents()
const resourceEvent = intakeRegistry.rumResourceEvents.find((event) => event.resource.url.includes('/graphql'))!
expect(resourceEvent).toBeDefined()
expect(resourceEvent.resource.graphql?.operationType).toBe('query')
})

createTest('use operationType from GET URL params when query param is absent')
.withRum(buildGraphQlConfig())
.run(async ({ intakeRegistry, flushEvents, page }) => {
await page.evaluate(() => {
const url = `/graphql?operationName=CreateUser&operationType=mutation&variables=${encodeURIComponent(JSON.stringify({ name: 'Bob' }))}`
return window.fetch(url, { method: 'GET' })
})

await flushEvents()
const resourceEvent = intakeRegistry.rumResourceEvents.find((event) => event.resource.url.includes('/graphql'))!
expect(resourceEvent).toBeDefined()
expect(resourceEvent.resource.method).toBe('GET')
expect(resourceEvent.resource.graphql).toEqual({
operationType: 'mutation',
operationName: 'CreateUser',
variables: '{"name":"Bob"}',
payload: undefined,
})
})

createTest('query param operationType takes precedence over explicit operationType GET URL param')
.withRum(buildGraphQlConfig())
.run(async ({ intakeRegistry, flushEvents, page }) => {
await page.evaluate(() => {
const url = `/graphql?query=${encodeURIComponent('query GetUser { user { id } }')}&operationName=GetUser&operationType=mutation`
return window.fetch(url, { method: 'GET' })
})

await flushEvents()
const resourceEvent = intakeRegistry.rumResourceEvents.find((event) => event.resource.url.includes('/graphql'))!
expect(resourceEvent).toBeDefined()
expect(resourceEvent.resource.graphql?.operationType).toBe('query')
})
})
Loading