Skip to content

Commit 899f2a0

Browse files
committed
feat: extracted to standalone project
0 parents  commit 899f2a0

13 files changed

Lines changed: 519 additions & 0 deletions
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Test and Release
2+
3+
on: push
4+
5+
env:
6+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
7+
8+
permissions:
9+
contents: write
10+
issues: write
11+
id-token: write
12+
13+
jobs:
14+
tests:
15+
runs-on: ubuntu-24.04
16+
17+
steps:
18+
- uses: actions/checkout@v5
19+
20+
- uses: actions/setup-node@v4
21+
with:
22+
node-version: "22.x"
23+
cache: "npm"
24+
25+
- name: Install dependencies
26+
run: npm ci --no-audit
27+
28+
- run: npm test
29+
30+
- name: Semantic release
31+
continue-on-error: true
32+
run: |
33+
npm install -D @sebbo2002/semantic-release-jsr
34+
npx semantic-release

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
node_modules/

FetchImplementation.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
export type FetchImplementation = (
2+
url: URL,
3+
options?: Omit<RequestInit, 'headers'> & {
4+
headers?: Record<string, string>
5+
},
6+
) => Promise<Response>

LICENSE

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
BSD 3-Clause License
2+
3+
Copyright (c) 2025, Nordic Semiconductor ASA | nordicsemi.no
4+
All rights reserved.
5+
6+
Redistribution and use in source and binary forms, with or without
7+
modification, are permitted provided that the following conditions are met:
8+
9+
1. Redistributions of source code must retain the above copyright notice, this
10+
list of conditions and the following disclaimer.
11+
12+
2. Redistributions in binary form must reproduce the above copyright notice,
13+
this list of conditions and the following disclaimer in the documentation
14+
and/or other materials provided with the distribution.
15+
16+
3. Neither the name of the copyright holder nor the names of its
17+
contributors may be used to endorse or promote products derived from
18+
this software without specific prior written permission.
19+
20+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
# `@nrfcloud/fetch-with-debug`
2+
3+
<https://jsr.io/@nrfcloud/fetch-with-debug>
4+
5+
Simple wrapper around fetch that logs request and response.
6+
7+
## Install with NPM
8+
9+
```bash
10+
npx jsr add --save-dev @nrfcloud/fetch-with-debug
11+
```
12+
13+
## Usage
14+
15+
```typescript
16+
import { fetchWithDebug } from "@bifravst/fetch-with-debug";
17+
18+
const fetch = fetchWithDebug(
19+
(type, details) => console.log("[My Service]", type, JSON.stringify(details)),
20+
(args) => console.error("[My Service]", JSON.stringify(args)),
21+
(args) => console.debug("[My Service]", JSON.stringify(args)),
22+
);
23+
24+
const res = await fetch(new URL("https://example.com"));
25+
```

export.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from './FetchImplementation.ts'
2+
export * from './fetchWithDebug.ts'

fetchWithDebug.spec.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import nock from 'nock'
2+
import assert from 'node:assert'
3+
import { describe, it, mock } from 'node:test'
4+
import { fetchWithDebug } from './fetchWithDebug.ts'
5+
6+
void describe('fetchWithDebug', (): void => {
7+
void it('should debug a request', async () => {
8+
const scope = nock('https://example.com').post('/foo').reply(
9+
200,
10+
{ foo: 'bar' },
11+
{
12+
'Content-Type': 'application/json',
13+
},
14+
)
15+
16+
const mockLog = mock.fn()
17+
const mockError = mock.fn()
18+
const mockBody = mock.fn()
19+
20+
const res = await fetchWithDebug(
21+
mockLog,
22+
mockError,
23+
mockBody,
24+
)(new URL('https://example.com/foo'), {
25+
method: 'POST',
26+
body: JSON.stringify({ bar: 'baz' }),
27+
headers: {
28+
'Content-Type': 'application/json',
29+
},
30+
})
31+
32+
assert.equal(res.status, 200)
33+
assert.deepEqual(await res.json(), { foo: 'bar' })
34+
35+
const requestLog = mockLog.mock.calls.find(
36+
(call) => call.arguments[0] === 'request',
37+
)
38+
const responseLog = mockLog.mock.calls.find(
39+
(call) => call.arguments[0] === 'response',
40+
)
41+
42+
assert.ok(requestLog)
43+
assert.ok(responseLog)
44+
45+
assert.deepEqual(requestLog?.arguments[1], {
46+
url: 'https://example.com/foo',
47+
method: 'POST',
48+
headers: {
49+
'Content-Type': 'application/json',
50+
},
51+
body: JSON.stringify({ bar: 'baz' }),
52+
})
53+
54+
assert.partialDeepStrictEqual(responseLog?.arguments[1], {
55+
url: 'https://example.com/foo',
56+
status: 200,
57+
ok: true,
58+
headers: {},
59+
})
60+
61+
assert.deepEqual(mockBody.mock.calls[0]?.arguments[0], { foo: 'bar' })
62+
63+
assert.ok(scope.isDone())
64+
})
65+
})

