Bug 1915183 - Collect telemetry for usage of storage.sync r=rpl

Differential Revision: https://phabricator.services.mozilla.com/D222442
This commit is contained in:
Tomislav Jovanovic 2024-10-23 13:06:24 +00:00
parent 3ae4f3573a
commit b1146d27a1
7 changed files with 151 additions and 0 deletions

View File

@ -82,6 +82,7 @@ export class ExtensionStorageSync {
// migration failure, it will become false. In practice, this will only ever
// happen on the first operation.
this.migrationOk = true;
this.backend = "rust";
}
// The main entry-point to our bridge. It performs some important roles:

View File

@ -735,6 +735,7 @@ export class ExtensionStorageSyncKinto {
this._fxaService = fxaService;
this.cryptoCollection = new CryptoCollection(fxaService);
this.listeners = new WeakMap();
this.backend = "kinto";
}
/**

View File

@ -366,6 +366,37 @@ extensions.data:
type: string
expires: 140
sync_usage_quotas:
type: event
description: |
These events record the basic stat about usage, and are collected the
first time an extension uses the sync storage API during a session.
bugs:
- https://bugzilla.mozilla.org/1915183
data_reviews:
- https://phabricator.services.mozilla.com/D222442
data_sensitivity:
- technical
notification_emails:
- addons-dev-internal@mozilla.com
extra_keys:
addon_id:
description: Id of the addon.
type: string
total_size_bytes:
description: Size of the sync data.
type: quantity
items_count:
description: Number of items in sync storage.
type: quantity
items_over_quota:
description: Count of items larger than QUOTA_BYTES_PER_ITEM.
type: quantity
backend:
description: Backend engine used, currently either "kinto" or "rust"
type: string
expires: 140
extensions.counters:
browser_action_preload_result:

View File

@ -30,6 +30,13 @@ ChromeUtils.defineLazyGetter(this, "extensionStorageSync", () => {
return extensionStorageSync;
});
XPCOMUtils.defineLazyPreferenceGetter(
this,
"prefStorageSyncEnabled",
"webextensions.storage.sync.enabled",
true
);
const enforceNoTemporaryAddon = extensionId => {
const EXCEPTION_MESSAGE =
"The storage API will not work with a temporary addon ID. " +
@ -40,6 +47,35 @@ const enforceNoTemporaryAddon = extensionId => {
}
};
// Set of extensions that already recorded the sync quota event.
const syncQuotasRecorded = new WeakSet();
const QUOTA_BYTES_PER_ITEM = 8_192;
async function recordSyncQuotaTelemetry(extension, context) {
if (syncQuotasRecorded.has(extension) || !prefStorageSyncEnabled) {
return;
}
syncQuotasRecorded.add(extension);
let items = await extensionStorageSync.get(extension, null, context);
let items_over_quota = 0;
let total_size_bytes = 0;
let entries = Object.entries(items);
for (let [key, value] of entries) {
let bytes = JSON.stringify(value).length;
total_size_bytes += key.length + bytes;
if (bytes > QUOTA_BYTES_PER_ITEM) {
items_over_quota++;
}
}
Glean.extensionsData.syncUsageQuotas.record({
addon_id: extension.id,
total_size_bytes,
items_count: entries.length,
items_over_quota,
backend: extensionStorageSync.backend,
});
}
// WeakMap[extension -> Promise<SerializableMap?>]
const managedStorage = new WeakMap();
@ -324,22 +360,27 @@ this.storage = class extends ExtensionAPIPersistent {
sync: {
get(spec) {
enforceNoTemporaryAddon(extension.id);
recordSyncQuotaTelemetry(extension, context);
return extensionStorageSync.get(extension, spec, context);
},
set(items) {
enforceNoTemporaryAddon(extension.id);
recordSyncQuotaTelemetry(extension, context);
return extensionStorageSync.set(extension, items, context);
},
remove(keys) {
enforceNoTemporaryAddon(extension.id);
recordSyncQuotaTelemetry(extension, context);
return extensionStorageSync.remove(extension, keys, context);
},
clear() {
enforceNoTemporaryAddon(extension.id);
recordSyncQuotaTelemetry(extension, context);
return extensionStorageSync.clear(extension, context);
},
getBytesInUse(keys) {
enforceNoTemporaryAddon(extension.id);
recordSyncQuotaTelemetry(extension, context);
return extensionStorageSync.getBytesInUse(extension, keys, context);
},
onChanged: new EventManager({

View File

@ -1398,3 +1398,58 @@ async function test_storage_change_event_page(areaName) {
return runWithPrefs([["extensions.eventPages.enabled", true]], testFn);
}
async function test_storage_sync_telemetry_quota(backend, enforced = false) {
Services.fog.testResetFOG();
let id = "my-extension-id@23";
// Repeat twice to get the glean events "before" and "after".
for (let i = 0; i < 2; i++) {
let ext = ExtensionTestUtils.loadExtension({
manifest: {
browser_specific_settings: { gecko: { id: id } },
permissions: ["storage"],
},
background() {
browser.test.onMessage.addListener(async enforced => {
await browser.storage.sync.set({
a: "1",
b: "x".repeat(enforced ? 1_000 : 10_000),
});
browser.test.notifyPass("done");
});
},
});
await ext.startup();
ext.sendMessage(enforced);
await ext.awaitFinish("done");
await ext.unload();
}
let events = Glean.extensionsData.syncUsageQuotas.testGetValue();
events = events.filter(e => e.extra?.addon_id === id);
Assert.deepEqual(
events[0].extra,
{
addon_id: id,
total_size_bytes: 0,
items_count: 0,
items_over_quota: 0,
backend: backend,
},
"Expected event values before setting sync storage"
);
Assert.deepEqual(
events[1].extra,
{
addon_id: id,
total_size_bytes: enforced ? 1_007 : 10_007,
items_count: 2,
items_over_quota: enforced ? 0 : 1,
backend: backend,
},
"Expected event values after setting sync storage"
);
}

View File

@ -8,6 +8,11 @@ AddonTestUtils.init(this);
add_task(async function setup() {
await ExtensionTestUtils.startAddonManager();
// FOG needs a profile directory to put its data in.
do_get_profile();
// FOG needs to be initialized in order for data to flow.
Services.fog.initializeFOG();
});
add_task(test_config_flag_needed);
@ -33,3 +38,9 @@ add_task(function test_storage_onChanged_event_page() {
test_storage_change_event_page("sync")
);
});
add_task(async function test_storage_sync_telemetry() {
return runWithPrefs([[STORAGE_SYNC_PREF, true]], () =>
test_storage_sync_telemetry_quota("rust", true)
);
});

View File

@ -664,6 +664,11 @@ function uuid() {
add_task(async function test_setup() {
await promiseStartupManager();
// FOG needs a profile directory to put its data in.
do_get_profile();
// FOG needs to be initialized in order for data to flow.
Services.fog.initializeFOG();
});
add_task(async function test_single_initialization() {
@ -2307,3 +2312,9 @@ add_task(function test_storage_onChanged_event_page() {
test_storage_change_event_page("sync")
);
});
add_task(async function test_storage_sync_telemetry() {
return runWithPrefs([[STORAGE_SYNC_PREF, true]], () =>
test_storage_sync_telemetry_quota("kinto", false)
);
});