Skip to content

Commit 3615f15

Browse files
feat(1 3223): add client identification headers (#235)
This PR adds client identification headers to the feature and metrics calls that the client makes to Unleash. The headers are: - `x-unleash-appname`: the name of the application that is using the client - `x-unleash-connection-id`: a unique identifier for the current instance of the client - `x-unleash-sdk`: sdk information in the format `unleash-js@<version>`
1 parent 1bf7223 commit 3615f15

File tree

5 files changed

+72
-1
lines changed

5 files changed

+72
-1
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "unleash-proxy-client",
33
"version": "3.6.1",
4-
"description": "A browser client that can be used together with the unleash-proxy.",
4+
"description": "A browser client that can be used together with Unleash Edge or the Unleash Frontend API.",
55
"type": "module",
66
"main": "./build/index.cjs",
77
"types": "./build/cjs/index.d.ts",

src/index.test.ts

+42
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import { FetchMock } from 'jest-fetch-mock';
2+
// eslint-disable-next-line @typescript-eslint/no-var-requires
3+
const packageJSON = require('../package.json');
24
import 'jest-localstorage-mock';
35
import * as data from './test/testdata.json';
46
import IStorageProvider from './storage-provider';
@@ -1356,6 +1358,46 @@ test('Should pass custom headers', async () => {
13561358
});
13571359
});
13581360

