Bug 1525076 - Part 4: Cache attribution code on macOS. r=mixedpuppy

Querying the macOS system quarantine database is relatively slow, so
we'd like to do it only once.  This is relevant because the Telemetry
subsystem causes the attribution data to be fetched relatively early
during startup.  By caching, we accept additional disk activity but
don't have to invoke an external process, query a database, etc.

We must make BROWSER_ATTRIBUTION_ERRORS histogram apply to macOS as
well as Windows.  We add error codes to capture macOS-specific
detalis.  And we push this out to a later cycle, since there's no
reason to revisit this immediately.

Differential Revision: https://phabricator.services.mozilla.com/D92695
This commit is contained in:
Nick Alexander 2020-10-09 17:28:51 +00:00
parent 70222a9a39
commit 0c64a008ae
11 changed files with 600 additions and 85 deletions

View File

@ -19,6 +19,23 @@ ChromeUtils.defineModuleGetter(
"Services",
"resource://gre/modules/Services.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"MacAttribution",
"resource:///modules/MacAttribution.jsm"
);
XPCOMUtils.defineLazyGetter(this, "log", () => {
let ConsoleAPI = ChromeUtils.import("resource://gre/modules/Console.jsm", {})
.ConsoleAPI;
let consoleOptions = {
// tip: set maxLogLevel to "debug" and use log.debug() to create detailed
// messages during development. See LOG_LEVELS in Console.jsm for details.
maxLogLevel: "error",
maxLogLevelPref: "browser.attribution.loglevel",
prefix: "AttributionCode",
};
return new ConsoleAPI(consoleOptions);
});
XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
const ATTR_CODE_MAX_LENGTH = 1010;
@ -39,15 +56,55 @@ let gCachedAttrData = null;
var AttributionCode = {
/**
* Returns an nsIFile for the file containing the attribution data.
* Returns a platform-specific nsIFile for the file containing the attribution
* data, or null if the current platform does not support (caching)
* attribution data.
*/
get attributionFile() {
let file = Services.dirsvc.get("LocalAppData", Ci.nsIFile);
// appinfo does not exist in xpcshell, so we need defaults.
file.append(Services.appinfo.vendor || "mozilla");
file.append(AppConstants.MOZ_APP_NAME);
file.append("postSigningData");
return file;
if (AppConstants.platform == "win") {
let file = Services.dirsvc.get("LocalAppData", Ci.nsIFile);
// appinfo does not exist in xpcshell, so we need defaults.
file.append(Services.appinfo.vendor || "mozilla");
file.append(AppConstants.MOZ_APP_NAME);
if (!file.exists()) {
file.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
}
file.append("postSigningData");
return file;
} else if (AppConstants.platform == "macosx") {
// There's no `UpdRootD` in xpcshell tests. Some existing tests override
// it, which is onerous and difficult to share across tests. When
// testing, if it's not defined, fallback to the xpcshell temp directory.
let file;
try {
file = Services.dirsvc.get("UpdRootD", Ci.nsIFile);
} catch (ex) {
let env = Cc["@mozilla.org/process/environment;1"].getService(
Ci.nsIEnvironment
);
// It's most common to test for the profile dir, even though we actually
// are using the temp dir.
if (
ex instanceof Ci.nsIException &&
ex.result == Cr.NS_ERROR_FAILURE &&
env.exists("XPCSHELL_TEST_PROFILE_DIR")
) {
let path = env.get("XPCSHELL_TEST_TEMP_DIR");
file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
file.initWithPath(path);
} else {
throw ex;
}
}
if (!file.exists()) {
file.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
}
file.append("macAttributionData");
return file;
}
return null;
},
/**
@ -89,6 +146,9 @@ var AttributionCode = {
parsed[key] = value;
}
} else {
log.debug(
`parseAttributionCode: "${code}" => isValid = false: "${key}", "${value}"`
);
isValid = false;
break;
}
@ -105,6 +165,43 @@ var AttributionCode = {
return {};
},
/**
* Returns an object containing a key-value pair for each piece of attribution
* data included in the passed-in URL containing a query string encoding an
* attribution code.
*
* We have less control of the attribution codes on macOS so we accept more
* URLs than we accept attribution codes on Windows.
*
* If the URL is empty, returns an empty object.
*
* If the URL doesn't parse, throws.
*/
parseAttributionCodeFromUrl(url) {
if (!url) {
return {};
}
let parsed = {};
let params = new URL(url).searchParams;
for (let key of ATTR_CODE_KEYS) {
// We support the key prefixed with utm_ or not, but intentionally
// choose non-utm params over utm params.
for (let paramKey of [`utm_${key}`, `funnel_${key}`, key]) {
if (params.has(paramKey)) {
// We expect URI-encoded components in our attribution codes.
let value = encodeURIComponent(params.get(paramKey));
if (value && ATTR_CODE_VALUE_REGEX.test(value)) {
parsed[key] = value;
}
}
}
}
return parsed;
},
/**
* Returns a string serializing the given attribution data.
*
@ -137,58 +234,125 @@ var AttributionCode = {
*/
async getAttrDataAsync() {
if (gCachedAttrData != null) {
log.debug(
`getAttrDataAsync: attribution is cached: ${JSON.stringify(
gCachedAttrData
)}`
);
return gCachedAttrData;
}
gCachedAttrData = {};
if (AppConstants.platform == "win") {
let bytes;
try {
bytes = await OS.File.read(this.attributionFile.path);
} catch (ex) {
if (ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
return gCachedAttrData;
}
Services.telemetry
.getHistogramById("BROWSER_ATTRIBUTION_ERRORS")
.add("read_error");
}
if (bytes) {
try {
let decoder = new TextDecoder();
let code = decoder.decode(bytes);
gCachedAttrData = this.parseAttributionCode(code);
} catch (ex) {
// TextDecoder can throw an error
Services.telemetry
.getHistogramById("BROWSER_ATTRIBUTION_ERRORS")
.add("decode_error");
}
}
} else if (AppConstants.platform == "macosx") {
const { MacAttribution } = ChromeUtils.import(
"resource:///modules/MacAttribution.jsm"
let attributionFile = this.attributionFile;
if (!attributionFile) {
// This platform doesn't support attribution.
log.debug(`getAttrDataAsync: no attribution (attributionFile is null)`);
return gCachedAttrData;
}
if (
AppConstants.platform == "macosx" &&
!(await OS.File.exists(attributionFile.path))
) {
log.debug(
`getAttrDataAsync: macOS && !exists("${attributionFile.path}")`
);
// On macOS, we fish the attribution data from the system quarantine DB.
try {
let referrer = await MacAttribution.getReferrerUrl();
let params = new URL(referrer).searchParams;
for (let key of ATTR_CODE_KEYS) {
// We support the key prefixed with utm_ or not, but intentionally
// choose non-utm params over utm params.
for (let paramKey of [`utm_${key}`, `funnel_${key}`, key]) {
if (params.has(paramKey)) {
// We expect URI-encoded components.
let value = encodeURIComponent(params.get(paramKey));
if (value && ATTR_CODE_VALUE_REGEX.test(value)) {
gCachedAttrData[key] = value;
}
}
}
}
log.debug(
`getAttrDataAsync: macOS attribution getReferrerUrl: "${referrer}"`
);
gCachedAttrData = this.parseAttributionCodeFromUrl(referrer);
} catch (ex) {
// No attributions
// Avoid partial attribution data.
gCachedAttrData = {};
// No attributions. Just `warn` 'cuz this isn't necessarily an error.
log.warn("Caught exception fetching macOS attribution codes!", ex);
if (
ex instanceof Ci.nsIException &&
ex.result == Cr.NS_ERROR_UNEXPECTED
) {
// Bad quarantine data.
Services.telemetry
.getHistogramById("BROWSER_ATTRIBUTION_ERRORS")
.add("quarantine_error");
}
}
log.debug(`macOS attribution data is ${JSON.stringify(gCachedAttrData)}`);
// We only want to try to fetch the referrer from the quarantine
// database once on macOS.
try {
let s = this.serializeAttributionData(gCachedAttrData);
log.debug(`macOS attribution data serializes as "${s}"`);
let bytes = new TextEncoder().encode(s);
await OS.File.writeAtomic(attributionFile.path, bytes);
} catch (ex) {
log.debug(`Caught exception writing "${attributionFile.path}"`, ex);
Services.telemetry
.getHistogramById("BROWSER_ATTRIBUTION_ERRORS")
.add("write_error");
return gCachedAttrData;
}
log.debug(
`Returning after successfully writing "${attributionFile.path}"`
);
return gCachedAttrData;
}
log.debug(`getAttrDataAsync: !macOS || !exists("${attributionFile.path}")`);
let bytes;
try {
bytes = await OS.File.read(attributionFile.path);
} catch (ex) {
if (ex instanceof OS.File.Error && ex.becauseNoSuchFile) {
log.debug(
`getAttrDataAsync: !exists("${
attributionFile.path
}"), returning ${JSON.stringify(gCachedAttrData)}`
);
return gCachedAttrData;
}
Services.telemetry
.getHistogramById("BROWSER_ATTRIBUTION_ERRORS")
.add("read_error");
}
if (bytes) {
try {
let decoder = new TextDecoder();
let code = decoder.decode(bytes);
log.debug(
`getAttrDataAsync: ${attributionFile.path} deserializes to ${code}`
);
if (AppConstants.platform == "macosx" && !code) {
// On macOS, an empty attribution code is fine. (On Windows, that
// means the stub/full installer has been incorrectly attributed,
// which is an error.)
return gCachedAttrData;
}
gCachedAttrData = this.parseAttributionCode(code);
log.debug(
`getAttrDataAsync: ${code} parses to ${JSON.stringify(
gCachedAttrData
)}`
);
} catch (ex) {
// TextDecoder can throw an error
Services.telemetry
.getHistogramById("BROWSER_ATTRIBUTION_ERRORS")
.add("decode_error");
}
}
return gCachedAttrData;
},

View File

@ -1,4 +1,8 @@
[DEFAULT]
support-files =
head.js
[browser_AttributionCode_telemetry.js]
skip-if = os != "win" # Windows only telemetry
skip-if = (os != "win" && toolkit != "cocoa") # Windows and macOS only telemetry.
[browser_AttributionCode_Mac_telemetry.js]
skip-if = toolkit != "cocoa" # macOS only telemetry.

View File

@ -0,0 +1,249 @@
ChromeUtils.defineModuleGetter(
this,
"TelemetryTestUtils",
"resource://testing-common/TelemetryTestUtils.jsm"
);
const { MacAttribution } = ChromeUtils.import(
"resource:///modules/MacAttribution.jsm"
);
const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
async function assertCacheExistsAndIsEmpty() {
// We should have written to the cache, and be able to read back
// with no errors.
const histogram = Services.telemetry.getHistogramById(
"BROWSER_ATTRIBUTION_ERRORS"
);
histogram.clear();
ok(await OS.File.exists(AttributionCode.attributionFile.path));
Assert.deepEqual(
"",
new TextDecoder().decode(
await OS.File.read(AttributionCode.attributionFile.path)
)
);
AttributionCode._clearCache();
let result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(result, {}, "Should be able to get cached result");
Assert.deepEqual({}, histogram.snapshot().values || {});
}
add_task(async function test_write_error() {
const sandbox = sinon.createSandbox();
let attributionSvc = Cc["@mozilla.org/mac-attribution;1"].getService(
Ci.nsIMacAttributionService
);
attributionSvc.setReferrerUrl(
MacAttribution.applicationPath,
"https://example.com?content=content",
true
);
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
const histogram = Services.telemetry.getHistogramById(
"BROWSER_ATTRIBUTION_ERRORS"
);
try {
// Clear any existing telemetry
histogram.clear();
// Force the file to not exist and then cause a write error. This is delicate
// because various background tasks may invoke `OS.File.writeAtomic` while
// this test is running. Be careful to only stub the one call.
const writeAtomic = sandbox.stub(OS.File, "writeAtomic");
writeAtomic
.withArgs(
sinon.match(AttributionCode.attributionFile.path),
sinon.match.any
)
.throws(() => new Error("write_error"));
OS.File.writeAtomic.callThrough();
// Try to read the attribution code.
let result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(
result,
{ content: "content" },
"Should be able to get a result even if the file doesn't write"
);
TelemetryTestUtils.assertHistogram(histogram, INDEX_WRITE_ERROR, 1);
} finally {
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
histogram.clear();
sandbox.restore();
}
});
add_task(async function test_unusual_referrer() {
// This referrer URL looks malformed, but the malformed bits are dropped, so
// it's actually ok. This is what allows extraneous bits like `fbclid` tags
// to be ignored.
let attributionSvc = Cc["@mozilla.org/mac-attribution;1"].getService(
Ci.nsIMacAttributionService
);
attributionSvc.setReferrerUrl(
MacAttribution.applicationPath,
"https://example.com?content=&=campaign",
true
);
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
const histogram = Services.telemetry.getHistogramById(
"BROWSER_ATTRIBUTION_ERRORS"
);
try {
// Clear any existing telemetry
histogram.clear();
// Try to read the attribution code
await AttributionCode.getAttrDataAsync();
let result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(result, {}, "Should be able to get empty result");
Assert.deepEqual({}, histogram.snapshot().values || {});
await assertCacheExistsAndIsEmpty();
} finally {
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
histogram.clear();
}
});
add_task(async function test_blank_referrer() {
let attributionSvc = Cc["@mozilla.org/mac-attribution;1"].getService(
Ci.nsIMacAttributionService
);
attributionSvc.setReferrerUrl(MacAttribution.applicationPath, "", true);
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
const histogram = Services.telemetry.getHistogramById(
"BROWSER_ATTRIBUTION_ERRORS"
);
try {
// Clear any existing telemetry
histogram.clear();
// Try to read the attribution code
let result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(result, {}, "Should be able to get empty result");
Assert.deepEqual({}, histogram.snapshot().values || {});
await assertCacheExistsAndIsEmpty();
} finally {
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
histogram.clear();
}
});
add_task(async function test_no_referrer() {
const sandbox = sinon.createSandbox();
let newApplicationPath = MacAttribution.applicationPath + ".test";
sandbox.stub(MacAttribution, "applicationPath").get(() => newApplicationPath);
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
const histogram = Services.telemetry.getHistogramById(
"BROWSER_ATTRIBUTION_ERRORS"
);
try {
// Clear any existing telemetry
histogram.clear();
// Try to read the attribution code
await AttributionCode.getAttrDataAsync();
let result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(result, {}, "Should be able to get empty result");
Assert.deepEqual({}, histogram.snapshot().values || {});
await assertCacheExistsAndIsEmpty();
} finally {
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
histogram.clear();
sandbox.restore();
}
});
add_task(async function test_broken_referrer() {
let attributionSvc = Cc["@mozilla.org/mac-attribution;1"].getService(
Ci.nsIMacAttributionService
);
attributionSvc.setReferrerUrl(
MacAttribution.applicationPath,
"https://example.com?content=content",
true
);
// This uses macOS internals to change the GUID so that it will look like the
// application has quarantine data but nothing will be pressent in the
// quarantine database. This shouldn't happen in the wild.
function generateQuarantineGUID() {
let str = Cc["@mozilla.org/uuid-generator;1"]
.getService(Ci.nsIUUIDGenerator)
.generateUUID()
.toString()
.toUpperCase();
// Strip {}.
return str.substring(1, str.length - 1);
}
// These magic constants are macOS GateKeeper flags.
let string = [
"01c1",
"5991b778",
"Safari.app",
generateQuarantineGUID(),
].join(";");
let bytes = new TextEncoder().encode(string);
await OS.File.macSetXAttr(
MacAttribution.applicationPath,
"com.apple.quarantine",
bytes
);
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
const histogram = Services.telemetry.getHistogramById(
"BROWSER_ATTRIBUTION_ERRORS"
);
try {
// Clear any existing telemetry
histogram.clear();
// Try to read the attribution code
await AttributionCode.getAttrDataAsync();
let result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(result, {}, "Should be able to get empty result");
TelemetryTestUtils.assertHistogram(histogram, INDEX_QUARANTINE_ERROR, 1);
histogram.clear();
await assertCacheExistsAndIsEmpty();
} finally {
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
histogram.clear();
}
});

View File

@ -3,25 +3,21 @@ ChromeUtils.defineModuleGetter(
"TelemetryTestUtils",
"resource://testing-common/TelemetryTestUtils.jsm"
);
const { AttributionCode } = ChromeUtils.import(
"resource:///modules/AttributionCode.jsm"
);
ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
add_task(function setup() {
// Clear cache call is only possible in a testing environment
let env = Cc["@mozilla.org/process/environment;1"].getService(
Ci.nsIEnvironment
);
env.set("XPCSHELL_TEST_PROFILE_DIR", "testing");
registerCleanupFunction(() => {
env.set("XPCSHELL_TEST_PROFILE_DIR", null);
});
});
add_task(async function test_parse_error() {
if (AppConstants.platform == "macosx") {
// On macOS, the underlying data is the OS-level quarantine
// database. We need to start from nothing to isolate the cache.
const { MacAttribution } = ChromeUtils.import(
"resource:///modules/MacAttribution.jsm"
);
let attributionSvc = Cc["@mozilla.org/mac-attribution;1"].getService(
Ci.nsIMacAttributionService
);
attributionSvc.setReferrerUrl(MacAttribution.applicationPath, "", true);
}
registerCleanupFunction(async () => {
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
@ -44,13 +40,16 @@ add_task(async function test_parse_error() {
// Write an invalid file to trigger a decode error
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
await AttributionCode.writeAttributionFile(""); // empty string is invalid
// Empty string is valid on macOS.
await AttributionCode.writeAttributionFile(
AppConstants.platform == "macosx" ? "invalid" : ""
);
result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(result, {}, "Should have failed to parse");
// `assertHistogram` also ensures that `read_error` index 0 is 0
// as we should not have recorded telemetry from the previous `getAttrDataAsync` call
TelemetryTestUtils.assertHistogram(histogram, 1, 1);
TelemetryTestUtils.assertHistogram(histogram, INDEX_DECODE_ERROR, 1);
// Reset
histogram.clear();
});
@ -70,14 +69,16 @@ add_task(async function test_read_error() {
// Clear any existing telemetry
histogram.clear();
// Force a read error
const stub = sandbox.stub(OS.File, "read");
stub.throws(() => new Error("read_error"));
// Force the file to exist but then cause a read error
const exists = sandbox.stub(OS.File, "exists");
exists.resolves(true);
const read = sandbox.stub(OS.File, "read");
read.throws(() => new Error("read_error"));
// Try to read the file
await AttributionCode.getAttrDataAsync();
// It should record the read error
TelemetryTestUtils.assertHistogram(histogram, 0, 1);
TelemetryTestUtils.assertHistogram(histogram, INDEX_READ_ERROR, 1);
// Clear any existing telemetry
histogram.clear();

View File

@ -0,0 +1,27 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/
*/
"use strict";
const { AttributionCode } = ChromeUtils.import(
"resource:///modules/AttributionCode.jsm"
);
ChromeUtils.defineModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm");
// Keep in sync with `BROWSER_ATTRIBUTION_ERRORS` in Histograms.json.
const INDEX_READ_ERROR = 0;
const INDEX_DECODE_ERROR = 1;
const INDEX_WRITE_ERROR = 2;
const INDEX_QUARANTINE_ERROR = 3;
add_task(function setup() {
// AttributionCode._clearCache is only possible in a testing environment
let env = Cc["@mozilla.org/process/environment;1"].getService(
Ci.nsIEnvironment
);
env.set("XPCSHELL_TEST_PROFILE_DIR", "testing");
registerCleanupFunction(() => {
env.set("XPCSHELL_TEST_PROFILE_DIR", null);
});
});

View File

@ -62,3 +62,56 @@ let invalidAttrCodes = [
// Empty key name
"source%3Dgoogle.com%26medium%3Dorganic%26%3Dgeneticallymodified",
];
/**
* Arrange for each test to have a unique application path for storing
* quarantine data.
*
* The quarantine data is necessarily a shared system resource, managed by the
* OS, so we need to avoid polluting it during tests.
*
* There are at least two ways to achieve this. Here we use Sinon to stub the
* relevant accessors: this has the advantage of being local and relatively easy
* to follow. In the App Update Service tests, an `nsIDirectoryServiceProvider`
* is installed, which is global and much harder to extract for re-use.
*/
async function setupStubs() {
// Local imports to avoid polluting the global namespace.
const { AppConstants } = ChromeUtils.import(
"resource://gre/modules/AppConstants.jsm"
);
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
const { sinon } = ChromeUtils.import("resource://testing-common/Sinon.jsm");
// This depends on the caller to invoke it by name. We do try to
// prevent the most obvious incorrect invocation, namely
// `add_task(setupStubs)`.
let caller = Components.stack.caller;
const testID = caller.filename
.toString()
.split("/")
.pop()
.split(".")[0];
notEqual(testID, "head");
let applicationFile = do_get_tempdir();
applicationFile.append(testID);
applicationFile.append("App.app");
if (AppConstants.platform == "macosx") {
// We're implicitly using the fact that modules are shared between importers here.
const { MacAttribution } = ChromeUtils.import(
"resource:///modules/MacAttribution.jsm"
);
sinon
.stub(MacAttribution, "applicationPath")
.get(() => applicationFile.path);
}
// The macOS quarantine database applies to existing paths only, so make
// sure our mock application path exists. This also creates the parent
// directory for the attribution file, needed on both macOS and Windows. We
// don't ignore existing paths because we're inside a temporary directory:
// this should never be invoked twice for the same test.
await OS.File.makeDir(applicationFile.path, { from: do_get_tempdir().path });
}

View File

@ -9,6 +9,10 @@ const { AppConstants } = ChromeUtils.import(
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
add_task(async () => {
await setupStubs();
});
/**
* Test validation of attribution codes,
* to make sure we reject bad ones and accept good ones.

View File

@ -8,6 +8,10 @@ const { MacAttribution } = ChromeUtils.import(
"resource:///modules/MacAttribution.jsm"
);
add_task(async () => {
await setupStubs();
});
add_task(async function testValidAttrCodes() {
let appPath = MacAttribution.applicationPath;
let attributionSvc = Cc["@mozilla.org/mac-attribution;1"].getService(
@ -25,7 +29,8 @@ add_task(async function testValidAttrCodes() {
let referrer = await MacAttribution.getReferrerUrl(appPath);
equal(referrer, url, "overwrite referrer url");
// Read attribution code from referrer to ensure cache is fresh.
// Read attribution code from referrer.
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
let result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(
@ -34,6 +39,15 @@ add_task(async function testValidAttrCodes() {
"Parsed code should match expected value, code was: " + entry.code
);
// Read attribution code from file.
AttributionCode._clearCache();
result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(
result,
entry.parsed,
"Parsed code should match expected value, code was: " + entry.code
);
// Does not overwrite cached existing attribution code.
attributionSvc.setReferrerUrl(appPath, "http://test.com", false);
referrer = await MacAttribution.getReferrerUrl(appPath);
@ -70,7 +84,8 @@ add_task(async function testInvalidAttrCodes() {
}
equal(referrer, url, "overwrite referrer url");
// Read attribution code from referrer to ensure cache is fresh.
// Read attribution code from referrer.
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
let result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(result, {}, "Code should have failed to parse: " + code);

View File

@ -1,11 +1,9 @@
[DEFAULT]
firefox-appdir = browser
skip-if = toolkit == 'android'
skip-if = (os != "win" && toolkit != "cocoa") # Only available on Windows and macOS
head = head.js
[test_AttributionCode.js]
skip-if = os != 'win' # windows specific tests
[test_MacAttribution.js]
skip-if = toolkit != "cocoa" # osx specific tests
[test_attribution_parsing.js]
skip-if = os != 'win' # windows specific tests

View File

@ -11118,14 +11118,14 @@
"BROWSER_ATTRIBUTION_ERRORS": {
"record_in_processes": ["main", "content"],
"products": ["firefox"],
"expires_in_version": "86",
"expires_in_version": "90",
"kind": "categorical",
"labels": ["read_error", "decode_error"],
"description": "Count for the number of errors encountered trying to read the attribution data from the stub installer.",
"labels": ["read_error", "decode_error", "write_error", "quarantine_error"],
"description": "Count for the number of errors encountered trying to determine attribution data: on Windows, from the installers (stub and full); on macOS, from the quarantine database.",
"releaseChannelCollection": "opt-out",
"bug_numbers": [1621402],
"bug_numbers": [1621402, 1525076],
"alert_emails": ["aoprea@mozilla.com"],
"operating_systems": ["windows"]
"operating_systems": ["mac", "windows"]
},
"MIXED_CONTENT_PAGE_LOAD": {
"record_in_processes": ["main", "content"],

View File

@ -341,14 +341,14 @@ function spoofPartnerInfo() {
}
async function spoofAttributionData() {
if (gIsWindows) {
if (gIsWindows || gIsMac) {
AttributionCode._clearCache();
await AttributionCode.writeAttributionFile(ATTRIBUTION_CODE);
}
}
function cleanupAttributionData() {
if (gIsWindows) {
if (gIsWindows || gIsMac) {
AttributionCode.attributionFile.remove(false);
AttributionCode._clearCache();
}
@ -490,7 +490,7 @@ function checkSettingsSection(data) {
Assert.equal(typeof data.settings.defaultPrivateSearchEngineData, "object");
}
if (gIsWindows && AppConstants.MOZ_BUILD_APP == "browser") {
if ((gIsWindows || gIsMac) && AppConstants.MOZ_BUILD_APP == "browser") {
Assert.equal(typeof data.settings.attribution, "object");
Assert.equal(data.settings.attribution.source, "google.com");
}