-
Notifications
You must be signed in to change notification settings - Fork 170
Expand file tree
/
Copy pathgraphql.ts
More file actions
135 lines (112 loc) · 3.46 KB
/
graphql.ts
File metadata and controls
135 lines (112 loc) · 3.46 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
import { isNonEmptyArray, matchList, ONE_KIBI_BYTE, safeTruncate } from '@datadog/browser-core'
import type { RumConfiguration, GraphQlUrlOption } from '../configuration'
import type { RequestCompleteEvent } from '../requestCollection'
/**
* arbitrary value, byte precision not needed
*/
const GRAPHQL_PAYLOAD_LIMIT = 32 * ONE_KIBI_BYTE
export interface GraphQlError {
message: string
code?: string
locations?: Array<{ line: number; column: number }>
path?: Array<string | number>
}
export interface GraphQlMetadata {
operationType?: 'query' | 'mutation' | 'subscription'
operationName?: string
variables?: string
payload?: string
error_count?: number
errors?: GraphQlError[]
}
export function extractGraphQlMetadata(
request: RequestCompleteEvent,
graphQlConfig: GraphQlUrlOption
): GraphQlMetadata | undefined {
const metadata = extractGraphQlRequestMetadata(request.requestBody, graphQlConfig.trackPayload)
if (!metadata) {
return
}
if (graphQlConfig.trackResponseErrors && request.responseBody) {
const responseErrors = parseGraphQlResponse(request.responseBody)
if (responseErrors) {
metadata.error_count = responseErrors.length
metadata.errors = responseErrors
}
}
return metadata
}
export function parseGraphQlResponse(responseText: string): GraphQlError[] | undefined {
let response: unknown
try {
response = JSON.parse(responseText)
} catch {
// Invalid JSON response
return
}
if (!response || typeof response !== 'object') {
return
}
const responseObj = response as Record<string, unknown>
if (!isNonEmptyArray(responseObj.errors)) {
return
}
const errors = (responseObj.errors as Array<Record<string, unknown>>).map((error) => {
const graphqlError: GraphQlError = {
message: error.message as string,
path: error.path as Array<string | number>,
locations: error.locations as Array<{ line: number; column: number }>,
code: (error.extensions as Record<string, unknown> | undefined)?.code as string,
}
return graphqlError
})
return errors
}
export function findGraphQlConfiguration(url: string, configuration: RumConfiguration): GraphQlUrlOption | undefined {
return configuration.allowedGraphQlUrls.find((graphQlOption) => matchList([graphQlOption.match], url))
}
export function extractGraphQlRequestMetadata(
requestBody: unknown,
trackPayload: boolean = false
): GraphQlMetadata | undefined {
if (!requestBody || typeof requestBody !== 'string') {
return
}
let graphqlBody: {
query?: string
operationName?: string
variables?: unknown
}
try {
graphqlBody = JSON.parse(requestBody)
} catch {
// Not valid JSON
return
}
if (!graphqlBody) {
return
}
let operationType: 'query' | 'mutation' | 'subscription' | undefined
let payload: string | undefined
if (graphqlBody.query) {
const trimmedQuery = graphqlBody.query.trim()
operationType = getOperationType(trimmedQuery)
if (trackPayload) {
payload = safeTruncate(trimmedQuery, GRAPHQL_PAYLOAD_LIMIT, '...')
}
}
const operationName = graphqlBody.operationName
let variables: string | undefined
if (graphqlBody.variables) {
variables = JSON.stringify(graphqlBody.variables)
}
return {
operationType,
operationName,
variables,
payload,
}
}
function getOperationType(query: string) {
return query.match(/^\s*(query|mutation|subscription)\b/i)?.[1] as 'query' | 'mutation' | 'subscription' | undefined
}