@planningcenter/chat-react-native 3.37.0-rc.3 → 3.37.0-rc.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/utils/client/instrumented_fetch.d.ts +2 -0
- package/build/utils/client/instrumented_fetch.d.ts.map +1 -0
- package/build/utils/client/instrumented_fetch.js +64 -0
- package/build/utils/client/instrumented_fetch.js.map +1 -0
- package/build/utils/client/request_helpers.d.ts.map +1 -1
- package/build/utils/client/request_helpers.js +2 -1
- package/build/utils/client/request_helpers.js.map +1 -1
- package/package.json +2 -2
- package/src/utils/client/__tests__/instrumented_fetch.test.ts +84 -0
- package/src/utils/client/instrumented_fetch.ts +69 -0
- package/src/utils/client/request_helpers.ts +2 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instrumented_fetch.d.ts","sourceRoot":"","sources":["../../../src/utils/client/instrumented_fetch.ts"],"names":[],"mappings":"AASA,wBAAsB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,CAUzF"}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Log } from '../native_adapters/configuration';
|
|
2
|
+
const SHARED_TAGS = {
|
|
3
|
+
team: 'chat',
|
|
4
|
+
package: 'chat-react-native',
|
|
5
|
+
};
|
|
6
|
+
const SKIPPED_STATUSES = [401, 403, 404];
|
|
7
|
+
export async function instrumentedFetch(url, init) {
|
|
8
|
+
const method = init.method ?? 'GET';
|
|
9
|
+
try {
|
|
10
|
+
const response = await fetch(url, init);
|
|
11
|
+
if (!response.ok)
|
|
12
|
+
reportHttpError(response, method, url);
|
|
13
|
+
return response;
|
|
14
|
+
}
|
|
15
|
+
catch (networkError) {
|
|
16
|
+
reportNetworkError(networkError, method, url);
|
|
17
|
+
throw networkError;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function reportHttpError(response, method, url) {
|
|
21
|
+
const { status } = response;
|
|
22
|
+
if (SKIPPED_STATUSES.includes(status))
|
|
23
|
+
return;
|
|
24
|
+
const path = templatePath(url);
|
|
25
|
+
const error = new Error(`HTTP ${status} ${method} ${path}`);
|
|
26
|
+
error.name = `HTTPError${status}`;
|
|
27
|
+
Log.reportError(error, {
|
|
28
|
+
scope: 'http',
|
|
29
|
+
tags: {
|
|
30
|
+
...SHARED_TAGS,
|
|
31
|
+
'http.status': String(status),
|
|
32
|
+
'http.method': method,
|
|
33
|
+
'http.path': path,
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function reportNetworkError(networkError, method, url) {
|
|
38
|
+
const path = templatePath(url);
|
|
39
|
+
const error = new Error(`Network failure ${method} ${path}: ${networkError.message}`);
|
|
40
|
+
error.name = 'NetworkError';
|
|
41
|
+
Log.reportError(error, {
|
|
42
|
+
scope: 'http',
|
|
43
|
+
tags: {
|
|
44
|
+
...SHARED_TAGS,
|
|
45
|
+
'http.method': method,
|
|
46
|
+
'http.path': path,
|
|
47
|
+
'http.error': 'network',
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
function templatePath(url) {
|
|
52
|
+
let path;
|
|
53
|
+
try {
|
|
54
|
+
path = new URL(url).pathname;
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
path = url;
|
|
58
|
+
}
|
|
59
|
+
return path
|
|
60
|
+
.split('/')
|
|
61
|
+
.map(segment => (/^\d+$/.test(segment) ? ':id' : segment))
|
|
62
|
+
.join('/');
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=instrumented_fetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instrumented_fetch.js","sourceRoot":"","sources":["../../../src/utils/client/instrumented_fetch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,kCAAkC,CAAA;AAEtD,MAAM,WAAW,GAAG;IAClB,IAAI,EAAE,MAAM;IACZ,OAAO,EAAE,mBAAmB;CACpB,CAAA;AAEV,MAAM,gBAAgB,GAAG,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAA;AAExC,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,GAAW,EAAE,IAAiB;IACpE,MAAM,MAAM,GAAG,IAAI,CAAC,MAAM,IAAI,KAAK,CAAA;IACnC,IAAI,CAAC;QACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,GAAG,EAAE,IAAI,CAAC,CAAA;QACvC,IAAI,CAAC,QAAQ,CAAC,EAAE;YAAE,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,CAAC,CAAA;QACxD,OAAO,QAAQ,CAAA;IACjB,CAAC;IAAC,OAAO,YAAY,EAAE,CAAC;QACtB,kBAAkB,CAAC,YAAqB,EAAE,MAAM,EAAE,GAAG,CAAC,CAAA;QACtD,MAAM,YAAY,CAAA;IACpB,CAAC;AACH,CAAC;AAED,SAAS,eAAe,CAAC,QAAkB,EAAE,MAAc,EAAE,GAAW;IACtE,MAAM,EAAE,MAAM,EAAE,GAAG,QAAQ,CAAA;IAC3B,IAAI,gBAAgB,CAAC,QAAQ,CAAC,MAAM,CAAC;QAAE,OAAM;IAE7C,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,CAAA;IAC9B,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,QAAQ,MAAM,IAAI,MAAM,IAAI,IAAI,EAAE,CAAC,CAAA;IAC3D,KAAK,CAAC,IAAI,GAAG,YAAY,MAAM,EAAE,CAAA;IAEjC,GAAG,CAAC,WAAW,CAAC,KAAK,EAAE;QACrB,KAAK,EAAE,MAAM;QACb,IAAI,EAAE;YACJ,GAAG,WAAW;YACd,aAAa,EAAE,MAAM,CAAC,MAAM,CAAC;YAC7B,aAAa,EAAE,MAAM;YACrB,WAAW,EAAE,IAAI;SAClB;KACF,CAAC,CAAA;AACJ,CAAC;AAED,SAAS,kBAAkB,CAAC,YAAmB,EAAE,MAAc,EAAE,GAAW;IAC1E,MAAM,IAAI,GAAG,YAAY,CAAC,GAAG,CAAC,CAAA;IAC9B,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,mBAAmB,MAAM,IAAI,IAAI,KAAK,YAAY,CAAC,OAAO,EAAE,CAAC,CAAA;IACrF,KAAK,CAAC,IAAI,GAAG,cAAc,CAAA;IAE3B,GAAG,CAAC,WAAW,CAAC,KAAK,EAAE;QACrB,KAAK,EAAE,MAAM;QACb,IAAI,EAAE;YACJ,GAAG,WAAW;YACd,aAAa,EAAE,MAAM;YACrB,WAAW,EAAE,IAAI;YACjB,YAAY,EAAE,SAAS;SACxB;KACF,CAAC,CAAA;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,GAAW;IAC/B,IAAI,IAAY,CAAA;IAChB,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,CAAA;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,IAAI,GAAG,GAAG,CAAA;IACZ,CAAC;IAED,OAAO,IAAI;SACR,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC;SACzD,IAAI,CAAC,GAAG,CAAC,CAAA;AACd,CAAC","sourcesContent":["import { Log } from '../native_adapters/configuration'\n\nconst SHARED_TAGS = {\n team: 'chat',\n package: 'chat-react-native',\n} as const\n\nconst SKIPPED_STATUSES = [401, 403, 404]\n\nexport async function instrumentedFetch(url: string, init: RequestInit): Promise<Response> {\n const method = init.method ?? 'GET'\n try {\n const response = await fetch(url, init)\n if (!response.ok) reportHttpError(response, method, url)\n return response\n } catch (networkError) {\n reportNetworkError(networkError as Error, method, url)\n throw networkError\n }\n}\n\nfunction reportHttpError(response: Response, method: string, url: string) {\n const { status } = response\n if (SKIPPED_STATUSES.includes(status)) return\n\n const path = templatePath(url)\n const error = new Error(`HTTP ${status} ${method} ${path}`)\n error.name = `HTTPError${status}`\n\n Log.reportError(error, {\n scope: 'http',\n tags: {\n ...SHARED_TAGS,\n 'http.status': String(status),\n 'http.method': method,\n 'http.path': path,\n },\n })\n}\n\nfunction reportNetworkError(networkError: Error, method: string, url: string) {\n const path = templatePath(url)\n const error = new Error(`Network failure ${method} ${path}: ${networkError.message}`)\n error.name = 'NetworkError'\n\n Log.reportError(error, {\n scope: 'http',\n tags: {\n ...SHARED_TAGS,\n 'http.method': method,\n 'http.path': path,\n 'http.error': 'network',\n },\n })\n}\n\nfunction templatePath(url: string): string {\n let path: string\n try {\n path = new URL(url).pathname\n } catch {\n path = url\n }\n\n return path\n .split('/')\n .map(segment => (/^\\d+$/.test(segment) ? ':id' : segment))\n .join('/')\n}\n"]}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request_helpers.d.ts","sourceRoot":"","sources":["../../../src/utils/client/request_helpers.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"request_helpers.d.ts","sourceRoot":"","sources":["../../../src/utils/client/request_helpers.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,SAAS,CAAA;AAExF,MAAM,MAAM,eAAe,GAAG;IAC5B,MAAM,EAAE,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAA;IAC3C,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,CAAC,EAAE,OAAO,CAAC,WAAW,CAAC,CAAA;IAC3B,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;CAChC,CAAA;AAED,eAAO,MAAM,WAAW,GAAI,gCAAkD,eAAe,iBAgC5F,CAAA;AAcD,eAAO,MAAM,aAAa,GAAI,SAAS,WAAW,EAAE,aAAa,WAAW;;;;CAM1E,CAAA;AAEF,KAAK,IAAI,GAAG,CAAC,IAAI,EAAE,eAAe,GAAG,WAAW,KAAK,OAAO,CAAC,GAAG,CAAC,CAAA;AACjE,KAAK,QAAQ,GAAG,eAAe,GAAG;IAChC,IAAI,CAAC,EAAE;QACL,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAA;KAC7B,CAAA;CACF,CAAA;AAED,eAAO,MAAM,yBAAyB,GAAU,MAAM,IAAI,EAAE,MAAM,QAAQ,iBAuBzE,CAAA;AAED,eAAO,MAAM,cAAc,GAAI,MAAM,IAAI,EAAE,MAAM,QAAQ,iBAG7B,CAAA;AAE5B,eAAO,MAAM,2BAA2B,GAAI,SAAS,OAAO,WAAW,EAAE,MAAM,QAAQ,iBAG3D,CAAA;AAK5B,eAAO,MAAM,iBAAiB,SAbO,IAAI,QAAQ,QAAQ,iBAac,CAAA;AAEvE,eAAO,MAAM,wBAAwB,YAVgB,OAAO,WAAW,QAAQ,QAAQ,iBAUI,CAAA;AAE3F,eAAO,MAAM,uBAAuB,GAAI,KAAK,MAAM,kBAMlD,CAAA;AAED,eAAO,MAAM,kBAAkB,GAAI,MAAM,UAAU,KAAG,WAkBrD,CAAA"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import _ from 'lodash';
|
|
2
2
|
import URI from 'urijs';
|
|
3
|
+
import { instrumentedFetch } from './instrumented_fetch';
|
|
3
4
|
import { transformRequestData } from './transform_request_data';
|
|
4
5
|
import transformResponse from './transform_response';
|
|
5
6
|
export const makeRequest = ({ action = 'GET', url, data = {}, headers = {} }) => {
|
|
@@ -19,7 +20,7 @@ export const makeRequest = ({ action = 'GET', url, data = {}, headers = {} }) =>
|
|
|
19
20
|
uri = uri.query(combinedQuery);
|
|
20
21
|
}
|
|
21
22
|
const body = ['POST', 'PATCH'].includes(action) ? JSON.stringify(data) : undefined;
|
|
22
|
-
return
|
|
23
|
+
return instrumentedFetch(decodeURIComponent(uri.toString()), {
|
|
23
24
|
method: action,
|
|
24
25
|
headers,
|
|
25
26
|
body,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"request_helpers.js","sourceRoot":"","sources":["../../../src/utils/client/request_helpers.ts"],"names":[],"mappings":"AAAA,OAAO,CAAC,MAAM,QAAQ,CAAA;AACtB,OAAO,GAAG,MAAM,OAAO,CAAA;AACvB,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAA;AAC/D,OAAO,iBAAiB,MAAM,sBAAsB,CAAA;AAUpD,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,EAAE,MAAM,GAAG,KAAK,EAAE,GAAG,EAAE,IAAI,GAAG,EAAE,EAAE,OAAO,GAAG,EAAE,EAAmB,EAAE,EAAE;IAC/F,sEAAsE;IACtE,IAAI,GAAG,GAAG,IAAI,GAAG,CAAC,GAAG,CAAC,CAAA;IACtB,MAAM,KAAK,GAAG,oBAAoB,CAAC,IAAI,CAAC,CAAA;IACxC,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAA;IAEhC,kFAAkF;IAClF,MAAM,aAAa,GAAG,MAAM,CAAC,OAAO,CAAC,EAAE,GAAG,KAAK,EAAE,GAAG,QAAQ,EAAE,CAAC;SAC5D,IAAI,EAAE;SACN,MAAM,CAAC,CAAC,GAAG,EAAE,CAAC,GAAG,EAAE,KAAK,CAAC,EAAE,EAAE;QAC5B,aAAa;QACb,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAA;QAChB,OAAO,GAAG,CAAA;IACZ,CAAC,EAAE,EAAE,CAAC,CAAA;IAER,IAAI,MAAM,KAAK,KAAK,EAAE,CAAC;QACrB,GAAG,GAAG,GAAG,CAAC,KAAK,CAAC,aAAa,CAAC,CAAA;IAChC,CAAC;IAED,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IAElF,OAAO,
|
|
1
|
+
{"version":3,"file":"request_helpers.js","sourceRoot":"","sources":["../../../src/utils/client/request_helpers.ts"],"names":[],"mappings":"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","sourcesContent":["import _ from 'lodash'\nimport URI from 'urijs'\nimport { instrumentedFetch } from './instrumented_fetch'\nimport { transformRequestData } from './transform_request_data'\nimport transformResponse from './transform_response'\nimport { Accumulator, GetRequest, PostRequest, RequestData, WalkRequest } from './types'\n\nexport type MakeRequestArgs = {\n action: 'GET' | 'POST' | 'PATCH' | 'DELETE'\n url: string\n data?: Partial<RequestData>\n headers: Record<string, string>\n}\n\nexport const makeRequest = ({ action = 'GET', url, data = {}, headers = {} }: MakeRequestArgs) => {\n // break apart url so we can reconstruct the url with the query params\n let uri = new URI(url)\n const query = transformRequestData(data)\n const urlQuery = uri.query(true)\n\n // This likely doesn't matter but will enforce consistent ordering of query params\n const combinedQuery = Object.entries({ ...query, ...urlQuery })\n .sort()\n .reduce((obj, [key, value]) => {\n // @ts-ignore\n obj[key] = value\n return obj\n }, {})\n\n if (action === 'GET') {\n uri = uri.query(combinedQuery)\n }\n\n const body = ['POST', 'PATCH'].includes(action) ? JSON.stringify(data) : undefined\n\n return instrumentedFetch(decodeURIComponent(uri.toString()), {\n method: action,\n headers,\n body,\n }).then(response => {\n if (response.ok) {\n return response.text().then(t => (t === '' ? '' : JSON.parse(t)))\n } else {\n return Promise.reject(response)\n }\n })\n}\n\nconst STANDARD_META_ATTRIBUTES = [\n 'total_count',\n 'count',\n 'prev',\n 'next',\n 'can_order_by',\n 'can_query_by',\n 'can_include',\n 'can_filter',\n 'parent',\n]\n\nexport const concatRecords = (records: Accumulator, moreRecords: Accumulator) => ({\n data: [...records.data, ...moreRecords.data],\n included: [...records.included, ...moreRecords.included],\n // Because custom meta can be computed on a per-page bases,\n // it is safer to remove all custom meta when concatenating multiple pages of data\n meta: _.pick(moreRecords.meta, STANDARD_META_ATTRIBUTES),\n})\n\ntype Walk = (args: MakeRequestArgs | WalkRequest) => Promise<any>\ntype WalkArgs = MakeRequestArgs & {\n data?: {\n fields?: Record<string, any>\n }\n}\n\nexport const throwErrorIfFieldsMissing = async (walk: Walk, args: WalkArgs) => {\n const fields = args?.data?.fields\n\n if (!fields) {\n throw new Error(`Must pass fields for request: ${args.url}`)\n }\n\n const fieldTypes = Object.keys(fields)\n\n const response = await walk(args)\n\n const dataTypes = _(response.data).castArray().map('type').value()\n const includedTypes = _.map(response.included, 'type')\n const responseTypes = _.uniq(dataTypes.concat(includedTypes))\n const missingFields = _.difference(responseTypes, fieldTypes).filter(t => t)\n\n if (missingFields.length > 0) {\n throw new Error(\n `Must include fields for types: ${JSON.stringify(missingFields)} for request ${args.url}`\n )\n }\n\n return transformResponse(response)\n}\n\nexport const friendlyErrors = (walk: Walk, args: WalkArgs) =>\n throwErrorIfQueryParams(args.url)\n .then(() => throwErrorIfFieldsMissing(walk, args))\n .then(transformResponse)\n\nexport const noQueryParamsFriendlyErrors = (request: typeof makeRequest, args: WalkArgs) =>\n throwErrorIfQueryParams(args.url)\n .then(() => request(args))\n .then(transformResponse)\n\nconst passthrough = async <T, R>(walk: (args: T) => Promise<R>, args: T) =>\n Promise.resolve(transformResponse(await walk(args)))\n\nexport const ensureFieldsInDev = __DEV__ ? friendlyErrors : passthrough\n\nexport const ensureNoQueryParamsInDev = __DEV__ ? noQueryParamsFriendlyErrors : passthrough\n\nexport const throwErrorIfQueryParams = (url: string) => {\n if (new URI(url).search().length) {\n throw new Error('Must pass query params as data arg')\n }\n\n return Promise.resolve()\n}\n\nexport const transformGetToPost = (args: GetRequest): PostRequest => {\n const { data, ...rest } = args\n const { fields, include } = data\n const fieldsArray = Object.entries(fields).map(([key, value]) => {\n if (Array.isArray(value)) {\n return [key, value.join(',')]\n }\n\n return [key, value]\n })\n\n return {\n ...rest,\n data: {\n fields: Object.fromEntries(fieldsArray),\n include,\n },\n }\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@planningcenter/chat-react-native",
|
|
3
|
-
"version": "3.37.0-rc.
|
|
3
|
+
"version": "3.37.0-rc.4",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"react-native": "./src/index.tsx",
|
|
@@ -72,5 +72,5 @@
|
|
|
72
72
|
"react-native-url-polyfill": "^2.0.0",
|
|
73
73
|
"typescript": "~5.9.2"
|
|
74
74
|
},
|
|
75
|
-
"gitHead": "
|
|
75
|
+
"gitHead": "47b2675fc5a5c7b4dcca6a62690fe01a4e4151f1"
|
|
76
76
|
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Log } from '../../native_adapters/configuration'
|
|
2
|
+
import { instrumentedFetch } from '../instrumented_fetch'
|
|
3
|
+
|
|
4
|
+
const buildResponse = (status: number) => new Response('', { status })
|
|
5
|
+
|
|
6
|
+
describe('instrumentedFetch', () => {
|
|
7
|
+
let reportError: jest.SpyInstance
|
|
8
|
+
let fetchSpy: jest.SpyInstance
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
reportError = jest.spyOn(Log, 'reportError').mockImplementation(() => {})
|
|
12
|
+
fetchSpy = jest.spyOn(globalThis, 'fetch')
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
reportError.mockRestore()
|
|
17
|
+
fetchSpy.mockRestore()
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('passes successful responses through without reporting', async () => {
|
|
21
|
+
const response = buildResponse(200)
|
|
22
|
+
fetchSpy.mockResolvedValueOnce(response)
|
|
23
|
+
|
|
24
|
+
const result = await instrumentedFetch('https://api.example.com/conversations', {
|
|
25
|
+
method: 'GET',
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
expect(result).toBe(response)
|
|
29
|
+
expect(reportError).not.toHaveBeenCalled()
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it.each([400, 422, 500])(
|
|
33
|
+
'reports %d with method, status, and templated path tags',
|
|
34
|
+
async status => {
|
|
35
|
+
fetchSpy.mockResolvedValueOnce(buildResponse(status))
|
|
36
|
+
|
|
37
|
+
await instrumentedFetch('https://api.example.com/conversations/123/messages', {
|
|
38
|
+
method: 'POST',
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
expect(reportError).toHaveBeenCalledTimes(1)
|
|
42
|
+
const [error, context] = reportError.mock.calls[0]
|
|
43
|
+
expect(error.name).toBe(`HTTPError${status}`)
|
|
44
|
+
expect(error.message).toBe(`HTTP ${status} POST /conversations/:id/messages`)
|
|
45
|
+
expect(context).toMatchObject({
|
|
46
|
+
scope: 'http',
|
|
47
|
+
tags: {
|
|
48
|
+
team: 'chat',
|
|
49
|
+
package: 'chat-react-native',
|
|
50
|
+
'http.status': String(status),
|
|
51
|
+
'http.method': 'POST',
|
|
52
|
+
'http.path': '/conversations/:id/messages',
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
it.each([401, 403, 404])('does not report %d (expected user states)', async status => {
|
|
59
|
+
fetchSpy.mockResolvedValueOnce(buildResponse(status))
|
|
60
|
+
|
|
61
|
+
await instrumentedFetch('https://api.example.com/x', { method: 'GET' })
|
|
62
|
+
|
|
63
|
+
expect(reportError).not.toHaveBeenCalled()
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('reports network failures with http.error=network', async () => {
|
|
67
|
+
const networkError = new TypeError('Network request failed')
|
|
68
|
+
fetchSpy.mockRejectedValueOnce(networkError)
|
|
69
|
+
|
|
70
|
+
await expect(
|
|
71
|
+
instrumentedFetch('https://api.example.com/conversations/123', { method: 'GET' })
|
|
72
|
+
).rejects.toBe(networkError)
|
|
73
|
+
|
|
74
|
+
expect(reportError).toHaveBeenCalledTimes(1)
|
|
75
|
+
const [error, context] = reportError.mock.calls[0]
|
|
76
|
+
expect(error.name).toBe('NetworkError')
|
|
77
|
+
expect(error.message).toContain('Network failure GET /conversations/:id')
|
|
78
|
+
expect(context.tags).toMatchObject({
|
|
79
|
+
'http.method': 'GET',
|
|
80
|
+
'http.path': '/conversations/:id',
|
|
81
|
+
'http.error': 'network',
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
})
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Log } from '../native_adapters/configuration'
|
|
2
|
+
|
|
3
|
+
const SHARED_TAGS = {
|
|
4
|
+
team: 'chat',
|
|
5
|
+
package: 'chat-react-native',
|
|
6
|
+
} as const
|
|
7
|
+
|
|
8
|
+
const SKIPPED_STATUSES = [401, 403, 404]
|
|
9
|
+
|
|
10
|
+
export async function instrumentedFetch(url: string, init: RequestInit): Promise<Response> {
|
|
11
|
+
const method = init.method ?? 'GET'
|
|
12
|
+
try {
|
|
13
|
+
const response = await fetch(url, init)
|
|
14
|
+
if (!response.ok) reportHttpError(response, method, url)
|
|
15
|
+
return response
|
|
16
|
+
} catch (networkError) {
|
|
17
|
+
reportNetworkError(networkError as Error, method, url)
|
|
18
|
+
throw networkError
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function reportHttpError(response: Response, method: string, url: string) {
|
|
23
|
+
const { status } = response
|
|
24
|
+
if (SKIPPED_STATUSES.includes(status)) return
|
|
25
|
+
|
|
26
|
+
const path = templatePath(url)
|
|
27
|
+
const error = new Error(`HTTP ${status} ${method} ${path}`)
|
|
28
|
+
error.name = `HTTPError${status}`
|
|
29
|
+
|
|
30
|
+
Log.reportError(error, {
|
|
31
|
+
scope: 'http',
|
|
32
|
+
tags: {
|
|
33
|
+
...SHARED_TAGS,
|
|
34
|
+
'http.status': String(status),
|
|
35
|
+
'http.method': method,
|
|
36
|
+
'http.path': path,
|
|
37
|
+
},
|
|
38
|
+
})
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function reportNetworkError(networkError: Error, method: string, url: string) {
|
|
42
|
+
const path = templatePath(url)
|
|
43
|
+
const error = new Error(`Network failure ${method} ${path}: ${networkError.message}`)
|
|
44
|
+
error.name = 'NetworkError'
|
|
45
|
+
|
|
46
|
+
Log.reportError(error, {
|
|
47
|
+
scope: 'http',
|
|
48
|
+
tags: {
|
|
49
|
+
...SHARED_TAGS,
|
|
50
|
+
'http.method': method,
|
|
51
|
+
'http.path': path,
|
|
52
|
+
'http.error': 'network',
|
|
53
|
+
},
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function templatePath(url: string): string {
|
|
58
|
+
let path: string
|
|
59
|
+
try {
|
|
60
|
+
path = new URL(url).pathname
|
|
61
|
+
} catch {
|
|
62
|
+
path = url
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return path
|
|
66
|
+
.split('/')
|
|
67
|
+
.map(segment => (/^\d+$/.test(segment) ? ':id' : segment))
|
|
68
|
+
.join('/')
|
|
69
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import _ from 'lodash'
|
|
2
2
|
import URI from 'urijs'
|
|
3
|
+
import { instrumentedFetch } from './instrumented_fetch'
|
|
3
4
|
import { transformRequestData } from './transform_request_data'
|
|
4
5
|
import transformResponse from './transform_response'
|
|
5
6
|
import { Accumulator, GetRequest, PostRequest, RequestData, WalkRequest } from './types'
|
|
@@ -32,7 +33,7 @@ export const makeRequest = ({ action = 'GET', url, data = {}, headers = {} }: Ma
|
|
|
32
33
|
|
|
33
34
|
const body = ['POST', 'PATCH'].includes(action) ? JSON.stringify(data) : undefined
|
|
34
35
|
|
|
35
|
-
return
|
|
36
|
+
return instrumentedFetch(decodeURIComponent(uri.toString()), {
|
|
36
37
|
method: action,
|
|
37
38
|
headers,
|
|
38
39
|
body,
|