Skip to content

Commit b1d4acb

Browse files
Merge branch 'main' into SDKS-8407_baseline
2 parents 91802ec + c7d8e4c commit b1d4acb

File tree

14 files changed

+447
-211
lines changed

14 files changed

+447
-211
lines changed

CHANGES.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
- BREAKING CHANGES:
33
- Removed `/mySegments` endpoint from SplitAPI module, as it is replaced by `/memberships` endpoint.
44

5-
1.16.1 (July 10, 2024)
5+
1.17.0 (September 6, 2024)
6+
- Added `sync.requestOptions.getHeaderOverrides` configuration option to enhance SDK HTTP request Headers for Authorization Frameworks.
7+
- Added `isTimedout` and `lastUpdate` properties to IStatusInterface to keep track of the timestamp of the last SDK event, used on React and Redux SDKs.
68
- Updated some transitive dependencies for vulnerability fixes.
79

810
1.16.0 (June 13, 2024)

package-lock.json

Lines changed: 157 additions & 125 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/readiness/__tests__/readinessManager.spec.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,16 @@ const settingsWithTimeout = {
1616
}
1717
} as unknown as ISettings;
1818

19-
const statusFlagsCount = 5;
19+
const statusFlagsCount = 7;
2020

2121
function assertInitialStatus(readinessManager: IReadinessManager) {
2222
expect(readinessManager.isReady()).toBe(false);
2323
expect(readinessManager.isReadyFromCache()).toBe(false);
24+
expect(readinessManager.isTimedout()).toBe(false);
2425
expect(readinessManager.hasTimedout()).toBe(false);
2526
expect(readinessManager.isDestroyed()).toBe(false);
2627
expect(readinessManager.isOperational()).toBe(false);
28+
expect(readinessManager.lastUpdate()).toBe(0);
2729
}
2830

