From c89aabd96236b45e90cdb18ee316b05be34cbd7a Mon Sep 17 00:00:00 2001 From: louiszawadzki Date: Fri, 8 Sep 2023 11:53:09 +0200 Subject: [PATCH 1/8] Add Apollo Client integration --- CONTRIBUTING.md | 4 +- packages/react-native-apollo-client/README.md | 1 + .../babel.config.js | 3 + .../react-native-apollo-client/package.json | 73 +++++++++++++++++++ .../react-native-apollo-client/src/index.ts | 0 .../react-native-apollo-client/tsconfig.json | 3 + yarn.lock | 13 ++++ 7 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 packages/react-native-apollo-client/README.md create mode 100644 packages/react-native-apollo-client/babel.config.js create mode 100644 packages/react-native-apollo-client/package.json create mode 100644 packages/react-native-apollo-client/src/index.ts create mode 100644 packages/react-native-apollo-client/tsconfig.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1fe0f65ab..2434cc737 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -17,10 +17,12 @@ yarn install This repository contains 2 main projects: -* SDK project (in the `packages` folder), which consists of 4 workspaces: +* SDK project (in the `packages` folder), which consists of the following workspaces: * `codepush`: an integration for the [react-native-code-push](https://github.com/microsoft/react-native-code-push) library. * `core`: the core React Native SDK allowing tracking of logs, spans and RUM events. + * `react-native-apollo-client`: an integration for the [Apollo Client](https://www.apollographql.com/docs/react/integrations/react-native/) library. * `react-native-navigation`: an integration for the [react-native-navigation](https://github.com/wix/react-native-navigation) library. + * `react-native-webview`: an integration for the [`react-native-webview`](https://github.com/react-native-webview/react-native-webview) library. * `react-navigation`: an integration for the [react-navigation](https://github.com/react-navigation/react-navigation) library. * Sample app project (in the `example` folder) diff --git a/packages/react-native-apollo-client/README.md b/packages/react-native-apollo-client/README.md new file mode 100644 index 000000000..04780b4fd --- /dev/null +++ b/packages/react-native-apollo-client/README.md @@ -0,0 +1 @@ +# React-Native Monitoring for @apollo/client diff --git a/packages/react-native-apollo-client/babel.config.js b/packages/react-native-apollo-client/babel.config.js new file mode 100644 index 000000000..c50a8a001 --- /dev/null +++ b/packages/react-native-apollo-client/babel.config.js @@ -0,0 +1,3 @@ +module.exports = { + presets: ['module:metro-react-native-babel-preset'] +}; diff --git a/packages/react-native-apollo-client/package.json b/packages/react-native-apollo-client/package.json new file mode 100644 index 000000000..d56883bc2 --- /dev/null +++ b/packages/react-native-apollo-client/package.json @@ -0,0 +1,73 @@ +{ + "name": "@datadog/mobile-react-native-apollo-client", + "version": "1.8.3", + "description": "A client-side React Native module to interact with Apollo Client and Datadog", + "keywords": [ + "datadog", + "react-native", + "ios", + "android", + "apollo", + "graphql" + ], + "author": "Datadog (https://github.com/DataDog)", + "homepage": "https://github.com/DataDog/dd-sdk-reactnative/packages/react-native-apollo-client#readme", + "repository": { + "url": "https://github.com/DataDog/dd-sdk-reactnative", + "directory": "packages/react-native-apollo-client" + }, + "bugs": { + "url": "https://github.com/DataDog/dd-sdk-reactnative/issues" + }, + "license": "Apache-2.0", + "main": "lib/commonjs/index", + "files": [ + "src/**", + "lib/**" + ], + "types": "lib/typescript/index.d.ts", + "react-native": "src/index", + "source": "src/index", + "module": "lib/module/index", + "publishConfig": { + "access": "public" + }, + "scripts": { + "test": "jest", + "lint": "eslint .", + "prepare": "rm -rf lib && yarn bob build" + }, + "devDependencies": { + "@testing-library/react-native": "7.0.2", + "react-native-builder-bob": "0.17.1" + }, + "peerDependencies": { + "@datadog/mobile-react-native": ">=1.5.0", + "react": ">=16.13.1", + "react-native": ">=0.63.4 <1.0" + }, + "jest": { + "preset": "react-native", + "modulePathIgnorePatterns": [ + "/lib/" + ], + "testPathIgnorePatterns": [ + "/__utils__/" + ], + "transformIgnorePatterns": [] + }, + "react-native-builder-bob": { + "source": "src", + "output": "lib", + "targets": [ + "commonjs", + "module", + [ + "typescript", + { + "tsc": "./../../node_modules/.bin/tsc" + } + ] + ] + } +} diff --git a/packages/react-native-apollo-client/src/index.ts b/packages/react-native-apollo-client/src/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/packages/react-native-apollo-client/tsconfig.json b/packages/react-native-apollo-client/tsconfig.json new file mode 100644 index 000000000..41716a7dd --- /dev/null +++ b/packages/react-native-apollo-client/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig" +} diff --git a/yarn.lock b/yarn.lock index c107a24f1..f3899c92d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3506,6 +3506,19 @@ __metadata: languageName: node linkType: hard +"@datadog/mobile-react-native-apollo-client@workspace:packages/react-native-apollo-client": + version: 0.0.0-use.local + resolution: "@datadog/mobile-react-native-apollo-client@workspace:packages/react-native-apollo-client" + dependencies: + "@testing-library/react-native": 7.0.2 + react-native-builder-bob: 0.17.1 + peerDependencies: + "@datadog/mobile-react-native": ">=1.5.0" + react: ">=16.13.1" + react-native: ">=0.63.4 <1.0" + languageName: unknown + linkType: soft + "@datadog/mobile-react-native-code-push@workspace:packages/codepush": version: 0.0.0-use.local resolution: "@datadog/mobile-react-native-code-push@workspace:packages/codepush" From 45028d676abe4c0049779551782548aecacdf071 Mon Sep 17 00:00:00 2001 From: louiszawadzki Date: Fri, 8 Sep 2023 16:27:04 +0200 Subject: [PATCH 2/8] Add apollo client as dependency for integration --- .../react-native-apollo-client/package.json | 3 + yarn.lock | 143 ++++++++++++++++++ 2 files changed, 146 insertions(+) diff --git a/packages/react-native-apollo-client/package.json b/packages/react-native-apollo-client/package.json index d56883bc2..f12d7ef76 100644 --- a/packages/react-native-apollo-client/package.json +++ b/packages/react-native-apollo-client/package.json @@ -38,10 +38,13 @@ "prepare": "rm -rf lib && yarn bob build" }, "devDependencies": { + "@apollo/client": "^3.8.3", "@testing-library/react-native": "7.0.2", + "graphql": "^16.8.0", "react-native-builder-bob": "0.17.1" }, "peerDependencies": { + "@apollo/client": ">=3.0", "@datadog/mobile-react-native": ">=1.5.0", "react": ">=16.13.1", "react-native": ">=0.63.4 <1.0" diff --git a/yarn.lock b/yarn.lock index f3899c92d..82986d4c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25,6 +25,42 @@ __metadata: languageName: node linkType: hard +"@apollo/client@npm:^3.8.3": + version: 3.8.3 + resolution: "@apollo/client@npm:3.8.3" + dependencies: + "@graphql-typed-document-node/core": ^3.1.1 + "@wry/context": ^0.7.3 + "@wry/equality": ^0.5.6 + "@wry/trie": ^0.4.3 + graphql-tag: ^2.12.6 + hoist-non-react-statics: ^3.3.2 + optimism: ^0.17.5 + prop-types: ^15.7.2 + response-iterator: ^0.2.6 + symbol-observable: ^4.0.0 + ts-invariant: ^0.10.3 + tslib: ^2.3.0 + zen-observable-ts: ^1.2.5 + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 + graphql-ws: ^5.5.5 + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + subscriptions-transport-ws: ^0.9.0 || ^0.11.0 + peerDependenciesMeta: + graphql-ws: + optional: true + react: + optional: true + react-dom: + optional: true + subscriptions-transport-ws: + optional: true + checksum: cdecd5d613c0f941d00fc1904b9ecdeb502fc157c10400430231adb47d8f67727c78eed1b7489aed91cce6e20e32fc44e16b6c2d5424a42d7c100dcef65b942b + languageName: node + linkType: hard + "@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13": version: 7.12.13 resolution: "@babel/code-frame@npm:7.12.13" @@ -3510,9 +3546,12 @@ __metadata: version: 0.0.0-use.local resolution: "@datadog/mobile-react-native-apollo-client@workspace:packages/react-native-apollo-client" dependencies: + "@apollo/client": ^3.8.3 "@testing-library/react-native": 7.0.2 + graphql: ^16.8.0 react-native-builder-bob: 0.17.1 peerDependencies: + "@apollo/client": ">=3.0" "@datadog/mobile-react-native": ">=1.5.0" react: ">=16.13.1" react-native: ">=0.63.4 <1.0" @@ -3718,6 +3757,15 @@ __metadata: languageName: node linkType: hard +"@graphql-typed-document-node/core@npm:^3.1.1": + version: 3.2.0 + resolution: "@graphql-typed-document-node/core@npm:3.2.0" + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: fa44443accd28c8cf4cb96aaaf39d144a22e8b091b13366843f4e97d19c7bfeaf609ce3c7603a4aeffe385081eaf8ea245d078633a7324c11c5ec4b2011bb76d + languageName: node + linkType: hard + "@hapi/hoek@npm:^9.0.0": version: 9.3.0 resolution: "@hapi/hoek@npm:9.3.0" @@ -6086,6 +6134,33 @@ __metadata: languageName: node linkType: hard +"@wry/context@npm:^0.7.0, @wry/context@npm:^0.7.3": + version: 0.7.3 + resolution: "@wry/context@npm:0.7.3" + dependencies: + tslib: ^2.3.0 + checksum: 91c1e9eee9046c48ff857d60dcbb59f22246ce0f9bb2d9b190e0555227e7ba3f86024032cc057f3f5141d3bee93fc6b2a15ce2c79fa512569d3432eb8e1af02b + languageName: node + linkType: hard + +"@wry/equality@npm:^0.5.6": + version: 0.5.6 + resolution: "@wry/equality@npm:0.5.6" + dependencies: + tslib: ^2.3.0 + checksum: 9addf8891bdff5e23eecff03641846e7a56c1de3c9362c25e69c0b2ee3303e74b22e9a0376920283cd9d3bdd1bada12df54be5eaa29c2d801d33d94992672e14 + languageName: node + linkType: hard + +"@wry/trie@npm:^0.4.3": + version: 0.4.3 + resolution: "@wry/trie@npm:0.4.3" + dependencies: + tslib: ^2.3.0 + checksum: 106e021125cfafd22250a6631a0438a6a3debae7bd73f6db87fe42aa0757fe67693db0dfbe200ae1f60ba608c3e09ddb8a4e2b3527d56ed0a7e02aa0ee4c94e1 + languageName: node + linkType: hard + "@yarnpkg/lockfile@npm:^1.1.0": version: 1.1.0 resolution: "@yarnpkg/lockfile@npm:1.1.0" @@ -10872,6 +10947,24 @@ __metadata: languageName: node linkType: hard +"graphql-tag@npm:^2.12.6": + version: 2.12.6 + resolution: "graphql-tag@npm:2.12.6" + dependencies: + tslib: ^2.1.0 + peerDependencies: + graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + checksum: b15162a3d62f17b9b79302445b9ee330e041582f1c7faca74b9dec5daa74272c906ec1c34e1c50592bb6215e5c3eba80a309103f6ba9e4c1cddc350c46f010df + languageName: node + linkType: hard + +"graphql@npm:^16.8.0": + version: 16.8.0 + resolution: "graphql@npm:16.8.0" + checksum: d853d4085b0c911a7e2a926c3b0d379934ec61cd4329e70cdf281763102f024fd80a97db7a505b8b04fed9050cb4875f8f518150ea854557a500a0b41dcd7f4e + languageName: node + linkType: hard + "handlebars@npm:^4.7.7": version: 4.7.7 resolution: "handlebars@npm:4.7.7" @@ -15733,6 +15826,17 @@ __metadata: languageName: node linkType: hard +"optimism@npm:^0.17.5": + version: 0.17.5 + resolution: "optimism@npm:0.17.5" + dependencies: + "@wry/context": ^0.7.0 + "@wry/trie": ^0.4.3 + tslib: ^2.3.0 + checksum: 5990217d989e9857dc523a64cb6e5a9205eae68c7acac78f7dde8fbe50045d0f11ca8068cdbb51b1eae15510d96ad593a99cf98c6f86c41d1b6f90e54956ff11 + languageName: node + linkType: hard + "optionator@npm:^0.8.1": version: 0.8.3 resolution: "optionator@npm:0.8.3" @@ -17473,6 +17577,13 @@ __metadata: languageName: node linkType: hard +"response-iterator@npm:^0.2.6": + version: 0.2.6 + resolution: "response-iterator@npm:0.2.6" + checksum: b0db3c0665a0d698d65512951de9623c086b9c84ce015a76076d4bd0bf733779601d0b41f0931d16ae38132fba29e1ce291c1f8e6550fc32daaa2dc3ab4f338d + languageName: node + linkType: hard + "responselike@npm:^1.0.2": version: 1.0.2 resolution: "responselike@npm:1.0.2" @@ -18736,6 +18847,13 @@ __metadata: languageName: node linkType: hard +"symbol-observable@npm:^4.0.0": + version: 4.0.0 + resolution: "symbol-observable@npm:4.0.0" + checksum: 212c7edce6186634d671336a88c0e0bbd626c2ab51ed57498dc90698cce541839a261b969c2a1e8dd43762133d47672e8b62e0b1ce9cf4157934ba45fd172ba8 + languageName: node + linkType: hard + "tar-stream@npm:~2.2.0": version: 2.2.0 resolution: "tar-stream@npm:2.2.0" @@ -18990,6 +19108,15 @@ __metadata: languageName: node linkType: hard +"ts-invariant@npm:^0.10.3": + version: 0.10.3 + resolution: "ts-invariant@npm:0.10.3" + dependencies: + tslib: ^2.1.0 + checksum: bb07d56fe4aae69d8860e0301dfdee2d375281159054bc24bf1e49e513fb0835bf7f70a11351344d213a79199c5e695f37ebbf5a447188a377ce0cd81d91ddb5 + languageName: node + linkType: hard + "tsconfig-paths@npm:^3.12.0": version: 3.14.1 resolution: "tsconfig-paths@npm:3.14.1" @@ -20159,3 +20286,19 @@ __metadata: checksum: f77b3d8d00310def622123df93d4ee654fc6a0096182af8bd60679ddcdfb3474c56c6c7190817c84a2785648cdee9d721c0154eb45698c62176c322fb46fc700 languageName: node linkType: hard + +"zen-observable-ts@npm:^1.2.5": + version: 1.2.5 + resolution: "zen-observable-ts@npm:1.2.5" + dependencies: + zen-observable: 0.8.15 + checksum: 3b707b7a0239a9bc40f73ba71b27733a689a957c1f364fabb9fa9cbd7d04b7c2faf0d517bf17004e3ed3f4330ac613e84c0d32313e450ddaa046f3350af44541 + languageName: node + linkType: hard + +"zen-observable@npm:0.8.15": + version: 0.8.15 + resolution: "zen-observable@npm:0.8.15" + checksum: b7289084bc1fc74a559b7259faa23d3214b14b538a8843d2b001a35e27147833f4107590b1b44bf5bc7f6dfe6f488660d3a3725f268e09b3925b3476153b7821 + languageName: node + linkType: hard From 76c715de3f093230a0a60e9d95481942cd0dd386 Mon Sep 17 00:00:00 2001 From: louiszawadzki Date: Mon, 11 Sep 2023 11:47:59 +0200 Subject: [PATCH 3/8] Create first version of Apollo link --- .../src/DatadogLink.ts | 33 +++++ .../src/__tests__/__utils__/createCat.json | 125 ++++++++++++++++++ .../src/__tests__/__utils__/getCountries.json | 103 +++++++++++++++ .../src/__tests__/__utils__/getCountry.json | 96 ++++++++++++++ .../src/__tests__/__utils__/operationMock.ts | 32 +++++ .../src/__tests__/helpers.test.ts | 65 +++++++++ .../react-native-apollo-client/src/helpers.ts | 54 ++++++++ .../react-native-apollo-client/src/index.ts | 7 + 8 files changed, 515 insertions(+) create mode 100644 packages/react-native-apollo-client/src/DatadogLink.ts create mode 100644 packages/react-native-apollo-client/src/__tests__/__utils__/createCat.json create mode 100644 packages/react-native-apollo-client/src/__tests__/__utils__/getCountries.json create mode 100644 packages/react-native-apollo-client/src/__tests__/__utils__/getCountry.json create mode 100644 packages/react-native-apollo-client/src/__tests__/__utils__/operationMock.ts create mode 100644 packages/react-native-apollo-client/src/__tests__/helpers.test.ts create mode 100644 packages/react-native-apollo-client/src/helpers.ts diff --git a/packages/react-native-apollo-client/src/DatadogLink.ts b/packages/react-native-apollo-client/src/DatadogLink.ts new file mode 100644 index 000000000..aed0ea7f3 --- /dev/null +++ b/packages/react-native-apollo-client/src/DatadogLink.ts @@ -0,0 +1,33 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import { ApolloLink } from '@apollo/client'; + +import { getOperationName, getVariables, getOperationType } from './helpers'; + +export class DatadogLink extends ApolloLink { + constructor() { + super((operation, forward) => { + const operationName = getOperationName(operation); + const formattedVariables = getVariables(operation); + const operationType = getOperationType(operation); + + operation.setContext(({ headers = {} }) => { + return { + headers: { + ...headers, + // TODO: import headers from core + '_dd-graph-ql-operation-name': operationName, + '_dd-graph-ql-variables': formattedVariables, + '_dd-graph-ql-operation-type': operationType + } + }; + }); + + return forward(operation); + }); + } +} diff --git a/packages/react-native-apollo-client/src/__tests__/__utils__/createCat.json b/packages/react-native-apollo-client/src/__tests__/__utils__/createCat.json new file mode 100644 index 000000000..6bbae11b6 --- /dev/null +++ b/packages/react-native-apollo-client/src/__tests__/__utils__/createCat.json @@ -0,0 +1,125 @@ +{ + "variables": { "name": "Croute", "age": 22 }, + "extensions": {}, + "operationName": "CreateCat", + "query": { + "kind": "Document", + "definitions": [ + { + "kind": "OperationDefinition", + "operation": "mutation", + "name": { "kind": "Name", "value": "CreateCat" }, + "variableDefinitions": [ + { + "kind": "VariableDefinition", + "variable": { + "kind": "Variable", + "name": { "kind": "Name", "value": "name" } + }, + "type": { + "kind": "NamedType", + "name": { "kind": "Name", "value": "String" } + }, + "directives": [] + }, + { + "kind": "VariableDefinition", + "variable": { + "kind": "Variable", + "name": { "kind": "Name", "value": "age" } + }, + "type": { + "kind": "NamedType", + "name": { "kind": "Name", "value": "Int" } + }, + "directives": [] + } + ], + "directives": [], + "selectionSet": { + "kind": "SelectionSet", + "selections": [ + { + "kind": "Field", + "name": { "kind": "Name", "value": "createCat" }, + "arguments": [ + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "createCatInput" + }, + "value": { + "kind": "ObjectValue", + "fields": [ + { + "kind": "ObjectField", + "name": { + "kind": "Name", + "value": "name" + }, + "value": { + "kind": "Variable", + "name": { + "kind": "Name", + "value": "name" + } + } + }, + { + "kind": "ObjectField", + "name": { + "kind": "Name", + "value": "age" + }, + "value": { + "kind": "Variable", + "name": { + "kind": "Name", + "value": "age" + } + } + } + ] + } + } + ], + "directives": [], + "selectionSet": { + "kind": "SelectionSet", + "selections": [ + { + "kind": "Field", + "name": { + "kind": "Name", + "value": "name" + }, + "arguments": [], + "directives": [] + }, + { + "kind": "Field", + "name": { + "kind": "Name", + "value": "age" + }, + "arguments": [], + "directives": [] + }, + { + "kind": "Field", + "name": { + "kind": "Name", + "value": "__typename" + } + } + ] + } + } + ] + } + } + ], + "loc": { "start": 0, "end": 139 } + } +} diff --git a/packages/react-native-apollo-client/src/__tests__/__utils__/getCountries.json b/packages/react-native-apollo-client/src/__tests__/__utils__/getCountries.json new file mode 100644 index 000000000..4b887c700 --- /dev/null +++ b/packages/react-native-apollo-client/src/__tests__/__utils__/getCountries.json @@ -0,0 +1,103 @@ +{ + "variables": {}, + "extensions": {}, + "query": { + "kind": "Document", + "definitions": [ + { + "kind": "OperationDefinition", + "operation": "query", + "variableDefinitions": [], + "directives": [], + "selectionSet": { + "kind": "SelectionSet", + "selections": [ + { + "kind": "Field", + "name": { "kind": "Name", "value": "countries" }, + "arguments": [ + { + "kind": "Argument", + "name": { + "kind": "Name", + "value": "filter" + }, + "value": { + "kind": "ObjectValue", + "fields": [ + { + "kind": "ObjectField", + "name": { + "kind": "Name", + "value": "continent" + }, + "value": { + "kind": "ObjectValue", + "fields": [ + { + "kind": "ObjectField", + "name": { + "kind": "Name", + "value": "eq" + }, + "value": { + "kind": "StringValue", + "value": "EU", + "block": false + } + } + ] + } + } + ] + } + } + ], + "directives": [], + "selectionSet": { + "kind": "SelectionSet", + "selections": [ + { + "kind": "Field", + "name": { + "kind": "Name", + "value": "name" + }, + "arguments": [], + "directives": [] + }, + { + "kind": "Field", + "name": { + "kind": "Name", + "value": "emoji" + }, + "arguments": [], + "directives": [] + }, + { + "kind": "Field", + "name": { + "kind": "Name", + "value": "code" + }, + "arguments": [], + "directives": [] + }, + { + "kind": "Field", + "name": { + "kind": "Name", + "value": "__typename" + } + } + ] + } + } + ] + } + } + ], + "loc": { "start": 0, "end": 98 } + } +} diff --git a/packages/react-native-apollo-client/src/__tests__/__utils__/getCountry.json b/packages/react-native-apollo-client/src/__tests__/__utils__/getCountry.json new file mode 100644 index 000000000..5c00c4a70 --- /dev/null +++ b/packages/react-native-apollo-client/src/__tests__/__utils__/getCountry.json @@ -0,0 +1,96 @@ +{ + "variables": { "code": "BE" }, + "extensions": {}, + "operationName": "CountryDetails", + "query": { + "kind": "Document", + "definitions": [ + { + "kind": "OperationDefinition", + "operation": "query", + "name": { "kind": "Name", "value": "CountryDetails" }, + "variableDefinitions": [ + { + "kind": "VariableDefinition", + "variable": { + "kind": "Variable", + "name": { "kind": "Name", "value": "code" } + }, + "type": { + "kind": "NonNullType", + "type": { + "kind": "NamedType", + "name": { "kind": "Name", "value": "ID" } + } + }, + "directives": [] + } + ], + "directives": [], + "selectionSet": { + "kind": "SelectionSet", + "selections": [ + { + "kind": "Field", + "name": { "kind": "Name", "value": "country" }, + "arguments": [ + { + "kind": "Argument", + "name": { "kind": "Name", "value": "code" }, + "value": { + "kind": "Variable", + "name": { + "kind": "Name", + "value": "code" + } + } + } + ], + "directives": [], + "selectionSet": { + "kind": "SelectionSet", + "selections": [ + { + "kind": "Field", + "name": { + "kind": "Name", + "value": "name" + }, + "arguments": [], + "directives": [] + }, + { + "kind": "Field", + "name": { + "kind": "Name", + "value": "emoji" + }, + "arguments": [], + "directives": [] + }, + { + "kind": "Field", + "name": { + "kind": "Name", + "value": "capital" + }, + "arguments": [], + "directives": [] + }, + { + "kind": "Field", + "name": { + "kind": "Name", + "value": "__typename" + } + } + ] + } + } + ] + } + } + ], + "loc": { "start": 0, "end": 112 } + } +} diff --git a/packages/react-native-apollo-client/src/__tests__/__utils__/operationMock.ts b/packages/react-native-apollo-client/src/__tests__/__utils__/operationMock.ts new file mode 100644 index 000000000..455239347 --- /dev/null +++ b/packages/react-native-apollo-client/src/__tests__/__utils__/operationMock.ts @@ -0,0 +1,32 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import type { Operation } from '@apollo/client'; + +import createCat from './createCat.json'; +import getCountries from './getCountries.json'; +import getCountry from './getCountry.json'; + +/** + * - operation type: `"query"` + * - operation name: `"CountryDetails"` + * - variables: `{ "code": "BE" }` + */ +export const getCountryOperation = (getCountry as unknown) as Operation; + +/** + * - operation type: `"query"` + * - operation name: `undefined` + * - variables: `{}` + */ +export const getCountriesOperation = (getCountries as unknown) as Operation; + +/** + * - operation type: `"mutation"` + * - operation name: `CreateCat` + * - variables: `{ "name": "Croute", "age": 22 }` + */ +export const createCatOperation = (createCat as unknown) as Operation; diff --git a/packages/react-native-apollo-client/src/__tests__/helpers.test.ts b/packages/react-native-apollo-client/src/__tests__/helpers.test.ts new file mode 100644 index 000000000..8dfbc7de9 --- /dev/null +++ b/packages/react-native-apollo-client/src/__tests__/helpers.test.ts @@ -0,0 +1,65 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import { getOperationName, getVariables, getOperationType } from '../helpers'; + +import { + createCatOperation, + getCountriesOperation, + getCountryOperation +} from './__utils__/operationMock'; + +describe('helpers', () => { + describe('getOperationName', () => { + it('returns operation name if it exists', () => { + expect(getOperationName(getCountryOperation)).toBe( + 'CountryDetails' + ); + }); + it('returns null if the query is unnamed', () => { + expect(getOperationName(getCountriesOperation)).toBeNull(); + }); + }); + + describe('getVariables', () => { + it('returns variables as a string if they exist', () => { + expect(getVariables(getCountryOperation)).toBe('{"code":"BE"}'); + expect(getVariables(getCountriesOperation)).toBe('{}'); + }); + it('returns null if there are no variables', () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(getVariables({})).toBeNull(); + }); + it('does not crash if the variables are not serializable', () => { + // Case of circular reference + const root = {}; + const child = { root }; + root['circular'] = child; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(getVariables({ variables: { root } })).toBeNull(); + }); + }); + + describe('getOperationType', () => { + it('returns operation type for a query', () => { + expect(getOperationType(getCountryOperation)).toBe('query'); + expect(getOperationType(getCountriesOperation)).toBe('query'); + }); + it('returns operation type for a mutation', () => { + expect(getOperationType(createCatOperation)).toBe('mutation'); + }); + it('does not crash if there is no operation type', () => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(getOperationType({})).toBeNull(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + expect(getOperationType({ query: { definitions: [] } })).toBeNull(); + }); + }); +}); diff --git a/packages/react-native-apollo-client/src/helpers.ts b/packages/react-native-apollo-client/src/helpers.ts new file mode 100644 index 000000000..9cd68065e --- /dev/null +++ b/packages/react-native-apollo-client/src/helpers.ts @@ -0,0 +1,54 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +/** + * NOTE: Do not import from '@apollo/client/utilities' as the package does not exist in Apollo Client < v3. + */ + +import type { Operation } from '@apollo/client'; +import type { DefinitionNode, OperationDefinitionNode } from 'graphql'; + +export const getVariables = (operation: Operation): string | null => { + if (operation.variables) { + try { + return JSON.stringify(operation.variables); + } catch (e) { + // TODO: telemetry + return null; + } + } + return null; +}; + +export const getOperationName = (operation: Operation): string | null => { + if (operation.operationName) { + return operation.operationName; + } + return null; +}; + +const getOperationDefinitionNode = ( + definition: DefinitionNode +): definition is OperationDefinitionNode => { + return definition.kind === 'OperationDefinition' && !!definition.operation; +}; + +export const getOperationType = ( + operation: Operation +): 'query' | 'mutation' | 'subscription' | null => { + try { + return ( + operation.query.definitions + .filter(getOperationDefinitionNode) + .map(operationDefinitionNode => { + return operationDefinitionNode.operation; + })[0] || null + ); + } catch (e) { + // TODO: telemetry + return null; + } +}; diff --git a/packages/react-native-apollo-client/src/index.ts b/packages/react-native-apollo-client/src/index.ts index e69de29bb..0f55350c7 100644 --- a/packages/react-native-apollo-client/src/index.ts +++ b/packages/react-native-apollo-client/src/index.ts @@ -0,0 +1,7 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +export { DatadogLink } from './DatadogLink'; From 179e711a81c5d696a08405ca17c2b7536c16e142 Mon Sep 17 00:00:00 2001 From: louiszawadzki Date: Mon, 11 Sep 2023 14:09:28 +0200 Subject: [PATCH 4/8] Catch gql header in XHR proxy --- packages/core/src/index.tsx | 10 ++++- .../graphql/__tests__/graphqlHeaders.test.ts | 28 +++++++++++++ .../graphql/graphqlHeaders.ts | 15 +++++++ .../requestProxy/XHRProxy/XHRProxy.ts | 40 +++++++++++++++++++ .../src/DatadogLink.ts | 27 +++++++++---- 5 files changed, 112 insertions(+), 8 deletions(-) create mode 100644 packages/core/src/rum/instrumentation/resourceTracking/graphql/__tests__/graphqlHeaders.test.ts create mode 100644 packages/core/src/rum/instrumentation/resourceTracking/graphql/graphqlHeaders.ts diff --git a/packages/core/src/index.tsx b/packages/core/src/index.tsx index 6c2e3fbe8..a0e279d0d 100644 --- a/packages/core/src/index.tsx +++ b/packages/core/src/index.tsx @@ -19,6 +19,11 @@ import { SdkVerbosity } from './SdkVerbosity'; import { TrackingConsent } from './TrackingConsent'; import { DdLogs } from './logs/DdLogs'; import { DdRum } from './rum/DdRum'; +import { + DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER, + DATADOG_GRAPH_QL_OPERATION_NAME_HEADER, + DATADOG_GRAPH_QL_VARIABLES_HEADER +} from './rum/instrumentation/resourceTracking/graphql/graphqlHeaders'; import { RumActionType, ErrorSource, PropagatorType } from './rum/types'; import { DatadogProvider } from './sdk/DatadogProvider/DatadogProvider'; import { DdTrace } from './trace/DdTrace'; @@ -42,5 +47,8 @@ export { VitalsUpdateFrequency, PropagatorType, UploadFrequency, - BatchSize + BatchSize, + DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER, + DATADOG_GRAPH_QL_OPERATION_NAME_HEADER, + DATADOG_GRAPH_QL_VARIABLES_HEADER }; diff --git a/packages/core/src/rum/instrumentation/resourceTracking/graphql/__tests__/graphqlHeaders.test.ts b/packages/core/src/rum/instrumentation/resourceTracking/graphql/__tests__/graphqlHeaders.test.ts new file mode 100644 index 000000000..b7d7dfa10 --- /dev/null +++ b/packages/core/src/rum/instrumentation/resourceTracking/graphql/__tests__/graphqlHeaders.test.ts @@ -0,0 +1,28 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +import { + DATADOG_GRAPH_QL_OPERATION_NAME_HEADER, + DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER, + DATADOG_GRAPH_QL_VARIABLES_HEADER, + isDatadogCustomHeader +} from '../graphqlHeaders'; + +describe('GraphQL custom headers', () => { + it.each([ + DATADOG_GRAPH_QL_OPERATION_NAME_HEADER, + DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER, + DATADOG_GRAPH_QL_VARIABLES_HEADER + ])('%s matches the custom header pattern', header => { + expect(isDatadogCustomHeader(header)).toBeTruthy(); + }); + + describe('isDatadogCustomHeader', () => { + it('returns false for non-custom headers', () => { + expect(isDatadogCustomHeader('non-custom-header')).toBeFalsy(); + }); + }); +}); diff --git a/packages/core/src/rum/instrumentation/resourceTracking/graphql/graphqlHeaders.ts b/packages/core/src/rum/instrumentation/resourceTracking/graphql/graphqlHeaders.ts new file mode 100644 index 000000000..87c79e65e --- /dev/null +++ b/packages/core/src/rum/instrumentation/resourceTracking/graphql/graphqlHeaders.ts @@ -0,0 +1,15 @@ +/* + * Unless explicitly stated otherwise all files in this repository are licensed under the Apache License Version 2.0. + * This product includes software developed at Datadog (https://www.datadoghq.com/). + * Copyright 2016-Present Datadog, Inc. + */ + +const DATADOG_CUSTOM_HEADER_PREFIX = '_dd-custom-header'; + +export const DATADOG_GRAPH_QL_OPERATION_NAME_HEADER = `${DATADOG_CUSTOM_HEADER_PREFIX}-graph-ql-operation-name`; +export const DATADOG_GRAPH_QL_VARIABLES_HEADER = `${DATADOG_CUSTOM_HEADER_PREFIX}-graph-ql-variables`; +export const DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER = `${DATADOG_CUSTOM_HEADER_PREFIX}-graph-ql-operation-type`; + +export const isDatadogCustomHeader = (header: string) => { + return header.match(new RegExp(`^${DATADOG_CUSTOM_HEADER_PREFIX}`)); +}; diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts index 57562a176..2b42c0d6d 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts @@ -8,6 +8,12 @@ import Timer from '../../../../../Timer'; import { getTracingHeaders } from '../../distributedTracing/distributedTracingHeaders'; import type { DdRumResourceTracingAttributes } from '../../distributedTracing/distributedTracing'; import { getTracingAttributes } from '../../distributedTracing/distributedTracing'; +import { + DATADOG_GRAPH_QL_OPERATION_NAME_HEADER, + DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER, + DATADOG_GRAPH_QL_VARIABLES_HEADER, + isDatadogCustomHeader +} from '../../graphql/graphqlHeaders'; import type { RequestProxyOptions } from '../interfaces/RequestProxy'; import { RequestProxy } from '../interfaces/RequestProxy'; @@ -41,6 +47,7 @@ export class XHRProxy extends RequestProxy { private providers: XHRProxyProviders; private static originalXhrOpen: typeof XMLHttpRequest.prototype.open; private static originalXhrSend: typeof XMLHttpRequest.prototype.send; + private static originalXhrSetRequestHeader: typeof XMLHttpRequest.prototype.setRequestHeader; constructor(providers: XHRProxyProviders) { super(); @@ -50,12 +57,15 @@ export class XHRProxy extends RequestProxy { onTrackingStart = (context: RequestProxyOptions) => { XHRProxy.originalXhrOpen = this.providers.xhrType.prototype.open; XHRProxy.originalXhrSend = this.providers.xhrType.prototype.send; + XHRProxy.originalXhrSetRequestHeader = this.providers.xhrType.prototype.setRequestHeader; proxyRequests(this.providers, context); }; onTrackingStop = () => { this.providers.xhrType.prototype.open = XHRProxy.originalXhrOpen; this.providers.xhrType.prototype.send = XHRProxy.originalXhrSend; + this.providers.xhrType.prototype.setRequestHeader = + XHRProxy.originalXhrSetRequestHeader; }; } @@ -65,6 +75,7 @@ const proxyRequests = ( ): void => { proxyOpen(providers, context); proxySend(providers); + proxySetRequestHeader(providers); }; const proxyOpen = ( @@ -181,3 +192,32 @@ const reportXhr = async ( resourceContext: xhrProxy }); }; + +const proxySetRequestHeader = (providers: XHRProxyProviders): void => { + const xhrType = providers.xhrType; + const originalXhrSetRequestHeader = xhrType.prototype.setRequestHeader; + + xhrType.prototype.setRequestHeader = function ( + this: DdRumXhr, + header: string, + value: string + ) { + if (isDatadogCustomHeader(header)) { + if (header === DATADOG_GRAPH_QL_OPERATION_NAME_HEADER) { + // TODO: add information to request + return; + } + if (header === DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER) { + // TODO: add information to request + return; + } + if (header === DATADOG_GRAPH_QL_VARIABLES_HEADER) { + // TODO: add information to request + return; + } + } + + // eslint-disable-next-line prefer-rest-params + return originalXhrSetRequestHeader.apply(this, arguments as any); + }; +}; diff --git a/packages/react-native-apollo-client/src/DatadogLink.ts b/packages/react-native-apollo-client/src/DatadogLink.ts index aed0ea7f3..414afd29b 100644 --- a/packages/react-native-apollo-client/src/DatadogLink.ts +++ b/packages/react-native-apollo-client/src/DatadogLink.ts @@ -5,6 +5,11 @@ */ import { ApolloLink } from '@apollo/client'; +import { + DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER, + DATADOG_GRAPH_QL_OPERATION_NAME_HEADER, + DATADOG_GRAPH_QL_VARIABLES_HEADER +} from '@datadog/mobile-react-native'; import { getOperationName, getVariables, getOperationType } from './helpers'; @@ -16,14 +21,22 @@ export class DatadogLink extends ApolloLink { const operationType = getOperationType(operation); operation.setContext(({ headers = {} }) => { + const newHeaders: Record = { + ...headers + }; + + newHeaders[ + DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER + ] = operationType; + newHeaders[ + DATADOG_GRAPH_QL_OPERATION_NAME_HEADER + ] = operationName; + newHeaders[ + DATADOG_GRAPH_QL_VARIABLES_HEADER + ] = formattedVariables; + return { - headers: { - ...headers, - // TODO: import headers from core - '_dd-graph-ql-operation-name': operationName, - '_dd-graph-ql-variables': formattedVariables, - '_dd-graph-ql-operation-type': operationType - } + headers: newHeaders }; }); From 20280726f17ad439233697ac870649e7ace55a40 Mon Sep 17 00:00:00 2001 From: louiszawadzki Date: Thu, 21 Sep 2023 17:31:32 +0200 Subject: [PATCH 5/8] Pass graphql information to native SDKs --- .../DatadogRumResource/ResourceReporter.ts | 55 +++++--- .../requestProxy/XHRProxy/XHRProxy.ts | 13 +- .../XHRProxy/__tests__/XHRProxy.test.ts | 131 +++++++++++++++++- .../requestProxy/interfaces/RumResource.ts | 7 + 4 files changed, 181 insertions(+), 25 deletions(-) diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/DatadogRumResource/ResourceReporter.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/DatadogRumResource/ResourceReporter.ts index 261d7534d..84c9986de 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/DatadogRumResource/ResourceReporter.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/DatadogRumResource/ResourceReporter.ts @@ -34,29 +34,44 @@ export class ResourceReporter { const formatResourceStartContext = ( tracingAttributes: RUMResource['tracingAttributes'] -): Record | undefined => { - return tracingAttributes.samplingPriorityHeader === '0' - ? undefined - : { - '_dd.span_id': tracingAttributes.spanId.toString(10), - '_dd.trace_id': tracingAttributes.traceId.toString(10), - '_dd.rule_psr': tracingAttributes.rulePsr - }; +): Record => { + const attributes: Record = {}; + if (tracingAttributes.samplingPriorityHeader !== '0') { + attributes['_dd.span_id'] = tracingAttributes.spanId.toString(10); + attributes['_dd.trace_id'] = tracingAttributes.traceId.toString(10); + attributes['_dd.rule_psr'] = tracingAttributes.rulePsr; + } + + return attributes; }; const formatResourceStopContext = ( - timings: RUMResource['timings'] + timings: RUMResource['timings'], + graphqlAttributes: RUMResource['graphqlAttributes'] ): Record => { - return { - '_dd.resource_timings': - timings.responseStartTime !== undefined - ? createTimings( - timings.startTime, - timings.responseStartTime, - timings.stopTime - ) - : null - }; + const attributes: Record = {}; + + if (timings.responseStartTime !== undefined) { + attributes['_dd.resource_timings'] = createTimings( + timings.startTime, + timings.responseStartTime, + timings.stopTime + ); + } + + if (graphqlAttributes?.operationType) { + attributes['_dd.graphql.operation_type'] = + graphqlAttributes.operationType; + if (graphqlAttributes.operationName) { + attributes['_dd.graphql.operation_name'] = + graphqlAttributes.operationName; + } + if (graphqlAttributes.variables) { + attributes['_dd.graphql.variables'] = graphqlAttributes.variables; + } + } + + return attributes; }; const reportResource = async (resource: RUMResource) => { @@ -73,7 +88,7 @@ const reportResource = async (resource: RUMResource) => { resource.response.statusCode, resource.request.kind, resource.response.size, - formatResourceStopContext(resource.timings), + formatResourceStopContext(resource.timings, resource.graphqlAttributes), resource.timings.stopTime, resource.resourceContext ); diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts index 2b42c0d6d..ff776509e 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/XHRProxy.ts @@ -28,6 +28,11 @@ interface DdRumXhr extends XMLHttpRequest { } interface DdRumXhrContext { + graphql: { + operationType?: string; + operationName?: string; + variables?: string; + }; method: string; url: string; reported: boolean; @@ -99,6 +104,7 @@ const proxyOpen = ( url, reported: false, timer: new Timer(), + graphql: {}, tracingAttributes: getTracingAttributes({ hostname, firstPartyHostsRegexMap, @@ -177,6 +183,7 @@ const reportXhr = async ( url: context.url, kind: 'xhr' }, + graphqlAttributes: context.graphql, tracingAttributes: context.tracingAttributes, response: { statusCode: xhrProxy.status, @@ -204,15 +211,15 @@ const proxySetRequestHeader = (providers: XHRProxyProviders): void => { ) { if (isDatadogCustomHeader(header)) { if (header === DATADOG_GRAPH_QL_OPERATION_NAME_HEADER) { - // TODO: add information to request + this._datadog_xhr.graphql.operationName = value; return; } if (header === DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER) { - // TODO: add information to request + this._datadog_xhr.graphql.operationType = value; return; } if (header === DATADOG_GRAPH_QL_VARIABLES_HEADER) { - // TODO: add information to request + this._datadog_xhr.graphql.variables = value; return; } } diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts index 00a1804cb..7c5737d45 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts @@ -25,6 +25,11 @@ import { ORIGIN_HEADER_KEY } from '../../../distributedTracing/distributedTracingHeaders'; import { firstPartyHostsRegexMapBuilder } from '../../../distributedTracing/firstPartyHosts'; +import { + DATADOG_GRAPH_QL_OPERATION_NAME_HEADER, + DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER, + DATADOG_GRAPH_QL_VARIABLES_HEADER +} from '../../../graphql/graphqlHeaders'; import { ResourceReporter } from '../DatadogRumResource/ResourceReporter'; import { XHRProxy } from '../XHRProxy'; import { @@ -77,7 +82,7 @@ afterEach(() => { DdRum.unregisterResourceEventMapper(); }); -describe('XHRPr', () => { +describe('XHRProxy', () => { describe('resource interception', () => { it('intercepts XHR request when startTracking() + XHR.open() + XHR.send()', async () => { // GIVEN @@ -798,7 +803,7 @@ describe('XHRPr', () => { // THEN const attributes = DdNativeRum.stopResource.mock.calls[0][4]; - expect(attributes['_dd.resource_timings']).toBeNull(); + expect(attributes['_dd.resource_timings']).toBeUndefined(); }); it('attaches the XMLHttpRequest object containing response to the event mapper', async () => { @@ -1045,4 +1050,126 @@ describe('XHRPr', () => { expect(size).toEqual(-1); }); }); + + describe('setRequestHeader', () => { + it('sets graphql attributes and drops corresponding headers', async () => { + // GIVEN + const method = 'POST'; + const url = 'https://api.example.com/graphql'; + xhrProxy.onTrackingStart({ + tracingSamplingRate: 100, + firstPartyHostsRegexMap: firstPartyHostsRegexMapBuilder([]) + }); + + // WHEN + const xhr = new XMLHttpRequestMock(); + xhr.open(method, url); + xhr.setRequestHeader( + DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER, + 'query' + ); + xhr.setRequestHeader( + DATADOG_GRAPH_QL_OPERATION_NAME_HEADER, + 'cats' + ); + xhr.setRequestHeader(DATADOG_GRAPH_QL_VARIABLES_HEADER, '{}'); + xhr.send(); + xhr.abort(); + xhr.complete(0, undefined); + await flushPromises(); + + // THEN + const attributes = DdNativeRum.stopResource.mock.calls[0][4]; + expect(attributes['_dd.graphql.operation_type']).toEqual('query'); + expect(attributes['_dd.graphql.operation_name']).toEqual('cats'); + expect(attributes['_dd.graphql.variables']).toEqual('{}'); + + expect( + xhr.requestHeaders[DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER] + ).not.toBeDefined(); + expect( + xhr.requestHeaders[DATADOG_GRAPH_QL_OPERATION_NAME_HEADER] + ).not.toBeDefined(); + expect( + xhr.requestHeaders[DATADOG_GRAPH_QL_VARIABLES_HEADER] + ).not.toBeDefined(); + }); + + it('sets graphql attributes and drops corresponding headers when operation name and variables are missing', async () => { + // GIVEN + const method = 'POST'; + const url = 'https://api.example.com/graphql'; + xhrProxy.onTrackingStart({ + tracingSamplingRate: 100, + firstPartyHostsRegexMap: firstPartyHostsRegexMapBuilder([]) + }); + + // WHEN + const xhr = new XMLHttpRequestMock(); + xhr.open(method, url); + xhr.setRequestHeader( + DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER, + 'query' + ); + xhr.send(); + xhr.abort(); + xhr.complete(0, undefined); + await flushPromises(); + + // THEN + const attributes = DdNativeRum.stopResource.mock.calls[0][4]; + expect(attributes['_dd.graphql.operation_type']).toEqual('query'); + expect(attributes['_dd.graphql.operation_name']).not.toBeDefined(); + expect(attributes['_dd.graphql.variables']).not.toBeDefined(); + + expect( + xhr.requestHeaders[DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER] + ).not.toBeDefined(); + expect( + xhr.requestHeaders[DATADOG_GRAPH_QL_OPERATION_NAME_HEADER] + ).not.toBeDefined(); + expect( + xhr.requestHeaders[DATADOG_GRAPH_QL_VARIABLES_HEADER] + ).not.toBeDefined(); + }); + + it('does not set graphql attributes but drops corresponding headers when operation type is missing', async () => { + // GIVEN + const method = 'POST'; + const url = 'https://api.example.com/graphql'; + xhrProxy.onTrackingStart({ + tracingSamplingRate: 100, + firstPartyHostsRegexMap: firstPartyHostsRegexMapBuilder([]) + }); + + // WHEN + const xhr = new XMLHttpRequestMock(); + xhr.open(method, url); + xhr.setRequestHeader( + DATADOG_GRAPH_QL_OPERATION_NAME_HEADER, + 'cats' + ); + xhr.setRequestHeader(DATADOG_GRAPH_QL_VARIABLES_HEADER, '{}'); + xhr.send(); + xhr.abort(); + xhr.complete(0, undefined); + await flushPromises(); + + // THEN + const attributes = DdNativeRum.stopResource.mock.calls[0][4]; + expect(attributes['_dd.graphql.operation_type']).not.toBeDefined(); + expect(attributes['_dd.graphql.operation_name']).not.toBeDefined(); + expect(attributes['_dd.graphql.variables']).not.toBeDefined(); + + expect( + xhr.requestHeaders[DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER] + ).not.toBeDefined(); + expect( + xhr.requestHeaders[DATADOG_GRAPH_QL_OPERATION_NAME_HEADER] + ).not.toBeDefined(); + expect( + xhr.requestHeaders[DATADOG_GRAPH_QL_VARIABLES_HEADER] + ).not.toBeDefined(); + }); + }); }); diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/interfaces/RumResource.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/interfaces/RumResource.ts index 27d1cde13..c3a9939c3 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/interfaces/RumResource.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/interfaces/RumResource.ts @@ -15,6 +15,7 @@ export interface RUMResource { kind: ResourceKind; }; tracingAttributes: DdRumResourceTracingAttributes; + graphqlAttributes?: DdRumResourceGraphqlAttributes; response: { statusCode: number; size: number; @@ -26,3 +27,9 @@ export interface RUMResource { }; resourceContext?: XMLHttpRequest; } + +export type DdRumResourceGraphqlAttributes = { + operationType?: string; + operationName?: string; + variables?: string; +}; From c72d31b4c2cecc65b2454654fa4867b17c23d1d1 Mon Sep 17 00:00:00 2001 From: louiszawadzki Date: Fri, 22 Sep 2023 10:37:23 +0200 Subject: [PATCH 6/8] Add test covering mapper for graphql attributes --- .../XHRProxy/__tests__/XHRProxy.test.ts | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts index 7c5737d45..3b39f242a 100644 --- a/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts +++ b/packages/core/src/rum/instrumentation/resourceTracking/requestProxy/XHRProxy/__tests__/XHRProxy.test.ts @@ -1171,5 +1171,54 @@ describe('XHRProxy', () => { xhr.requestHeaders[DATADOG_GRAPH_QL_VARIABLES_HEADER] ).not.toBeDefined(); }); + + it('enables mapper to edit graphql variables to remove sensitive information', async () => { + // GIVEN + const method = 'POST'; + const url = 'https://api.example.com/graphql'; + xhrProxy.onTrackingStart({ + tracingSamplingRate: 100, + firstPartyHostsRegexMap: firstPartyHostsRegexMapBuilder([]) + }); + DdRum.registerResourceEventMapper(event => { + if (event.context['_dd.graphql.variables']) { + const variables = JSON.parse( + event.context['_dd.graphql.variables'] + ); + if (variables.password) { + variables.password = '***'; + } + event.context['_dd.graphql.variables'] = JSON.stringify( + variables + ); + } + + return event; + }); + + // WHEN + const xhr = new XMLHttpRequestMock(); + xhr.open(method, url); + xhr.setRequestHeader( + DATADOG_GRAPH_QL_OPERATION_TYPE_HEADER, + 'query' + ); + xhr.setRequestHeader( + DATADOG_GRAPH_QL_VARIABLES_HEADER, + JSON.stringify({ password: 'SECRET' }) + ); + xhr.send(); + xhr.abort(); + xhr.complete(0, undefined); + await flushPromises(); + + // THEN + const attributes = DdNativeRum.stopResource.mock.calls[0][4]; + expect(attributes['_dd.graphql.operation_type']).toBe('query'); + expect(attributes['_dd.graphql.operation_name']).not.toBeDefined(); + expect(attributes['_dd.graphql.variables']).toBe( + '{"password":"***"}' + ); + }); }); }); From 1b2061693c135186854a7e7c641020e564067dfa Mon Sep 17 00:00:00 2001 From: louiszawadzki Date: Fri, 22 Sep 2023 14:04:06 +0200 Subject: [PATCH 7/8] Document Apollo client link --- packages/react-native-apollo-client/README.md | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/packages/react-native-apollo-client/README.md b/packages/react-native-apollo-client/README.md index 04780b4fd..07710627c 100644 --- a/packages/react-native-apollo-client/README.md +++ b/packages/react-native-apollo-client/README.md @@ -1 +1,93 @@ # React-Native Monitoring for @apollo/client + +## Notice: this integration is not ready for production + +Adding the GraphQL integration does not change any existing features. The information is not surfaced on the Datadog platform yet. + +## Overview + +Monitor your GraphQL resources with Real User Monitoring (RUM) and perform the following: + +- Identify GraphQL queries and mutations +- Identify GraphQL variables used in queries and mutations + +RUM supports GraphQL requests created using [@apollo/client][2]. + +## Setup + +### Prerequisites + +Set up the RUM React Native SDK on your mobile React Native application. For more information, see [RUM React Native Monitoring][1]. + +Add `@apollo/client` to your application following the [official installation documentation][3]. + +### Instrument your ApolloClient + +#### Migrate to HttpLink + +If you initialize your ApolloClient with the `uri` parameter, initialize it with a `HttpLink`: + +```javascript +import { ApolloClient, HttpLink } from '@apollo/client'; + +// before +const apolloClient = new ApolloClient({ + uri: 'https://my.api.com/graphql' +}); + +// after +const apolloClient = new ApolloClient({ + link: new HttpLink({ uri: 'https://my.api.com/graphql' }) +}); +``` + +#### Use the Datadog Apollo Client Link to collect information + +Import `DatadogLink` from `@datadog/mobile-react-native-apollo-client` and use it in your ApolloClient initialization: + +```javascript +import { ApolloClient, from, HttpLink } from '@apollo/client'; +import { DatadogLink } from '@datadog/mobile-react-native-apollo-client'; + +const apolloClient = new ApolloClient({ + link: from([ + new DatadogLink(), + new HttpLink({ uri: 'https://my.api.com/graphql' }) // always in last position + ]) +}); +``` + +For more information on Apollo Client Links, refer to the [official documentation][4]. + +### Removing GraphQL information + +Use a `resourceEventMapper` in your Datadog configuration to remove sensitive data from GraphQL variables: + +```javascript +const datadogConfiguration = new DatadogProviderConfiguration( + '', + '', + '', + true, + true, + true +); + +datadogConfiguration.resourceEventMapper = event => { + // Variables are stored in event.context['_dd.graphql.variables'] as a JSON string when present + if (event.context['_dd.graphql.variables']) { + const variables = JSON.parse(event.context['_dd.graphql.variables']); + if (variables.password) { + variables.password = '***'; + } + event.context['_dd.graphql.variables'] = JSON.stringify(variables); + } + + return event; +}; +``` + +[1]: https://docs.datadoghq.com/real_user_monitoring/reactnative/ +[2]: https://www.apollographql.com/docs/react/ +[3]: https://www.apollographql.com/docs/react/get-started +[4]: https://www.apollographql.com/docs/react/api/link/introduction/ From 10af1678cd1ec5fc4241319b00a1477daee9ddb0 Mon Sep 17 00:00:00 2001 From: louiszawadzki Date: Wed, 27 Sep 2023 11:22:15 +0200 Subject: [PATCH 8/8] Add RUM task link for telemetry --- packages/react-native-apollo-client/src/helpers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react-native-apollo-client/src/helpers.ts b/packages/react-native-apollo-client/src/helpers.ts index 9cd68065e..2154e7cfb 100644 --- a/packages/react-native-apollo-client/src/helpers.ts +++ b/packages/react-native-apollo-client/src/helpers.ts @@ -16,7 +16,7 @@ export const getVariables = (operation: Operation): string | null => { try { return JSON.stringify(operation.variables); } catch (e) { - // TODO: telemetry + // TODO RUM-1206: telemetry return null; } } @@ -48,7 +48,7 @@ export const getOperationType = ( })[0] || null ); } catch (e) { - // TODO: telemetry + // TODO RUM-1206: telemetry return null; } };