Skip to content

Commit b85af27

Browse files
authored
[FSSDK-8651] added support for user agent parser for odp (#854)
1 parent 2125cde commit b85af27

13 files changed

+235
-8
lines changed

.vscode/settings.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"jest.rootPath": "/workspaces/javascript-sdk/packages/optimizely-sdk",
33
"jest.jestCommandLine": "./node_modules/.bin/jest",
4-
"jest.autoRevealOutput": "on-exec-error"
4+
"jest.autoRevealOutput": "on-exec-error",
5+
"editor.tabSize": 2
56
}

lib/core/odp/odp_event_manager.ts

+34-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { OdpEvent } from './odp_event';
2323
import { OdpConfig } from './odp_config';
2424
import { IOdpEventApiManager } from './odp_event_api_manager';
2525
import { invalidOdpDataFound } from './odp_utils';
26+
import { IUserAgentParser } from './user_agent_parser';
2627

2728
const MAX_RETRIES = 3;
2829

@@ -123,6 +124,19 @@ export abstract class OdpEventManager implements IOdpEventManager {
123124
*/
124125
private readonly clientVersion: string;
125126

127+
/**
128+
* Version of the client being used
129+
* @private
130+
*/
131+
private readonly userAgentParser?: IUserAgentParser;
132+
133+
134+
/**
135+
* Information about the user agent
136+
* @private
137+
*/
138+
private readonly userAgentData?: Map<string, unknown>;
139+
126140
constructor({
127141
odpConfig,
128142
apiManager,
@@ -132,6 +146,7 @@ export abstract class OdpEventManager implements IOdpEventManager {
132146
queueSize,
133147
batchSize,
134148
flushInterval,
149+
userAgentParser,
135150
}: {
136151
odpConfig: OdpConfig;
137152
apiManager: IOdpEventApiManager;
@@ -141,6 +156,7 @@ export abstract class OdpEventManager implements IOdpEventManager {
141156
queueSize?: number;
142157
batchSize?: number;
143158
flushInterval?: number;
159+
userAgentParser?: IUserAgentParser;
144160
}) {
145161
this.odpConfig = odpConfig;
146162
this.apiManager = apiManager;
@@ -149,6 +165,22 @@ export abstract class OdpEventManager implements IOdpEventManager {
149165
this.clientVersion = clientVersion;
150166
this.initParams(batchSize, queueSize, flushInterval);
151167
this.state = STATE.STOPPED;
168+
this.userAgentParser = userAgentParser;
169+
170+
if (userAgentParser) {
171+
const { os, device } = userAgentParser.parseUserAgentInfo();
172+
173+
const userAgentInfo: Record<string, unknown> = {
174+
'os': os.name,
175+
'os_version': os.version,
176+
'device_type': device.type,
177+
'model': device.model,
178+
};
179+
180+
this.userAgentData = new Map<string, unknown>(
181+
Object.entries(userAgentInfo).filter(([key, value]) => value != null && value != undefined)
182+
);
183+
}
152184

153185
this.apiManager.updateSettings(odpConfig);
154186
}
@@ -408,7 +440,8 @@ export abstract class OdpEventManager implements IOdpEventManager {
408440
* @private
409441
*/
410442
private augmentCommonData(sourceData: Map<string, unknown>): Map<string, unknown> {
411-
const data = new Map<string, unknown>();
443+
const data = new Map<string, unknown>(this.userAgentData);
444+
412445
data.set('idempotence_id', uuid());
413446
data.set('data_source_type', 'sdk');
414447
data.set('data_source', this.clientEngine);

lib/core/odp/user_agent_info.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
/**
2+
* Copyright 2023, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
export type UserAgentInfo = {
18+
os: {
19+
name?: string,
20+
version?: string,
21+
},
22+
device: {
23+
type?: string,
24+
model?: string,
25+
}
26+
};

lib/core/odp/user_agent_parser.ts

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* Copyright 2023, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { UserAgentInfo } from "./user_agent_info";
18+
19+
export interface IUserAgentParser {
20+
parseUserAgentInfo(): UserAgentInfo,
21+
}

lib/index.browser.tests.js

+51
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,10 @@ if (!global.window) {
6767
}
6868
}
6969

70+
const pause = (timeoutMilliseconds) => {
71+
return new Promise(resolve => setTimeout(resolve, timeoutMilliseconds));
72+
};
73+
7074
describe('javascript-sdk (Browser)', function() {
7175
var clock;
7276
beforeEach(function() {
@@ -838,6 +842,53 @@ describe('javascript-sdk (Browser)', function() {
838842
sinon.assert.called(fakeEventManager.sendEvent);
839843
});
840844

845+
it('should augment odp events with user agent data if userAgentParser is provided', async () => {
846+
const userAgentParser = {
847+
parseUserAgentInfo() {
848+
return {
849+
os: { 'name': 'windows', 'version': '11' },
850+
device: { 'type': 'laptop', 'model': 'thinkpad' },
851+
}
852+
}
853+
}
854+
855+
const fakeRequestHandler = {
856+
makeRequest: sinon.spy(function (requestUrl, headers, method, data) {
857+
return {
858+
abort: () => {},
859+
responsePromise: Promise.resolve({ statusCode: 200 }),
860+
}
861+
})
862+
};
863+
864+
const client = optimizelyFactory.createInstance({
865+
datafile: testData.getOdpIntegratedConfigWithSegments(),
866+
errorHandler: fakeErrorHandler,
867+
eventDispatcher: fakeEventDispatcher,
868+
eventBatchSize: null,
869+
logger,
870+
odpOptions: {
871+
userAgentParser,
872+
eventRequestHandler: fakeRequestHandler,
873+
},
874+
});
875+
const readyData = await client.onReady();
876+
877+
assert.equal(readyData.success, true);
878+
assert.isUndefined(readyData.reason);
879+
880+
client.sendOdpEvent('test', '', new Map([['eamil', '[email protected]']]), new Map([['key', 'value']]));
881+
clock.tick(10000);
882+
883+
const eventRequestUrl = new URL(fakeRequestHandler.makeRequest.lastCall.args[0]);
884+
const searchParams = eventRequestUrl.searchParams;
885+
886+
assert.equal(searchParams.get('os'), 'windows');
887+
assert.equal(searchParams.get('os_version'), '11');
888+
assert.equal(searchParams.get('device_type'), 'laptop');
889+
assert.equal(searchParams.get('model'), 'thinkpad');
890+
});
891+
841892
it('should convert fs-user-id, FS-USER-ID, and FS_USER_ID to fs_user_id identifier when calling sendOdpEvent', async () => {
842893
const fakeEventManager = {
843894
updateSettings: sinon.spy(),

lib/index.browser.ts

+6
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ import { OptimizelyDecideOption, Client, Config, OptimizelyOptions } from './sha
2828
import { createHttpPollingDatafileManager } from './plugins/datafile_manager/browser_http_polling_datafile_manager';
2929
import { BrowserOdpManager } from './plugins/odp_manager/index.browser';
3030
import Optimizely from './optimizely';
31+
import { IUserAgentParser } from './core/odp/user_agent_parser';
32+
import { getUserAgentParser } from './plugins/odp/user_agent_parser/index.browser';
3133

3234
const logger = getLogger();
3335
logHelper.setLogHandler(loggerPlugin.createLogger());
@@ -164,6 +166,7 @@ const __internalResetRetryState = function(): void {
164166

165167
const setLogHandler = logHelper.setLogHandler;
166168
const setLogLevel = logHelper.setLogLevel;
169+
167170
export {
168171
loggerPlugin as logging,
169172
defaultErrorHandler as errorHandler,
@@ -174,6 +177,8 @@ export {
174177
createInstance,
175178
__internalResetRetryState,
176179
OptimizelyDecideOption,
180+
IUserAgentParser,
181+
getUserAgentParser,
177182
};
178183

179184
export default {
@@ -186,6 +191,7 @@ export default {
186191
createInstance,
187192
__internalResetRetryState,
188193
OptimizelyDecideOption,
194+
getUserAgentParser,
189195
};
190196

191197
export * from './export_types';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* Copyright 2023, Optimizely
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import { UAParser } from 'ua-parser-js';
18+
import { UserAgentInfo } from "../../../core/odp/user_agent_info";
19+
import { IUserAgentParser } from '../../../core/odp/user_agent_parser';
20+
21+
const userAgentParser: IUserAgentParser = {
22+
parseUserAgentInfo(): UserAgentInfo {
23+
const parser = new UAParser();
24+
const agentInfo = parser.getResult();
25+
const { os, device } = agentInfo;
26+
return { os, device };
27+
}
28+
}
29+
30+
export function getUserAgentParser(): IUserAgentParser {
31+
return userAgentParser;
32+
}
33+

lib/plugins/odp_manager/index.browser.ts

+1
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export class BrowserOdpManager extends OdpManager {
118118
flushInterval: odpOptions?.eventFlushInterval,
119119
batchSize: odpOptions?.eventBatchSize,
120120
queueSize: odpOptions?.eventQueueSize,
121+
userAgentParser: odpOptions?.userAgentParser,
121122
});
122123
}
123124

lib/shared_types.ts

+2
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { IOdpSegmentManager } from './core/odp/odp_segment_manager';
3535
import { IOdpEventApiManager } from './core/odp/odp_event_api_manager';
3636
import { IOdpEventManager } from './core/odp/odp_event_manager';
3737
import { IOdpManager } from './core/odp/odp_manager';
38+
import { IUserAgentParser } from './core/odp/user_agent_parser';
3839

3940
export interface BucketerParams {
4041
experimentId: string;
@@ -105,6 +106,7 @@ export interface OdpOptions {
105106
eventApiTimeout?: number;
106107
eventRequestHandler?: RequestHandler;
107108
eventManager?: IOdpEventManager;
109+
userAgentParser?: IUserAgentParser;
108110
}
109111

110112
export interface ListenerPayload {

package-lock.json

+15-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"decompress-response": "^4.2.1",
4646
"json-schema": "^0.4.0",
4747
"murmurhash": "^2.0.1",
48+
"ua-parser-js": "^1.0.35",
4849
"uuid": "^8.3.2"
4950
},
5051
"devDependencies": {
@@ -57,6 +58,7 @@
5758
"@types/mocha": "^5.2.7",
5859
"@types/nise": "^1.4.0",
5960
"@types/node": "^18.7.18",
61+
"@types/ua-parser-js": "^0.7.36",
6062
"@types/uuid": "^3.4.4",
6163
"@typescript-eslint/eslint-plugin": "^5.33.0",
6264
"@typescript-eslint/parser": "^5.33.0",

tests/odpEventManager.spec.ts

+37
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import { anything, capture, instance, mock, resetCalls, spy, verify, when } from
2323
import { IOdpEventApiManager } from '../lib/core/odp/odp_event_api_manager';
2424
import { LogHandler, LogLevel } from '../lib/modules/logging';
2525
import { OdpEvent } from '../lib/core/odp/odp_event';
26+
import { IUserAgentParser } from '../lib/core/odp/user_agent_parser';
27+
import { UserAgentInfo } from '../lib/core/odp/user_agent_info';
2628

2729
const API_KEY = 'test-api-key';
2830
const API_HOST = 'https://odp.example.com';
@@ -372,6 +374,41 @@ describe('OdpEventManager', () => {
372374
expect(events[1].data.size).toEqual(PROCESSED_EVENTS[1].data.size);
373375
});
374376

377+
it('should augment events with data from user agent parser', async () => {
378+
const userAgentParser : IUserAgentParser = {
379+
parseUserAgentInfo: function (): UserAgentInfo {
380+
return {
381+
os: { 'name': 'windows', 'version': '11' },
382+
device: { 'type': 'laptop', 'model': 'thinkpad' },
383+
}
384+
}
385+
}
386+
387+
const eventManager = new OdpEventManager({
388+
odpConfig,
389+
apiManager,
390+
logger,
391+
clientEngine,
392+
clientVersion,
393+
batchSize: 10,
394+
flushInterval: 100,
395+
userAgentParser,
396+
});
397+
398+
eventManager.start();
399+
EVENTS.forEach(event => eventManager.sendEvent(event));
400+
await pause(1000);
401+
402+
verify(mockApiManager.sendEvents(anything())).called();
403+
const [events] = capture(mockApiManager.sendEvents).last();
404+
const event = events[0];
405+
406+
expect(event.data.get('os')).toEqual('windows');
407+
expect(event.data.get('os_version')).toEqual('11');
408+
expect(event.data.get('device_type')).toEqual('laptop');
409+
expect(event.data.get('model')).toEqual('thinkpad');
410+
});
411+
375412
it('should retry failed events', async () => {
376413
// all events should fail ie shouldRetry = true
377414
when(mockApiManager.sendEvents(anything())).thenResolve(true);

0 commit comments

Comments
 (0)