1361+
test('Should add `x-unleash` headers', async () => {
1362+
fetchMock.mockResponses(
1363+
[JSON.stringify(data), { status: 200 }],
1364+
[JSON.stringify(data), { status: 200 }]
1365+
);
1366+
const appName = 'unleash-client-test';
1367+
const config: IConfig = {
1368+
url: 'http://localhost/test',
1369+
clientKey: 'some123key',
1370+
appName,
1371+
};
1372+
const client = new UnleashClient(config);
1373+
await client.start();
1374+
1375+
const featureRequest = getTypeSafeRequest(fetchMock, 0);
1376+
1377+
client.isEnabled('count-metrics');
1378+
jest.advanceTimersByTime(2001);
1379+
1380+
const metricsRequest = getTypeSafeRequest(fetchMock, 1);
1381+
1382+
const uuidFormat =
1383+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
1384+
1385+
const expectedHeaders = {
1386+
'x-unleash-sdk': `unleash-js@${packageJSON.version}`,
1387+
'x-unleash-connection-id': expect.stringMatching(uuidFormat),
1388+
'x-unleash-appname': appName,
1389+
};
1390+
1391+
const getConnectionId = (request: any) =>
1392+
request.headers['x-unleash-connection-id'];
1393+
1394+
expect(featureRequest.headers).toMatchObject(expectedHeaders);
1395+
expect(metricsRequest.headers).toMatchObject(expectedHeaders);
1396+
expect(getConnectionId(featureRequest)).toEqual(
1397+
getConnectionId(metricsRequest)
1398+
);
1399+
});
1400+
13591401
test('Should emit impression events on getVariant calls when impressionData is true', (done) => {
13601402
const bootstrap = [
13611403
{

src/index.ts

+11
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { TinyEmitter } from 'tiny-emitter';
2+
import { v4 as uuidv4 } from 'uuid';
23
import Metrics from './metrics';
34
import type IStorageProvider from './storage-provider';
45
import InMemoryStorageProvider from './storage-provider-inmemory';
@@ -10,6 +11,9 @@ import {
1011
urlWithContextAsQuery,
1112
} from './util';
1213

14+
// eslint-disable-next-line @typescript-eslint/no-var-requires
15+
const packageJSON = require('../package.json');
16+
1317
const DEFINED_FIELDS = [
1418
'userId',
1519
'sessionId',
@@ -169,6 +173,7 @@ export class UnleashClient extends TinyEmitter {
169173
private lastError: any;
170174
private experimental: IExperimentalConfig;
171175
private lastRefreshTimestamp: number;
176+
private connectionId: string;
172177

173178
constructor({
174179
storageProvider,
@@ -261,6 +266,8 @@ export class UnleashClient extends TinyEmitter {
261266
bootstrap && bootstrap.length > 0 ? bootstrap : undefined;
262267
this.bootstrapOverride = bootstrapOverride;
263268

269+
this.connectionId = uuidv4();
270+
264271
this.metrics = new Metrics({
265272
onError: this.emit.bind(this, EVENTS.ERROR),
266273
onSent: this.emit.bind(this, EVENTS.SENT),
@@ -273,6 +280,7 @@ export class UnleashClient extends TinyEmitter {
273280
headerName,
274281
customHeaders,
275282
metricsIntervalInitial,
283+
connectionId: this.connectionId,
276284
});
277285
}
278286

@@ -468,6 +476,9 @@ export class UnleashClient extends TinyEmitter {
468476
const headers = {
469477
[this.headerName]: this.clientKey,
470478
Accept: 'application/json',
479+
'x-unleash-sdk': `unleash-js@${packageJSON.version}`,
480+
'x-unleash-connection-id': this.connectionId,
481+
'x-unleash-appname': this.context.appName,
471482
};
472483
if (isPOST) {
473484
headers['Content-Type'] = 'application/json';

src/metrics.test.ts

+9
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ test('should be disabled by flag disableMetrics', async () => {
2222
fetch: fetchMock,
2323
headerName: 'Authorization',
2424
metricsIntervalInitial: 0,
25+
connectionId: '123',
2526
});
2627

2728
metrics.count('foo', true);
@@ -42,6 +43,7 @@ test('should send metrics', async () => {
4243
fetch: fetchMock,
4344
headerName: 'Authorization',
4445
metricsIntervalInitial: 0,
46+
connectionId: '123',
4547
});
4648

4749
metrics.count('foo', true);
@@ -79,6 +81,7 @@ test('should send metrics with custom auth header', async () => {
7981
fetch: fetchMock,
8082
headerName: 'NotAuthorization',
8183
metricsIntervalInitial: 0,
84+
connectionId: '123',
8285
});
8386

8487
metrics.count('foo', true);
@@ -103,6 +106,7 @@ test('Should send initial metrics after 2 seconds', () => {
103106
fetch: fetchMock,
104107
headerName: 'Authorization',
105108
metricsIntervalInitial: 2,
109+
connectionId: '123',
106110
});
107111

108112
metrics.start();
@@ -127,6 +131,7 @@ test('Should send initial metrics after 20 seconds, when metricsIntervalInitial
127131
fetch: fetchMock,
128132
headerName: 'Authorization',
129133
metricsIntervalInitial: 20,
134+
connectionId: '123',
130135
});
131136

132137
metrics.start();
@@ -151,6 +156,7 @@ test('Should send metrics for initial and after metrics interval', () => {
151156
fetch: fetchMock,
152157
headerName: 'Authorization',
153158
metricsIntervalInitial: 2,
159+
connectionId: '123',
154160
});
155161

156162
metrics.start();
@@ -178,6 +184,7 @@ test('Should not send initial metrics if disabled', () => {
178184
fetch: fetchMock,
179185
headerName: 'Authorization',
180186
metricsIntervalInitial: 0,
187+
connectionId: '123',
181188
});
182189

183190
metrics.start();
@@ -202,6 +209,7 @@ test('should send metrics based on timer interval', async () => {
202209
fetch: fetchMock,
203210
headerName: 'Authorization',
204211
metricsIntervalInitial: 2,
212+
connectionId: '123',
205213
});
206214

207215
metrics.start();
@@ -242,6 +250,7 @@ describe('Custom headers for metrics', () => {
242250
fetch: fetchMock,
243251
headerName: 'Authorization',
244252
customHeaders,
253+
connectionId: '123',
245254
metricsIntervalInitial: 2,
246255
});
247256

src/metrics.ts

+9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Simplified version of: https://github.com/Unleash/unleash-client-node/blob/main/src/metrics.ts
22

33
import { notNullOrUndefined } from './util';
4+
// eslint-disable-next-line @typescript-eslint/no-var-requires
5+
const packageJSON = require('../package.json');
46

57
export interface MetricsOptions {
68
onError: OnError;
@@ -14,6 +16,7 @@ export interface MetricsOptions {
1416
headerName: string;
1517
customHeaders?: Record<string, string>;
1618
metricsIntervalInitial: number;
19+
connectionId: string;
1720
}
1821

1922
interface VariantBucket {
@@ -53,6 +56,7 @@ export default class Metrics {
5356
private headerName: string;
5457
private customHeaders: Record<string, string>;
5558
private metricsIntervalInitial: number;
59+
private connectionId: string;
5660

5761
constructor({
5862
onError,
@@ -66,6 +70,7 @@ export default class Metrics {
6670
headerName,
6771
customHeaders = {},
6872
metricsIntervalInitial,
73+
connectionId,
6974
}: MetricsOptions) {
7075
this.onError = onError;
7176
this.onSent = onSent || doNothing;
@@ -79,6 +84,7 @@ export default class Metrics {
7984
this.fetch = fetch;
8085
this.headerName = headerName;
8186
this.customHeaders = customHeaders;
87+
this.connectionId = connectionId;
8288
}
8389

8490
public start() {
@@ -121,6 +127,9 @@ export default class Metrics {
121127
[this.headerName]: this.clientKey,
122128
Accept: 'application/json',
123129
'Content-Type': 'application/json',
130+
'x-unleash-sdk': `unleash-js@${packageJSON.version}`,
131+
'x-unleash-connection-id': this.connectionId,
132+
'x-unleash-appname': this.appName,
124133
};
125134

126135
Object.entries(this.customHeaders)

0 commit comments

Comments
 (0)