2931
test('READINESS MANAGER / Share splits but segments (without timeout enabled)', (done) => {
@@ -165,6 +167,7 @@ describe('READINESS MANAGER / Timeout ready event', () => {
165167
timeoutCounter = 0;
166168

167169
readinessManager.gate.on(SDK_READY_TIMED_OUT, () => {
170+
expect(readinessManager.isTimedout()).toBe(true);
168171
expect(readinessManager.hasTimedout()).toBe(true);
169172
if (!readinessManager.isReady()) timeoutCounter++;
170173
});
@@ -178,6 +181,8 @@ describe('READINESS MANAGER / Timeout ready event', () => {
178181
test('should be fired once', (done) => {
179182
readinessManager.gate.on(SDK_READY, () => {
180183
expect(readinessManager.isReady()).toBe(true);
184+
expect(readinessManager.isTimedout()).toBe(false);
185+
expect(readinessManager.hasTimedout()).toBe(true);
181186
expect(timeoutCounter).toBe(1);
182187
done();
183188
});
@@ -233,14 +238,24 @@ test('READINESS MANAGER / Destroy after it was ready but before timedout', () =>
233238
counter++;
234239
});
235240

241+
let lastUpdate = readinessManager.lastUpdate();
242+
expect(lastUpdate).toBe(0);
243+
236244
readinessManager.splits.emit(SDK_SPLITS_ARRIVED);
237245
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); // ready state
238246

247+
expect(readinessManager.lastUpdate()).toBeGreaterThan(lastUpdate);
248+
lastUpdate = readinessManager.lastUpdate();
249+
239250
readinessManager.segments.emit(SDK_SEGMENTS_ARRIVED); // fires an update
251+
expect(readinessManager.lastUpdate()).toBeGreaterThan(lastUpdate);
252+
lastUpdate = readinessManager.lastUpdate();
240253

241254
expect(readinessManager.isDestroyed()).toBe(false);
242255
readinessManager.destroy(); // Destroy the gate, removing all the listeners and clearing the ready timeout.
243256
expect(readinessManager.isDestroyed()).toBe(true);
257+
expect(readinessManager.lastUpdate()).toBeGreaterThan(lastUpdate);
258+
244259
readinessManager.destroy(); // no-op
245260
readinessManager.destroy(); // no-op
246261

src/readiness/__tests__/sdkReadinessManager.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@ describe('SDK Readiness Manager - Event emitter', () => {
5151
});
5252

5353
expect(typeof sdkStatus.ready).toBe('function'); // The sdkStatus exposes a .ready() function.
54+
expect(typeof sdkStatus.__getStatus).toBe('function'); // The sdkStatus exposes a .__getStatus() function.
55+
expect(sdkStatus.__getStatus()).toEqual({
56+
isReady: false, isReadyFromCache: false, isTimedout: false, hasTimedout: false, isDestroyed: false, isOperational: false, lastUpdate: 0
57+
});
5458

5559
expect(typeof sdkStatus.Event).toBe('object'); // It also exposes the Event map,
5660
expect(sdkStatus.Event.SDK_READY).toBe(SDK_READY); // which contains the constants for the events, for backwards compatibility.

src/readiness/readinessManager.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,13 @@ export function readinessManagerFactory(
4141
const segments: ISegmentsEventEmitter = segmentsEventEmitterFactory(EventEmitter);
4242
const gate: IReadinessEventEmitter = new EventEmitter();
4343

44+
let lastUpdate = 0;
45+
function syncLastUpdate() {
46+
const dateNow = Date.now();
47+
// ensure lastUpdate is always increasing per event, is case Date.now() is mocked or its value is the same
48+
lastUpdate = dateNow > lastUpdate ? dateNow : lastUpdate + 1;
49+
}
50+
4451
// emit SDK_READY_FROM_CACHE
4552
let isReadyFromCache = false;
4653
if (splits.splitsCacheLoaded) isReadyFromCache = true; // ready from cache, but doesn't emit SDK_READY_FROM_CACHE
@@ -52,6 +59,7 @@ export function readinessManagerFactory(
5259
function timeout() {
5360
if (hasTimedout) return;
5461
hasTimedout = true;
62+
syncLastUpdate();
5563
gate.emit(SDK_READY_TIMED_OUT, 'Split SDK emitted SDK_READY_TIMED_OUT event.');
5664
}
5765

@@ -72,6 +80,7 @@ export function readinessManagerFactory(
7280
// Don't emit SDK_READY_FROM_CACHE if SDK_READY has been emitted
7381
if (!isReady) {
7482
try {
83+
syncLastUpdate();
7584
gate.emit(SDK_READY_FROM_CACHE);
7685
} catch (e) {
7786
// throws user callback exceptions in next tick
@@ -83,6 +92,7 @@ export function readinessManagerFactory(
8392
function checkIsReadyOrUpdate(diff: any) {
8493
if (isReady) {
8594
try {
95+
syncLastUpdate();
8696
gate.emit(SDK_UPDATE, diff);
8797
} catch (e) {
8898
// throws user callback exceptions in next tick
@@ -93,6 +103,7 @@ export function readinessManagerFactory(
93103
clearTimeout(readyTimeoutId);
94104
isReady = true;
95105
try {
106+
syncLastUpdate();
96107
gate.emit(SDK_READY);
97108
} catch (e) {
98109
// throws user callback exceptions in next tick
@@ -123,6 +134,7 @@ export function readinessManagerFactory(
123134

124135
destroy() {
125136
isDestroyed = true;
137+
syncLastUpdate();
126138

127139
segments.removeAllListeners();
128140
gate.removeAllListeners();
@@ -133,10 +145,12 @@ export function readinessManagerFactory(
133145
},
134146

135147
isReady() { return isReady; },
136-
hasTimedout() { return hasTimedout; },
137148
isReadyFromCache() { return isReadyFromCache; },
149+
isTimedout() { return hasTimedout && !isReady; },
150+
hasTimedout() { return hasTimedout; },
138151
isDestroyed() { return isDestroyed; },
139-
isOperational() { return (isReady || isReadyFromCache) && !isDestroyed; }
152+
isOperational() { return (isReady || isReadyFromCache) && !isDestroyed; },
153+
lastUpdate() { return lastUpdate; }
140154
};
141155

142156
}

src/readiness/sdkReadinessManager.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -121,14 +121,15 @@ export function sdkReadinessManagerFactory(
121121
return readyPromise;
122122
},
123123

124-
// Expose status for internal purposes only. Not considered part of the public API, and might be updated eventually.
125124
__getStatus() {
126125
return {
127126
isReady: readinessManager.isReady(),
128127
isReadyFromCache: readinessManager.isReadyFromCache(),
129-
isOperational: readinessManager.isOperational(),
128+
isTimedout: readinessManager.isTimedout(),
130129
hasTimedout: readinessManager.hasTimedout(),
131130
isDestroyed: readinessManager.isDestroyed(),
131+
isOperational: readinessManager.isOperational(),
132+
lastUpdate: readinessManager.lastUpdate(),
132133
};
133134
},
134135
}

src/readiness/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,11 @@ export interface IReadinessManager {
5050
/** Readiness status */
5151
isReady(): boolean,
5252
isReadyFromCache(): boolean,
53+
isTimedout(): boolean,
5354
hasTimedout(): boolean,
5455
isDestroyed(): boolean,
5556
isOperational(): boolean,
57+
lastUpdate(): number,
5658

5759
timeout(): void,
5860
setDestroyed(): void,
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { ISettings } from '../../types';
2+
import { decorateHeaders } from '../decorateHeaders';
3+
4+
const HEADERS = {
5+
Authorization: 'Bearer SDK-KEY',
6+
SplitSDKVersion: 'JS' // Overriding is forbidden
7+
};
8+
9+
describe('decorateHeaders', () => {
10+
11+
test('should not decorate headers if getHeaderOverrides is not provided', () => {
12+
const headers = { ...HEADERS };
13+
const settings = { sync: {} };
14+
15+
expect(decorateHeaders(settings as unknown as ISettings, headers)).toEqual(HEADERS);
16+
});
17+
18+
test('should decorate headers with header overrides, ignoring forbidden headers', () => {
19+
const headers = { ...HEADERS };
20+
const settings = {
21+
sync: {
22+
requestOptions: {
23+
getHeaderOverrides: (context: { headers: Record<string, string> }) => {
24+
context.headers['Authorization'] = 'ignored';
25+
return { 'Authorization': 'updated', 'OTHER_HEADER': 'other_value', 'SplitSdkVersion': 'FORBIDDEN', 'splitsdkversion': 'FORBIDDEN TOO' };
26+
}
27+
}
28+
}
29+
};
30+
31+
expect(decorateHeaders(settings as unknown as ISettings, headers)).toEqual({ 'Authorization': 'updated', 'OTHER_HEADER': 'other_value', 'SplitSDKVersion': 'JS' });
32+
});
33+
34+
test('should handle errors when decorating headers', () => {
35+
const headers = { ...HEADERS };
36+
const settings = {
37+
sync: {
38+
requestOptions: {
39+
getHeaderOverrides: () => {
40+
throw new Error('Unexpected error');
41+
}
42+
}
43+
},
44+
log: { error: jest.fn() }
45+
};
46+
47+
expect(decorateHeaders(settings as unknown as ISettings, headers)).toEqual(HEADERS);
48+
expect(settings.log.error).toHaveBeenCalledWith('Problem adding custom headers to request decorator: Error: Unexpected error');
49+
});
50+
});

src/services/decorateHeaders.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { objectAssign } from '../utils/lang/objectAssign';
2+
import { _Set } from '../utils/lang/sets';
3+
import { ISettings } from '../types';
4+
5+
const FORBIDDEN_HEADERS = new _Set([
6+
'splitsdkclientkey',
7+
'splitsdkversion',
8+
'splitsdkmachineip',
9+
'splitsdkmachinename',
10+
'splitsdkimpressionsmode',
11+
'host',
12+
'referrer',
13+
'content-type',
14+
'content-length',
15+
'content-encoding',
16+
'accept',
17+
'keep-alive',
18+
'x-fastly-debug'
19+
]);
20+
21+
export function decorateHeaders(settings: ISettings, headers: Record<string, string>) {
22+
if (settings.sync.requestOptions?.getHeaderOverrides) {
23+
try {
24+
const headerOverrides = settings.sync.requestOptions.getHeaderOverrides({ headers: objectAssign({}, headers) });
25+
Object.keys(headerOverrides)
26+
.filter(key => !FORBIDDEN_HEADERS.has(key.toLowerCase()))
27+
.forEach(key => headers[key] = headerOverrides[key]);
28+
} catch (e) {
29+
settings.log.error('Problem adding custom headers to request decorator: ' + e);
30+
}
31+
}
32+
return headers;
33+
}

src/services/splitHttpClient.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { objectAssign } from '../utils/lang/objectAssign';
33
import { ERROR_HTTP, ERROR_CLIENT_CANNOT_GET_READY } from '../logger/constants';
44
import { ISettings } from '../types';
55
import { IPlatform } from '../sdkFactory/types';
6+
import { decorateHeaders } from './decorateHeaders';
67

78
const messageNoFetch = 'Global fetch API is not available.';
89

@@ -21,20 +22,20 @@ export function splitHttpClientFactory(settings: ISettings, { getOptions, getFet
2122
// if fetch is not available, log Error
2223
if (!fetch) log.error(ERROR_CLIENT_CANNOT_GET_READY, [messageNoFetch]);
2324

24-
const headers: Record<string, string> = {
25+
const commonHeaders: Record<string, string> = {
2526
'Accept': 'application/json',
2627
'Content-Type': 'application/json',
2728
'Authorization': `Bearer ${authorizationKey}`,
2829
'SplitSDKVersion': version
2930
};
3031

31-
if (ip) headers['SplitSDKMachineIP'] = ip;
32-
if (hostname) headers['SplitSDKMachineName'] = hostname;
32+
if (ip) commonHeaders['SplitSDKMachineIP'] = ip;
33+
if (hostname) commonHeaders['SplitSDKMachineName'] = hostname;
3334

3435
return function httpClient(url: string, reqOpts: IRequestOptions = {}, latencyTracker: (error?: NetworkError) => void = () => { }, logErrorsAsInfo: boolean = false): Promise<IResponse> {
3536

3637
const request = objectAssign({
37-
headers: reqOpts.headers ? objectAssign({}, headers, reqOpts.headers) : headers,
38+
headers: decorateHeaders(settings, objectAssign({}, commonHeaders, reqOpts.headers || {})),
3839
method: reqOpts.method || 'GET',
3940
body: reqOpts.body
4041
}, options);

0 commit comments

Comments
 (0)