Skip to content

Commit 02a9553

Browse files
dbajpeyijonathanKingstonshakyShane
authored
[ContentFeature] de-decouple build specific code from content-features (#1545)
* (wip) refactor: move utility function to a separate class * refactor: parameterize trackerLookup * refactor: move out import config into content-scope-features * fix: add content feature type * test: fix unit test config passing * Ensure setArgs is called * Move to isDebug instead of using args * Remove surplus trackerLookup in integration test * Change trackerLookup verification * string type for `name` --------- Co-authored-by: Jonathan Kingston <[email protected]> Co-authored-by: Shane Osbourne <[email protected]>
1 parent 26d05f3 commit 02a9553

19 files changed

+237
-233
lines changed

injected/entry-points/android.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
*/
44
import { load, init } from '../src/content-scope-features.js';
55
import { processConfig, isGloballyDisabled } from './../src/utils';
6-
import { isTrackerOrigin } from '../src/trackers';
76
import { AndroidMessagingConfig } from '../../messaging/index.js';
87

98
function initCode() {
@@ -33,8 +32,6 @@ function initCode() {
3332

3433
load({
3534
platform: processedConfig.platform,
36-
trackerLookup: processedConfig.trackerLookup,
37-
documentOriginIsTracker: isTrackerOrigin(processedConfig.trackerLookup),
3835
site: processedConfig.site,
3936
bundledConfig: processedConfig.bundledConfig,
4037
messagingConfig: processedConfig.messagingConfig,

injected/entry-points/apple.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
*/
44
import { load, init } from '../src/content-scope-features.js';
55
import { processConfig, isGloballyDisabled, platformSpecificFeatures } from './../src/utils';
6-
import { isTrackerOrigin } from '../src/trackers';
76
import { WebkitMessagingConfig, TestTransportConfig } from '../../messaging/index.js';
87

98
function initCode() {
@@ -44,8 +43,6 @@ function initCode() {
4443

4544
load({
4645
platform: processedConfig.platform,
47-
trackerLookup: processedConfig.trackerLookup,
48-
documentOriginIsTracker: isTrackerOrigin(processedConfig.trackerLookup),
4946
site: processedConfig.site,
5047
bundledConfig: processedConfig.bundledConfig,
5148
messagingConfig: processedConfig.messagingConfig,

injected/entry-points/chrome.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
/**
22
* @module Chrome integration
33
*/
4-
import { isTrackerOrigin } from '../src/trackers';
54
import { computeLimitedSiteObject } from '../src/utils';
65

76
/**
@@ -39,8 +38,6 @@ function randomString() {
3938
}
4039

4140
function init() {
42-
const trackerLookup = import.meta.trackerLookup;
43-
const documentOriginIsTracker = isTrackerOrigin(trackerLookup);
4441
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
4542
const bundledConfig = $BUNDLED_CONFIG$;
4643
const randomMethodName = '_d' + randomString();
@@ -54,9 +51,7 @@ function init() {
5451
platform: {
5552
name: 'extension'
5653
},
57-
trackerLookup: ${JSON.stringify(trackerLookup)},
5854
site: ${JSON.stringify(siteObject)},
59-
documentOriginIsTracker: ${documentOriginIsTracker},
6055
bundledConfig: ${JSON.stringify(bundledConfig)}
6156
})
6257
// Define a random function we call later.

injected/entry-points/extension-mv3.js

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,14 @@
22
* @module main world integration for Chrome MV3 and Firefox (enhanced) MV2
33
*/
44
import { load, init, update } from '../src/content-scope-features.js';
5-
import { isTrackerOrigin } from '../src/trackers.js';
65
import { computeLimitedSiteObject } from '../src/utils.js';
76

87
const secret = (crypto.getRandomValues(new Uint32Array(1))[0] / 2 ** 32).toString().replace('0.', '');
98

10-
const trackerLookup = import.meta.trackerLookup;
11-
129
load({
1310
platform: {
1411
name: 'extension',
1512
},
16-
trackerLookup,
17-
documentOriginIsTracker: isTrackerOrigin(trackerLookup),
1813
site: computeLimitedSiteObject(),
1914
// @ts-expect-error https://app.asana.com/0/1201614831475344/1203979574128023/f
2015
bundledConfig: $BUNDLED_CONFIG$,

injected/entry-points/integration.js

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { load, init } from '../src/content-scope-features.js';
2-
import { isTrackerOrigin } from '../src/trackers';
32
import { TestTransportConfig } from '../../messaging/index.js';
43
function getTopLevelURL() {
54
try {
@@ -17,7 +16,6 @@ function getTopLevelURL() {
1716

1817
function generateConfig() {
1918
const topLevelUrl = getTopLevelURL();
20-
const trackerLookup = import.meta.trackerLookup;
2119
return {
2220
debug: false,
2321
sessionKey: 'randomVal',
@@ -30,7 +28,6 @@ function generateConfig() {
3028
allowlisted: false,
3129
enabledFeatures: ['fingerprintingCanvas', 'fingerprintingScreenSize', 'navigatorInterface', 'cookie'],
3230
},
33-
trackerLookup,
3431
};
3532
}
3633

@@ -84,12 +81,12 @@ async function initCode() {
8481
};
8582
},
8683
});
84+
8785
load({
8886
// @ts-expect-error Types of property 'name' are incompatible.
8987
platform: processedConfig.platform,
90-
trackerLookup: processedConfig.trackerLookup,
91-
documentOriginIsTracker: isTrackerOrigin(processedConfig.trackerLookup),
9288
site: processedConfig.site,
89+
bundledConfig: processedConfig.bundledConfig,
9390
messagingConfig: processedConfig.messagingConfig,
9491
});
9592

injected/entry-points/windows.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
*/
44
import { load, init } from '../src/content-scope-features.js';
55
import { processConfig, isGloballyDisabled, platformSpecificFeatures } from './../src/utils';
6-
import { isTrackerOrigin } from '../src/trackers';
76
import { WindowsMessagingConfig } from '../../messaging/index.js';
87

98
function initCode() {
@@ -31,8 +30,6 @@ function initCode() {
3130

3231
load({
3332
platform: processedConfig.platform,
34-
trackerLookup: processedConfig.trackerLookup,
35-
documentOriginIsTracker: isTrackerOrigin(processedConfig.trackerLookup),
3633
site: processedConfig.site,
3734
bundledConfig: processedConfig.bundledConfig,
3835
messagingConfig: processedConfig.messagingConfig,

injected/src/config-feature.js

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
import { immutableJSONPatch } from 'immutable-json-patch';
2+
import { camelcase, computeEnabledFeatures, matchHostname, parseFeatureSettings } from './utils.js';
3+
4+
export default class ConfigFeature {
5+
/** @type {import('./utils.js').RemoteConfig | undefined} */
6+
#bundledConfig;
7+
8+
/** @type {string} */
9+
name;
10+
11+
/** @type {{ debug?: boolean, desktopModeEnabled?: boolean, forcedZoomEnabled?: boolean, featureSettings?: Record<string, unknown>, assets?: import('./content-feature.js').AssetConfig | undefined, site: import('./content-feature.js').Site, messagingConfig?: import('@duckduckgo/messaging').MessagingConfig } | null} */
12+
#args;
13+
14+
/**
15+
* @param {string} name
16+
* @param {import('./content-scope-features.js').LoadArgs} args
17+
*/
18+
constructor(name, args) {
19+
this.name = name;
20+
const { bundledConfig, site, platform } = args;
21+
this.#bundledConfig = bundledConfig;
22+
this.#args = args;
23+
// If we have a bundled config, treat it as a regular config
24+
// This will be overriden by the remote config if it is available
25+
if (this.#bundledConfig && this.#args) {
26+
const enabledFeatures = computeEnabledFeatures(bundledConfig, site.domain, platform.version);
27+
this.#args.featureSettings = parseFeatureSettings(bundledConfig, enabledFeatures);
28+
}
29+
}
30+
31+
get args() {
32+
return this.#args;
33+
}
34+
35+
set args(args) {
36+
this.#args = args;
37+
}
38+
39+
get featureSettings() {
40+
return this.#args?.featureSettings;
41+
}
42+
43+
/**
44+
* Given a config key, interpret the value as a list of domain overrides, and return the elements that match the current page
45+
* Consider using patchSettings instead as per `getFeatureSetting`.
46+
* @param {string} featureKeyName
47+
* @return {any[]}
48+
* @protected
49+
*/
50+
matchDomainFeatureSetting(featureKeyName) {
51+
const domain = this.args?.site.domain;
52+
if (!domain) return [];
53+
const domains = this._getFeatureSettings()?.[featureKeyName] || [];
54+
return domains.filter((rule) => {
55+
if (Array.isArray(rule.domain)) {
56+
return rule.domain.some((domainRule) => {
57+
return matchHostname(domain, domainRule);
58+
});
59+
}
60+
return matchHostname(domain, rule.domain);
61+
});
62+
}
63+
64+
/**
65+
* Return the settings object for a feature
66+
* @param {string} [featureName] - The name of the feature to get the settings for; defaults to the name of the feature
67+
* @returns {any}
68+
*/
69+
_getFeatureSettings(featureName) {
70+
const camelFeatureName = featureName || camelcase(this.name);
71+
return this.featureSettings?.[camelFeatureName];
72+
}
73+
74+
/**
75+
* For simple boolean settings, return true if the setting is 'enabled'
76+
* For objects, verify the 'state' field is 'enabled'.
77+
* This allows for future forwards compatibility with more complex settings if required.
78+
* For example:
79+
* ```json
80+
* {
81+
* "toggle": "enabled"
82+
* }
83+
* ```
84+
* Could become later (without breaking changes):
85+
* ```json
86+
* {
87+
* "toggle": {
88+
* "state": "enabled",
89+
* "someOtherKey": 1
90+
* }
91+
* }
92+
* ```
93+
* This also supports domain overrides as per `getFeatureSetting`.
94+
* @param {string} featureKeyName
95+
* @param {string} [featureName]
96+
* @returns {boolean}
97+
*/
98+
getFeatureSettingEnabled(featureKeyName, featureName) {
99+
const result = this.getFeatureSetting(featureKeyName, featureName);
100+
if (typeof result === 'object') {
101+
return result.state === 'enabled';
102+
}
103+
return result === 'enabled';
104+
}
105+
106+
/**
107+
* Return a specific setting from the feature settings
108+
* If the "settings" key within the config has a "domains" key, it will be used to override the settings.
109+
* This uses JSONPatch to apply the patches to settings before getting the setting value.
110+
* For example.com getFeatureSettings('val') will return 1:
111+
* ```json
112+
* {
113+
* "settings": {
114+
* "domains": [
115+
* {
116+
* "domain": "example.com",
117+
* "patchSettings": [
118+
* { "op": "replace", "path": "/val", "value": 1 }
119+
* ]
120+
* }
121+
* ]
122+
* }
123+
* }
124+
* ```
125+
* "domain" can either be a string or an array of strings.
126+
127+
* For boolean states you should consider using getFeatureSettingEnabled.
128+
* @param {string} featureKeyName
129+
* @param {string} [featureName]
130+
* @returns {any}
131+
*/
132+
getFeatureSetting(featureKeyName, featureName) {
133+
let result = this._getFeatureSettings(featureName);
134+
if (featureKeyName === 'domains') {
135+
throw new Error('domains is a reserved feature setting key name');
136+
}
137+
const domainMatch = [...this.matchDomainFeatureSetting('domains')].sort((a, b) => {
138+
return a.domain.length - b.domain.length;
139+
});
140+
for (const match of domainMatch) {
141+
if (match.patchSettings === undefined) {
142+
continue;
143+
}
144+
try {
145+
result = immutableJSONPatch(result, match.patchSettings);
146+
} catch (e) {
147+
console.error('Error applying patch settings', e);
148+
}
149+
}
150+
return result?.[featureKeyName];
151+
}
152+
153+
/**
154+
* @returns {import('./utils.js').RemoteConfig | undefined}
155+
**/
156+
get bundledConfig() {
157+
return this.#bundledConfig;
158+
}
159+
}

0 commit comments

Comments
 (0)