fetchWithDebug.ts

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import type { FetchImplementation } from './FetchImplementation.ts'
2+
3+
export const fetchWithDebug: (
4+
log?: (type: 'request' | 'response', details: any) => void,
5+
error?: (details: any) => void,
6+
body?: (body: any) => void,
7+
) => FetchImplementation =
8+
(log = console.log, logError = console.error, body = console.debug) =>
9+
async (url, options) => {
10+
// Log request details
11+
log('request', {
12+
url: url.toString(),
13+
method: options?.method ?? 'GET',
14+
headers: options?.headers,
15+
body: options?.body,
16+
...(options?.signal !== undefined && { signal: 'AbortSignal provided' }),
17+
...(options?.credentials !== undefined && {
18+
credentials: options.credentials,
19+
}),
20+
...(options?.cache !== undefined && { cache: options.cache }),
21+
...(options?.redirect !== undefined && { redirect: options.redirect }),
22+
...(options?.referrer !== undefined && { referrer: options.referrer }),
23+
...(options?.referrerPolicy !== undefined && {
24+
referrerPolicy: options.referrerPolicy,
25+
}),
26+
...(options?.integrity !== undefined && { integrity: options.integrity }),
27+
...(options?.keepalive !== undefined && { keepalive: options.keepalive }),
28+
...(options?.mode !== undefined && { mode: options.mode }),
29+
})
30+
31+
const startTime = Date.now()
32+
33+
try {
34+
const response = await fetch(url, options)
35+
const duration = Date.now() - startTime
36+
37+
// Log response details
38+
log('response', {
39+
url: url.toString(),
40+
status: response.status,
41+
statusText: response.statusText,
42+
ok: response.ok,
43+
headers: Object.fromEntries(response.headers.entries()),
44+
duration: `${duration}ms`,
45+
type: response.type,
46+
redirected: response.redirected,
47+
...(response.url !== url.toString() && { finalUrl: response.url }),
48+
})
49+
50+
// eslint-disable-next-line @typescript-eslint/unbound-method
51+
const originalTextBody = response.text
52+
// eslint-disable-next-line @typescript-eslint/unbound-method
53+
const originalJSONBody = response.json
54+
// eslint-disable-next-line @typescript-eslint/unbound-method
55+
const originalArrayBufferBody = response.arrayBuffer
56+
57+
response.text = async () => {
58+
const textBody = await originalTextBody.call(response)
59+
body(textBody)
60+
return textBody
61+
}
62+
63+
response.json = async () => {
64+
const jsonBody = await originalJSONBody.call(response)
65+
body(jsonBody)
66+
return jsonBody
67+
}
68+
69+
response.arrayBuffer = async () => {
70+
const arrayBufferBody = await originalArrayBufferBody.call(response)
71+
body(arrayBufferBody)
72+
return arrayBufferBody
73+
}
74+
75+
return response
76+
} catch (error) {
77+
const duration = Date.now() - startTime
78+
79+
// Log error details
80+
logError({
81+
url: url.toString(),
82+
duration: `${duration}ms`,
83+
error:
84+
error instanceof Error
85+
? {
86+
name: error.name,
87+
message: error.message,
88+
...(error.cause !== undefined &&
89+
typeof error.cause === 'object' &&
90+
error.cause !== null
91+
? { cause: error.cause }
92+
: {}),
93+
}
94+
: error,
95+
})
96+
97+
throw error
98+
}
99+
}

jsr.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"$schema": "https://jsr.io/schema/config-file.v1.json",
3+
"name": "@nrfcloud/fetch-with-debug",
4+
"version": "0.0.0-development",
5+
"license": "BSD-3-Clause",
6+
"exports": "./export.ts"
7+
}

0 commit comments

Comments
 (0)