Backed out 3 changesets (bug 1865845) for bc failures on browser_AttributionCode_telemetry.js . CLOSED TREE

Backed out changeset 4f799b89c628 (bug 1865845)
Backed out changeset e825fb03ff56 (bug 1865845)
Backed out changeset 9a8c396a22db (bug 1865845)
This commit is contained in:
Narcis Beleuzu 2024-01-09 21:26:48 +02:00
parent a8c700aee3
commit b7ba2ef9c7
13 changed files with 244 additions and 170 deletions

View File

@ -16,6 +16,7 @@ import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
MacAttribution: "resource:///modules/MacAttribution.sys.mjs",
UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
});
ChromeUtils.defineLazyGetter(lazy, "log", () => {
let { ConsoleAPI } = ChromeUtils.importESModule(
@ -81,6 +82,49 @@ export var AttributionCode = {
let file = Services.dirsvc.get("GreD", Ci.nsIFile);
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 a nested subdirectory of the xpcshell
// temp directory. Nesting more closely replicates the situation where the
// update directory does not (yet) exist, testing a scenario witnessed in
// development.
let file;
try {
file = Services.dirsvc.get("UpdRootD", Ci.nsIFile);
} catch (ex) {
// 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 &&
Services.env.exists("XPCSHELL_TEST_PROFILE_DIR")
) {
let path = Services.env.get("XPCSHELL_TEST_TEMP_DIR");
file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
file.initWithPath(path);
file.append("nested_UpdRootD_1");
file.append("nested_UpdRootD_2");
} else {
throw ex;
}
}
// Note: this file is in a location that includes the absolute path
// to the running install, and the filename includes the update channel.
// To ensure consistency regardless of when `attributionFile` is accessed we
// explicitly do not include partner IDs that may be part of the full update channel.
// These are not necessarily applied when this is first accessed, and we want to
// ensure consistency between early and late accesses.
// Partner builds never contain attribution information, so this has no known
// consequences.
// For example:
// ~/Library/Caches/Mozilla/updates/Applications/Firefox/macAttributionDataCache-release
// This is done to ensure that attribution data is preserved through a
// pave over install of an install on the same channel.
file.append(
"macAttributionDataCache-" + lazy.UpdateUtils.getUpdateChannel(false)
);
return file;
}
return null;
@ -91,8 +135,8 @@ export var AttributionCode = {
* @param {String} code to write.
*/
async writeAttributionFile(code) {
// Writing attribution files is only used as part of test code
// so bailing here for MSIX builds is no big deal.
// Writing attribution files is only used as part of test code, and Mac
// attribution, so bailing here for MSIX builds is no big deal.
if (
AppConstants.platform === "win" &&
Services.sysinfo.getProperty("hasWinPackageId")
@ -183,58 +227,6 @@ export var AttributionCode = {
return s;
},
async _getMacAttrDataAsync() {
// On macOS, we fish the attribution data from an extended attribute on
// the .app bundle directory.
try {
let attrStr = await lazy.MacAttribution.getAttributionString();
lazy.log.debug(
`_getMacAttrDataAsync: getAttributionString: "${attrStr}"`
);
gCachedAttrData = this.parseAttributionCode(attrStr);
} catch (ex) {
// Avoid partial attribution data.
gCachedAttrData = {};
// No attributions. Just `warn` 'cuz this isn't necessarily an error.
lazy.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");
}
}
lazy.log.debug(
`macOS attribution data is ${JSON.stringify(gCachedAttrData)}`
);
return gCachedAttrData;
},
async _getWindowsNSISAttrDataAsync() {
return AttributionIOUtils.read(this.attributionFile.path);
},
async _getWindowsMSIXAttrDataAsync() {
// This comes out of windows-package-manager _not_ URL encoded or in an ArrayBuffer,
// but the parsing code wants it that way. It's easier to just provide that
// than have the parsing code support both.
lazy.log.debug(
`winPackageFamilyName is: ${Services.sysinfo.getProperty(
"winPackageFamilyName"
)}`
);
let encoder = new TextEncoder();
return encoder.encode(encodeURIComponent(await this.msixCampaignId));
},
/**
* Reads the attribution code, either from disk or a cached version.
* Returns a promise that fulfills with an object containing the parsed
@ -246,10 +238,6 @@ export var AttributionCode = {
* strip "utm_" while retrieving the params.
*/
async getAttrDataAsync() {
if (AppConstants.platform != "win" && AppConstants.platform != "macosx") {
// This platform doesn't support attribution.
return gCachedAttrData;
}
if (gCachedAttrData != null) {
lazy.log.debug(
`getAttrDataAsync: attribution is cached: ${JSON.stringify(
@ -271,26 +259,98 @@ export var AttributionCode = {
}
gCachedAttrData = {};
if (AppConstants.platform == "macosx") {
lazy.log.debug(`getAttrDataAsync: macOS`);
return this._getMacAttrDataAsync();
let attributionFile = this.attributionFile;
if (!attributionFile) {
// This platform doesn't support attribution.
lazy.log.debug(
`getAttrDataAsync: no attribution (attributionFile is null)`
);
return gCachedAttrData;
}
lazy.log.debug("getAttrDataAsync: !macOS");
if (
AppConstants.platform == "macosx" &&
!(await AttributionIOUtils.exists(attributionFile.path))
) {
lazy.log.debug(
`getAttrDataAsync: macOS && !exists("${attributionFile.path}")`
);
// On macOS, we fish the attribution data from an extended attribute on
// the .app bundle directory.
try {
let attrStr = await lazy.MacAttribution.getAttributionString();
lazy.log.debug(
`getAttrDataAsync: macOS attribution getAttributionString: "${attrStr}"`
);
gCachedAttrData = this.parseAttributionCode(attrStr);
} catch (ex) {
// Avoid partial attribution data.
gCachedAttrData = {};
// No attributions. Just `warn` 'cuz this isn't necessarily an error.
lazy.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");
}
}
lazy.log.debug(
`macOS attribution data is ${JSON.stringify(gCachedAttrData)}`
);
// We only want to try to fetch the attribution string once on macOS
try {
let code = this.serializeAttributionData(gCachedAttrData);
lazy.log.debug(`macOS attribution data serializes as "${code}"`);
await this.writeAttributionFile(code);
} catch (ex) {
lazy.log.debug(
`Caught exception writing "${attributionFile.path}"`,
ex
);
Services.telemetry
.getHistogramById("BROWSER_ATTRIBUTION_ERRORS")
.add("write_error");
return gCachedAttrData;
}
lazy.log.debug(
`Returning after successfully writing "${attributionFile.path}"`
);
return gCachedAttrData;
}
lazy.log.debug(
`getAttrDataAsync: !macOS || !exists("${attributionFile.path}")`
);
let attributionFile = this.attributionFile;
let bytes;
try {
if (
AppConstants.platform === "win" &&
Services.sysinfo.getProperty("hasWinPackageId")
) {
lazy.log.debug("getAttrDataAsync: MSIX");
bytes = await this._getWindowsMSIXAttrDataAsync();
// This comes out of windows-package-manager _not_ URL encoded or in an ArrayBuffer,
// but the parsing code wants it that way. It's easier to just provide that
// than have the parsing code support both.
lazy.log.debug(
`winPackageFamilyName is: ${Services.sysinfo.getProperty(
"winPackageFamilyName"
)}`
);
let encoder = new TextEncoder();
bytes = encoder.encode(encodeURIComponent(await this.msixCampaignId()));
} else {
lazy.log.debug("getAttrDataAsync: NSIS");
bytes = await this._getWindowsNSISAttrDataAsync();
bytes = await AttributionIOUtils.read(attributionFile.path);
}
} catch (ex) {
if (DOMException.isInstance(ex) && ex.name == "NotFoundError") {
@ -359,15 +419,12 @@ export var AttributionCode = {
* or if the file couldn't be deleted (the promise is never rejected).
*/
async deleteFileAsync() {
// There is no cache file on macOS
if (AppConstants.platform == "win") {
try {
await IOUtils.remove(this.attributionFile.path);
} catch (ex) {
// The attribution file may already have been deleted,
// or it may have never been installed at all;
// failure to delete it isn't an error.
}
try {
await IOUtils.remove(this.attributionFile.path);
} catch (ex) {
// The attribution file may already have been deleted,
// or it may have never been installed at all;
// failure to delete it isn't an error.
}
},

View File

@ -18,7 +18,7 @@ export var MacAttribution = {
return IOUtils.setMacXAttr(
path,
"com.apple.application-instance",
new TextEncoder().encode(`__MOZCUSTOM__${aAttrStr}`)
new TextEncoder().encode(aAttrStr)
);
},

View File

@ -6,6 +6,4 @@ prefs = ["browser.attribution.macos.enabled=true"]
skip-if = ["toolkit != 'cocoa'"] # macOS only telemetry.
["browser_AttributionCode_telemetry.js"]
# These tests only cover the attribution cache file - which only exists on
# Windows.
skip-if = ["os != 'win'"]
skip-if = ["(os != 'win' && toolkit != 'cocoa')"] # Windows and macOS only telemetry.

View File

@ -11,20 +11,77 @@ const { sinon } = ChromeUtils.importESModule(
"resource://testing-common/Sinon.sys.mjs"
);
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 AttributionIOUtils.exists(AttributionCode.attributionFile.path));
Assert.deepEqual(
"",
new TextDecoder().decode(
await AttributionIOUtils.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();
await MacAttribution.setAttributionString("__MOZCUSTOM__content%3Dcontent");
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
const histogram = Services.telemetry.getHistogramById(
"BROWSER_ATTRIBUTION_ERRORS"
);
let oldExists = AttributionIOUtils.exists;
let oldWrite = AttributionIOUtils.write;
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 `IOUtils.writeAtomic` while
// this test is running. Be careful to only stub the one call.
AttributionIOUtils.exists = () => false;
AttributionIOUtils.write = () => {
throw new Error("write_error");
};
// 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 {
AttributionIOUtils.exists = oldExists;
AttributionIOUtils.write = oldWrite;
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
histogram.clear();
sandbox.restore();
}
});
add_task(async function test_blank_attribution() {
// Ensure no attribution information is present
try {
await MacAttribution.delAttributionString();
} catch (ex) {
// NS_ERROR_DOM_NOT_FOUND_ERR means there was not an attribution
// string to delete - which we can safely ignore.
if (
!(ex instanceof Ci.nsIException) ||
ex.result != Cr.NS_ERROR_DOM_NOT_FOUND_ERR
) {
throw ex;
}
}
await MacAttribution.delAttributionString();
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
const histogram = Services.telemetry.getHistogramById(
@ -40,7 +97,10 @@ add_task(async function test_blank_attribution() {
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();
}
@ -51,6 +111,7 @@ add_task(async function test_no_attribution() {
let newApplicationPath = MacAttribution.applicationPath + ".test";
sandbox.stub(MacAttribution, "applicationPath").get(() => newApplicationPath);
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
const histogram = Services.telemetry.getHistogramById(
@ -67,7 +128,10 @@ add_task(async function test_no_attribution() {
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();

View File

@ -6,6 +6,13 @@ const { AttributionIOUtils } = ChromeUtils.importESModule(
);
add_task(async function test_parse_error() {
if (AppConstants.platform == "macosx") {
const { MacAttribution } = ChromeUtils.importESModule(
"resource:///modules/MacAttribution.sys.mjs"
);
MacAttribution.setAttributionString("");
}
registerCleanupFunction(async () => {
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
@ -34,7 +41,10 @@ add_task(async function test_parse_error() {
) {
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
await AttributionCode.writeAttributionFile("");
// 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");

View File

@ -39,14 +39,7 @@ add_task(async function testValidAttrCodes() {
// are not URI encoded, and the AttributionCode code that deals with
// them expects that - so we have to simulate that as well.
msixCampaignIdStub.callsFake(async () => decodeURIComponent(currentCode));
} else if (AppConstants.platform === "macosx") {
const { MacAttribution } = ChromeUtils.importESModule(
"resource:///modules/MacAttribution.sys.mjs"
);
await MacAttribution.setAttributionString(currentCode);
} else {
// non-msix windows
await AttributionCode.writeAttributionFile(currentCode);
}
AttributionCode._clearCache();
@ -87,14 +80,7 @@ add_task(async function testInvalidAttrCodes() {
}
msixCampaignIdStub.callsFake(async () => decodeURIComponent(currentCode));
} else if (AppConstants.platform === "macosx") {
const { MacAttribution } = ChromeUtils.importESModule(
"resource:///modules/MacAttribution.sys.mjs"
);
await MacAttribution.setAttributionString(currentCode);
} else {
// non-msix windows
await AttributionCode.writeAttributionFile(currentCode);
}
AttributionCode._clearCache();
@ -116,12 +102,11 @@ add_task(async function testInvalidAttrCodes() {
* and making sure we still get the expected code.
*/
let condition = {
// macOS and MSIX attribution codes are not cached by us, thus this test is
// MSIX attribution codes are not cached by us, thus this test is
// unnecessary for those builds.
skip_if: () =>
(AppConstants.platform === "win" &&
Services.sysinfo.getProperty("hasWinPackageId")) ||
AppConstants.platform === "macosx",
AppConstants.platform === "win" &&
Services.sysinfo.getProperty("hasWinPackageId"),
};
add_task(condition, async function testDeletedFile() {
// Set up the test by clearing the cache and writing a valid file.

View File

@ -61,13 +61,7 @@ add_task(async () => {
// data.
add_task(async function testExtendedAttributeProcessing() {
for (let entry of extendedAttributeTestCases) {
// We use setMacXAttr directly here rather than setAttributionString because
// we need the ability to set invalid attribution strings.
await IOUtils.setMacXAttr(
MacAttribution.applicationPath,
"com.apple.application-instance",
new TextEncoder().encode(entry.raw)
);
await MacAttribution.setAttributionString(entry.raw);
try {
let got = await MacAttribution.getAttributionString();
if (entry.error === true) {
@ -95,9 +89,10 @@ add_task(async function testValidAttrCodes() {
continue;
}
await MacAttribution.setAttributionString(entry.code);
await MacAttribution.setAttributionString("__MOZCUSTOM__" + entry.code);
// Read attribution code from xattr.
await AttributionCode.deleteFileAsync();
AttributionCode._clearCache();
let result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(
@ -107,6 +102,7 @@ add_task(async function testValidAttrCodes() {
);
// Read attribution code from cache.
AttributionCode._clearCache();
result = await AttributionCode.getAttrDataAsync();
Assert.deepEqual(
result,
@ -129,7 +125,8 @@ add_task(async function testInvalidAttrCodes() {
for (let code of invalidAttrCodes) {
await MacAttribution.setAttributionString("__MOZCUSTOM__" + code);
// Read attribution code from xattr
// 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

@ -1865,7 +1865,12 @@ class _ASRouter {
encodeURIComponent(attributionData)
);
} else if (AppConstants.platform === "macosx") {
await this.setAttributionString(encodeURIComponent(attributionData));
await this.setAttributionString(
`__MOZCUSTOM__${encodeURIComponent(attributionData)}`
);
// Delete attribution data file
await AttributionCode.deleteFileAsync();
}
// Clear cache call is only possible in a testing environment

View File

@ -21,7 +21,7 @@ add_task(async function check_attribution_data() {
// Some setup to fake the correct attribution data
const campaign = "non-fx-button";
const source = "addons.mozilla.org";
const attrStr = `campaign%3D${campaign}%26source%3D${source}`;
const attrStr = `__MOZCUSTOM__campaign%3D${campaign}%26source%3D${source}`;
await MacAttribution.setAttributionString(attrStr);
AttributionCode._clearCache();
await AttributionCode.getAttrDataAsync();

View File

@ -148,27 +148,16 @@ export var TelemetryEnvironmentTesting = {
},
async spoofAttributionData() {
if (gIsWindows) {
if (gIsWindows || gIsMac) {
lazy.AttributionCode._clearCache();
await lazy.AttributionCode.writeAttributionFile(ATTRIBUTION_CODE);
} else if (gIsMac) {
lazy.AttributionCode._clearCache();
const { MacAttribution } = ChromeUtils.importESModule(
"resource:///modules/MacAttribution.sys.mjs"
);
await MacAttribution.setAttributionString(ATTRIBUTION_CODE);
}
},
async cleanupAttributionData() {
if (gIsWindows) {
cleanupAttributionData() {
if (gIsWindows || gIsMac) {
lazy.AttributionCode.attributionFile.remove(false);
lazy.AttributionCode._clearCache();
} else if (gIsMac) {
const { MacAttribution } = ChromeUtils.importESModule(
"resource:///modules/MacAttribution.sys.mjs"
);
await MacAttribution.delAttributionString();
}
},

View File

@ -175,9 +175,7 @@ add_task(async function setup() {
// The attribution functionality only exists in Firefox.
if (AppConstants.MOZ_BUILD_APP == "browser") {
TelemetryEnvironmentTesting.spoofAttributionData();
registerCleanupFunction(async function () {
await TelemetryEnvironmentTesting.cleanupAttributionData;
});
registerCleanupFunction(TelemetryEnvironmentTesting.cleanupAttributionData);
}
await TelemetryEnvironmentTesting.spoofProfileReset();

View File

@ -69,9 +69,7 @@ add_task(async function setup() {
// The attribution functionality only exists in Firefox.
if (AppConstants.MOZ_BUILD_APP == "browser") {
TelemetryEnvironmentTesting.spoofAttributionData();
registerCleanupFunction(async function () {
await TelemetryEnvironmentTesting.cleanupAttributionData;
});
registerCleanupFunction(TelemetryEnvironmentTesting.cleanupAttributionData);
}
await TelemetryEnvironmentTesting.spoofProfileReset();

View File

@ -61,8 +61,6 @@ const LOG_COMPLETE_SUCCESS = "complete_log_success" + COMPARE_LOG_SUFFIX;
const LOG_PARTIAL_SUCCESS = "partial_log_success" + COMPARE_LOG_SUFFIX;
const LOG_PARTIAL_FAILURE = "partial_log_failure" + COMPARE_LOG_SUFFIX;
const LOG_REPLACE_SUCCESS = "replace_log_success";
const MAC_APP_XATTR_KEY = "com.apple.application-instance";
const MAC_APP_XATTR_VALUE = "dlsource%3Dmozillaci";
const USE_EXECV = AppConstants.platform == "linux";
@ -3167,15 +3165,6 @@ async function setupUpdaterTest(
debugDump("finish - setup test file: " + aTestFile.fileName);
});
// Set a similar extended attribute on the `.app` directory as we see in
// the wild. We will verify that it is preserved at the end of tests.
if (AppConstants.platform == "macosx") {
await IOUtils.setMacXAttr(
getApplyDirFile().path,
MAC_APP_XATTR_KEY,
new TextEncoder().encode(MAC_APP_XATTR_VALUE)
);
}
// Add the test directory that will be updated for a successful update or left
// in the initial state for a failed update.
gTestDirs.forEach(function SUT_TD_FE(aTestDir) {
@ -3756,22 +3745,6 @@ function checkFilesAfterUpdateSuccess(
}
});
if (AppConstants.platform == "macosx") {
debugDump("testing that xattrs were preserved after a successful update");
IOUtils.getMacXAttr(getApplyDirFile().path, MAC_APP_XATTR_KEY).then(
bytes => {
Assert.equal(
new TextDecoder().decode(bytes),
MAC_APP_XATTR_VALUE,
"xattr value changed"
);
},
reason => {
Assert.fail(MAC_APP_XATTR_KEY + " xattr is missing!");
}
);
}
checkFilesAfterUpdateCommon(aStageDirExists, aToBeDeletedDirExists);
}