зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1515172 support funnel attributes in attribution code r=mconley
Differential Revision: https://phabricator.services.mozilla.com/D37668 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
2a0d22e428
Коммит
c3d51446f5
|
@ -22,11 +22,17 @@ ChromeUtils.defineModuleGetter(
|
|||
XPCOMUtils.defineLazyGlobalGetters(this, ["URL"]);
|
||||
|
||||
const ATTR_CODE_MAX_LENGTH = 200;
|
||||
const ATTR_CODE_KEYS_REGEX = /^source|medium|campaign|content$/;
|
||||
const ATTR_CODE_VALUE_REGEX = /[a-zA-Z0-9_%\\-\\.\\(\\)]*/;
|
||||
const ATTR_CODE_FIELD_SEPARATOR = "%26"; // URL-encoded &
|
||||
const ATTR_CODE_KEY_VALUE_SEPARATOR = "%3D"; // URL-encoded =
|
||||
const ATTR_CODE_KEYS = ["source", "medium", "campaign", "content"];
|
||||
const ATTR_CODE_KEYS = [
|
||||
"source",
|
||||
"medium",
|
||||
"campaign",
|
||||
"content",
|
||||
"experiment",
|
||||
"variation",
|
||||
];
|
||||
|
||||
let gCachedAttrData = null;
|
||||
|
||||
|
@ -42,33 +48,33 @@ function getAttributionFile() {
|
|||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object containing a key-value pair for each piece of attribution
|
||||
* data included in the passed-in attribution code string.
|
||||
* If the string isn't a valid attribution code, returns an empty object.
|
||||
*/
|
||||
function parseAttributionCode(code) {
|
||||
if (code.length > ATTR_CODE_MAX_LENGTH) {
|
||||
return {};
|
||||
}
|
||||
|
||||
let isValid = true;
|
||||
let parsed = {};
|
||||
for (let param of code.split(ATTR_CODE_FIELD_SEPARATOR)) {
|
||||
let [key, value] = param.split(ATTR_CODE_KEY_VALUE_SEPARATOR, 2);
|
||||
if (key && ATTR_CODE_KEYS_REGEX.test(key)) {
|
||||
if (value && ATTR_CODE_VALUE_REGEX.test(value)) {
|
||||
parsed[key] = value;
|
||||
}
|
||||
} else {
|
||||
isValid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return isValid ? parsed : {};
|
||||
}
|
||||
|
||||
var AttributionCode = {
|
||||
/**
|
||||
* Returns an object containing a key-value pair for each piece of attribution
|
||||
* data included in the passed-in attribution code string.
|
||||
* If the string isn't a valid attribution code, returns an empty object.
|
||||
*/
|
||||
parseAttributionCode(code) {
|
||||
if (code.length > ATTR_CODE_MAX_LENGTH) {
|
||||
return {};
|
||||
}
|
||||
|
||||
let isValid = true;
|
||||
let parsed = {};
|
||||
for (let param of code.split(ATTR_CODE_FIELD_SEPARATOR)) {
|
||||
let [key, value] = param.split(ATTR_CODE_KEY_VALUE_SEPARATOR, 2);
|
||||
if (key && ATTR_CODE_KEYS.includes(key)) {
|
||||
if (value && ATTR_CODE_VALUE_REGEX.test(value)) {
|
||||
parsed[key] = value;
|
||||
}
|
||||
} else {
|
||||
isValid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
return isValid ? parsed : {};
|
||||
},
|
||||
|
||||
/**
|
||||
* Reads the attribution code, either from disk or a cached version.
|
||||
* Returns a promise that fulfills with an object containing the parsed
|
||||
|
@ -79,51 +85,49 @@ var AttributionCode = {
|
|||
* On OSX the attributions are set directly on download and retain "utm_". We
|
||||
* strip "utm_" while retrieving the params.
|
||||
*/
|
||||
getAttrDataAsync() {
|
||||
return (async function() {
|
||||
if (gCachedAttrData != null) {
|
||||
return gCachedAttrData;
|
||||
}
|
||||
async getAttrDataAsync() {
|
||||
if (gCachedAttrData != null) {
|
||||
return gCachedAttrData;
|
||||
}
|
||||
|
||||
gCachedAttrData = {};
|
||||
if (AppConstants.platform == "win") {
|
||||
try {
|
||||
let bytes = await OS.File.read(getAttributionFile().path);
|
||||
let decoder = new TextDecoder();
|
||||
let code = decoder.decode(bytes);
|
||||
gCachedAttrData = parseAttributionCode(code);
|
||||
} catch (ex) {
|
||||
// The attribution file may already have been deleted,
|
||||
// or it may have never been installed at all;
|
||||
// failure to open or read it isn't an error.
|
||||
}
|
||||
} else if (AppConstants.platform == "macosx") {
|
||||
try {
|
||||
let appPath = Services.dirsvc.get("GreD", Ci.nsIFile).parent.parent
|
||||
.path;
|
||||
let attributionSvc = Cc["@mozilla.org/mac-attribution;1"].getService(
|
||||
Ci.nsIMacAttributionService
|
||||
);
|
||||
let referrer = attributionSvc.getReferrerUrl(appPath);
|
||||
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}`, key]) {
|
||||
if (params.has(paramKey)) {
|
||||
let value = params.get(paramKey);
|
||||
if (value && ATTR_CODE_VALUE_REGEX.test(value)) {
|
||||
gCachedAttrData[key] = value;
|
||||
}
|
||||
gCachedAttrData = {};
|
||||
if (AppConstants.platform == "win") {
|
||||
try {
|
||||
let bytes = await OS.File.read(getAttributionFile().path);
|
||||
let decoder = new TextDecoder();
|
||||
let code = decoder.decode(bytes);
|
||||
gCachedAttrData = this.parseAttributionCode(code);
|
||||
} catch (ex) {
|
||||
// The attribution file may already have been deleted,
|
||||
// or it may have never been installed at all;
|
||||
// failure to open or read it isn't an error.
|
||||
}
|
||||
} else if (AppConstants.platform == "macosx") {
|
||||
try {
|
||||
let appPath = Services.dirsvc.get("GreD", Ci.nsIFile).parent.parent
|
||||
.path;
|
||||
let attributionSvc = Cc["@mozilla.org/mac-attribution;1"].getService(
|
||||
Ci.nsIMacAttributionService
|
||||
);
|
||||
let referrer = attributionSvc.getReferrerUrl(appPath);
|
||||
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)) {
|
||||
let value = params.get(paramKey);
|
||||
if (value && ATTR_CODE_VALUE_REGEX.test(value)) {
|
||||
gCachedAttrData[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ex) {
|
||||
// No attributions
|
||||
}
|
||||
} catch (ex) {
|
||||
// No attributions
|
||||
}
|
||||
return gCachedAttrData;
|
||||
})();
|
||||
}
|
||||
return gCachedAttrData;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -141,16 +145,14 @@ var AttributionCode = {
|
|||
* Returns a promise that resolves when the file is deleted,
|
||||
* or if the file couldn't be deleted (the promise is never rejected).
|
||||
*/
|
||||
deleteFileAsync() {
|
||||
return (async function() {
|
||||
try {
|
||||
await OS.File.remove(getAttributionFile().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.
|
||||
}
|
||||
})();
|
||||
async deleteFileAsync() {
|
||||
try {
|
||||
await OS.File.remove(getAttributionFile().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.
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/* 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"
|
||||
);
|
||||
|
||||
let validAttrCodes = [
|
||||
{
|
||||
code:
|
||||
"source%3Dgoogle.com%26medium%3Dorganic%26campaign%3D(not%20set)%26content%3D(not%20set)",
|
||||
parsed: {
|
||||
source: "google.com",
|
||||
medium: "organic",
|
||||
campaign: "(not%20set)",
|
||||
content: "(not%20set)",
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "source%3Dgoogle.com%26medium%3Dorganic%26campaign%3D%26content%3D",
|
||||
parsed: { source: "google.com", medium: "organic" },
|
||||
},
|
||||
{
|
||||
code: "source%3Dgoogle.com%26medium%3Dorganic%26campaign%3D(not%20set)",
|
||||
parsed: {
|
||||
source: "google.com",
|
||||
medium: "organic",
|
||||
campaign: "(not%20set)",
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "source%3Dgoogle.com%26medium%3Dorganic",
|
||||
parsed: { source: "google.com", medium: "organic" },
|
||||
},
|
||||
{ code: "source%3Dgoogle.com", parsed: { source: "google.com" } },
|
||||
{ code: "medium%3Dgoogle.com", parsed: { medium: "google.com" } },
|
||||
{ code: "campaign%3Dgoogle.com", parsed: { campaign: "google.com" } },
|
||||
{ code: "content%3Dgoogle.com", parsed: { content: "google.com" } },
|
||||
{
|
||||
code: "experiment%3Dexperimental",
|
||||
parsed: { experiment: "experimental" },
|
||||
},
|
||||
{ code: "variation%3Dvaried", parsed: { variation: "varied" } },
|
||||
];
|
||||
|
||||
let invalidAttrCodes = [
|
||||
// Empty string
|
||||
"",
|
||||
// Not escaped
|
||||
"source=google.com&medium=organic&campaign=(not set)&content=(not set)",
|
||||
// Too long
|
||||
"source%3Dreallyreallyreallyreallyreallyreallyreallyreallyreallylongdomain.com%26medium%3Dorganic%26campaign%3D(not%20set)%26content%3Dalmostexactlyenoughcontenttomakethisstringlongerthanthe200characterlimit",
|
||||
// Unknown key name
|
||||
"source%3Dgoogle.com%26medium%3Dorganic%26large%3Dgeneticallymodified",
|
||||
// Empty key name
|
||||
"source%3Dgoogle.com%26medium%3Dorganic%26%3Dgeneticallymodified",
|
||||
];
|
|
@ -6,58 +6,9 @@
|
|||
const { AppConstants } = ChromeUtils.import(
|
||||
"resource://gre/modules/AppConstants.jsm"
|
||||
);
|
||||
const { AttributionCode } = ChromeUtils.import(
|
||||
"resource:///modules/AttributionCode.jsm"
|
||||
);
|
||||
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
|
||||
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
let validAttrCodes = [
|
||||
{
|
||||
code:
|
||||
"source%3Dgoogle.com%26medium%3Dorganic%26campaign%3D(not%20set)%26content%3D(not%20set)",
|
||||
parsed: {
|
||||
source: "google.com",
|
||||
medium: "organic",
|
||||
campaign: "(not%20set)",
|
||||
content: "(not%20set)",
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "source%3Dgoogle.com%26medium%3Dorganic%26campaign%3D%26content%3D",
|
||||
parsed: { source: "google.com", medium: "organic" },
|
||||
},
|
||||
{
|
||||
code: "source%3Dgoogle.com%26medium%3Dorganic%26campaign%3D(not%20set)",
|
||||
parsed: {
|
||||
source: "google.com",
|
||||
medium: "organic",
|
||||
campaign: "(not%20set)",
|
||||
},
|
||||
},
|
||||
{
|
||||
code: "source%3Dgoogle.com%26medium%3Dorganic",
|
||||
parsed: { source: "google.com", medium: "organic" },
|
||||
},
|
||||
{ code: "source%3Dgoogle.com", parsed: { source: "google.com" } },
|
||||
{ code: "medium%3Dgoogle.com", parsed: { medium: "google.com" } },
|
||||
{ code: "campaign%3Dgoogle.com", parsed: { campaign: "google.com" } },
|
||||
{ code: "content%3Dgoogle.com", parsed: { content: "google.com" } },
|
||||
];
|
||||
|
||||
let invalidAttrCodes = [
|
||||
// Empty string
|
||||
"",
|
||||
// Not escaped
|
||||
"source=google.com&medium=organic&campaign=(not set)&content=(not set)",
|
||||
// Too long
|
||||
"source%3Dreallyreallyreallyreallyreallyreallyreallyreallyreallylongdomain.com%26medium%3Dorganic%26campaign%3D(not%20set)%26content%3Dalmostexactlyenoughcontenttomakethisstringlongerthanthe200characterlimit",
|
||||
// Unknown key name
|
||||
"source%3Dgoogle.com%26medium%3Dorganic%26large%3Dgeneticallymodified",
|
||||
// Empty key name
|
||||
"source%3Dgoogle.com%26medium%3Dorganic%26%3Dgeneticallymodified",
|
||||
];
|
||||
|
||||
async function writeAttributionFile(data) {
|
||||
let appDir = Services.dirsvc.get("LocalAppData", Ci.nsIFile);
|
||||
let file = appDir.clone();
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/
|
||||
*/
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
* This test file exists to be run on any platform during development,
|
||||
* whereas the test_AttributionCode.js will test the attribution file
|
||||
* in the app local data dir on Windows. It will only run under
|
||||
* Windows on try.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Test validation of attribution codes.
|
||||
*/
|
||||
add_task(async function testValidAttrCodes() {
|
||||
for (let entry of validAttrCodes) {
|
||||
let result = AttributionCode.parseAttributionCode(entry.code);
|
||||
Assert.deepEqual(
|
||||
result,
|
||||
entry.parsed,
|
||||
"Parsed code should match expected value, code was: " + entry.code
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Make sure codes with various formatting errors are not seen as valid.
|
||||
*/
|
||||
add_task(async function testInvalidAttrCodes() {
|
||||
for (let code of invalidAttrCodes) {
|
||||
let result = AttributionCode.parseAttributionCode(code);
|
||||
Assert.deepEqual(result, {}, "Code should have failed to parse: " + code);
|
||||
}
|
||||
});
|
|
@ -3,6 +3,10 @@ firefox-appdir = browser
|
|||
skip-if = toolkit == 'android'
|
||||
|
||||
[test_AttributionCode.js]
|
||||
head = head_win.js
|
||||
skip-if = os != 'win' # windows specific tests
|
||||
[test_attribution.js]
|
||||
skip-if = toolkit != "cocoa" # osx specific tests
|
||||
[test_attribution_parsing.js]
|
||||
head = head_win.js
|
||||
skip-if = os != 'win' # windows specific tests
|
||||
|
|
|
@ -75,6 +75,8 @@ Structure:
|
|||
medium: <string>, // category of the source, such as "organic" for a search engine
|
||||
campaign: <string>, // identifier of the particular campaign that led to the download of the product
|
||||
content: <string>, // identifier to indicate the particular link within a campaign
|
||||
variation: <string>, // name/id of the variation cohort used in the enrolled funnel experiment
|
||||
experiment: <string>, // name/id of the enrolled funnel experiment
|
||||
},
|
||||
sandbox: {
|
||||
effectiveContentProcessLevel: <integer>,
|
||||
|
|
Загрузка…
Ссылка в новой задаче