summaryrefslogtreecommitdiffstats
path: root/toolkit/components/telemetry
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /toolkit/components/telemetry
parentInitial commit. (diff)
downloadfirefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz
firefox-26a029d407be480d791972afb5975cf62c9360a6.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/components/telemetry')
-rw-r--r--toolkit/components/telemetry/Events.yaml4232
-rw-r--r--toolkit/components/telemetry/Histograms.json17633
-rw-r--r--toolkit/components/telemetry/Makefile.in13
-rw-r--r--toolkit/components/telemetry/Processes.yaml40
-rw-r--r--toolkit/components/telemetry/Scalars.yaml9785
-rw-r--r--toolkit/components/telemetry/TelemetryStartup.manifest1
-rw-r--r--toolkit/components/telemetry/TelemetryStartup.sys.mjs44
-rw-r--r--toolkit/components/telemetry/UserInteractions.yaml37
-rw-r--r--toolkit/components/telemetry/app/ClientID.sys.mjs372
-rw-r--r--toolkit/components/telemetry/app/TelemetryArchive.sys.mjs121
-rw-r--r--toolkit/components/telemetry/app/TelemetryController.sys.mjs41
-rw-r--r--toolkit/components/telemetry/app/TelemetryControllerBase.sys.mjs141
-rw-r--r--toolkit/components/telemetry/app/TelemetryControllerContent.sys.mjs84
-rw-r--r--toolkit/components/telemetry/app/TelemetryControllerParent.sys.mjs1413
-rw-r--r--toolkit/components/telemetry/app/TelemetryEnvironment.sys.mjs2134
-rw-r--r--toolkit/components/telemetry/app/TelemetryReportingPolicy.sys.mjs587
-rw-r--r--toolkit/components/telemetry/app/TelemetryScheduler.sys.mjs422
-rw-r--r--toolkit/components/telemetry/app/TelemetrySend.sys.mjs1701
-rw-r--r--toolkit/components/telemetry/app/TelemetryStorage.sys.mjs2208
-rw-r--r--toolkit/components/telemetry/app/TelemetryTimestamps.sys.mjs53
-rw-r--r--toolkit/components/telemetry/app/TelemetryUtils.sys.mjs282
-rw-r--r--toolkit/components/telemetry/build_scripts/README.md5
-rw-r--r--toolkit/components/telemetry/build_scripts/gen_event_data.py227
-rw-r--r--toolkit/components/telemetry/build_scripts/gen_event_enum.py81
-rw-r--r--toolkit/components/telemetry/build_scripts/gen_histogram_data.py297
-rw-r--r--toolkit/components/telemetry/build_scripts/gen_histogram_enum.py94
-rw-r--r--toolkit/components/telemetry/build_scripts/gen_histogram_phf.py73
-rw-r--r--toolkit/components/telemetry/build_scripts/gen_process_data.py80
-rw-r--r--toolkit/components/telemetry/build_scripts/gen_process_enum.py69
-rw-r--r--toolkit/components/telemetry/build_scripts/gen_scalar_data.py216
-rw-r--r--toolkit/components/telemetry/build_scripts/gen_scalar_enum.py60
-rw-r--r--toolkit/components/telemetry/build_scripts/gen_userinteraction_data.py105
-rw-r--r--toolkit/components/telemetry/build_scripts/gen_userinteraction_phf.py70
-rw-r--r--toolkit/components/telemetry/build_scripts/mozparsers/__init__.py3
-rw-r--r--toolkit/components/telemetry/build_scripts/mozparsers/parse_events.py477
-rw-r--r--toolkit/components/telemetry/build_scripts/mozparsers/parse_histograms.py836
-rw-r--r--toolkit/components/telemetry/build_scripts/mozparsers/parse_scalars.py503
-rw-r--r--toolkit/components/telemetry/build_scripts/mozparsers/parse_user_interactions.py256
-rw-r--r--toolkit/components/telemetry/build_scripts/mozparsers/shared_telemetry_utils.py185
-rw-r--r--toolkit/components/telemetry/build_scripts/run_glean_parser.py17
-rw-r--r--toolkit/components/telemetry/build_scripts/setup.py32
-rw-r--r--toolkit/components/telemetry/components.conf23
-rw-r--r--toolkit/components/telemetry/core/EventInfo.h57
-rw-r--r--toolkit/components/telemetry/core/ScalarInfo.h94
-rw-r--r--toolkit/components/telemetry/core/Stopwatch.cpp752
-rw-r--r--toolkit/components/telemetry/core/Stopwatch.h84
-rw-r--r--toolkit/components/telemetry/core/Telemetry.cpp2035
-rw-r--r--toolkit/components/telemetry/core/Telemetry.h577
-rw-r--r--toolkit/components/telemetry/core/TelemetryCommon.cpp209
-rw-r--r--toolkit/components/telemetry/core/TelemetryCommon.h198
-rw-r--r--toolkit/components/telemetry/core/TelemetryEvent.cpp1387
-rw-r--r--toolkit/components/telemetry/core/TelemetryEvent.h71
-rw-r--r--toolkit/components/telemetry/core/TelemetryHistogram.cpp3677
-rw-r--r--toolkit/components/telemetry/core/TelemetryHistogram.h124
-rw-r--r--toolkit/components/telemetry/core/TelemetryScalar.cpp4190
-rw-r--r--toolkit/components/telemetry/core/TelemetryScalar.h133
-rw-r--r--toolkit/components/telemetry/core/TelemetryUserInteraction.cpp101
-rw-r--r--toolkit/components/telemetry/core/TelemetryUserInteraction.h20
-rw-r--r--toolkit/components/telemetry/core/UserInteractionInfo.h30
-rw-r--r--toolkit/components/telemetry/core/components.conf21
-rw-r--r--toolkit/components/telemetry/core/ipc/TelemetryComms.h400
-rw-r--r--toolkit/components/telemetry/core/ipc/TelemetryIPC.cpp59
-rw-r--r--toolkit/components/telemetry/core/ipc/TelemetryIPC.h114
-rw-r--r--toolkit/components/telemetry/core/ipc/TelemetryIPCAccumulator.cpp351
-rw-r--r--toolkit/components/telemetry/core/ipc/TelemetryIPCAccumulator.h56
-rw-r--r--toolkit/components/telemetry/core/nsITelemetry.idl680
-rw-r--r--toolkit/components/telemetry/dap/DAPTelemetry.cpp223
-rw-r--r--toolkit/components/telemetry/dap/DAPTelemetry.h25
-rw-r--r--toolkit/components/telemetry/dap/DAPTelemetryBindings.h11
-rw-r--r--toolkit/components/telemetry/dap/DAPTelemetrySender.sys.mjs335
-rw-r--r--toolkit/components/telemetry/dap/DAPVisitCounter.sys.mjs160
-rw-r--r--toolkit/components/telemetry/dap/components.conf10
-rw-r--r--toolkit/components/telemetry/dap/ffi-gtest/Cargo.toml20
-rw-r--r--toolkit/components/telemetry/dap/ffi-gtest/TestDAPTelemetry.cpp19
-rw-r--r--toolkit/components/telemetry/dap/ffi-gtest/moz.build6
-rw-r--r--toolkit/components/telemetry/dap/ffi-gtest/test.rs204
-rw-r--r--toolkit/components/telemetry/dap/ffi/Cargo.toml13
-rw-r--r--toolkit/components/telemetry/dap/ffi/cbindgen.toml11
-rw-r--r--toolkit/components/telemetry/dap/ffi/src/lib.rs335
-rw-r--r--toolkit/components/telemetry/dap/ffi/src/types.rs358
-rw-r--r--toolkit/components/telemetry/dap/metrics.yaml54
-rw-r--r--toolkit/components/telemetry/dap/nsIDAPTelemetry.idl37
-rw-r--r--toolkit/components/telemetry/dap/tests/xpcshell/test_dap.js149
-rw-r--r--toolkit/components/telemetry/dap/tests/xpcshell/xpcshell.toml4
-rw-r--r--toolkit/components/telemetry/docs/collection/custom-pings.rst80
-rw-r--r--toolkit/components/telemetry/docs/collection/events.rst349
-rw-r--r--toolkit/components/telemetry/docs/collection/experiments.rst41
-rw-r--r--toolkit/components/telemetry/docs/collection/histograms.rst410
-rw-r--r--toolkit/components/telemetry/docs/collection/index.rst48
-rw-r--r--toolkit/components/telemetry/docs/collection/measuring-time.rst116
-rw-r--r--toolkit/components/telemetry/docs/collection/sampleHistogram.pngbin0 -> 2825 bytes
-rw-r--r--toolkit/components/telemetry/docs/collection/scalars.rst326
-rw-r--r--toolkit/components/telemetry/docs/collection/uptake.rst114
-rw-r--r--toolkit/components/telemetry/docs/collection/user-interactions.rst272
-rw-r--r--toolkit/components/telemetry/docs/collection/webextension-api.rst158
-rw-r--r--toolkit/components/telemetry/docs/concepts/archiving.rst23
-rw-r--r--toolkit/components/telemetry/docs/concepts/crashes.rst25
-rw-r--r--toolkit/components/telemetry/docs/concepts/index.rst23
-rw-r--r--toolkit/components/telemetry/docs/concepts/pings.rst29
-rw-r--r--toolkit/components/telemetry/docs/concepts/sessions.rst37
-rw-r--r--toolkit/components/telemetry/docs/concepts/submission.rst42
-rw-r--r--toolkit/components/telemetry/docs/concepts/subsession_triggers.pngbin0 -> 857375 bytes
-rw-r--r--toolkit/components/telemetry/docs/data/addons-malware-ping.rst42
-rw-r--r--toolkit/components/telemetry/docs/data/anonymous-ping.rst68
-rw-r--r--toolkit/components/telemetry/docs/data/backgroundhangmonitor-ping.rst162
-rw-r--r--toolkit/components/telemetry/docs/data/common-ping.rst42
-rw-r--r--toolkit/components/telemetry/docs/data/coverage-ping.rst40
-rw-r--r--toolkit/components/telemetry/docs/data/crash-ping.rst264
-rw-r--r--toolkit/components/telemetry/docs/data/default-browser-ping.rst99
-rw-r--r--toolkit/components/telemetry/docs/data/deletion-request-ping.rst68
-rw-r--r--toolkit/components/telemetry/docs/data/downgrade-ping.rst30
-rw-r--r--toolkit/components/telemetry/docs/data/environment.rst622
-rw-r--r--toolkit/components/telemetry/docs/data/event-ping.rst92
-rw-r--r--toolkit/components/telemetry/docs/data/first-shutdown-ping.rst11
-rw-r--r--toolkit/components/telemetry/docs/data/health-ping.rst92
-rw-r--r--toolkit/components/telemetry/docs/data/heartbeat-ping.rst62
-rw-r--r--toolkit/components/telemetry/docs/data/index.rst19
-rw-r--r--toolkit/components/telemetry/docs/data/install-ping.rst234
-rw-r--r--toolkit/components/telemetry/docs/data/launcher-process-failure-ping.rst96
-rw-r--r--toolkit/components/telemetry/docs/data/main-ping.rst504
-rw-r--r--toolkit/components/telemetry/docs/data/modules-ping.rst46
-rw-r--r--toolkit/components/telemetry/docs/data/new-profile-ping.rst83
-rw-r--r--toolkit/components/telemetry/docs/data/pioneer-study.rst58
-rw-r--r--toolkit/components/telemetry/docs/data/sync-ping.rst357
-rw-r--r--toolkit/components/telemetry/docs/data/third-party-modules-ping.rst135
-rw-r--r--toolkit/components/telemetry/docs/data/uitour-ping.rst25
-rw-r--r--toolkit/components/telemetry/docs/data/uninstall-ping.rst36
-rw-r--r--toolkit/components/telemetry/docs/data/update-ping.rst79
-rw-r--r--toolkit/components/telemetry/docs/data/xfocsp-error-report-ping.rst69
-rw-r--r--toolkit/components/telemetry/docs/index.rst31
-rw-r--r--toolkit/components/telemetry/docs/internals/index.rst25
-rw-r--r--toolkit/components/telemetry/docs/internals/integration_tests/index.rst143
-rw-r--r--toolkit/components/telemetry/docs/internals/mentored-bugs.rst49
-rw-r--r--toolkit/components/telemetry/docs/internals/pingsender.rst36
-rw-r--r--toolkit/components/telemetry/docs/internals/preferences.rst280
-rw-r--r--toolkit/components/telemetry/docs/internals/review.rst144
-rw-r--r--toolkit/components/telemetry/docs/internals/tests.rst99
-rw-r--r--toolkit/components/telemetry/docs/obsolete/activation-ping.rst69
-rw-r--r--toolkit/components/telemetry/docs/obsolete/core-ping.rst510
-rw-r--r--toolkit/components/telemetry/docs/obsolete/deletion-ping.rst26
-rw-r--r--toolkit/components/telemetry/docs/obsolete/ecosystem-telemetry.rst109
-rw-r--r--toolkit/components/telemetry/docs/obsolete/fhr/architecture.rst226
-rw-r--r--toolkit/components/telemetry/docs/obsolete/fhr/dataformat.rst1998
-rw-r--r--toolkit/components/telemetry/docs/obsolete/fhr/identifiers.rst83
-rw-r--r--toolkit/components/telemetry/docs/obsolete/fhr/index.rst34
-rw-r--r--toolkit/components/telemetry/docs/obsolete/geckoview-streaming.rst26
-rw-r--r--toolkit/components/telemetry/docs/obsolete/hybrid-content.rst374
-rw-r--r--toolkit/components/telemetry/docs/obsolete/index.rst15
-rw-r--r--toolkit/components/telemetry/docs/obsolete/optout-ping.rst33
-rw-r--r--toolkit/components/telemetry/docs/obsolete/uitelemetry/index.rst146
-rw-r--r--toolkit/components/telemetry/docs/start/adding-a-new-probe.rst151
-rw-r--r--toolkit/components/telemetry/docs/start/index.rst28
-rw-r--r--toolkit/components/telemetry/docs/start/report-gecko-telemetry-in-glean.rst36
-rw-r--r--toolkit/components/telemetry/geckoview/gtest/TestGeckoViewStreaming.cpp237
-rw-r--r--toolkit/components/telemetry/geckoview/gtest/moz.build28
-rw-r--r--toolkit/components/telemetry/geckoview/streaming/GeckoViewStreamingTelemetry.cpp282
-rw-r--r--toolkit/components/telemetry/geckoview/streaming/GeckoViewStreamingTelemetry.h55
-rw-r--r--toolkit/components/telemetry/geckoview/streaming/metrics.yaml13
-rw-r--r--toolkit/components/telemetry/histogram-allowlists.json941
-rw-r--r--toolkit/components/telemetry/metrics.yaml44
-rw-r--r--toolkit/components/telemetry/moz.build275
-rw-r--r--toolkit/components/telemetry/other/CombinedStacks.cpp257
-rw-r--r--toolkit/components/telemetry/other/CombinedStacks.h108
-rw-r--r--toolkit/components/telemetry/other/ProcessedStack.cpp188
-rw-r--r--toolkit/components/telemetry/other/ProcessedStack.h135
-rw-r--r--toolkit/components/telemetry/other/TelemetryIOInterposeObserver.cpp182
-rw-r--r--toolkit/components/telemetry/other/TelemetryIOInterposeObserver.h116
-rw-r--r--toolkit/components/telemetry/other/UntrustedModules.cpp305
-rw-r--r--toolkit/components/telemetry/other/UntrustedModules.h31
-rw-r--r--toolkit/components/telemetry/other/UntrustedModulesBackupService.cpp95
-rw-r--r--toolkit/components/telemetry/other/UntrustedModulesBackupService.h73
-rw-r--r--toolkit/components/telemetry/other/UntrustedModulesDataSerializer.cpp606
-rw-r--r--toolkit/components/telemetry/other/UntrustedModulesDataSerializer.h84
-rw-r--r--toolkit/components/telemetry/pings.yaml6
-rw-r--r--toolkit/components/telemetry/pings/BackgroundTask_pingsender.sys.mjs41
-rw-r--r--toolkit/components/telemetry/pings/CoveragePing.sys.mjs154
-rw-r--r--toolkit/components/telemetry/pings/EventPing.sys.mjs241
-rw-r--r--toolkit/components/telemetry/pings/HealthPing.sys.mjs271
-rw-r--r--toolkit/components/telemetry/pings/ModulesPing.sys.mjs116
-rw-r--r--toolkit/components/telemetry/pings/TelemetrySession.sys.mjs1411
-rw-r--r--toolkit/components/telemetry/pings/UninstallPing.sys.mjs34
-rw-r--r--toolkit/components/telemetry/pings/UntrustedModulesPing.sys.mjs72
-rw-r--r--toolkit/components/telemetry/pings/UpdatePing.sys.mjs181
-rw-r--r--toolkit/components/telemetry/pingsender/moz.build38
-rw-r--r--toolkit/components/telemetry/pingsender/pingsender.cpp228
-rw-r--r--toolkit/components/telemetry/pingsender/pingsender.exe.manifest19
-rw-r--r--toolkit/components/telemetry/pingsender/pingsender.h41
-rw-r--r--toolkit/components/telemetry/pingsender/pingsender_unix_common.cpp301
-rw-r--r--toolkit/components/telemetry/pingsender/pingsender_win.cpp180
-rw-r--r--toolkit/components/telemetry/telemetry-constants.mozbuild7
-rw-r--r--toolkit/components/telemetry/tests/addons/long-fields/manifest.json12
-rw-r--r--toolkit/components/telemetry/tests/addons/restartless/manifest.json12
-rw-r--r--toolkit/components/telemetry/tests/addons/signed-webext/.web-extension-id3
-rw-r--r--toolkit/components/telemetry/tests/addons/signed-webext/META-INF/manifest.mf7
-rw-r--r--toolkit/components/telemetry/tests/addons/signed-webext/META-INF/mozilla.rsabin0 -> 4193 bytes
-rw-r--r--toolkit/components/telemetry/tests/addons/signed-webext/META-INF/mozilla.sf4
-rw-r--r--toolkit/components/telemetry/tests/addons/signed-webext/manifest.json12
-rw-r--r--toolkit/components/telemetry/tests/addons/system/manifest.json13
-rw-r--r--toolkit/components/telemetry/tests/browser/browser.toml13
-rw-r--r--toolkit/components/telemetry/tests/browser/browser_DynamicScalars.js247
-rw-r--r--toolkit/components/telemetry/tests/browser/browser_UpdatePingSuccess.js166
-rw-r--r--toolkit/components/telemetry/tests/browser/browser_media_element_in_page_scalar.js128
-rw-r--r--toolkit/components/telemetry/tests/browser/file_iframe.html9
-rw-r--r--toolkit/components/telemetry/tests/browser/file_media.html9
-rw-r--r--toolkit/components/telemetry/tests/browser/gizmo.mp4bin0 -> 455255 bytes
-rw-r--r--toolkit/components/telemetry/tests/gtest/TelemetryFixture.cpp31
-rw-r--r--toolkit/components/telemetry/tests/gtest/TelemetryFixture.h40
-rw-r--r--toolkit/components/telemetry/tests/gtest/TelemetryTestHelpers.cpp372
-rw-r--r--toolkit/components/telemetry/tests/gtest/TelemetryTestHelpers.h68
-rw-r--r--toolkit/components/telemetry/tests/gtest/TestCombinedStacks.cpp158
-rw-r--r--toolkit/components/telemetry/tests/gtest/TestCounters.cpp173
-rw-r--r--toolkit/components/telemetry/tests/gtest/TestEvents.cpp115
-rw-r--r--toolkit/components/telemetry/tests/gtest/TestHistograms.cpp891
-rw-r--r--toolkit/components/telemetry/tests/gtest/TestScalars.cpp622
-rw-r--r--toolkit/components/telemetry/tests/gtest/moz.build29
-rw-r--r--toolkit/components/telemetry/tests/integration/tests/conftest.py321
-rw-r--r--toolkit/components/telemetry/tests/integration/tests/python.toml12
-rw-r--r--toolkit/components/telemetry/tests/integration/tests/resources/helloworld/helloworld.html18
-rw-r--r--toolkit/components/telemetry/tests/integration/tests/resources/helloworld/manifest.json12
-rw-r--r--toolkit/components/telemetry/tests/integration/tests/test_deletion_request_ping.py64
-rw-r--r--toolkit/components/telemetry/tests/integration/tests/test_event_ping.py50
-rw-r--r--toolkit/components/telemetry/tests/integration/tests/test_main_tab_scalars.py33
-rw-r--r--toolkit/components/telemetry/tests/integration/tests/test_search_counts_across_sessions.py169
-rw-r--r--toolkit/components/telemetry/tests/integration/tests/test_subsession_management.py147
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/MANIFEST.in3
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/requirements.txt2
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/setup.py48
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/__init__.py3
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_ping_filters.py31
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_ping_server.py86
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_testcase.py63
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/ping_filters.py75
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/ping_server.py77
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/resources/dynamic_addon/dynamic-probe-telemetry-extension-signed.xpibin0 -> 7965 bytes
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/resources/helloworld/helloworld.html18
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/resources/helloworld/manifest.json12
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/runner.py63
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/runtests.py15
-rw-r--r--toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/testcase.py233
-rw-r--r--toolkit/components/telemetry/tests/marionette/mach_commands.py95
-rw-r--r--toolkit/components/telemetry/tests/marionette/moz.build11
-rw-r--r--toolkit/components/telemetry/tests/marionette/tests/client/manifest.toml20
-rw-r--r--toolkit/components/telemetry/tests/marionette/tests/client/test_deletion_request_ping.py83
-rw-r--r--toolkit/components/telemetry/tests/marionette/tests/client/test_dynamic_probes.py26
-rw-r--r--toolkit/components/telemetry/tests/marionette/tests/client/test_fog_custom_ping.py24
-rw-r--r--toolkit/components/telemetry/tests/marionette/tests/client/test_fog_deletion_request_ping.py67
-rw-r--r--toolkit/components/telemetry/tests/marionette/tests/client/test_fog_user_activity.py46
-rw-r--r--toolkit/components/telemetry/tests/marionette/tests/client/test_main_tab_scalars.py54
-rw-r--r--toolkit/components/telemetry/tests/marionette/tests/client/test_shutdown_pings_succeed.py55
-rw-r--r--toolkit/components/telemetry/tests/marionette/tests/client/test_subsession_management.py147
-rw-r--r--toolkit/components/telemetry/tests/marionette/tests/client/test_unicode_encoding.py39
-rw-r--r--toolkit/components/telemetry/tests/marionette/tests/manifest.toml5
-rw-r--r--toolkit/components/telemetry/tests/marionette/tests/unit/manifest.toml4
-rw-r--r--toolkit/components/telemetry/tests/marionette/tests/unit/test_ping_server_received_ping.py45
-rw-r--r--toolkit/components/telemetry/tests/modules-test.cpp27
-rw-r--r--toolkit/components/telemetry/tests/moz.build25
-rw-r--r--toolkit/components/telemetry/tests/python/python.toml14
-rw-r--r--toolkit/components/telemetry/tests/python/test_gen_event_data_json.py102
-rw-r--r--toolkit/components/telemetry/tests/python/test_gen_scalar_data_json.py100
-rw-r--r--toolkit/components/telemetry/tests/python/test_histogramtools_non_strict.py114
-rw-r--r--toolkit/components/telemetry/tests/python/test_histogramtools_strict.py566
-rw-r--r--toolkit/components/telemetry/tests/python/test_parse_events.py166
-rw-r--r--toolkit/components/telemetry/tests/python/test_parse_scalars.py267
-rw-r--r--toolkit/components/telemetry/tests/unit/TelemetryArchiveTesting.sys.mjs76
-rw-r--r--toolkit/components/telemetry/tests/unit/TelemetryEnvironmentTesting.sys.mjs875
-rw-r--r--toolkit/components/telemetry/tests/unit/data/search-extensions/engines.json14
-rw-r--r--toolkit/components/telemetry/tests/unit/data/search-extensions/telemetrySearchIdentifier/manifest.json29
-rw-r--r--toolkit/components/telemetry/tests/unit/engine.xml7
-rw-r--r--toolkit/components/telemetry/tests/unit/file_UninstallPing.worker.js35
-rw-r--r--toolkit/components/telemetry/tests/unit/head.js582
-rw-r--r--toolkit/components/telemetry/tests/unit/testNoPDB32.dllbin0 -> 8704 bytes
-rw-r--r--toolkit/components/telemetry/tests/unit/testNoPDB64.dllbin0 -> 10240 bytes
-rwxr-xr-xtoolkit/components/telemetry/tests/unit/testNoPDBAArch64.dllbin0 -> 1536 bytes
-rw-r--r--toolkit/components/telemetry/tests/unit/testUnicodePDB32.dllbin0 -> 8704 bytes
-rw-r--r--toolkit/components/telemetry/tests/unit/testUnicodePDB64.dllbin0 -> 10752 bytes
-rwxr-xr-xtoolkit/components/telemetry/tests/unit/testUnicodePDBAArch64.dllbin0 -> 7168 bytes
-rw-r--r--toolkit/components/telemetry/tests/unit/test_ChildEvents.js222
-rw-r--r--toolkit/components/telemetry/tests/unit/test_ChildHistograms.js333
-rw-r--r--toolkit/components/telemetry/tests/unit/test_ChildScalars.js241
-rw-r--r--toolkit/components/telemetry/tests/unit/test_CoveragePing.js118
-rw-r--r--toolkit/components/telemetry/tests/unit/test_EventPing.js280
-rw-r--r--toolkit/components/telemetry/tests/unit/test_HealthPing.js282
-rw-r--r--toolkit/components/telemetry/tests/unit/test_MigratePendingPings.js153
-rw-r--r--toolkit/components/telemetry/tests/unit/test_ModulesPing.js297
-rw-r--r--toolkit/components/telemetry/tests/unit/test_PingAPI.js709
-rw-r--r--toolkit/components/telemetry/tests/unit/test_PingSender.js281
-rw-r--r--toolkit/components/telemetry/tests/unit/test_RDDScalars.js53
-rw-r--r--toolkit/components/telemetry/tests/unit/test_SocketScalars.js49
-rw-r--r--toolkit/components/telemetry/tests/unit/test_SubsessionChaining.js286
-rw-r--r--toolkit/components/telemetry/tests/unit/test_SyncPingIntegration.js65
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryAndroidEnvironment.js64
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryChildEvents_buildFaster.js129
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryClientID_reset.js191
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryController.js1225
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryControllerBuildID.js74
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryControllerShutdown.js82
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryController_idle.js71
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js1472
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment_search.js398
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryEvents.js1109
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryEvents_buildFaster.js463
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryFlagClear.js29
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryHistograms.js2073
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryLateWrites.js143
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryLockCount.js56
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryReportingPolicy.js399
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryScalars.js1088
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryScalars_buildFaster.js226
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryScalars_impressionId.js48
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryScalars_multistore.js415
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetrySend.js1110
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js586
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetrySession.js2389
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetrySession_abortedSessionQueued.js197
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetrySession_activeTicks.js126
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryStopwatch.js196
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryTimestamps.js79
-rw-r--r--toolkit/components/telemetry/tests/unit/test_TelemetryUtils.js46
-rw-r--r--toolkit/components/telemetry/tests/unit/test_ThirdPartyModulesPing.js282
-rw-r--r--toolkit/components/telemetry/tests/unit/test_UninstallPing.js126
-rw-r--r--toolkit/components/telemetry/tests/unit/test_UserInteraction.js134
-rw-r--r--toolkit/components/telemetry/tests/unit/test_UserInteraction_annotations.js470
-rw-r--r--toolkit/components/telemetry/tests/unit/test_UtilityScalars.js65
-rw-r--r--toolkit/components/telemetry/tests/unit/test_bug1555798.js48
-rw-r--r--toolkit/components/telemetry/tests/unit/test_client_id.js163
-rw-r--r--toolkit/components/telemetry/tests/unit/test_failover_retry.js263
-rw-r--r--toolkit/components/telemetry/tests/unit/xpcshell.toml183
-rw-r--r--toolkit/components/telemetry/tests/utils/TelemetryTestUtils.sys.mjs430
328 files changed, 108468 insertions, 0 deletions
diff --git a/toolkit/components/telemetry/Events.yaml b/toolkit/components/telemetry/Events.yaml
new file mode 100644
index 0000000000..4fc7482a77
--- /dev/null
+++ b/toolkit/components/telemetry/Events.yaml
@@ -0,0 +1,4232 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+partner_link:
+ click:
+ objects: ["newtab", "urlbar"]
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ description: >
+ This is recorded when clicking a partner link. The value is the partner.
+ bug_numbers: [1637217, 1644442, 1643426]
+ notification_emails:
+ - "dao@mozilla.com"
+ expiry_version: never
+ attribution:
+ objects: ["success", "failure", "abort"]
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ description: >
+ This is recorded when sending an attribution request for a partner link. The value is the partner.
+ bug_numbers: [1637217, 1644442, 1643426]
+ notification_emails:
+ - "dao@mozilla.com"
+ expiry_version: never
+
+activity_stream:
+ event:
+ objects: [
+ "ARCHIVE_FROM_POCKET",
+ "BLOCK",
+ "BOOKMARK_ADD",
+ "BOOKMARK_DELETE",
+ "CLICK",
+ "CLICK_PRIVACY_INFO",
+ "CLOSE_NEWTAB_PREFS",
+ "SHOW_PERSONALIZE",
+ "HIDE_PERSONALIZE",
+ "DELETE",
+ "DELETE_FROM_POCKET",
+ "DELETE_CONFIRM",
+ "DIALOG_CANCEL",
+ "DIALOG_OPEN",
+ "DRAG",
+ "DROP",
+ "IMPRESSION",
+ "MIGRATION_CANCEL",
+ "MIGRATION_START",
+ "OPEN_NEWTAB_PREFS",
+ "OPEN_NEW_WINDOW",
+ "OPEN_PRIVATE_WINDOW",
+ "PIN",
+ "PREF_CHANGED",
+ "PREVIEW_REQUEST",
+ "SAVE_TO_POCKET",
+ "SEARCH",
+ "SEARCH_EDIT_ADD",
+ "SEARCH_EDIT_CLOSE",
+ "SEARCH_EDIT_DELETE",
+ "SEARCH_HANDOFF",
+ "SHOW_PRIVACY_INFO",
+ "SKIPPED_SIGNIN",
+ "SUBMIT_EMAIL",
+ "DISCLAIMER_ACKED",
+ "MENU_ADD_SEARCH",
+ "MENU_ADD_TOPSITE",
+ "MENU_COLLAPSE",
+ "MENU_EXPAND",
+ "MENU_MANAGE",
+ "MENU_MOVE_DOWN",
+ "MENU_MOVE_UP",
+ "MENU_PRIVACY_NOTICE",
+ "MENU_REMOVE",
+ "TOP_SITES_EDIT",
+ "TOP_SITES_EDIT_CLOSE",
+ "TOPSITE_SPONSOR_INFO",
+ "UNPIN"]
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: >
+ This is recorded with every user interaction on Activity Stream elements.
+ bug_numbers: [1429497, 1429489, 1514732]
+ notification_emails:
+ - "najiang@mozilla.com"
+ - "msamuel@mozilla.com"
+ - "rrosario@mozilla.com"
+ expiry_version: never
+ extra_keys:
+ addon_version: The Activity Stream addon version.
+ session_id: The ID of the Activity Stream session in which the event occurred
+ page: about:home or about_newtab - the page where the event occurred
+ user_prefs: An integer representaing a user's A-S settings.
+ action_position: The index of card receiving interactions.
+ end:
+ objects: ["session"]
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: >
+ This is recorded with every session ended in Activity Stream.
+ bug_numbers: [1429497, 1429489]
+ notification_emails:
+ - "najiang@mozilla.com"
+ - "msamuel@mozilla.com"
+ expiry_version: never
+ extra_keys:
+ addon_version: The Activity Stream addon version.
+ session_id: The ID of the Activity Stream session in which the event occurred
+ page: about:home or about_newtab - the page where the event occurred
+ user_prefs: An integer representaing a user's A-S settings.
+
+aboutprivatebrowsing:
+ click:
+ objects: ["info_link", "promo_link", "dismiss_button"]
+ release_channel_collection: opt-out
+ record_in_processes: ["content"]
+ description: >
+ This is recorded when a user clicks a link on the private browsing newtab.
+ bug_numbers: [1709344, 1754635, 1781973, 1811146, 1841925, 1871197]
+ notification_emails:
+ - "pbz@mozilla.com"
+ - "tihuang@mozilla.com"
+ expiry_version: "134"
+ products:
+ - "firefox"
+
+readermode:
+ view:
+ objects: ["on", "off"]
+ release_channel_collection: opt-out
+ record_in_processes: ["content"]
+ description: >
+ This is recorded any time Reader Mode is turned on or off.
+ bug_numbers: [1689201]
+ notification_emails:
+ - "kirill@getpocket.com"
+ - "sdowne@getpocket.com"
+ expiry_version: never
+ extra_keys:
+ subcategory: The broad event category for this probe. E.g. navigation
+ reader_time: Time spent in Reader Mode session in seconds (whole number)
+ scroll_position: Percentage of window scrolled in Reader Mode (whole number)
+ products:
+ - "firefox"
+ button:
+ objects: ["click"]
+ release_channel_collection: opt-out
+ record_in_processes: ["content"]
+ description: >
+ This is recorded any time a Reader Mode button is clicked.
+ bug_numbers: [1693362]
+ notification_emails:
+ - "kirill@getpocket.com"
+ - "sdowne@getpocket.com"
+ expiry_version: never
+ extra_keys:
+ label: The type of button clicked.
+ products:
+ - "firefox"
+
+addonsManager:
+ install:
+ description: >
+ These events are recorded during the install and update flow for extensions and themes,
+ the value of the event is an install_id shared by the events related to the same install
+ or update flow.
+ objects:
+ - "extension"
+ - "theme"
+ - "locale"
+ - "dictionary"
+ - "sitepermission"
+ - "siteperm_deprecated"
+ - "other"
+ - "unknown"
+ methods: ["install", "update"]
+ extra_keys:
+ addon_id: A string which identify the extension (when available)
+ download_time: The number of ms needed to complete the download
+ error: The AddonManager error related to an install or update failure.
+ source: >
+ The source that originally triggered the add-on installation, one of "about:addons",
+ "about:debugging", "about:preferences", "amo", "browser-import", "disco", "distribution",
+ "extension", "enterprise-policy", "file-url", "geckoview-app", "gmp-plugin",
+ "internal", "plugin", "rtamo", "siteperm-addon-provider" "sync", "system-addon",
+ "temporary-addon", "unknown".
+ For events with method set to "sideload", the source value is derived from the XPIProvider
+ location name (e.g. possible values are "app-builtin", "app-global", "app-profile",
+ "app-system-addons", "app-system-defaults", "app-system-local", "app-system-profile",
+ "app-system-share", "app-system-user", "winreg-app-user", "winreg-app-gobal")
+ method: >
+ The method used by the source to install the add-on (included when the source can use more than one,
+ e.g. install events with source "about:addons" may have "install-from-file" or "url" as method),
+ one of "amWebAPI", "drag-and-drop", "installTrigger", "install-from-file", "link",
+ "management-webext-api", "sideload", "synthetic-install", "url", "product-updates".
+ e.g. install events with source "about:addons" may have "install-from-file" or "drag-and-drop" as method),
+ one of "amWebAPI", "drag-and-drop", "installTrigger", "install-from-file", "link", "management-webext-api",
+ "sideload", "synthetic-install", "url", "product-updates", "manager".
+ num_strings: The number of permission description strings in the extension permission doorhanger
+ updated_from: Determine if an update has been requested by the user or the application ("app" / "user")
+ install_origins: This flag indicates whether install_origins is defined in the addon manifest. ("1" / "0")
+ step: >
+ The current step in the install or update flow:
+ - started, postponed, cancelled, failed, permissions_prompt, completed
+ - site_warning, site_blocked, install_disabled_warning
+ - download_started, download_completed, download_failed
+ notification_emails: ["addons-dev-internal@mozilla.com"]
+ expiry_version: "132"
+ products:
+ - "firefox"
+ - "fennec"
+ - "thunderbird"
+ record_in_processes: ["main"]
+ bug_numbers: [1433335, 1515697, 1523641, 1549770, 1590736, 1630596, 1672570, 1714251, 1749878, 1781974, 1817100, 1861295]
+ release_channel_collection: opt-out
+ install_stats:
+ description: >
+ These events are recorded at the end of the install flow, but only when
+ the source that originally triggered the add-on installation is "amo",
+ "rtamo" or "disco". The value of an event is the SHA256 hash of the
+ addon_id.
+ objects:
+ - "extension"
+ - "theme"
+ - "locale"
+ - "dictionary"
+ - "sitepermission"
+ - "siteperm_deprecated"
+ - "other"
+ - "unknown"
+ extra_keys:
+ addon_id: >
+ A string which identifies the add-on. This value might be trimmed.
+ taar_based: >
+ This extra key is only set for install flows related to the discovery
+ addon. When available it is going to be a string set to "1" for
+ TAAR based recommendations, "0" for manually curated and unset if
+ not relevant for the particular install flow.
+ utm_campaign: >
+ The specific product promotion or strategic campaign that drives
+ traffic to the install page.
+ utm_content: >
+ The specific item that a person clicks on to access the install page
+ (such as an A/B test, a website banner, or a specific ad).
+ utm_medium: The channel used to share the install page.
+ utm_source: >
+ The name of the product, domain of the website that drives traffic to
+ the install page.
+ notification_emails: ["addons-dev-internal@mozilla.com"]
+ expiry_version: never
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ bug_numbers: [1653020, 1699225]
+ release_channel_collection: opt-out
+ manage:
+ description: >
+ This events are recorded when an installed add-ons is being disable/enabled/uninstalled,
+ the value of the event is the addon_id (which also allow to correlate multiple events
+ related to each other).
+ objects:
+ - "extension"
+ - "theme"
+ - "locale"
+ - "dictionary"
+ - "sitepermission"
+ - "siteperm_deprecated"
+ - "other"
+ - "unknown"
+ methods: ["disable", "enable", "sideload_prompt", "uninstall"]
+ extra_keys:
+ source: >
+ The source from which the addon has been installed (See extra_keys.source description from
+ addonsManager.install telemetry event definition).
+ method: >
+ The method used by the source to install the add-on (included when the source can use more than one,
+ e.g. install events with source "about:addons" may have "install-from-file" or "url" as method).
+ num_strings: The number of permission description strings in the extension permission doorhanger
+ notification_emails: ["addons-dev-internal@mozilla.com"]
+ expiry_version: "132"
+ products:
+ - "firefox"
+ - "fennec"
+ - "thunderbird"
+ record_in_processes: ["main"]
+ bug_numbers: [1433335, 1515697, 1523641, 1549770, 1590736, 1630596, 1672570, 1714251, 1749878, 1781974, 1817100, 1861295]
+ release_channel_collection: opt-out
+ report:
+ description: >
+ An abuse report submitted by a user for a given extension. The object of the event
+ represent the report entry point, the value is the id of the addon being reported.
+ objects:
+ - amo
+ - menu
+ - toolbar_context_menu
+ - unified_context_menu
+ - uninstall
+ extra_keys:
+ addon_type: >
+ The type of the add-on being reported (missing on ERROR_ADDON_NOT_FOUND, ERROR_AMODETAILS_NOTFOUND
+ and ERROR_AMODETAILS_FAILURE).
+ error_type: >
+ AbuseReport Error Type (included in case of submission failures). The error types include
+ ERROR_ABORTED_SUBMIT, ERROR_ADDON_NOT_FOUND, ERROR_CLIENT, ERROR_NETWORK, ERROR_UNKNOWN,
+ ERROR_RECENT_SUBMIT, ERROR_SERVER, ERROR_AMODETAILS_NOTFOUND, ERROR_AMODETAILS_FAILURE.
+ notification_emails: ["addons-dev-internal@mozilla.com"]
+ expiry_version: "132"
+ products:
+ - "firefox"
+ - "fennec"
+ - "thunderbird"
+ record_in_processes: ["main"]
+ bug_numbers: [1544927, 1580561, 1590736, 1630596, 1672570, 1714251, 1749878, 1780746, 1781974, 1817100, 1861295]
+ release_channel_collection: opt-out
+ reportSuspiciousSite:
+ description: >
+ Sent when a user clicks "Report Suspicious Site" on the dropdown menu of the third-
+ party xpinstall doorhanger. The object is always suspiciousSite. The value is the
+ site domain, or "(unknown)" if there is no site associated with the prompt.
+ objects:
+ - suspiciousSite
+ notification_emails: ["addons-dev-internal@mozilla.com"]
+ expiry_version: "132"
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ bug_numbers: [1806056, 1817100, 1861295]
+ release_channel_collection: opt-out
+
+blocklist:
+ addonBlockChange:
+ description: >
+ An add-on is blocked, or an installed add-on is unblocked.
+ When an add-on install/update is blocked, its installation is aborted and the
+ add-on is no longer listed in the activeAddons field of TelemetryEnvironment.
+ The value is the ID of the add-on.
+ The object represents the reason for triggering the blocklistState check:
+ "addon_install" is when an add-on is installed.
+ "addon_update" is when an add-on is updated after an update check.
+ "addon_update_check" is when an add-on is blocked during the update check.
+ "addon_db_modified" is when an add-on's blocklistState was altered between application restarts.
+ "blocklist_update" is when an add-on's blocklistState changed due to a blocklist update. This
+ may be due to the blocklist being disabled by preferences or enterprise policies, but it is
+ more commonly the result of updating entries in the blocklist.
+ objects: ["addon_install", "addon_update", "addon_update_check", "addon_db_modified", "blocklist_update"]
+ extra_keys:
+ blocklistState: >
+ The blocklistState of the add-on. 0 is unblocked, 2 is blocked.
+ 1 is soft blocked (this state does not exist in blocklist v3).
+ addon_version: >
+ Version of the add-on. Used together with an add-on's ID (value) to identify
+ add-ons to block.
+ signed_date: >
+ Timestamp of the add-on (when it was signed via AMO).
+ This field is missing (0) for "addon_update_check".
+ hours_since: >
+ The number of hours that have passed since this version of the add-on was installed or updated.
+ At least zero when the blocklist is updated, -1 otherwise.
+ mlbf_last_time: >
+ The generation time of the most recent entry in the blocklist.
+ Time generated by AMO when the blocklist entry was created.
+ May be 0 when the blocklist is disabled.
+ mlbf_generation: >
+ The generation time to identify the bloomfilter that was used for this blocklist decision.
+ The bloomfilter is updated less frequently than the so-called stashes in the RemoteSettings
+ collection that holds the blocklist data. The stashes take precedence over the bloomfilter
+ in blocklist decisions.
+ Time generated by AMO when the blocklist entry was created.
+ May be 0 when the blocklist is disabled.
+ mlbf_source: >
+ The source of the RemoteSettings attachment that holds the bloom filter.
+ This field is documented in more detail in the definition of the blocklist.mlbf_source scalar.
+ Possible values are "dump_match", "cache_match", "remote_match", "dump_fallback", "cache_fallback", "unknown".
+ notification_emails: ["addons-dev-internal@mozilla.com", "rwu@mozilla.com"]
+ expiry_version: "132"
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ bug_numbers: [1662857, 1730037, 1763529, 1811159, 1861296]
+ release_channel_collection: opt-out
+
+downloads:
+ added:
+ description: >
+ Sent when downloading a new file. Possible values are in contained in DownloadList::kFileExtensions.
+ All other downloads not in the listare marked as other.
+ objects: ["fileExtension"]
+ bug_numbers: [1627676, 1706355]
+ notification_emails: ["rtestard@mozilla.com", "emalysz@mozilla.com"]
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ release_channel_collection: opt-out
+ expiry_version: never
+
+form_autocomplete:
+ show:
+ objects: ["logins"]
+ bug_numbers: [1619498, 1628849]
+ description: >-
+ An content form autocomplete popup was shown. Details on the timing and context are provided.
+ The `value` is the number of milliseconds since the autocomplete search started.
+ expiry_version: never
+ extra_keys:
+ acFieldName: The "field name" token (last one) of the field's autocomplete attribute.
+ fieldType: The `type` property value of the field.
+ generatedPasswo: The number of generated password rows shown.
+ hadPrevious: Whether the autocomplete results had cached previous results it could use.
+ typeWasPassword: Whether the input `type` was ever "password".
+ insecureWarning: The number of insecure login field warning rows shown (should be 0 or 1).
+ login: The number of login rows shown (with or without the domain line).
+ importableLogin: The number of import suggestion rows shown.
+ loginsFooter: The number of "Manage Passwords" footer row shown.
+ stringLength: Length of the text in the field that triggered these results.
+ notification_emails: ["passwords-dev@mozilla.org"]
+ products:
+ - firefox
+ record_in_processes: ["main"]
+ release_channel_collection: opt-out
+
+address:
+ doorhanger:
+ description: >-
+ User interactions for the browser address autofill doorhanger.
+ objects:
+ - "capture_doorhanger"
+ - "update_doorhanger"
+ - "edit_doorhanger"
+ methods:
+ - "show"
+ - "save"
+ - "update"
+ - "cancel"
+ - "disable"
+ - "pref"
+ - "learn_more"
+ bug_numbers: [1801039]
+ notification_emails: ["autofill@lists.mozilla.org", "passwords-dev@mozilla.org"]
+ expiry_version: never
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ release_channel_collection: opt-out
+ manage:
+ description: >-
+ User interactions for address autofill preferences management UI.
+ objects:
+ - "manage"
+ methods:
+ - "show"
+ - "add"
+ - "delete"
+ - "show_entry"
+ - "edit"
+ bug_numbers: [1801039]
+ notification_emails: ["autofill@lists.mozilla.org", "passwords-dev@mozilla.org"]
+ expiry_version: never
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ release_channel_collection: opt-out
+ address_form:
+ description: >-
+ User interactions for address autofill forms
+ 'detected': Recorded when a form is recognized as a credit card form.
+ The possible value of cc_* in extra_keys are "true", "false", or 0
+ When the value is "true", the field is identified via autocomplete attribute
+ When the value is "false", the field is not detected in the form
+ When the value is an 0, then the field is identified by regexp-based heuristic
+ 'popup_shown': Recorded when autofill popup is shown. Using field_name to record the field
+ that triggers this event
+ 'filled': Recorded when a form is autofiled. Possible values are
+ `filled`, `not_filled`, `user_filled` or `unavailable`
+ 'filled_modified': Recorded when a field is autofilled and then modified by the user.
+ Using field_name to record the field that triggers this event.
+ 'submitted': Recorded when a form is submitted. Possible values are `autofilled`, `not_filled`, `user_filled` or `unavailable`
+ 'cleared' Recorded when a form is cleared . Using field_name to record the field that triggers this event
+ objects: ["address_form"]
+ methods: ["detected", "popup_shown", "filled", "filled_modified", "submitted", "cleared"]
+ bug_numbers: [1804901]
+ notification_emails: ["autofill@lists.mozilla.org", "passwords-dev@mozilla.org"]
+ expiry_version: never
+ products:
+ - "firefox"
+ record_in_processes: ["content"]
+ release_channel_collection: opt-out
+ extra_keys:
+ street_address: street address result, filled when method is `detected`, `filled`, or `filled_modified`
+ address_line1: address_line1 result, filled when method is `detected`, `filled`, or `filled_modified`.
+ address_line2: address_line2 result, filled when method is `detected`, `filled`, or `filled_modified`.
+ address_line3: address_line3 result, filled when method is `detected`, `filled`, or `filled_modified`.
+ address_level1: address_level1 result, filled when method is `detected`, `filled`, or `filled_modified`.
+ address_level2: address_level2 result, filled when method is `detected`, `filled`, or `filled_modified`.
+ postal_code: postal_code result, filled when method is `detected`, `filled`, or `filled_modified`.
+ country: country result, filled when method is `detected`, `filled`, or `filled_modified`.
+ field_name: Name of the field being affected by the method, filled when method is `popup_shown` or `filled_modified`.
+ address_form_ext:
+ description: >-
+ Address has too many fields so we cannot cover them all in the above `address_form` telemetry
+ This telemetry is used to record address fields that are not listed in `address_form`
+ objects: ["address_form_ext"]
+ methods: ["detected", "filled", "submitted"]
+ bug_numbers: [1804901]
+ notification_emails: ["autofill@lists.mozilla.org", "passwords-dev@mozilla.org"]
+ expiry_version: never
+ products:
+ - "firefox"
+ record_in_processes: ["content"]
+ release_channel_collection: opt-out
+ extra_keys:
+ name: name
+ given_name: give_name
+ additional_name: additional_name
+ family_name: family_name
+ email: email
+ organization: organization
+ tel: telephone
+
+creditcard:
+ doorhanger:
+ description: >-
+ User interactions for the browser credit card autofill doorhanger.
+ objects:
+ - "capture_doorhanger"
+ - "update_doorhanger"
+ methods:
+ - "show"
+ - "save"
+ - "update"
+ - "cancel"
+ - "disable"
+ bug_numbers: [1653073, 1653083, 1720608, 1767907]
+ notification_emails: ["autofill@lists.mozilla.org", "passwords-dev@mozilla.org"]
+ expiry_version: never
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ release_channel_collection: opt-out
+ manage:
+ description: >-
+ User interactions for credit card autofill preferences management UI.
+ objects:
+ - "manage"
+ methods:
+ - "show"
+ - "add"
+ - "delete"
+ - "show_entry"
+ - "edit"
+ bug_numbers: [1653073, 1654167, 1720608, 1767907]
+ notification_emails: ["autofill@lists.mozilla.org", "passwords-dev@mozilla.org"]
+ expiry_version: never
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ release_channel_collection: opt-out
+ cc_form:
+ description: >-
+ User interactions for credit card autofill forms
+ objects:
+ - "cc_form"
+ methods:
+ - "detected"
+ - "popup_shown"
+ - "filled"
+ - "filled_modified"
+ - "submitted"
+ bug_numbers: [1653073, 1653162, 1720608, 1767907]
+ notification_emails: ["autofill@lists.mozilla.org", "passwords-dev@mozilla.org"]
+ expiry_version: never
+ products:
+ - "firefox"
+ record_in_processes: ["content"]
+ release_channel_collection: opt-out
+ extra_keys:
+ cc_name_found: Whether the cardholder name field was identified
+ cc_number_found: Whether the credit card number field was identified
+ cc_exp_found: Whether the expiration date was identified
+ cc_name: Whether the name was autofilled (autofilled / not_filled / user_filled / unavailable)
+ cc_number: Whether the credit card number field was autofilled (autofilled / not_filled / user_filled / unavailable)
+ cc_exp: Whether the expiration date was autofilled (autofilled / not_filled / user_filled / unavailable)
+ field_name: Name of the field being affected by the method
+ fields_not_auto: >-
+ Number of fields in a `submitted` event that were not autofilled.
+ Note that this number counts all fields in a form, even if some of those fields might have been identified as password and/or address fields.
+ Additionally, it may include input elements that are not rendered as user-modifiable, such as hidden fields as well as <button> elements.
+ fields_auto: Number of fields in the `submitted` event that were autofilled
+ fields_modified: Number of fields in a `submitted` event that were autofilled and later modified
+ cc_form_v2:
+ description: >-
+ User interactions for credit card autofill forms
+ Expected values are store in extra_keys and are varies depends on the methods
+ 'detected': Recorded when a form is recognized as a credit card form.
+ The possible value of cc_* in extra_keys are "true", "false", or an integer between 0-100
+ When the value is "true", the field is identified via autocomplete attribute
+ When the value is "false", the field is not detected in the form
+ When the value is an integer greater than 0, the value indicates the confidence value from fathom (normalized to 0-100)
+ When the value is an 0, then the field is identified by regexp-based heuristic
+ 'popup_shown': Recorded when autofill popup is shown. Using field_name to record the field
+ that triggers this event
+ 'filled': Recorded when a form is autofiled. Possible values are
+ `filled`, `not_filled`, `user_filled` or `unavailable`
+ 'filled_modified': Recorded when a field is autofilled and then modified by the user.
+ Using field_name to record the field that triggers this event.
+ 'submitted': Recorded when a form is submitted. Possible values are `autofilled`, `user_filled` or `unavailable`
+ 'cleared' Recorded when a form is cleared . Using field_name to record the field that triggers this event
+ objects:
+ - "cc_form_v2"
+ methods:
+ - "detected"
+ - "popup_shown"
+ - "filled"
+ - "filled_modified"
+ - "submitted"
+ - "cleared"
+ bug_numbers: [1757731]
+ notification_emails: ["autofill@lists.mozilla.org", "passwords-dev@mozilla.org"]
+ expiry_version: never
+ products:
+ - "firefox"
+ record_in_processes: ["content"]
+ release_channel_collection: opt-out
+ extra_keys:
+ cc_name: cardholder name field result. Used by `detected`, `filled`, and `filled_modified` methods.
+ cc_number: credit card number field result
+ cc_type: credit card type result
+ cc_exp: credit card expiration date
+ cc_exp_month: credit card expiration month
+ cc_exp_year: credit card expiration year
+ field_name: Name of the field being affected by the method. Used by `popup_shown` and `filled_modified` methods.
+
+extensions.data:
+ migrateResult:
+ objects: ["storageLocal"]
+ bug_numbers: [1470213, 1553297, 1590736, 1630596, 1672570, 1714251, 1749878, 1781974, 1817100, 1861295]
+ notification_emails: ["addons-dev-internal@mozilla.com"]
+ expiry_version: "132"
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ release_channel_collection: opt-out
+ extra_keys:
+ backend: The selected backend ("JSONFile" / "IndexedDB").
+ data_migrated: The old extension data has been migrated ("y" / "n").
+ error_name: >
+ A DOMException error name if any ("OtherError" for unknown errors).
+ The error has been fatal if the `backend` extra key is "JSONFile",
+ otherwise it is a non fatal error which didn't prevented the
+ extension from switching to the IndexedDB backend.
+ has_jsonfile: The extension has a JSONFile ("y" / "n").
+ has_olddata: The extension had some data stored in the JSONFile ("y" / "n").
+ description: >
+ These events are sent when an extension is migrating its data to the new IndexedDB backend,
+ the value of this event is the addon id.
+ storageLocalError:
+ objects: ["get", "set", "remove", "clear"]
+ bug_numbers: [1606903, 1649948, 1689255, 1730038, 1763523, 1811148, 1861297]
+ notification_emails: ["addons-dev-internal@mozilla.com"]
+ expiry_version: "132"
+ products:
+ - "firefox"
+ record_in_processes: ["main", "content"]
+ release_channel_collection: opt-out
+ extra_keys:
+ error_name: >
+ A DOMException error name if any ("OtherError" for unknown errors).
+ description: >
+ These events are collected when an extension triggers an unexpected error
+ while running a storage.local API call (e.g. because of some underlying
+ QuotaManager and/or IndexedDB error), the value of this event is the
+ addon id.
+
+homepage:
+ preference:
+ objects: ["ignore"]
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: >
+ This is recorded whenever the homepage preference is either reset due to
+ being on the ignore list, or setting is blocked due to being on the same
+ list.
+ The value field records the reason for the ignore. "saved_reset" for
+ when a saved preference value is reset. "set_blocked" for when the setting
+ was blocked and "set_blocked_extension" for when we know a WebExtension
+ attempting to set it was blocked.
+ bug_numbers: [1535049]
+ notification_emails:
+ - "mdeboer@mozilla.com"
+ - "rharter@mozilla.com"
+ expiry_version: never
+ extra_keys:
+ webExtensionId: The identifier of the webextension, if known.
+
+navigation:
+ search:
+ objects: ["about_home", "about_newtab", "contextmenu", "oneoff",
+ "suggestion", "alias", "enter", "searchbar", "urlbar",
+ "urlbar_handoff", "urlbar_persisted", "urlbar_searchmode",
+ "webextension"]
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: >
+ This is recorded on each search navigation.
+ The value field records the action used to trigger the search:
+ "enter", "oneoff", "suggestion", "alias", null (for contextmenu and webextension)
+ bug_numbers: [1316281, 1496764]
+ notification_emails:
+ - "mdeboer@mozilla.com"
+ - "rharter@mozilla.com"
+ expiry_version: never
+ extra_keys:
+ engine: The id of the search engine used.
+
+upgrade_dialog:
+ trigger:
+ objects: ["reason"]
+ bug_numbers: [1697222]
+ description: >
+ Triggering behaviors of the upgrade dialog. Value indicates which condition failed or all satisfied.
+ expiry_version: never
+ notification_emails: ["edilee@mozilla.com"]
+ products: ["firefox"]
+ record_in_processes: ["main"]
+ release_channel_collection: opt-out
+
+aboutpreferences:
+ show:
+ objects: ["initial", "click", "hash"]
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ description: >
+ This is recorded whenever a pane is shown inside about:preferences.
+ The object field records the type of action that resulted in showing a pane. Possible values are as follows,
+ "initial" is used when a pane is shown direclty. This happens with or without a hash in the URL. The "general" pane
+ loads without such a hash, when the pane is loaded via the "main" entrypoints in the menu system or with shortcuts.
+ "click" is used when a pane is shown when user clicks category name inside about:preferences.
+ "hash" is used when a pane is shown via a hash change in url.
+ The value field records identifier of pane shown
+ bug_numbers: [1738187]
+ notification_emails:
+ - "pdahiya@mozilla.com"
+ - "gijs@mozilla.com"
+ expiry_version: never
+
+normandy:
+ enroll:
+ objects: ["preference_study", "addon_study", "preference_rollout", "addon_rollout", "nimbus_experiment"]
+ description: >
+ Sent when applying a Normandy recipe of the above types has succeeded.
+ extra_keys:
+ experimentType: >
+ For preference_study and nimbus_experiment recipes, the type of experiment this is ("exp" or "exp-highpop").
+ branch: >
+ The slug of the branch that was chosen for this client.
+ addonId: For addon_study recipes, the ID of the addon that was installed.
+ addonVersion: For addon_study recipes, the version of the addon that was installed.
+ bug_numbers: [1443560]
+ notification_emails: ["normandy-notifications@mozilla.com"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: [main]
+ release_channel_collection: opt-out
+ expiry_version: never
+
+ enroll_failed:
+ methods: ["enrollFailed"]
+ objects: ["addon_study", "preference_rollout", "preference_study", "addon_rollout", "nimbus_experiment"]
+ description: >
+ Sent when applying a Normandy recipe of the above types has failed.
+ extra_keys:
+ reason: An error code describing the failure.
+ preference: >
+ For preference_rollout when reason=conflict, the name of the preference
+ that was going to be modified.
+ detail: >
+ For addon_study and branched_addon study, extra text describing the failure.
+ branch: >
+ The branch that failed to enroll.
+ addonId: The ID of the addon for the rollout when reason=conflict.
+ conflictingSlug: The slug for the conflicting rollout.
+ enrollmentId: The enrollment ID of the conflicting rollout.
+ prefBranch: For preference_study when reason=invalid-branch, the branch that was invalid.
+ bug_numbers: [1443560]
+ notification_emails: ["normandy-notifications@mozilla.com"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: [main]
+ release_channel_collection: opt-out
+ expiry_version: never
+
+ update:
+ objects: ["addon_study", "preference_rollout", "addon_rollout", "nimbus_experiment"]
+ description: >
+ This event is fired when a client detects that a recipe of the
+ ahove types has changed on the server, and the new version of the
+ recipe is being applied over an existing, older version previously
+ fetched from the server.
+ extra_keys:
+ previousState: >
+ For preference_rollout recipes, the state of the rollout that had been applied
+ previously.
+ addonId: For addon_study recipes, the ID of the addon that was updated.
+ addonVersion: For addon_study recipes, the version of the addon that was installed.
+ branch: The branch that was updated.
+ enrollmentId: A unique ID for this enrollment that will be included in all related Telemetry.
+ bug_numbers: [1443560, 1474413]
+ notification_emails: ["normandy-notifications@mozilla.com"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: [main]
+ release_channel_collection: opt-out
+ expiry_version: never
+
+ update_failed:
+ methods: ["updateFailed"]
+ objects: ["addon_study", "addon_rollout"]
+ description: >
+ Sent when applying a new version of a Normandy recipe of the above types (over an
+ existing, older version previously fetched from the server) has failed.
+ extra_keys:
+ reason: An error code describing the failure.
+ detail: >
+ Extra text describing the failure. Currently only provided for addon_study.
+ branch: The branch that failed to update.
+ enrollmentId: A unique ID for this enrollment that will be included in all related Telemetry.
+ bug_numbers: [1474413]
+ notification_emails: ["normandy-notifications@mozilla.com"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: [main]
+ release_channel_collection: opt-out
+ expiry_version: never
+
+ unenroll:
+ objects: ["preference_study", "addon_study", "preference_rollback", "addon_rollback", "nimbus_experiment"]
+ description: >
+ Sent when a Normandy recipe of certain types "ends". N.B. For
+ preference_rollback, this is fired when the recipe is fired (the
+ recipe that "ends" is a corresponding preference_rollout).
+ extra_keys:
+ reason: A code describing the reason why the recipe ended.
+ didResetValue: >
+ For preference_study, "true" or "false" according to whether we put the preference back the way it was.
+ addonId: For addon_study, the ID of the addon that ended.
+ addonVersion: For addon_study, the version of the addon for which the recipe ended.
+ branch: The branch of the experiment that this client was on.
+ enrollmentId: A unique ID for this enrollment that will be included in all related Telemetry.
+ changedPref: >
+ For preference_study or nimbus_experiment, the preference that was
+ detected to change that caused the unenrollment.
+ bug_numbers: [1443560, 1843126]
+ notification_emails: ["normandy-notifications@mozilla.com"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: [main]
+ release_channel_collection: opt-out
+ expiry_version: never
+
+ unenroll_failed:
+ methods: ["unenrollFailed"]
+ description: >
+ Sent when unenrolling a user fails (see the unenroll event).
+ objects: ["preference_rollback", "preference_study", "addon_rollback", "nimbus_experiment"]
+ extra_keys:
+ reason: A code describing the reason the unenroll failed.
+ enrollmentId: A unique ID for this enrollment that will be included in all related Telemetry.
+ changedPref: For preference_study, the preference that was detected to change that caused the attempted unenrollment.
+ caller: The function that caused the stop to happen, included on Nightly to aid debugging.
+ originalReason: The code that would had been used for the unenrollment, had it not failed.
+ bug_numbers: [1443560, 1693402]
+ notification_emails: ["normandy-notifications@mozilla.com"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: [main]
+ release_channel_collection: opt-out
+ expiry_version: never
+
+ graduate:
+ objects: ["preference_rollout"]
+ description: >
+ Sent when a preference rollout ends due to the rolled-out
+ preference becoming a new default.
+ bug_numbers: [1443560]
+ notification_emails: ["normandy-notifications@mozilla.com"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: [main]
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ enrollmentId: A unique ID for this enrollment that will be included in all related Telemetry.
+ reason: The reason the rollout graduated
+
+ expose:
+ objects: [
+ "nimbus_experiment",
+ ]
+ methods: ["expose"]
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ record_in_processes: ["main", "content"]
+ description: >
+ This records an event at the moment the user is exposed to an experiment
+ treatment. The event is triggered either by the code checking that a
+ certain experiment feature is enabled or when that feature value is used.
+ This is different from enrollment or experiment activation because it
+ registers when a user actually gets exposed to the experiment feature.
+ bug_numbers: [1675104]
+ notification_emails: ["ujet@mozilla.com"]
+ expiry_version: "never"
+ extra_keys:
+ branchSlug: The slug for the branch the user is enrolled in.
+ featureId: The type of experiment variant the user was enrolled into.
+
+ expPrefChanged:
+ objects: ["preference_study"]
+ methods: ["expPrefChanged"]
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ record_in_processes: [main]
+ description: >
+ This records that a preference that was set by an experiment has been
+ changed away from its experimental value. This can be triggered by a
+ user changing a preference at runtime, by the preference being changed
+ on disk when Firefox is not running, or by the preference already
+ having a user-set value when a user has enrolls in a default branch
+ experiment.
+ bug_numbers: [1698684]
+ notification_emails: ["mcooper@mozilla.com"]
+ expiry_version: never
+ extra_keys:
+ preferenceName: The preference that changed
+ reason: The way that the preference change was detected ("observer", "sideload", or "onEnroll")
+ enrollmentId: A unique ID for this enrollment that will be included in all related Telemetry.
+
+ validation_failed:
+ objects: ["nimbus_experiment"]
+ methods: ["validationFailed"]
+ release_channel_collection: "opt-out"
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ description: >
+ This records when validation of a recipe fails.
+ bug_numbers: [1762652]
+ notification_emails: ["barret@mozilla.com"]
+ expiry_version: never
+ extra_keys:
+ reason: Why validation failed (one of "invalid-recipe", "invalid-branch", or "invalid-reason").
+ branch: If reason == invalid-branch, the branch that failed validation.
+ feature: If reason == invalid-feature, the invalid feature ID.
+ locale: >
+ If reason == missing-locale, the locale that was missing from the
+ localization table.
+ If reason == missing-l10n-entry, the locale that was missing the
+ localization entries.
+ l10n_ids: >
+ If reason == missing-l10n-entry, a comma-separated list of missing
+ localization entries.
+
+browser.launched_to_handle:
+ system_notification:
+ objects: ["toast"]
+ description: >
+ Recorded when Firefox launches to complete a native notification popped by
+ a system (chrome privileged) alert. Windows-only at the time of writing.
+ bug_numbers:
+ - 1788960
+ notification_emails:
+ - nalexander@mozilla.com
+ - rtestard@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes: [main]
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ name: >
+ The `name` of the system (chrome privileged) alert that Firefox was
+ launched to complete.
+ action: >
+ The `action` of the system (chrome privileged) alert that Firefox was
+ launched to complete.
+
+browser.migration:
+ opened:
+ objects: ["wizard"]
+ description: >
+ Recorded when the migration wizard opens.
+ bug_numbers:
+ - 1824786
+ notification_emails:
+ - mconley@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes: [main]
+ release_channel_collection: opt-out
+ expiry_version: never
+ no_browsers_found:
+ objects: ["wizard"]
+ description: >
+ Recorded when the migration wizard reports that there are no browsers to migrate from.
+ bug_numbers:
+ - 1824786
+ notification_emails:
+ - mconley@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes: [main]
+ release_channel_collection: opt-out
+ expiry_version: never
+ browser_selected:
+ objects: ["wizard"]
+ description: >
+ Recorded when the user selects a browser to migrate from.
+ bug_numbers:
+ - 1824786
+ notification_emails:
+ - mconley@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes: [main]
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ migrator_key: The key of the browser that was selected.
+ profile_selected:
+ objects: ["wizard"]
+ description: >
+ Recorded when the user selects a profile to migrate from. If the browser doesn't
+ support multiple profiles, this will not be recorded.
+ bug_numbers:
+ - 1824786
+ notification_emails:
+ - mconley@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes: [main]
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ migrator_key: The key of the browser that had a profile selected for it.
+ resources_selected:
+ objects: ["wizard"]
+ description: >
+ Recorded when the user selects resources from the browser / profile to import.
+ bug_numbers:
+ - 1824786
+ notification_emails:
+ - mconley@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes: [main]
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ migrator_key: The key of the browser that had resources selected for it.
+ history: >
+ "1" if history was selected. "0" otherwise.
+ formdata: >
+ "1" if form data was selected. "0" otherwise.
+ passwords: >
+ "1" if passwords was selected. "0" otherwise.
+ bookmarks: >
+ "1" if bookmarks were selected. "0" otherwise.
+ payment_methods: >
+ "1" if payment methods were selected. "0" otherwise.
+ extensions: >
+ "1" if extensions were selected. "0" otherwise.
+ other: A count of the number of other resource types that were selected.
+ configured: >
+ "1" if Variant 2 of the new Migration Wizard was being used, and the list of
+ resources was expanded. "0" otherwise.
+ linux_perms:
+ objects: ["wizard"]
+ description: >
+ Recorded if the user is on Linux and the browser is installed in a sandboxed
+ environment that prevents it from reading other browser's data, and the user
+ is presented with instructions and an option to grant the browser permission
+ to read that other data.
+ bug_numbers:
+ - 1824786
+ notification_emails:
+ - mconley@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes: [main]
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ migrator_key: The key of the migrator that will perform the migration.
+ safari_perms:
+ objects: ["wizard"]
+ description: >
+ Recorded if the user is on macOS, chose to migrate from Safari, and was presented with
+ the page of the wizard requesting permission to read from the Safari profile folder.
+ bug_numbers:
+ - 1824786
+ notification_emails:
+ - mconley@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes: [main]
+ release_channel_collection: opt-out
+ expiry_version: never
+ safari_password_file:
+ objects: ["wizard"]
+ description: >
+ Recorded if the user is on macOS, chose to migrate from Safari, and was presented with
+ the page of the wizard requesting to import passwords from a file. This only gets recorded
+ in the new migration wizard.
+ bug_numbers:
+ - 1824786
+ notification_emails:
+ - mconley@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes: [main]
+ release_channel_collection: opt-out
+ expiry_version: never
+ migration_started:
+ objects: ["wizard"]
+ description: >
+ Recorded when the user begins a migration.
+ bug_numbers:
+ - 1824786
+ notification_emails:
+ - mconley@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes: [main]
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ migrator_key: The key of the migrator that will perform the migration.
+ history: >
+ "1" if history is being migrated. "0" otherwise.
+ formdata: >
+ "1" if form data is being migrated. "0" otherwise.
+ passwords: >
+ "1" if passwords are being migrated. "0" otherwise.
+ bookmarks: >
+ "1" if bookmarks are being migrated. "0" otherwise.
+ payment_methods: >
+ "1" if payment methods are being migrated. "0" otherwise.
+ extensions: >
+ "1" if extensions are being migrated. "0" otherwise.
+ other: A count of the number of other resource types that are being migrated.
+ migration_finished:
+ objects: ["wizard"]
+ description: >
+ Recorded when the user finishes a migration.
+ bug_numbers:
+ - 1824786
+ notification_emails:
+ - mconley@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes: [main]
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ migrator_key: The key of the migrator that will perform the migration.
+ history: >
+ "1" if history was migrated. "0" otherwise.
+ formdata: >
+ "1" if form data was migrated. "0" otherwise.
+ passwords: >
+ "1" if passwords were migrated. "0" otherwise.
+ bookmarks: >
+ "1" if bookmarks were migrated. "0" otherwise.
+ payment_methods: >
+ "1" if payment methods were migrated. "0" otherwise.
+ extensions: >
+ "1" if no extensions were matched, "2" if only some extensions
+ were matched, "3" if all extensions were matched, and "0" if extensions
+ weren't selected for migration.
+ other: A count of the number of other resource types that were migrated.
+
+pwmgr:
+ open_management:
+ objects: ["aboutprotections", "autocomplete", "capturedoorhanger", "contextmenu", "direct", "fxamenu", "mainmenu", "pageinfo", "preferences", "snippet"]
+ methods: ["open_management"]
+ description: >
+ Sent when opening the password management UI.
+ bug_numbers: [1543499, 1454733, 1545172, 1550631, 1622971]
+ notification_emails: ["loines@mozilla.com", "passwords-dev@mozilla.org", "sfoster@mozilla.com"]
+ products:
+ - "firefox"
+ record_in_processes: [main, content]
+ release_channel_collection: opt-out
+ expiry_version: never
+ reauthenticate:
+ description: >
+ Measure how often users are asked to authenticate with their Operating System or Master Password to gain access to stored passwords.
+ Possible values are as follows,
+ "success" should be used when the user authenticates and provides a password or other authentication factor.
+ "success_no_prompt" should be used when the feature is enabled but no prompt is given to the user because they have recently authenticated.
+ "success_disabled" is used when the feature is disabled.
+ "success_unsupported_platform" should be set when the user attempts to authenticate on an unsupported platform.
+ "success_no_password" should be used when the user doesn't have an OS password set.
+ "fail" should be used when the user cancels the authentication prompt or an unexpected exception is encountered. The user may or may not have provided an incorrect password before cancelling.
+ objects: [
+ "master_password",
+ "os_auth",
+ ]
+ methods: ["reauthenticate"]
+ extra_keys:
+ auto_admin: >
+ If the AutoAdminLogon Windows feature is enabled. This feature disables password prompt when logging in to Windows.
+ require_signon: >
+ If the Power Settings on Windows are configured to not prompt for password upon resuming from sleep.
+ bug_numbers:
+ - 1628029
+ - 1623745
+ - 1636729
+ - 1642267
+ expiry_version: never
+ notification_emails: ["loines@mozilla.com", "passwords-dev@mozilla.org", "jaws@mozilla.com"]
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ record_in_processes: [main, content]
+ mgmt_interaction:
+ description: >
+ These events record interactions on the about:logins page.
+ extra_keys:
+ breached: >
+ Whether the login is marked as breached or not. If a login is both breached and vulnerable, it will only be reported as breached.
+ vulnerable: >
+ Whether the login is marked as vulnerable or not. If a login is both breached and vulnerable, it will only be reported as breached.
+ sort_key: The key that is used for sorting the login-list. Should only be set with the "sort" method.
+ objects: [
+ "existing_login",
+ "list",
+ "new_login",
+ "password",
+ "username",
+ ]
+ methods: [
+ "cancel",
+ "copy",
+ "delete",
+ "dismiss_breach_alert",
+ "edit",
+ "filter",
+ "hide",
+ "learn_more_breach",
+ "learn_more_vuln",
+ "new",
+ "open_site",
+ "save",
+ "select",
+ "show",
+ "sort",
+ ]
+ bug_numbers:
+ - 1548463
+ - 1600958
+ - 1549115
+ - 1628165
+ expiry_version: never
+ notification_emails: ["loines@mozilla.com", "passwords-dev@mozilla.org", "jaws@mozilla.com"]
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ record_in_processes: [content]
+ autocomplete_field:
+ objects: ["generatedpassword"]
+ methods: ["autocomplete_field", "autocomplete_shown"]
+ description: >
+ "autocomplete_field": The first time each unique generated password is used to fill a login field - i.e. the user selects it from from the autocomplete dropdown on a password input
+ "autocomplete_shown": The first time the password generation option is shown in the autocomplete dropdown on a password input for a site per session
+ bug_numbers: [1548878, 1616356]
+ notification_emails: ["loines@mozilla.com", "passwords-dev@mozilla.org", "sfoster@mozilla.com"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ release_channel_collection: opt-out
+ expiry_version: never
+ filled_field_edited:
+ objects: ["generatedpassword"]
+ methods: ["filled_field_edited"]
+ description: >
+ The first time each generated password filled in a website form field is edited by the user in a field it was filled in
+ bug_numbers: [1548880]
+ notification_emails: ["loines@mozilla.com", "passwords-dev@mozilla.org"]
+ products:
+ - "firefox"
+ - "thunderbird"
+ record_in_processes: ["main"]
+ release_channel_collection: opt-out
+ expiry_version: never
+ saved_login_used:
+ objects: ["form_login", "form_password", "auth_login", "prompt_login"]
+ description: >
+ Each time a saved login is used in a form or authentication dialog.
+ bug_numbers: [1631130]
+ notification_emails: ["loines@mozilla.com", "passwords-dev@mozilla.org"]
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ filled: Whether the browser filled the login details for the user vs. typing saved values.
+ mgmt_menu_item_used:
+ description: >
+ Record interactions with the about:logins menu.
+ objects: [
+ "import_from_browser",
+ "import_from_csv",
+ "import_csv_complete",
+ "export",
+ "export_complete",
+ "preferences",
+ ]
+ bug_numbers: [1641396, 1641777, 1641393]
+ expiry_version: never
+ notification_emails: ["loines@mozilla.com", "passwords-dev@mozilla.org"]
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ record_in_processes: [content, main]
+ doorhanger_submitted:
+ description: >
+ A login is saved or updated via the capture doorhanger. Carries information about whether
+ the username and password _that were saved/updated by the user_ were modified in the
+ doorhanger, selected from the suggestion autocomplete, or neither. `did_edit_X` and
+ `did_select_X` will never both be true in the same event.
+
+ The `object` describes the type of doorhanger when it was originally created. Note that user
+ updates to the doorhanger may change whether a login is actually saved or updated, but will
+ not impact the sent object.
+ objects: ["save", "update"]
+ bug_numbers: [1650929, 1650941, 1678200, 1714252, 1754637, 1850872]
+ expiry_version: "142"
+ release_channel_collection: opt-out
+ products: ["firefox"]
+ record_in_processes: [main]
+ notification_emails: ["passwords-dev@mozilla.org"]
+ extra_keys:
+ did_edit_un: >
+ Whether or not the saved/updated username was modified by the user typing into the
+ username field.
+ did_select_un: >
+ Whether or not the saved/updated username was selected by the user choosing a suggested
+ value from the autocomplete popup.
+ did_edit_pw: >
+ Whether or not the saved/updated password was modified by the user typing into the
+ password field.
+ did_select_pw: >
+ Whether or not the saved/updated password was selected by the user choosing a suggested
+ value from the autocomplete popup.
+
+# Record telemetry based on individual Firefox relay UI (autocomplete popup, notification panel)
+relay_integration:
+ popup_option:
+ description: >
+ Firefox relay integration autocomplete popup
+ objects: ["offer_relay", "fill_username"]
+ methods: ["shown", "clicked"]
+ bug_numbers: [1804502]
+ expiry_version: "never"
+ products: ["firefox"]
+ record_in_processes: [main]
+ notification_emails: ["passwords-dev@mozilla.org"]
+ extra_keys:
+ scenario: Describes the auth context for now only SignupForm is supported
+ error_code: >
+ The error code after users click the fill username autocomplete entry.
+ Only present if the object is "fill_username".
+ When the event is successful, the error_code is 0.
+ is_relay_user: >
+ Whether the user is a relay user or not.
+ Only present if the object is "offer_relay"
+ release_channel_collection: opt-out
+ mask_panel:
+ description: >
+ Panels to show the state of the email alias generation
+ objects: ["reuse_panel"]
+ methods: ["shown", "get_unlimited_masks", "reuse_mask"]
+ bug_numbers: [1804502]
+ expiry_version: "never"
+ products: ["firefox"]
+ record_in_processes: [main]
+ notification_emails: ["passwords-dev@mozilla.org"]
+ extra_keys:
+ error_code: >
+ The error code after users click the email alias generation panel.
+ When the event is successful, the error_code is 0.
+ release_channel_collection: opt-out
+ opt_in_panel:
+ description: >
+ Panel to opt-in Firefox Relay Integration
+ objects: ["opt_in_panel"]
+ methods: ["shown", "enabled", "postponed", "disabled"]
+ bug_numbers: [1804502]
+ expiry_version: "never"
+ products: ["firefox"]
+ record_in_processes: [main]
+ notification_emails: ["passwords-dev@mozilla.org"]
+ release_channel_collection: opt-out
+ pref_change:
+ description: >
+ Checkbox in the settings page to enable/disable relay
+ objects: ["pref_change"]
+ methods: ["enabled", "disabled"]
+ bug_numbers: [1804502]
+ expiry_version: "never"
+ products: ["firefox"]
+ record_in_processes: [main]
+ notification_emails: ["passwords-dev@mozilla.org"]
+ release_channel_collection: opt-out
+
+jsonfile:
+ load:
+ description: >
+ Records when JSONFile.sys.mjs consumers are trying to access a missing or corrupt json file.
+ For example, Login Store trying to access logins.json when it has gone missing or corrupt.
+ # Keep synchronized with TELEMETRY_BASENAMES from JSONFile.sys.mjs.
+ objects: ["logins", "autofillprofiles"]
+ bug_numbers: [1599567]
+ expiry_version: never
+ products: ["firefox"]
+ record_in_processes: [main]
+ notification_emails: ["prathiksha@mozilla.com", "passwords-dev@mozilla.org"]
+
+fxa:
+ connect:
+ objects: ["account"]
+ methods: ["connect", "disconnect"]
+ description: >
+ Records when a Firefox Account, or a Firefox Account service, is explicitly
+ connected or disconnected from the browser via an intentional user action.
+ extra_keys:
+ fxa: Whether the account itself was connected or disconnected.
+ sync: Whether sync was connected or disconnected.
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ bug_numbers: [1595954]
+ notification_emails: ["sync-dev@mozilla.org"]
+ release_channel_collection: opt-out
+ expiry_version: never
+
+fxa_avatar_menu:
+ click:
+ objects: [
+ "account_settings",
+ "cad",
+ "login",
+ "send_tab",
+ "sync_now",
+ "sync_settings",
+ "sync_tabs",
+ "sync_tabs_sidebar",
+ "toolbar_icon",
+ "unver_sync_settings",
+ "open_monitor",
+ "open_send",
+ "monitor_cta",
+ "relay_cta",
+ "vpn_cta",
+ "sync_cta"
+ ]
+ methods: ["click"]
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ description: This is recorded on interactions with the FxA avatar menu on the toolbar
+ bug_numbers: [1524665, 1585459, 1606203]
+ notification_emails: ["vbudhram@mozilla.com", "loines@mozilla.com"]
+ expiry_version: "never"
+ extra_keys:
+ fxa_status: >
+ The current state of the user. Possible states are "not_configured", "unverified",
+ "signedin" and "login_failed".
+ fxa_avatar: Boolean for whether or not account has set an avatar
+
+fxa_app_menu:
+ click:
+ objects: [
+ "account_settings",
+ "cad",
+ "login",
+ "send_tab",
+ "sync_now",
+ "sync_settings",
+ "sync_tabs",
+ "sync_tabs_sidebar",
+ "toolbar_icon",
+ "unver_sync_settings",
+ "open_monitor",
+ "open_send",
+ "monitor_cta",
+ "relay_cta",
+ "vpn_cta",
+ "sync_cta"
+ ]
+ methods: ["click"]
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ description: This is recorded on interactions with the FxA menu in the app (hamburger) menu
+ bug_numbers: [1542334, 1606203]
+ notification_emails: ["vbudhram@mozilla.com", "loines@mozilla.com"]
+ expiry_version: "never"
+ extra_keys:
+ fxa_status: >
+ The current state of the user. Possible states are "not_configured", "unverified",
+ "signedin" and "login_failed".
+ fxa_avatar: Boolean for whether or not account has set an avatar
+
+messaging_experiments:
+ reach:
+ objects: [
+ "cfr",
+ "moments_page",
+ "infobar",
+ "spotlight",
+ "featureCallout"
+ ]
+ methods: ["reach"]
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ description: >
+ This records whether a branch's targeting is satisfied for Messaging System
+ experiments. All qualified branch ID(s) will be recorded in the 'extra_keys'
+ for each active experiment, and the event 'value' will be the experiment ID
+ bug_numbers: [1471318]
+ notification_emails: ["ujet@mozilla.com"]
+ expiry_version: "never"
+ extra_keys:
+ branches: >
+ A semicolon separated string for all the qualified branch ID(s).
+ e.g. "control;variant_01;treatment_02".
+ targeting:
+ objects:
+ - attribute_error
+ - attribute_timeout
+ methods:
+ - targeting
+ extra_keys:
+ source: "Source of targeting expression: experiment slug or message id"
+ release_channel_collection: opt-out
+ products:
+ - firefox
+ record_in_processes:
+ - main
+ description: >
+ Record generic JEXL errors that result from issues with experiment or
+ message targeting expressions. The value field contains the namespace and
+ attribute name that caused the error.
+ bug_numbers:
+ - 1644743
+ notification_emails:
+ - ujet@mozilla.com
+ expiry_version: never
+
+# This category contains event entries used for Telemetry tests.
+# They will not be sent out with any pings.
+telemetry.test:
+ test:
+ methods: ["test1", "test2"]
+ objects: ["object1", "object2"]
+ bug_numbers: [1286606]
+ notification_emails: ["telemetry-client-dev@mozilla.com"]
+ products:
+ - "firefox"
+ - "fennec"
+ - "thunderbird"
+ record_in_processes: ["main"]
+ description: This is a test entry for Telemetry.
+ expiry_version: never
+ extra_keys:
+ key1: This is just a test description.
+ key2: This is another test description.
+ optout:
+ objects: ["object1", "object2"]
+ bug_numbers: [1286606]
+ notification_emails: ["telemetry-client-dev@mozilla.com"]
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ - "fennec"
+ - "thunderbird"
+ record_in_processes: ["main"]
+ description: This is an opt-out test entry.
+ expiry_version: never
+ extra_keys:
+ key1: This is just a test description.
+ expired_version:
+ objects: ["object1", "object2"]
+ bug_numbers: [1286606]
+ notification_emails: ["telemetry-client-dev@mozilla.com"]
+ products:
+ - "firefox"
+ - "fennec"
+ - "thunderbird"
+ record_in_processes: ["main"]
+ description: This is a test entry with an expired version.
+ expiry_version: "3"
+ not_expired_optout:
+ objects: ["object1"]
+ bug_numbers: [1286606, 1685406]
+ notification_emails: ["telemetry-client-dev@mozilla.com"]
+ description: This is an opt-out test entry with unexpired date and version.
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ - "fennec"
+ - "thunderbird"
+ record_in_processes: ["main", "content"]
+ expiry_version: "999"
+ main_only:
+ objects: ["object1"]
+ bug_numbers: [1313326]
+ notification_emails: ["telemetry-client-dev@mozilla.com"]
+ products:
+ - "firefox"
+ - "fennec"
+ - "thunderbird"
+ record_in_processes: ["main"]
+ description: This event is used to test main-process only recording.
+ expiry_version: never
+ content_only:
+ objects: ["object1"]
+ bug_numbers: [1313326]
+ notification_emails: ["telemetry-client-dev@mozilla.com"]
+ products:
+ - "firefox"
+ - "fennec"
+ - "thunderbird"
+ record_in_processes: ["content"]
+ description: This event is used to test content-process only recording.
+ expiry_version: never
+ extra_keys:
+ foo: This is just a test description.
+ bar: And this is another test description.
+ main_and_content:
+ objects: ["object1"]
+ bug_numbers: [1313326]
+ notification_emails: ["telemetry-client-dev@mozilla.com"]
+ products:
+ - "firefox"
+ - "fennec"
+ - "thunderbird"
+ record_in_processes: ["main", "content"]
+ description: This event is used to test main and content process recording.
+ expiry_version: never
+ default_products:
+ objects: ["object1"]
+ bug_numbers: [1452552]
+ notification_emails: ["telemetry-client-dev@mozilla.com"]
+ products:
+ - "firefox"
+ - "fennec"
+ - "thunderbird"
+ record_in_processes: ["main"]
+ description: This event is used to test default products main recording.
+ expiry_version: never
+ desktop_only:
+ objects: ["object1"]
+ bug_numbers: [1452552]
+ notification_emails: ["telemetry-client-dev@mozilla.com"]
+ record_in_processes: ["main"]
+ description: This event is used to test desktop-only main recording.
+ expiry_version: never
+ products: ["firefox","thunderbird"]
+ multiproduct:
+ objects: ["object1"]
+ bug_numbers: [1452552]
+ notification_emails: ["telemetry-client-dev@mozilla.com"]
+ record_in_processes: ["main"]
+ description: This event is used to test multiproduct main recording.
+ expiry_version: never
+ products: ["firefox", "fennec", "thunderbird"]
+ mobile_only:
+ objects: ["object1"]
+ bug_numbers: [1452552]
+ notification_emails: ["telemetry-client-dev@mozilla.com"]
+ record_in_processes: ["main"]
+ description: This event is used to test mobile-only main recording.
+ expiry_version: never
+ products: ["fennec"]
+ mirror_with_extra:
+ objects: ["object1"]
+ bug_numbers: [1685406]
+ notification_emails: ["glean-team@mozilla.com"]
+ record_in_processes: ["all"]
+ description: Test-only. This event is mirrored to from Glean.
+ expiry_version: never
+ extra_keys:
+ extra1: "This must be kept in-sync"
+ extra2: "With the extra_keys in the Glean metric"
+ products: ["firefox", "thunderbird"]
+ release_channel_collection: opt-out
+
+# This is a secondary category used for Telemetry tests.
+# The events here will not be sent out with any pings.
+telemetry.test.second:
+ test:
+ objects: ["object1", "object2", "object3"]
+ bug_numbers: [1286606]
+ notification_emails: ["telemetry-client-dev@mozilla.com"]
+ products:
+ - "firefox"
+ - "fennec"
+ - "thunderbird"
+ record_in_processes: ["main"]
+ description: This is a test entry for Telemetry.
+ expiry_version: never
+ extra_keys:
+ key1: This is just a test description.
+
+devtools.main:
+ activate:
+ objects: ["responsive_design", "split_console"]
+ bug_numbers: [1455273]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User activates the responsive_design or split_console in the devtools toolbox.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ host: "Toolbox host (positioning): bottom, left, right, window, page or other."
+ width: Toolbox width rounded up to the nearest 50px.
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ add_breakpoint:
+ objects: ["debugger"]
+ bug_numbers: [1463123]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User has added a breakpoint to a script.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ blackbox:
+ objects: ["debugger"]
+ bug_numbers: [1463126]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User clicked the blackbox button to blackbox a script.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ close:
+ objects: ["tools"]
+ bug_numbers: [1453312]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User closes devtools toolbox.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ host: "Toolbox host (positioning): bottom, side, window or other."
+ width: Toolbox width rounded up to the nearest 50px.
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ close_adbg:
+ objects: ["aboutdebugging"]
+ bug_numbers: [1504173]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User closes about:debugging.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ width: Toolbox width rounded up to the nearest 50px.
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ connection_attempt:
+ objects: ["aboutdebugging"]
+ bug_numbers: [1549970]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User is trying to connect to a remote runtime.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ connection_id: Randomly generated id to keep to group various events related to the same connection attempt.
+ connection_type: Connection type
+ runtime_id: Random id generated to track events related to a single runtime
+ status: One of (cancelled, failed, not responding, start, success).
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ continue:
+ objects: ["debugger"]
+ bug_numbers: [1463122]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User has pressed the continue button on a paused script.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ deactivate:
+ objects: ["responsive_design", "split_console"]
+ bug_numbers: [1455275]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User deactivates the responsive_design or split_console in the devtools toolbox.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ host: "Toolbox host (positioning): bottom, side, window or other."
+ width: Toolbox width rounded up to the nearest 50px.
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ device_added:
+ objects: ["aboutdebugging"]
+ bug_numbers: [1521507]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: A new device was detected in about:debugging
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ connection_type: Connection type
+ device_name: Device name
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ device_removed:
+ objects: ["aboutdebugging"]
+ bug_numbers: [1521507]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: A previously listed device was removed in about:debugging
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ connection_type: Connection type
+ device_name: Device name
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ edit_html:
+ objects: ["inspector"]
+ bug_numbers: [1463080]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User is editing HTML via the context menu item in the markup view.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ made_changes: Indicates whether changes were made.
+ time_open: The amount of time in ms that the HTML editor was open.
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ edit_resend:
+ objects: ["netmonitor"]
+ bug_numbers: [1463171]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User has executed edit / resend in the netmonitor.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ session_id: The toolbox session start time e.g. 13963.
+ edit_rule:
+ objects: ["ruleview"]
+ bug_numbers: [1463081]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User is editing a CSS rule by clicking on or next to a CSS property, enabling / disabling a rule or creating a new property.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ enter:
+ objects: ["accessibility", "application", "dom", "inspector", "jsdebugger", "memory", "netmonitor", "options", "performance", "storage", "styleeditor", "webconsole", "whatsnew","other", "fakeTool4242", "testBlankPanel", "testTool", "testtool1", "testTool1072208", "testtool2"]
+ bug_numbers: [1441070]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User opens a tool in the devtools toolbox.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ host: "Toolbox host (positioning): bottom, side, window or other."
+ width: Toolbox width rounded up to the nearest 50px.
+ message_count: The number of cached console messages.
+ start_state: debuggerStatement, breakpoint, exception, tab_switch, toolbox_show, initial_panel, toggle_settings_off, toggle_settings_on, key_shortcut, select_next_key, select_prev_key, tool_unloaded, inspect_dom, unknown etc.
+ panel_name: The name of the panel opened or other
+ cold: Is this the first time the current panel has been opened in this toolbox?
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ execute_js:
+ objects: ["webconsole"]
+ bug_numbers: [1463083]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User has executed some JS in the Web Console.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ input: Indicates from which input the command was evaluated ("inline" for regular input, "multiline" for editor mode).
+ lines: The number of lines contained in the command.
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ reverse_search:
+ objects: ["webconsole"]
+ bug_numbers: [1489489]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User has toggled, navigated or evaluated expressions from reverse search .
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ functionality: Indicates functionality of reverse search being accessed.
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ exit:
+ objects: ["accessibility", "application", "dom", "inspector", "jsdebugger", "memory", "netmonitor", "options", "performance", "storage", "styleeditor", "webconsole", "whatsnew", "other", "fakeTool4242", "testBlankPanel", "testTool", "testtool1", "testTool1072208", "testtool2"]
+ bug_numbers: [1455270]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User closes a tool in the devtools toolbox.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ host: "Toolbox host (positioning): bottom, side, window or other."
+ width: Toolbox width rounded up to the nearest 50px.
+ next_panel: The name of the panel closed or other.
+ panel_name: The name of the panel opened or other
+ reason: debuggerStatement, breakpoint, exception, tab_switch, toolbox_show, initial_panel, toggle_settings_off, toggle_settings_on, key_shortcut, select_next_key, select_prev_key, tool_unloaded, inspect_dom, toolbox_closed, unknown etc.
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ filters_changed:
+ objects: ["netmonitor", "webconsole"]
+ bug_numbers: [1463144, 1463095]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User has changed filters in the web console.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ trigger: "The cause of the filter change: error, warn, log, info, debug, css, netxhr, net, text or reset and all, html, css, js, xhr, fonts, images, media, ws or other for netmonitor"
+ active: Comma separated list of active filters.
+ inactive: Comma separated list of inactive filters.
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ inspect:
+ objects: ["aboutdebugging"]
+ bug_numbers: [1504173]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User has clicked on the inspect button of one of the debug targets of aboutdebugging.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ runtime_type: The runtime type
+ target_type: The target type
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ jump_to_definition:
+ objects: ["webconsole"]
+ bug_numbers: [1463101]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User has clicked "Jump to definition" icon (next to logged functions) in the web console.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ jump_to_source:
+ objects: ["webconsole"]
+ bug_numbers: [1463092]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User has clicked a link to a source file in the web console.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ object_expanded:
+ objects: ["webconsole"]
+ bug_numbers: [1463104]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User has expanded an object in the web console.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ open:
+ objects: ["tools"]
+ bug_numbers: [1416024, 1456984]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User opens devtools toolbox.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ entrypoint: How was the toolbox opened? CommandLine, ContextMenu, HamburgerMenu, KeyShortcut, SessionRestore, SystemMenu or SlowScript
+ first_panel: The name of the first panel opened.
+ host: "Toolbox host (positioning): bottom, side, window or other."
+ splitconsole: Indicates whether the split console was open.
+ width: Toolbox width rounded up to the nearest 50px.
+ shortcut: The key combination pressed. Used only in the case that entrypoint === KeyShortcut.
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ open_adbg:
+ objects: ["aboutdebugging"]
+ bug_numbers: [1504173]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User opens about:debugging.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ width: Toolbox width rounded up to the nearest 50px.
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ pause_on_exceptions:
+ objects: ["debugger"]
+ bug_numbers: [1463117]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User has changed pausing behaviour in the debugger.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ exceptions: Pause on exceptions is checked.
+ caught_exceptio: Pause on caught exceptions is checked.
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ pause:
+ objects: ["debugger"]
+ bug_numbers: [1463118]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: Debugger has paused in a script due to a breakpoint or exception.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ reason: caught-exception, uncaught-exception, pausing, debugger-statement or breakpoint.
+ lib_stacks: Number of collapsed callstacks in the call tree. These are call stacks that are part of external libraries e.g. react, which are collapsed by default.
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ persist_changed:
+ objects: ["netmonitor", "webconsole"]
+ bug_numbers: [1531395, 1542312]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User has changed log persist status.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ pretty_print:
+ objects: ["debugger"]
+ bug_numbers: [1463125]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User clicked the pretty print button to pretty print a script.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ remove_breakpoint:
+ objects: ["debugger"]
+ bug_numbers: [1463124]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User has removed a breakpoint from a script.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ runtime_added:
+ objects: ["aboutdebugging"]
+ bug_numbers: [1521507]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: A new remote runtime has been detected in about:debugging
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ connection_type: Connection type
+ device_name: Name of the device on which the runtime is running (optional)
+ runtime_id: Random id generated to track events related to a single runtime
+ runtime_name: Name of the runtime
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ runtime_connected:
+ objects: ["aboutdebugging"]
+ bug_numbers: [1521507, 1530997]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: Connection was established with a remote runtime in about:debugging
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ connection_type: Connection type
+ device_name: Name of the device on which the runtime is running (optional)
+ runtime_id: Random id generated to track events related to a single runtime
+ runtime_name: Name of the runtime
+ runtime_os: Operating system on which the runtime is running (eg Android or Linux)
+ runtime_version: Version of the runtime (eg 67.0a1)
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ runtime_disconnected:
+ objects: ["aboutdebugging"]
+ bug_numbers: [1521507]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: Connection was lost with a remote runtime in about debugging
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ connection_type: Connection type
+ device_name: Name of the device on which the runtime is running (optional)
+ runtime_id: Random id generated to track events related to a single runtime
+ runtime_name: Name of the runtime
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ runtime_removed:
+ objects: ["aboutdebugging"]
+ bug_numbers: [1521507]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: A previously listed runtime was removed in about:debugging
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ connection_type: Connection type
+ device_name: Name of the device on which the runtime is running (optional)
+ runtime_id: Random id generated to track events related to a single runtime
+ runtime_name: Name of the runtime
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ select_page:
+ objects: ["aboutdebugging", "application"]
+ bug_numbers: [1504173, 1643253]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User navigates to a new page of an application such as about:debugging
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ page_type: Type of page the user navigates to (this-firefox, connect, runtime)
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ show_profiler:
+ objects: ["aboutdebugging"]
+ bug_numbers: [1521511]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User has clicked on the "Open Profiler" button in a runtime page of about:debugging
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ runtime_id: Random id generated to track events related to a single runtime
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ select_ws_frame:
+ objects: ["netmonitor"]
+ bug_numbers: [1555638]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User has selected a WebSocket frame.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ sidepanel_changed:
+ objects: ["inspector", "netmonitor"]
+ bug_numbers: [1463083, 1463169]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User has switched sidepanel tabs.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ oldpanel: The panel the user is switching from
+ newpanel: The panel the user is switching to
+ os: The OS name and version e.g. "Linux 4.4.0-1014-aws", "Darwin 14.5.0", "Windows_NT 6.1.7601" or "Windows_NT 10.0.15063." This can be used to make sense of data when a feature is only available from a particular operating system build number.
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ start_worker:
+ objects: ["application"]
+ bug_numbers: [1643253]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ description: User manually starts a service worker
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ throttle_changed:
+ objects: ["netmonitor"]
+ bug_numbers: [1463147]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User has changed the throttle setting in the netmonitor.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ mode: No throttling, GPRS, Regular 2G, Good 2G, Regular 3G, Good 3G, Regular 4G / LTE, DSL, WI-FI, or Offline.
+ session_id: The toolbox session start time e.g. 13963.
+ tool_timer:
+ objects: ["animationinspector", "compatibilityview", "computedview", "changesview", "fontinspector", "layoutview", "ruleview"]
+ bug_numbers: [1483817, 1639454]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: The amount of time a tool was opened for.
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ time_open: Time open.
+ os: The OS name and version e.g. "Linux 4.4.0-1014-aws", "Darwin 14.5.0", "Windows_NT 6.1.7601" or "Windows_NT 10.0.15063." This can be used to make sense of data when a feature is only available from a particular operating system build number.
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ unregister_worker:
+ objects: ["application"]
+ bug_numbers: [1643253]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ description: User manually unregisters a service worker registration
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+ update_conn_prompt:
+ objects: ["aboutdebugging"]
+ bug_numbers: [1521511]
+ notification_emails: ["dev-developer-tools@lists.mozilla.org"]
+ products:
+ - "firefox"
+ - "fennec"
+ record_in_processes: ["main"]
+ description: User has clicked on the "Enable/Disable connection prompt" button in a runtime page of about:debugging
+ release_channel_collection: opt-out
+ expiry_version: never
+ extra_keys:
+ prompt_enabled: True if the user enables the prompt, false otherwise.
+ runtime_id: Random id generated to track events related to a single runtime
+ session_id: The start time of the session in milliseconds since epoch (Unix Timestamp) e.g. 1396381378123.
+
+security.ui.protections:
+ show:
+ objects: [
+ "protection_report",
+ "vpn_banner",
+ ]
+ bug_numbers:
+ - 1557050
+ - 1610897
+ - 1643428
+ - 1650468
+ - 1661756
+ - 1678201
+ - 1739287
+ - 1787249
+ description: >
+ User arrived on the protection report. This also includes a 'value' attribute which defaults to 'direct' or will be the value that a referring website addds to the url. This also indicates if the vpn banner has been seen.
+ expiry_version: never
+ record_in_processes: ["content"]
+ release_channel_collection: opt-out
+ notification_emails:
+ - pbz@mozilla.com
+ - seceng-telemetry@mozilla.com
+ products:
+ - firefox
+ extra_keys:
+ category: The category of protections the user is in, standard, strict or custom.
+ close:
+ objects: [
+ "protection_report",
+ ]
+ bug_numbers:
+ - 1557050
+ - 1610897
+ - 1612091
+ - 1643428
+ - 1678201
+ - 1739287
+ - 1787249
+ description: >
+ User closed on the protection report.
+ expiry_version: never
+ record_in_processes: ["content"]
+ release_channel_collection: opt-out
+ notification_emails:
+ - pbz@mozilla.com
+ - seceng-telemetry@mozilla.com
+ products:
+ - firefox
+ extra_keys:
+ category: The category of protections the user is in, standard, strict or custom.
+ click:
+ bug_numbers:
+ - 1557050
+ - 1572825
+ - 1610897
+ - 1612088
+ - 1612091
+ - 1637615
+ - 1643428
+ - 1661756
+ - 1678201
+ - 1739287
+ - 1787249
+ description: >
+ User interaction by click events on the protection report.
+ objects: [
+ "lw_open_button",
+ "lw_sync_link",
+ "lw_about_link",
+ "mtr_about_link",
+ "mtr_report_link",
+ "mtr_signup_button",
+ "trackers_about_link",
+ "mobile_app_link",
+ "settings_link",
+ "vpn_banner_link",
+ "vpn_banner_close",
+ "vpn_card_link",
+ "vpn_app_link_android",
+ "vpn_app_link_ios",
+ ]
+ expiry_version: never
+ record_in_processes: ["content"]
+ release_channel_collection: opt-out
+ notification_emails:
+ - pbz@mozilla.com
+ - seceng-telemetry@mozilla.com
+ products:
+ - firefox
+ extra_keys:
+ category: The category of protections the user is in, standard, strict or custom.
+
+security.ui.app_menu:
+ click:
+ bug_numbers:
+ - 1603545
+ - 1616229
+ - 1643428
+ - 1678201
+ - 1739287
+ - 1787249
+ description: >
+ Privacy and Security click events on app menu.
+ objects: [
+ "open_full_report",
+ ]
+ expiry_version: never
+ record_in_processes:
+ - main
+ release_channel_collection: opt-out
+ notification_emails:
+ - pbz@mozilla.com
+ - seceng-telemetry@mozilla.com
+ products:
+ - firefox
+
+security.ui.protectionspopup:
+ open:
+ objects: ["protections_popup", "protectionspopup_cfr",]
+ extra_keys:
+ message: >
+ For protectionspopup_cfr, the message ID.
+ bug_numbers:
+ - 1560327
+ - 1607488
+ - 1643428
+ - 1678201
+ - 1739287
+ - 1787249
+ description: >
+ How many times the protections panel was opened.
+ expiry_version: never
+ notification_emails:
+ - pbz@mozilla.com
+ - seceng-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ record_in_processes:
+ - main
+ products:
+ - firefox
+ click:
+ objects: [
+ "etp_toggle_on",
+ "etp_toggle_off",
+ "sitenotworking_link",
+ "send_report_link",
+ "send_report_submit",
+ "social",
+ "cookies",
+ "trackers",
+ "fingerprinters",
+ "cryptominers",
+ "subview_settings",
+ "settings",
+ "full_report",
+ "milestone_message",
+ "cookieb_toggle_on",
+ "cookieb_toggle_off",
+ "protectionspopup_cfr",
+ ]
+ extra_keys:
+ message: >
+ For protectionspopup_cfr, the message ID.
+ bug_numbers:
+ - 1560327
+ - 1602015
+ - 1607488
+ - 1643428
+ - 1678201
+ - 1739287
+ - 1787249
+ - 1798669
+ description: >
+ User interaction by click events in the protections panel.
+ expiry_version: never
+ notification_emails:
+ - pbz@mozilla.com
+ - tihuang@mozilla.com
+ - seceng-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ record_in_processes:
+ - main
+ products:
+ - firefox
+
+privacy.ui.fpp:
+ click:
+ objects: [
+ "checkbox",
+ "menu",
+ ]
+ bug_numbers:
+ - 1841097
+ description: >
+ User interaction by click events on fingerprinting protection checkbox in
+ the ETP Custom subpanel.
+ expiry_version: never
+ notification_emails:
+ - tom@mozilla.com
+ - tschuster@mozilla.com
+ - tihuang@mozilla.com
+ - seceng-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ record_in_processes:
+ - all
+ products:
+ - firefox
+
+uptake.remotecontent.result:
+ uptake:
+ description: >
+ Was the remote content successfully pulled?
+ This uptake telemetry allows to monitor the behaviour of our clients when it comes
+ to fetching data from remote servers. This helps defect-detection and allow observation of
+ the proportion of success among clients and sources, the distribution of error causes, and
+ its evolution over time.
+ methods:
+ - uptake
+ objects:
+ - remotesettings
+ - normandy
+ extra_keys:
+ source: >
+ A label to distinguish what is being pulled or updated in the component (eg. recipe id,
+ settings collection name, ...).
+ trigger: >
+ A label to distinguish what triggered the polling/fetching of remote content (eg. "broadcast",
+ "timer", "forced", "manual")
+ age: >
+ The age of pulled data in seconds (ie. difference between publication time and fetch time).
+ duration: >
+ The duration of the synchronization process in milliseconds.
+ timestamp: >
+ The current timestamp, received during synchronization.
+ errorName: >
+ An optional string with the error name attribute in case of failure.
+ bug_numbers:
+ - 1517469
+ - 1617133
+ products:
+ - "firefox"
+ - "fennec"
+ - "thunderbird"
+ record_in_processes: ["main"]
+ release_channel_collection: opt-out
+ expiry_version: never
+ notification_emails:
+ - mleplatre@mozilla.com
+ - bens-directs@mozilla.com
+
+intl.ui.browserLanguage:
+ action:
+ description: >
+ User interactions for the browser language within about-preferences in the main pane and in
+ the browser language dialog. Each dialog event (on the dialog object, and the manage and
+ search methods of the main object) has a value which is a monotonically increasing number
+ that links it with other events related to the same dialog instance.
+ objects:
+ - dialog
+ - main
+ methods:
+ - manage
+ - search
+ - add
+ - remove
+ - reorder
+ - apply
+ - accept
+ - cancel
+ extra_keys:
+ installId: The id for an install.
+ products:
+ - firefox
+ expiry_version: "135"
+ notification_emails:
+ - flod@mozilla.com
+ - mstriemer@mozilla.com
+ release_channel_collection: opt-out
+ record_in_processes: ["main"]
+ bug_numbers:
+ - 1486507
+ - 1553311
+ - 1607501
+ - 1672571
+ - 1739288
+ - 1796396
+ - 1861299
+
+network.dns:
+ trrConfirmation:
+ objects: ["context"]
+ bug_numbers:
+ - 1691408
+ - 1694949
+ description: >
+ This telemetry records the status of the TRR confirmation across.
+ The value of the event is one of:
+ "ok"
+ "trying"
+ notification_emails:
+ - vgosu@mozilla.com
+ - necko@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - firefox
+ expiry_version: "never"
+ record_in_processes: ["main", "socket"]
+ extra_keys:
+ trigger: >
+ The reason why this confirmation was triggered.
+ Values: [cp-connectivity, pref-change, context-init, retry, failed-lookups]
+ contextReason: >
+ The cause of the current confirmation context.
+ Values: [success, pref-change, network-change, shutdown]
+ attemptCount: >
+ The number of NS requests that were issued for this confirmation.
+ results: >
+ String representation of the last 32 confirmation results.
+ Example: nnnnnnttttttttt indicates a number of network (n) failures
+ followed by timeouts (t).
+ failedLookups: >
+ When the trigger is failed-lookups, this contains the string
+ representation of the failures that triggered the confirmation.
+ networkID: >
+ The network ID for the recorded confirmation attempts
+ captivePortal: >
+ One of [unknown, not_captive, unlocked, locked]
+ time: >
+ Time from first confirmation attempt to event being recorded
+
+security:
+ prefUsage:
+ objects: ["contentProcess"]
+ bug_numbers:
+ - 1782544
+ - 1708798
+ description: >
+ We block certain preferences from being sent to the content process because they contain sensitive
+ user data. Some preferences are blocked by name, others by heuristic. If one of the preferences so
+ blocked is accidently accessed we want to know about it so we can allowlist it or refactor the access.
+ notification_emails:
+ - tom@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - firefox
+ expiry_version: "never"
+ record_in_processes:
+ - all_children
+ fissionPrincipals:
+ objects: ["contentParent"]
+ bug_numbers:
+ - 1687891
+ description: >
+ When running with Fission, we received an unexpected principal from the content process.
+ Expected values for this telemetry event are the method names in ContentParent that
+ received the unexpected principal.
+ notification_emails:
+ - tom@mozilla.com
+ - ckerschb@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - firefox
+ expiry_version: "never"
+ record_in_processes:
+ - main
+ extra_keys:
+ principalType: >
+ The type of principal that was received. Expected values for this field are
+ NullPtr
+ SystemPrincipal
+ ExpandedPrincipal
+ ContentPrincipal
+ Unknown
+ scheme: If the principal is a ContentPrincipal, we also collect the scheme
+ evalUsage:
+ objects: ["systemContext", "parentProcess"]
+ bug_numbers:
+ - 1567623
+ description: >
+ eval() (or an eval()-like method) was called while running in the System Principal
+ context or the Parent Process.
+ Expected values are:
+ chromeuri - chrome:// file
+ resourceuri - resource:// file
+ datauri - a data URI
+ bloburi - a blob URI
+ abouturi - an about URI
+ singlestring - A single file or string with no slashes
+ mozillaextension - An extension claiming to be from *mozilla.org (Deprecated)
+ otherextension - Another extension not from Mozilla (Deprecated)
+ mozillaextension_file - An extension claiming to be from *mozilla.org, loaded from a file://
+ otherextension_file - Another extension not from Mozilla, loaded from a file://
+ extension_uri - A URI with the moz-extension:// scheme
+ suspectedUserChromeJS - A filepath ending in .uc.js
+ sanitizedWindowsPath - A filepath, on Windows, sanitized by WinUtils::PreparePathForTelemetry
+ which is a bare filename or a subpath of %ProgramFiles%, %SystemRoot%,
+ or %TEMP%
+ sanitizedWindowsURL - A partial URL, on Windows, consisting of either file://../ followed by
+ the value prepared as for sanitizedWindowsPath, or the bare scheme of
+ the original url
+ other - Unknown
+ other-on-worker - We cannot do a regex; it is not a chrome, resource, data, or blob uri, but
+ could be any other.
+ regexfailure - Our Regex Matching code threw an error
+ The fileinfo key may contain additional information about the file that caused the eval()
+ depending on the above value. Resource, Chrome, About, and SingleString will contain the full value.
+ (About URIs will remove any querystring values.)
+ Extensions-from-file will contain the full value; however .xpi! will be shortened to !,
+ shield.mozilla.org! to s! and mozilla.org! to m!. Data, Blob, UserChromeJS, Other, and
+ Regexfailure should have no value.
+ notification_emails:
+ - tom@mozilla.com
+ - ckerschb@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - firefox
+ expiry_version: "never"
+ record_in_processes:
+ - all
+ extra_keys:
+ fileinfo: Information about the file that triggered eval
+ javascriptLoad:
+ objects: ["parentProcess"]
+ bug_numbers:
+ - 1582512
+ description: >
+ The javascript engine requested to load a filename that was not allowed.
+ Expected values, and fileinfo key, are the same possible values as above in 'evalUsage'
+ notification_emails:
+ - tom@mozilla.com
+ - gijs@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - firefox
+ expiry_version: "never"
+ record_in_processes:
+ - main
+ extra_keys:
+ fileinfo: Information about the filename that was requested to be loaded
+ unexpectedload:
+ objects: ["systemprincipal"]
+ bug_numbers:
+ - 1644671
+ description: >
+ Long term want to block all loads from the system principal that cause us to parse non-trivial data.
+ We don't look to break existing functionality and will allow exceptions, for this we are collecting
+ information about unexpected requests triggered by the system principal
+ Our measurement *excludes* acceptable requests. Those are:
+ - annotated using the 'AllowedDeprecatedSystemRequests' loadinfo flag
+ - using a nsContentPolicyType of FETCH, XMLHTTPREQUEST, WEBSOCKET, SAVEAS_DOWNLOAD or IMAGE
+ - with a requested URI object has the flag URI_IS_UI_RESOURCE
+ - if the URL scheme is 'view-source'
+ - if the URL scheme is 'file' and the Content-Type is STYLESHEET or OTHER
+ - if the scheme is 'jar', 'about' or 'moz-extension'
+ notification_emails:
+ - fbraun@mozilla.com
+ - ckerschb@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - firefox
+ expiry_version: "never"
+ record_in_processes:
+ - all
+ extra_keys:
+ contenttype: Information about the content-type we expect to receive
+ remotetype: Information about the remoteType in which we triggered the load
+ filedetails: Information about the file that triggered the load
+ redirects: Comma-separated string listing schemes for URLs that we redirected from
+
+pictureinpicture:
+ create:
+ objects: ["player"]
+ description: >
+ Recorded when the Picture-in-Picture player window is created.
+ extra_keys:
+ width: The width that the window was created at
+ height: The height that the window was created at
+ screenX: The screen X coordinate that the window was created at
+ screenY: The screen Y coordinate that the window was created at
+ ccEnabled: Are subtitles enabled for PiP
+ webVTTSubtitles: Are the subtitles WebVTT
+ notification_emails:
+ - mconley@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1560590
+ - 1678202
+ - 1730039
+ - 1756703
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ resize:
+ objects: ["player"]
+ description: >
+ Recorded when the Picture-in-Picture player window is resized.
+ extra_keys:
+ width: The width that the window was resized to
+ height: The height that the window was resize to
+ notification_emails:
+ - mconley@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1560590
+ - 1678202
+ - 1730039
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ saw_toggle:
+ objects: ["toggle"]
+ description: >
+ Recorded when the Picture-in-Picture toggle is shown
+ extra_keys:
+ firstTime: If the user has used Picture-in-Picture before
+ notification_emails:
+ - mconley@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes: ["main", "content"]
+ bug_numbers:
+ - 1749585
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ opened_method:
+ objects: ["toggle", "contextMenu", "urlBar", "shortcut"]
+ description: >
+ Records the method for opening the Picture-in-Picture window.
+ extra_keys:
+ firstTimeToggle: If the user has used Picture-in-Picture before
+ disableDialog: True when urlbar button opens the disable PiP dialog
+ callout: >
+ Boolean. True if PiP is being shown for the first time within 48 hours
+ of a feature callout message highlighting the urlbar toggle.
+ notification_emails:
+ - mconley@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes: ["main", "content"]
+ bug_numbers:
+ - 1749585
+ - 1834554
+ - 1858471
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ closed_method:
+ objects: ["closeButton", "unpip", "pagehide", "fullscreen", "setupFailure", "closePlayerShortcut", "contextMenu", "videoElRemove", "videoElEmptied", "urlBar", "shortcut"]
+ description: >
+ Records the method for closing the Picture-in-Picture window.
+ notification_emails:
+ - mconley@mozilla.com
+ - mtigley@mozilla.com
+ - mhowell@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - "main"
+ bug_numbers:
+ - 1756703
+ - 1834554
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ subtitles_shown:
+ objects: ["subtitles"]
+ description: >
+ Records if subtitles are shown in the Picture-in-Picture window.
+ extra_keys:
+ webVTTSubtitles: Will be true or false
+ notification_emails:
+ - mconley@mozilla.com
+ - mhowell@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - "content"
+ bug_numbers:
+ - 1772546
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ fullscreen:
+ objects: ["player"]
+ extra_keys:
+ enter: true if entering fullscreen, false if exiting fullscreen
+ description: >
+ Recorded when the user clicks the fullscreen button
+ notification_emails:
+ - mconley@mozilla.com
+ - mhowell@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - "main"
+ bug_numbers:
+ - 1822395
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ disrespect_disable:
+ objects: ["urlBar"]
+ description: >
+ Recorded when the user chooses to enable PiP anyway on a PiP disabled video
+ notification_emails:
+ - mconley@mozilla.com
+ - mhowell@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - "main"
+ bug_numbers:
+ - 1834554
+ expiry_version: "never"
+ release_channel_collection: opt-out
+
+pictureinpicture.settings:
+ enable:
+ objects: ["settings"]
+ description: Recorded when Picture-in-Picture is enabled.
+ notification_emails:
+ - mconley@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1639774
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ disable:
+ objects: ["player", "settings"]
+ description: >
+ Recorded with "settings" object when Picture-in-Picture is disabled via settings
+ Recorded with "player" object when Picture-in-Picture is disabled via PiP context menu
+ notification_emails:
+ - mconley@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1639774
+ expiry_version: "never"
+ release_channel_collection: opt-out
+
+launch_on_login:
+ last_profile_disable:
+ objects: ["startup"]
+ description: >
+ Recorded when Launch on login is disabled because the
+ start with last profile setting has been disabled.
+ notification_emails:
+ - nalexander@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1858223
+ expiry_version: "never"
+ release_channel_collection: opt-out
+
+doh:
+ evaluate_v2:
+ methods: ["evaluate_v2"]
+ objects: ["heuristics"]
+ bug_numbers:
+ - 1573840
+ - 1631609
+ - 1603779
+ - 1654714
+ description: >
+ Results of DoH heuristics at startup and after network changes.
+ expiry_version: never
+ record_in_processes:
+ - main
+ release_channel_collection: opt-out
+ notification_emails:
+ - nhnt11@mozilla.com
+ - ddamjanovic@mozilla.com
+ - seceng-telemetry@mozilla.com
+ - necko@mozilla.com
+ products:
+ - firefox
+ extra_keys:
+ canaries: Comma-separated list of canaries that were detected (canary,zscalerCanary)
+ filtering: Comma-separated list of detected parental controls/content filtering (browserParent,google,youtube)
+ enterprise: Comma-separated list of detected enterprise config (policy,modifiedRoots,thirdPartyRoots)
+ steeredProvider: Whether we detected a steering provider
+ evaluateReason: The reason for running heuristics - startup or netchange
+ networkID: An ID representing the network on which heuristics were run, unique to this client
+ captiveState: The Captive Portal Service state - unknown, not_captive, locked, or unlocked
+ platform: Comma-separated list of platform attributes that would cause split-horizon issues (vpn, proxy, nrpt)
+ evaluate:
+ methods: ["evaluate"]
+ objects: ["heuristics"]
+ bug_numbers:
+ - 1573840
+ - 1631609
+ - 1603779
+ description: >
+ Results of DoH heuristics at startup and after network changes. OLD FORMAT: here for documentation
+ expiry_version: never
+ record_in_processes:
+ - main
+ release_channel_collection: opt-out
+ notification_emails:
+ - nhnt11@mozilla.com
+ - ddamjanovic@mozilla.com
+ - seceng-telemetry@mozilla.com
+ - necko@mozilla.com
+ products:
+ - firefox
+ extra_keys:
+ google: Google safe search result
+ youtube: YouTube safe search result
+ zscalerCanary: ZScaler canary result
+ canary: Global canary result
+ modifiedRoots: Whether enterprise roots were enabled
+ browserParent: Whether OS parental controls were detected
+ thirdPartyRoots: Whether third party roots were installed
+ policy: Enterprise policy presence - no policy/with DoH/without DoH.
+ steeredProvider: Whether we detected a steering provider
+ evaluateReason: The reason for running heuristics - startup or netchange
+ state:
+ methods: ["state"]
+ objects: [
+ "enabled",
+ "disabled",
+ "manuallyDisabled",
+ "policyDisabled",
+ "uninstalled",
+ "UIOk",
+ "UIDisabled",
+ "rollback",
+ "shutdown",
+ ]
+ bug_numbers:
+ - 1573840
+ - 1631609
+ - 1603779
+ description: >
+ Results of DoH heuristics at startup and after network changes.
+ expiry_version: never
+ record_in_processes:
+ - main
+ release_channel_collection: opt-out
+ notification_emails:
+ - nhnt11@mozilla.com
+ - ddamjanovic@mozilla.com
+ - seceng-telemetry@mozilla.com
+ - necko@mozilla.com
+ products:
+ - firefox
+
+security.doh.trrPerformance:
+ resolved:
+ objects: ["record"]
+ bug_numbers:
+ - 1613790
+ description: >
+ How long it took to resolve a test domain using TRR.
+ expiry_version: never
+ record_in_processes:
+ - main
+ release_channel_collection: opt-out
+ notification_emails:
+ - nhnt11@mozilla.com
+ - ddamjanovic@mozilla.com
+ - seceng-telemetry@mozilla.com
+ products:
+ - firefox
+ extra_keys:
+ domain: The resolved domain.
+ trr: The TRR provider used.
+ time: The network time for the resolution.
+ status: The DNS status code.
+ retryCount: The number of lookup attempts before success.
+ networkUnstable: Whether there was network fluctuation while gathering the results.
+ captivePortal: Whether there a captive portal was detected during the run.
+ trrselect:
+ objects: ["dryrunresult"]
+ bug_numbers:
+ - 1631822
+ description: >
+ The URL of the DoH provider chosen by the TRR selection dry-run
+ expiry_version: never
+ record_in_processes:
+ - main
+ release_channel_collection: opt-out
+ notification_emails:
+ - nhnt11@mozilla.com
+ - ddamjanovic@mozilla.com
+ - seceng-telemetry@mozilla.com
+ products:
+ - firefox
+
+security.doh.neterror:
+ load:
+ objects: ["dohwarning"]
+ bug_numbers:
+ - 1829342
+ description: >
+ The DoH warning page is loaded.
+ expiry_version: never
+ notification_emails:
+ - necko@mozilla.com
+ - kershaw@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ record_in_processes: ["content"]
+ extra_keys:
+ mode: Current TRR mode
+ provider_key: TRR provider
+ skip_reason: The reason why DoH request is failed
+ click:
+ objects: [
+ "try_again_button",
+ "add_exception_button",
+ "settings_button",
+ "continue_button",
+ "disable_warning",
+ "learn_more_link",
+ ]
+ bug_numbers:
+ - 1829342
+ description: >
+ User interaction by click buttons on the DoH warning page.
+ expiry_version: never
+ notification_emails:
+ - necko@mozilla.com
+ - kershaw@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ record_in_processes: ["content"]
+ extra_keys:
+ mode: Current TRR mode
+ provider_key: TRR provider
+ skip_reason: The reason why DoH request is failed
+
+security.doh.settings:
+ mode_changed:
+ objects: ["button"]
+ bug_numbers:
+ - 1829905
+ description: >
+ User changed the TRR mode in about:preferences#privacy settings
+ Value is id of the clicked button.
+ expiry_version: never
+ record_in_processes:
+ - main
+ release_channel_collection: opt-out
+ notification_emails:
+ - vgosu@mozilla.com
+ - necko@mozilla.com
+ products:
+ - firefox
+ warn_checkbox:
+ objects: ["checkbox"]
+ bug_numbers:
+ - 1829905
+ description: >
+ User clicked the Warn if a third party actively prevents secure DNS
+ checkbox in about:preferences#privacy.
+ Value is true or false, reflecting if box is checked or unchecked.
+ expiry_version: never
+ record_in_processes:
+ - main
+ release_channel_collection: opt-out
+ notification_emails:
+ - vgosu@mozilla.com
+ - necko@mozilla.com
+ products:
+ - firefox
+ provider_choice:
+ objects: ["value"]
+ bug_numbers:
+ - 1829905
+ description: >
+ User changed their DoH provider. Recorded value is URL of one of the
+ existing providers or "custom".
+ expiry_version: never
+ record_in_processes:
+ - main
+ release_channel_collection: opt-out
+ notification_emails:
+ - vgosu@mozilla.com
+ - necko@mozilla.com
+ products:
+ - firefox
+
+security.ui.certerror:
+ load:
+ objects: ["aboutcerterror"]
+ bug_numbers:
+ - 1484255
+ - 1505310
+ - 1553181
+ - 1629826
+ description: >
+ The about:certerror page is loaded, keyed by error code, see https://searchfox.org/mozilla-central/source/security/nss/lib/mozpkix/include/pkix/Result.h
+ expiry_version: never
+ notification_emails:
+ - jhofmann@mozilla.com
+ - rtestard@mozilla.com
+ - seceng-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ record_in_processes: ["content"]
+ extra_keys:
+ is_frame: If the error page is loaded in an iframe.
+ has_sts: If the error page is for a site with HSTS headers or with a pinned key.
+ click:
+ objects: [
+ "advanced_button",
+ "exception_button",
+ "return_button_top",
+ "return_button_adv",
+ "learn_more_link",
+ "auto_report_cb",
+ "error_code_link",
+ "clipboard_button_top",
+ "clipboard_button_bot",
+ ]
+ bug_numbers:
+ - 1484255
+ - 1505310
+ - 1553181
+ - 1629826
+ description: >
+ User interaction by click events on the cert error page. Keyed by error code, see https://searchfox.org/mozilla-central/source/security/nss/lib/mozpkix/include/pkix/Result.h
+ expiry_version: never
+ notification_emails:
+ - jhofmann@mozilla.com
+ - rtestard@mozilla.com
+ - seceng-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ record_in_processes: ["content"]
+ extra_keys:
+ is_frame: If the error page is loaded in an iframe.
+ has_sts: If the error page is for a site with HSTS headers or with a pinned key.
+ panel_open: If the advanced panel was open at the time of the interaction.
+
+slow_script_warning:
+ shown:
+ bug_numbers:
+ - 1652613
+ - 1667245
+ - 1672572
+ description: >
+ Recorded when a slow script hang is resolved.
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ release_channel_collection: opt-out
+ expiry_version: "never"
+ notification_emails:
+ - dothayer@mozilla.com
+ - esmyth@mozilla.com
+ - gkruitbosch@mozilla.com
+ - perf-telemetry-alerts@mozilla.com
+ # Whether the hung script was for a content or browser process.
+ objects: [
+ "browser",
+ "content",
+ ]
+ extra_keys:
+ end_reason: Why the warning was hidden (user action, the process becoming responsive again, the browser quitting, etc.)
+ wait_count: How many times the user elected to wait.
+ hang_duration: How long we believe the hang continued (ms).
+ n_tab_deselect: How many times the user switched away from a tab affected by this hang.
+ uri_type: The kind of script URL that hung.
+ uptime: How long the notification was up (ms).
+
+webrtc.ui:
+ share_display:
+ objects:
+ - screen
+ - window
+ - browser_window
+ description: >
+ Recorded when a display is shared. The value for this event is a unique
+ ID that differentiates different sharing sessions. A new sharing session
+ is created when the user transitions from not sharing a display to
+ sharing at least one display.
+ extra_keys:
+ silence_notifs: True if the user opted in to silencing DOM notifications.
+ notification_emails:
+ - mconley@mozilla.com
+ - vchin@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1639283
+ expiry_version: "88"
+ release_channel_collection: opt-out
+
+ysod:
+ shown:
+ bug_numbers:
+ - 1657242
+ description: >-
+ This event is recorded only for Firefox UI documents (chrome).
+ Recorded when a Yellow Screen of Death is shown. The `value` is a url on which the error happened.
+ Yellow Screen of Death happens when XML or XHTML document encounters a parsing error. It is commonly a result
+ of the document being malformed or missing DTD entity.
+ Mozilla has a number of documents that rely on DTD entities for localization, and in some error scenarios those
+ entities may be missing or malformed themselves, in which case the user will see a broken UI.
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ release_channel_collection: opt-out
+ expiry_version: "never"
+ notification_emails:
+ - zbraniecki@mozilla.com
+ - vchin@mozilla.com
+ objects:
+ - "ysod"
+ extra_keys:
+ error_code: Code of the XML Error (See `XML_Error` enum in expat.h for the list of errors where XML_ERROR_NONE is 0).
+ location: Location as Row:Column of where the error happened.
+ last_line: The content of the last line that led to the error.
+ last_line_len: The length of the last line that led to the error.
+ hidden: The document is hidden.
+ destroyed: The container of this document is destroyed.
+
+dom.quota.try:
+ error:
+ objects: [
+ "step",
+ ]
+ bug_numbers:
+ - 1665088
+ description: >
+ An event recorded on an error of the quota manager or its clients. Since errors are usually
+ propagated up the call chain, all such propagation steps are recorded for an error. This
+ is only active during specific contexts, in particular during storage initialization.
+ No dynamic data is included in the event beyond the error code which will be added through
+ Bug 1670555.
+ notification_emails:
+ - jvarga@mozilla.com
+ - sgiesecke@mozilla.com
+ - storage-telemetry@mozilla.com
+ products:
+ - firefox
+ expiry_version: "never"
+ record_in_processes: ["main", "content"]
+ extra_keys:
+ context: The context in which the error occurred, e.g. during a storage initialization. Telemetry events are only emitted for selected contexts.
+ frame_id: Optionally, the frame within stack_id.
+ process_id: Optionally, the process in which the error occured.
+ result: Optionally, the name of the error that occurred.
+ seq: Sequence number.
+ severity: One of WARNING or ERROR.
+ source_file: The name of the source code file where the error occurred.
+ source_line: The line within source_file where the error occurred.
+ stack_id: Optionally, the stack within process_id.
+
+zero_byte_load:
+ load:
+ bug_numbers:
+ - 1675823
+ description: >-
+ Collection of zero_byte_load events as part of the Yellow Screen of Death analysis.
+ When a file is loaded synchronously, this event gets recorded in nsJARChannel::Open if the content length is zero.
+ For asynchronous loads, this event gets recorded in onStopRequest.
+ Note that when the load has been cancelled, the consumer could already receive some data, so this event also captures non-zero-byte loads.
+ products:
+ - "firefox"
+ record_in_processes: ["main", "content"]
+ release_channel_collection: opt-out
+ expiry_version: "never"
+ notification_emails:
+ - zbraniecki@mozilla.com
+ - vchin@mozilla.com
+ - kershaw@mozilla.com
+ objects:
+ - "ftl"
+ - "dtd"
+ - "properties"
+ - "js"
+ - "xml"
+ - "xhtml"
+ - "css"
+ - "json"
+ - "html"
+ - "png"
+ - "svg"
+ - "others"
+ extra_keys:
+ sync: Was the load synchronous
+ file_name: The name of the file being loaded.
+ status: The channel status code.
+ cancelled: Whether this load has been cancelled.
+
+avif:
+ dav1d_get_picture:
+ objects: ["return_value"]
+ bug_numbers: [1690406]
+ description: Return value from dav1d_get_picture
+ products:
+ - firefox # event telemetry is not supported on fenix
+ record_in_processes: ["all"]
+ release_channel_collection: opt-out
+ expiry_version: never
+ notification_emails:
+ - media-alerts@mozilla.com
+
+mfcdm:
+ eme_playback:
+ objects: ["gecko"]
+ bug_numbers: [1873394]
+ description:
+ Record the information about the EME playback when using the media engine.
+ The value of this event is the key system name.
+ products:
+ - firefox
+ record_in_processes: ["all"]
+ release_channel_collection: opt-out
+ expiry_version: "130"
+ notification_emails:
+ - media-alerts@mozilla.com
+ extra_keys:
+ key_system: The key system used for the EME playback
+ video_codec: The video codec used for playback
+ resolution: The video resolution used for playback
+ played_time: The amount of time the EME content has been played (in seconds)
+ rendered_frames: The amount of video frames has been rendered
+ dropped_frames: The amount of video frames don't get rendered but dropped
+ error:
+ objects: ["gecko"]
+ bug_numbers: [1873394]
+ description:
+ Record the error or crash happened while using the media engine playback.
+ The value of this event is the name of error.
+ products:
+ - firefox
+ record_in_processes: ["all"]
+ release_channel_collection: opt-out
+ expiry_version: "130"
+ notification_emails:
+ - media-alerts@mozilla.com
+ extra_keys:
+ error_name: The name of the error
+ current_state: The state of the external state machine was being used when the error or crash happened
+ video_codec: The video codec was being used when the error or crash happened
+ audio_codec: The audio codec was being used when the error or crash happened
+ resolution: The video resolution was being used when the error or crash happened
+ key_system: The key system was being used when the error or crash happened
+
+installation:
+ first_seen:
+ description: >
+ Recorded after the application has been installed or reinstalled, the first time that
+ a profile sees that there was a new installation. This includes information about how
+ the installer was run.
+ objects:
+ - full # if the full installer was run directly
+ - stub # if the stub installer was used
+ - msix # if the installation was done through an MSIX package
+ release_channel_collection: opt-out
+ record_in_processes: ["main"]
+ products: ["firefox"]
+ operating_systems: ["windows"]
+ extra_keys:
+ version: The application version installed by the installer (not necessarily the current version)
+ build_id: The build ID of the application installed by the installer (not necessarily the current version)
+ admin_user: Whether the installer is running from an elevated admin user
+ install_existed: Whether there was already an install in this location
+ other_inst: Whether there was already any non-MSIX install on this system
+ other_msix_inst: Whether there was already any MSIX install on this system
+ profdir_existed: Whether the top-level profile directory existed
+ silent: '(optional, present if object is "full") Whether this was a silent install'
+ from_msi: '(optional, present if object is "full") Whether this was an MSI install'
+ default_path: '(optional, present if object is "full") Whether the default path was used'
+ bug_numbers: [1660198, 1725295, 1743465, 1754638]
+ notification_emails:
+ - application-update-telemetry-alerts@mozilla.com
+ - rtestard@mozilla.com
+ expiry_version: never
+
+contextservices.quicksuggest:
+ data_collect_toggled:
+ objects: ["enabled", "disabled"]
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ description: >
+ This is recorded when the
+ `browser.urlbar.quicksuggest.dataCollection.enabled` boolean pref is
+ toggled.
+ bug_numbers: [1735976]
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - adw@mozilla.com
+ expiry_version: never
+ enable_toggled:
+ objects: ["enabled", "disabled"]
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ description: >
+ This is recorded when the
+ `browser.urlbar.suggest.quicksuggest.nonsponsored` boolean pref is
+ toggled.
+ bug_numbers: [1693126]
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - adw@mozilla.com
+ expiry_version: never
+ sponsored_toggled:
+ objects: ["enabled", "disabled"]
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ description: >
+ This is recorded when the `browser.urlbar.suggest.quicksuggest.sponsored`
+ boolean pref is toggled.
+ bug_numbers: [1728430, 1733687]
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - adw@mozilla.com
+ expiry_version: never
+ opt_in_dialog:
+ objects: ["accept_2", "reject_2", "learn_more_1", "learn_more_2", "close_1", "not_now_2", "dismiss_1", "dismiss_2"]
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ description: >
+ This is recorded when the user responds to the Firefox Suggest opt-in
+ onboarding dialog. 'accept_2' is recorded when the user accepts the dialog
+ and opts in, 'reject_2' is recorded when the user rejects the dialog and
+ opts out, 'learn_more_1' is recorded when the user clicks "Learn more"
+ on the introduction section (the user remains opted out), 'learn_more_2' is
+ recorded when the user clicks "Learn more" on the main section (the user
+ remains opted out), 'close_1' is recorded when the user clicks close button
+ on the introduction section (the user remains opted out), 'not_now_2' is
+ recorded when the user clicks "Not now" link on main section (the user
+ remains opted out), 'dismiss_1' recorded when the user dismisses the dialog
+ on the introduction section (the user remains opted out), 'dismiss_2'
+ recorded when the user dismisses the dialog on main (the user remains opted
+ out),
+ bug_numbers: [1723860, 1745026, 1761171]
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - adw@mozilla.com
+ expiry_version: never
+ impression_cap:
+ description: >
+ This is recorded when an event related to an impression cap occurs. "hit"
+ is recorded when an impression cap is hit. "reset" is recorded when a
+ cap's counter is reset because its interval period has elapsed.
+ objects: ["hit", "reset"]
+ extra_keys:
+ type: >
+ The type of cap, one of: "sponsored", "nonsponsored"
+ intervalSeconds: >
+ The number of seconds in the cap's interval period. For lifetime caps,
+ this value will be "Infinity".
+ maxCount: >
+ The maximum number of impressions allowed in the cap's interval period.
+ startDate: >
+ The timestamp at which the cap's interval period started, in number of
+ seconds since Unix epoch.
+ count: >
+ The number of impressions in the cap's interval period.
+ impressionDate: >
+ The timestamp of the cap's most recent impression, in number of
+ milliseconds since Unix epoch.
+ eventDate: >
+ The event's timestamp, in number of milliseconds since Unix epoch. For
+ "reset" events, this is the timestamp at which the cap's interval period
+ ended. If eventCount is greater than 1, it's the timestamp at which the
+ last interval period ended. For "hit" events, this is the timestamp at
+ which the cap was hit.
+ eventCount: >
+ The number of impression cap events reported in the telemetry event.
+ This is necessary because the implementation may batch multiple
+ consecutive "reset" events for a cap in a single telemetry event. When
+ that occurs, this value will be greater than 1, startDate will be the
+ timestamp at which the first event's interval period started, eventDate
+ will be the timestamp at which the last event's interval period ended,
+ and count will be the number of impressions during the first event's
+ interval period. (The implementation guarantees that reset events are
+ batched only when the number of impressions for all subsequent interval
+ periods is zero.) For "hit" events, eventCount will always be 1.
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ bug_numbers: [1761058]
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - adw@mozilla.com
+ expiry_version: never
+ engagement:
+ description: >
+ This event is recorded when an engagement occurs in the address bar while
+ a Firefox Suggest suggestion is present. The event's objects are the
+ following possible values:
+ "block": The user dismissed ("blocked") the suggestion.
+ "click": The user picked the suggestion.
+ "help": The user picked the suggestion's help button.
+ "impression_only": The user picked some other row.
+ "other": The user engaged with the suggestion in some other way, for
+ example by picking a command in the result menu. This is a catch-all
+ category and going forward Glean telemetry should be preferred.
+ objects: ["block", "click", "help", "impression_only", "other"]
+ extra_keys:
+ match_type: >
+ "best-match" if the suggestion was a best match or "firefox-suggest" if
+ it was a non-best-match suggestion.
+ position: >
+ The index of the suggestion in the list of results (1-based).
+ suggestion_type: >
+ The type of suggestion, one of: "sponsored", "nonsponsored",
+ "dynamic-wikipedia", "weather", "navigational"
+ source: >
+ Where the suggestion came from, one of: "remote-settings", "merino"
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ bug_numbers: [1761059]
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - adw@mozilla.com
+ expiry_version: never
+
+close_tab_warning:
+ shown:
+ description: >
+ Recorded whenever we show the 'Close Tabs' dialog, with details about
+ the reason it is shown and what choices the user makes (close or not).
+ objects: ["window", "application", "tabs"]
+ release_channel_collection: opt-out
+ expiry_version: "99"
+ record_in_processes: ["main"]
+ products: ["firefox"]
+ bug_numbers: [1712306, 1725296]
+ notification_emails:
+ - gkruitbosch@mozilla.com
+ - rtestard@mozilla.com
+ extra_keys:
+ source: The way in which the tabs were closed (shortcut, close button, menuitem, appmenu, etc.)
+ button: Which button the user clicked (Close tabs or Cancel)
+ warn_checkbox: Whether the checkbox to display the warning again was checked or not.
+ closing_wins: The number of windows being closed.
+ closing_tabs: The number of tabs being closed.
+ will_restore: Whether the session will be restored if closed now.
+
+memory_watcher:
+ on_high_memory:
+ description: >
+ This event is recorded when the memory situation is no longer low.
+ The "stats" object consists of three numbers comma-delimited:
+ 1) how many times a tab was unloaded
+ 2) how many memory-pressure events were dispatched
+ 3) how long we were in the low-memory situation in seconds
+ objects: ["stats"]
+ release_channel_collection: opt-out
+ expiry_version: never
+ record_in_processes: ["main"]
+ products: ["firefox"]
+ bug_numbers: [1715858]
+ notification_emails:
+ - tkikuchi@mozilla.com
+
+service_request:
+ bypass:
+ description: >
+ This event is recorded by a small set of services when a proxy failure
+ causes a service to re-request with a proxy bypass. It records some
+ basic information such as the type of proxy configuration, and the source
+ of the proxy configuration. The value of the event is the name of the
+ service that triggers the event (e.g. telemetry, remote-settings).
+ methods: ["bypass"]
+ objects: ["proxy_info"]
+ release_channel_collection: opt-out
+ expiry_version: never
+ record_in_processes: ["main"]
+ products: ["firefox"]
+ bug_numbers: [1732792, 1732793, 1733481, 1733994, 1732388]
+ notification_emails:
+ - scaraveo@mozilla.com
+ extra_keys:
+ source: the source of the proxy configuration. e.g. policy, prefs or extension_id
+ type: the type for the proxy configuration source. e.g. api or string version of nsIProtocolProxyService.proxyConfigType
+
+synced_tabs:
+ click:
+ objects: [
+ "fxa_avatar_menu",
+ "fxa_app_menu",
+ "synced_tabs_sidebar",
+ ]
+ methods: ["click"]
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ description: Record how users access and use synced tabs component
+ bug_numbers: [1756252]
+ notification_emails: ["sync-dev@mozilla.org"]
+ expiry_version: "never"
+ extra_keys:
+ tab_pos: position of the tab clicked
+ filter: was there a filter enabled
+
+firefoxview_next:
+ recently_closed:
+ objects: ["tabs"]
+ description: >
+ Recorded when a recently closed tab is clicked
+ extra_keys:
+ delta: Time since the tab was closed
+ position: The position of the tab in the recently closed tab list (starting at 1)
+ page: The short page name where the recently closed tab was clicked
+ notification_emails:
+ - firefoxview@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1842616
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ dismiss_closed_tab:
+ objects: ["tabs"]
+ description: >
+ Recorded when a recently closed tab is dismissed
+ extra_keys:
+ delta: Time since the tab was closed to when dismissed
+ position: The position of the tab in the recently closed tab list (starting at 1)
+ page: The short page name where the recently closed tab was dismissed
+ notification_emails:
+ - firefoxview@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1842616
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ card_collapsed:
+ objects: ["card_container"]
+ description: >
+ Recorded when a card-container is collapsed on the 'Recent browsing' page
+ extra_keys:
+ data_type: The type of data being collapsed in a card on the 'Recent browsing' page
+ notification_emails:
+ - firefoxview@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1842616
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ card_expanded:
+ objects: ["card_container"]
+ description: >
+ Recorded when a card-container is expanded on the 'Recent browsing' page
+ extra_keys:
+ data_type: The type of data being expanded in a card on the 'Recent browsing' page
+ notification_emails:
+ - firefoxview@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1842616
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ change_page:
+ objects: ["navigation"]
+ description: >
+ Recorded when the page/view is changed in Firefox View
+ extra_keys:
+ page: The short page name that the user is navigating to
+ source: Whether the page was changed via the side navigation vs the 'View all' link on cards on the Recent browsing page
+ notification_emails:
+ - firefoxview@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1842616
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ context_menu:
+ objects: ["tabs"]
+ description: >
+ Recorded when a context menu selection is made in Firefox View
+ extra_keys:
+ menu_action: The menu action the user has selected for a given tab
+ data_type: The type of data the user has used the context menu for
+ notification_emails:
+ - firefoxview@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1842616
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ browser_context_menu:
+ objects: ["tabs"]
+ description: >
+ Recorded when a browser context menu selection is made in Firefox View
+ extra_keys:
+ menu_action: The menu action the user has selected for a given tab
+ page: The short page name used as the location hash when selecting the Firefox View tab
+ notification_emails:
+ - firefoxview@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1851197
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ entered:
+ objects: ["firefoxview"]
+ description: >
+ Recorded when the Firefox View tab is selected
+ extra_keys:
+ page: The short page name used as the location hash when selecting the Firefox View tab
+ notification_emails:
+ - firefoxview@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1852036
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ fxa_continue:
+ objects: ["sync"]
+ description: >
+ Recorded when button to sign in or sign up is clicked
+ notification_emails:
+ - firefoxview@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1833671
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ fxa_mobile:
+ objects: ["sync"]
+ description: >
+ Recorded when the 'Try Firefox for mobile' button is clicked
+ extra_keys:
+ has_devices: True if user has other devices signed into sync
+ notification_emails:
+ - firefoxview@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1833671
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ synced_tabs:
+ objects: ["tabs"]
+ description: >
+ Recorded when a synced tab is clicked
+ extra_keys:
+ page: The short page name where the synced tab was clicked
+ notification_emails:
+ - firefoxview@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1833671
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ history:
+ objects: ["visits"]
+ description: >
+ Recorded when a history tab is clicked
+ notification_emails:
+ - firefoxview@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1842616
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ sort_history:
+ objects: ["tabs"]
+ description: >
+ Recorded when the sort option is changed for History in Firefox View
+ extra_keys:
+ sort_type: The type of sort the user has switched to
+ search_start: Boolean for if the sort by click occured during search activity.
+ notification_emails:
+ - firefoxview@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1833674
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ show_all_history:
+ objects: ["tabs"]
+ description: >
+ Recorded when the 'Show all history' button is clicked on the History page in Firefox View
+ notification_emails:
+ - firefoxview@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1833674
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ open_tab:
+ objects: ["tabs"]
+ description: Recorded when an open tab is clicked
+ extra_keys:
+ page: The short page name where the open tab was clicked
+ window: The window the open tab belongs to
+ notification_emails:
+ - firefoxview@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1833669
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ tab_selected:
+ objects: ["toolbarbutton"]
+ description: >
+ Recorded when the Firefox View tab is clicked
+ notification_emails:
+ - firefoxview@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1852989
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ search_initiated:
+ objects: ["search"]
+ description: >
+ Recorded when search is initiated and from what page.
+ extra_keys:
+ page: The short page name used as the location hash when selecting the Firefox View tab.
+ notification_emails:
+ - firefoxview@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1869765
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ search_show_all:
+ objects: ["showallbutton"]
+ description: >
+ Recorded when the show all button is clicked to show more search results.
+ extra_keys:
+ section: Section within which show all is clicked.
+ notification_emails:
+ - firefoxview@mozilla.com
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1869765
+ expiry_version: "never"
+ release_channel_collection: opt-out
+
+search:
+ engine:
+ objects: ["change_default", "change_private"]
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ record_in_processes: ["main"]
+ description: >
+ Recorded when the default search engine is changed, the value field
+ records the action used to trigger the change of default. For possible
+ values, see `REASON_CHANGE_MAP` in
+ https://searchfox.org/mozilla-central/source/toolkit/components/search/SearchService.sys.mjs
+ bug_numbers: [1634555]
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ expiry_version: never
+ extra_keys:
+ prev_id: The id of the previous default engine.
+ new_id: The id of the new default engine.
+ new_name: The display name of the new default engine.
+ new_load_path: The path relating to where the new default engine was installed/loaded from.
+ new_sub_url: The new submission URL of the new default engine.
+
+cookie_banner:
+ reload:
+ objects: ["browser"]
+ description: >
+ Recorded when the top-level page is reloaded. We use this event metric to
+ know whether or not the reloading domain has cookie banner rule.
+ record_in_processes: ["main"]
+ products:
+ - "firefox"
+ - "fennec"
+ release_channel_collection: opt-out
+ notification_emails:
+ - pbz@mozilla.com
+ - tihuang@mozilla.com
+ bug_numbers:
+ - 1797079
+ - 1861317
+ expiry_version: "128"
+ extra_keys:
+ no_rule: There is no cookie banner rule for the reloading domain.
+ has_cookie_rule: There is a matching cookie rule for the reloading domain.
+ has_click_rule: There is a matching click rule for the reloading domain.
+
+screenshots:
+ download:
+ objects: ["overlay_download", "preview_download"]
+ description: >
+ Recorded when a user downloads a screenshot from the overlay or the preview.
+ notification_emails:
+ - screenshots-dev@mozilla.org
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1801019
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ extra_keys:
+ fullpage: Number of times a full page was selected
+ visible: Number of times a visible page was selected
+ element: Number of times an element was selected
+ region: Number of times a region was selected
+ move: Number of times a region was moved
+ resize: Number of times a region was resized
+ copy:
+ objects: ["overlay_copy", "preview_copy"]
+ description: >
+ Recorded when a user copies a screenshot from the overlay or the preview.
+ notification_emails:
+ - screenshots-dev@mozilla.org
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1801019
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ extra_keys:
+ fullpage: Number of times a full page was selected
+ visible: Number of times a visible page was selected
+ element: Number of times an element was selected
+ region: Number of times a region was selected
+ move: Number of times a region was moved
+ resize: Number of times a region was resized
+ selected:
+ objects: ["element", "region_selection", "visible", "full_page"]
+ description: >
+ Recorded when a user selects a region by hand, selects an element, selects the save visible, or selects save full page.
+ Note that this is recorded independent of whether or not the screenshot is saved.
+ notification_emails:
+ - screenshots-dev@mozilla.org
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ - content
+ bug_numbers:
+ - 1801019
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ started:
+ objects: ["toolbar_button", "shortcut", "context_menu", "quick_actions", "preview_retry", "overlay_retry"]
+ description: >
+ Recorded when a user opens the screenshots ui.
+ notification_emails:
+ - screenshots-dev@mozilla.org
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ - content
+ bug_numbers:
+ - 1801019
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ canceled:
+ objects: ["toolbar_button", "shortcut", "context_menu", "quick_actions", "preview_cancel", "overlay_cancel", "escape", "navigation"]
+ description: >
+ Recorded when a user closes the screenshots ui. The screenshots ui is closed after
+ copying/downloading but we do not record that as canceling the screenshots ui.
+ notification_emails:
+ - screenshots-dev@mozilla.org
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1801019
+ expiry_version: "never"
+ release_channel_collection: opt-out
+ failed:
+ objects: ["screenshot_too_large"]
+ description: >
+ Recorded when a screenshot is too big to capture and we have to crop the screenshot.
+ notification_emails:
+ - screenshots-dev@mozilla.org
+ products:
+ - "firefox"
+ record_in_processes:
+ - main
+ bug_numbers:
+ - 1801019
+ expiry_version: "never"
+ release_channel_collection: opt-out
diff --git a/toolkit/components/telemetry/Histograms.json b/toolkit/components/telemetry/Histograms.json
new file mode 100644
index 0000000000..5611ca930d
--- /dev/null
+++ b/toolkit/components/telemetry/Histograms.json
@@ -0,0 +1,17633 @@
+{
+ "A11Y_INSTANTIATED_FLAG": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1382820],
+ "description": "Flag indicating accessibility support has been instantiated.",
+ "alert_emails": ["accessibility@mozilla.com"]
+ },
+ "A11Y_CONSUMERS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 11,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1382820, 1462238],
+ "description": "A list of known accessibility clients that inject into Firefox process space (see https://searchfox.org/mozilla-central/source/accessible/windows/msaa/Compatibility.h).",
+ "alert_emails": ["accessibility@mozilla.com", "jteh@mozilla.com"]
+ },
+ "A11Y_ISIMPLEDOM_USAGE_FLAG": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "flag",
+ "description": "Flag indicating the ISimpleDOM* accessibility interfaces has been used.",
+ "alert_emails": ["accessibility@mozilla.com"]
+ },
+ "A11Y_IATABLE_USAGE_FLAG": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "flag",
+ "description": "Flag indicating the IAccessibleTable accessibility interface has been used.",
+ "alert_emails": ["accessibility@mozilla.com"]
+ },
+ "A11Y_UIA_DETECTION_TIMING_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "60",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 20,
+ "keyed": false,
+ "description": "The amount of time the parent process blocked while detecting a UIA client.",
+ "bug_numbers": [1423989],
+ "alert_emails": ["dbolter@mozilla.com"]
+ },
+ "A11Y_TREE_UPDATE_TIMING_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["asurkov@mozilla.com"],
+ "bug_numbers": [1424768],
+ "expires_in_version": "never",
+ "description": "The amount of time taken to update the accessibility tree (ms)",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50
+ },
+ "ABOUT_CONFIG_FEATURES_USAGE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["pamadini@mozilla.com"],
+ "expires_in_version": "67",
+ "kind": "categorical",
+ "labels": [
+ "Show",
+ "Search",
+ "RegexSearch",
+ "SortByName",
+ "SortByStatus",
+ "SortByType",
+ "SortByValue",
+ "ModifyValue",
+ "Copy",
+ "CopyName",
+ "CopyValue",
+ "CreateNew",
+ "Reset"
+ ],
+ "bug_numbers": [1493445],
+ "description": "Record number of times particular features on about:config are used"
+ },
+ "APPLICATION_REPUTATION_BINARY_TYPE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "bug_numbers": [156404],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": [
+ "BinaryFile",
+ "NonBinaryFile",
+ "MozNonBinaryFile",
+ "UnknownFile",
+ "MissingFilename"
+ ],
+ "description": "Whether or not the file examined by download protection is a binary type (or it's not possible to tell because the filename is missing)."
+ },
+ "APPLICATION_REPUTATION_BINARY_ARCHIVE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "bug_numbers": [1480639, 1531034],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": ["OtherBinaryFile", "DmgFile", "RarFile", "ZipFile"],
+ "description": "Whether a binary file examined by download protection is one of the common archive formats."
+ },
+ "APPLICATION_REPUTATION_REASON": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "bug_numbers": [1500360],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": [
+ "NotSet",
+ "LocalWhitelist",
+ "LocalBlocklist",
+ "NonBinaryFile",
+ "VerdictSafe",
+ "VerdictUnknown",
+ "VerdictDangerous",
+ "VerdictDangerousHost",
+ "VerdictUnwanted",
+ "VerdictUncommon",
+ "VerdictUnrecognized",
+ "DangerousPrefOff",
+ "DangerousHostPrefOff",
+ "UnwantedPrefOff",
+ "UncommonPrefOff",
+ "NetworkError",
+ "RemoteLookupDisabled",
+ "InternalError",
+ "DPDisabled"
+ ],
+ "description": "The reason application reputation service blocks or allows the download."
+ },
+ "APPLICATION_REPUTATION_SHOULD_BLOCK": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "boolean",
+ "description": "Overall (local or remote) application reputation verdict (shouldBlock=false is OK)."
+ },
+ "APPLICATION_REPUTATION_LOCAL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "enumerated",
+ "n_values": 3,
+ "description": "Application reputation local results (0=ALLOW, 1=BLOCK, 2=NONE)"
+ },
+ "APPLICATION_REPUTATION_SERVER": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "enumerated",
+ "n_values": 3,
+ "description": "Status of the application reputation remote lookup (0=OK, 1=failed, 2=invalid protobuf response)"
+ },
+ "APPLICATION_REPUTATION_SERVER_2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "bug_numbers": [1479901],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": [
+ "ErrOthers",
+ "ResponseValid",
+ "FailGetChannel",
+ "FailGetResponse",
+ "HTTP1xx",
+ "HTTP2xx",
+ "HTTP204",
+ "HTTP3xx",
+ "HTTP400",
+ "HTTP4xx",
+ "HTTP403",
+ "HTTP404",
+ "HTTP408",
+ "HTTP413",
+ "HTTP5xx",
+ "HTTP502_504_511",
+ "HTTP503",
+ "HTTP505",
+ "HTTPOthers",
+ "ErrAlreadyConnected",
+ "ErrNotConnected",
+ "ErrConnectionRefused",
+ "ErrNetTimeout",
+ "ErrOffline",
+ "ErrPortAccess",
+ "ErrNetReset",
+ "ErrNetInterrupt",
+ "ErrProxyConnection",
+ "ErrNetPartial",
+ "ErrNetInadequate",
+ "ErrUnknownHost",
+ "ErrDNSLookupQueue",
+ "ErrUnknownProxyHost"
+ ],
+ "description": "Network status of the application reputation remote lookup"
+ },
+ "APPLICATION_REPUTATION_SERVER_VERDICT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1272788, 1531034],
+ "kind": "enumerated",
+ "n_values": 8,
+ "description": "Application reputation remote verdict (0=SAFE, 1=DANGEROUS, 2=UNCOMMON, 3=POTENTIALLY_UNWANTED, 4=DANGEROUS_HOST, 5=UNKNOWN)"
+ },
+ "APPLICATION_REPUTATION_SERVER_VERDICT_2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1501974, 1589042],
+ "kind": "categorical",
+ "keyed": true,
+ "labels": [
+ "Safe",
+ "Dangerous",
+ "Uncommon",
+ "PotentiallyUnwanted",
+ "DangerousHost",
+ "Unknown"
+ ],
+ "description": "Application reputation remote verdict, keyed by file extension"
+ },
+ "APPLICATION_REPUTATION_REMOTE_LOOKUP_RESPONSE_TIME": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1479898],
+ "kind": "linear",
+ "low": 1000,
+ "high": 15000,
+ "n_buckets": 17,
+ "description": "Server response time to remote lookup request (ms)."
+ },
+ "APPLICATION_REPUTATION_REMOTE_LOOKUP_TIMEOUT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "boolean",
+ "bug_numbers": [1172689, 1531034],
+ "description": "Recorded when application reputation remote lookup is performed, `true` is recorded if the lookup times out."
+ },
+ "ADDON_SIGNATURE_VERIFICATION_STATUS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "enumerated",
+ "n_values": 32,
+ "bug_numbers": [1771523],
+ "description": "Records the result of App Signature Verification. See the comments in OpenSignedAppFile. "
+ },
+ "BACKGROUNDFILESAVER_THREAD_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 21,
+ "description": "Maximum number of concurrent threads reached during a given download session"
+ },
+ "CHECKERBOARD_DURATION": {
+ "record_in_processes": ["main", "content", "gpu"],
+ "products": ["firefox"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "botond@mozilla.com"],
+ "bug_numbers": [1238040, 1539309, 1584109],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 50,
+ "description": "Duration of a checkerboard event in milliseconds"
+ },
+ "CHECKERBOARD_PEAK": {
+ "record_in_processes": ["main", "content", "gpu"],
+ "products": ["firefox"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "botond@mozilla.com"],
+ "bug_numbers": [1238040, 1539309, 1584109],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 66355200,
+ "n_buckets": 50,
+ "description": "Peak number of CSS pixels checkerboarded during a checkerboard event (the high value is the size of a 4k display with max APZ zooming)"
+ },
+ "CHECKERBOARD_POTENTIAL_DURATION": {
+ "record_in_processes": ["main", "content", "gpu"],
+ "products": ["firefox"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "botond@mozilla.com"],
+ "bug_numbers": [1238040, 1539309, 1584109],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "Duration of a chunk of time (in ms) that could reasonably have had checkerboarding"
+ },
+ "CHECKERBOARD_SEVERITY": {
+ "record_in_processes": ["main", "content", "gpu"],
+ "products": ["firefox"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "botond@mozilla.com"],
+ "bug_numbers": [1238040, 1539309, 1584109],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1073741824,
+ "n_buckets": 50,
+ "description": "Opaque measure of the severity of a checkerboard event"
+ },
+ "CHILD_PROCESS_LAUNCH_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "jld@mozilla.com",
+ "jimm@mozilla.com",
+ "mconley@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "bug_numbers": [1474991],
+ "kind": "exponential",
+ "high": 64000,
+ "n_buckets": 100,
+ "releaseChannelCollection": "opt-out",
+ "description": "Time spent in the generic child process launching code, which is run off-main-thread and used by all child process types"
+ },
+ "COMPOSITE_TIME": {
+ "record_in_processes": ["main", "content", "gpu"],
+ "products": ["firefox"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "jnicol@mozilla.com"],
+ "expires_in_version": "never",
+ "description": "Composite times in milliseconds",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1080160, 1529352, 1580129]
+ },
+ "COMPOSITE_SWAP_TIME": {
+ "record_in_processes": ["main", "gpu"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "gfx-telemetry-alerts@mozilla.com",
+ "jmuizelaar@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "description": "Time to do swap/present/commit in 100 microseconds",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1612511]
+ },
+ "COMPOSITE_FRAME_ROUNDTRIP_TIME": {
+ "record_in_processes": ["main", "content", "gpu"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "rhunt@mozilla.com"],
+ "expires_in_version": "never",
+ "description": "Time from vsync to finishing a composite in milliseconds.",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50
+ },
+ "CONTENT_PROCESS_LAUNCH_MAINTHREAD_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "jld@mozilla.com",
+ "jimm@mozilla.com",
+ "mconley@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "bug_numbers": [1474991],
+ "kind": "exponential",
+ "high": 64000,
+ "n_buckets": 100,
+ "releaseChannelCollection": "opt-out",
+ "description": "Time spent on the main thread during asynchronous content process launch."
+ },
+ "CONTENT_PROCESS_LAUNCH_TOTAL_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "jld@mozilla.com",
+ "jimm@mozilla.com",
+ "mconley@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "bug_numbers": [1474991],
+ "kind": "exponential",
+ "high": 64000,
+ "n_buckets": 100,
+ "releaseChannelCollection": "opt-out",
+ "description": "Total time elapsed during asynchronous content process launch, until the process is usable for loading content."
+ },
+ "CONTENT_PROCESS_SYNC_LAUNCH_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "jld@mozilla.com",
+ "jimm@mozilla.com",
+ "mconley@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "bug_numbers": [1474991],
+ "kind": "exponential",
+ "high": 64000,
+ "n_buckets": 100,
+ "releaseChannelCollection": "opt-out",
+ "description": "Time elapsed during synchronous content process launch until the process is usable for loading content."
+ },
+ "CONTENT_PROCESS_LAUNCH_IS_SYNC": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "jld@mozilla.com",
+ "jimm@mozilla.com",
+ "mconley@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "bug_numbers": [1474991],
+ "kind": "boolean",
+ "releaseChannelCollection": "opt-out",
+ "description": "Whether a content process was launched synchronously (unnecessarily delaying UI response)."
+ },
+ "COOKIE_BANNERS_CLICK_HANDLE_DURATION_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["pbz@mozilla.com", "tihuang@mozilla.com"],
+ "expires_in_version": "128",
+ "bug_numbers": [1797078, 1804259, 1827765, 1850874, 1861317],
+ "kind": "exponential",
+ "high": 600000,
+ "n_buckets": 100,
+ "description": "Counts how long it takes to handle cookie banners successfully from DOMContentLoaded until click."
+ },
+ "CYCLE_COLLECTOR": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent on one cycle collection (ms)"
+ },
+ "CYCLE_COLLECTOR_WORKER": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent on one cycle collection in a worker (ms)"
+ },
+ "CYCLE_COLLECTOR_FULL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Full pause time for one cycle collection, including preparation (ms)"
+ },
+ "CYCLE_COLLECTOR_MAX_PAUSE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "bug_numbers": [1364503],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "description": "Longest pause for an individual slice of one cycle collection, including preparation (ms)"
+ },
+ "CYCLE_COLLECTOR_FINISH_IGC": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Cycle collection finished an incremental GC"
+ },
+ "CYCLE_COLLECTOR_SYNC_SKIPPABLE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Cycle collection synchronously ran forget skippable"
+ },
+ "CYCLE_COLLECTOR_VISITED_REF_COUNTED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 300000,
+ "n_buckets": 50,
+ "description": "Number of ref counted objects visited by the cycle collector"
+ },
+ "CYCLE_COLLECTOR_WORKER_VISITED_REF_COUNTED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 300000,
+ "n_buckets": 50,
+ "description": "Number of ref counted objects visited by the cycle collector in a worker"
+ },
+ "CYCLE_COLLECTOR_VISITED_GCED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 300000,
+ "n_buckets": 50,
+ "description": "Number of JS objects visited by the cycle collector"
+ },
+ "CYCLE_COLLECTOR_WORKER_VISITED_GCED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 300000,
+ "n_buckets": 50,
+ "description": "Number of JS objects visited by the cycle collector in a worker"
+ },
+ "CYCLE_COLLECTOR_COLLECTED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 50,
+ "description": "Number of objects collected by the cycle collector"
+ },
+ "CYCLE_COLLECTOR_WORKER_COLLECTED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 50,
+ "description": "Number of objects collected by the cycle collector in a worker"
+ },
+ "CYCLE_COLLECTOR_NEED_GC": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Needed garbage collection before cycle collection."
+ },
+ "CYCLE_COLLECTOR_WORKER_NEED_GC": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Needed garbage collection before cycle collection in a worker."
+ },
+ "CYCLE_COLLECTOR_TIME_BETWEEN": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 120,
+ "n_buckets": 50,
+ "description": "Time spent in between cycle collections (seconds)"
+ },
+ "CYCLE_COLLECTOR_OOM": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Set if the cycle collector ran out of memory at some point"
+ },
+ "CYCLE_COLLECTOR_WORKER_OOM": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Set if the cycle collector in a worker ran out of memory at some point"
+ },
+ "CYCLE_COLLECTOR_ASYNC_SNOW_WHITE_FREEING": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent on one asynchronous SnowWhite freeing (ms)"
+ },
+ "CYCLE_COLLECTOR_SLICE_DURING_IDLE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 50,
+ "bug_numbers": [1372042],
+ "description": "Percent of cycle collector slice done during idle time"
+ },
+ "DEFERRED_FINALIZE_ASYNC": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Pause time for asynchronous deferred finalization (ms)"
+ },
+ "DEVICE_RESET_REASON": {
+ "record_in_processes": ["main", "content", "gpu"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "rhunt@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 10,
+ "bug_numbers": [1135408, 1529352],
+ "releaseChannelCollection": "opt-out",
+ "description": "GPU Device Reset Reason (ok, hung, removed, reset, internal error, invalid call, out of memory)"
+ },
+ "FINGERPRINTING_PROTECTION_CANVAS_NOISE_CALCULATE_TIME_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "tihuang@mozilla.com",
+ "tom@mozilla.com",
+ "tschuster@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "bug_numbers": [1838856],
+ "kind": "exponential",
+ "high": 600000,
+ "n_buckets": 100,
+ "description": "Counts how long to generate canvas random noises."
+ },
+ "FIREFOX_VIEW_CUMULATIVE_SEARCHES": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "alert_emails": ["firefoxview@mozilla.com"],
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1869765],
+ "kind": "enumerated",
+ "n_values": 100,
+ "description": "Cumulative no. of searches performed before selecting a resulting tab.",
+ "keyed": true,
+ "keys": [
+ "recentbrowsing",
+ "opentabs",
+ "recentlyclosed",
+ "history",
+ "syncedtabs"
+ ]
+ },
+ "FORCED_DEVICE_RESET_REASON": {
+ "record_in_processes": ["main", "content", "gpu"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "rhunt@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 50,
+ "bug_numbers": [1235407],
+ "releaseChannelCollection": "opt-out",
+ "description": "GPU Forced Device Reset Reason (OpenSharedHandle)"
+ },
+ "FORGET_SKIPPABLE_MAX": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Max time spent on one forget skippable (ms)"
+ },
+ "FORGET_SKIPPABLE_DURING_IDLE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 50,
+ "bug_numbers": [1372042],
+ "description": "Percent of the cycle collector's forget skippable done during idle time"
+ },
+ "FORGET_SKIPPABLE_FREQUENCY": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "smaug@mozilla.com",
+ "dev-telemetry-gc-alerts@mozilla.org"
+ ],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 500,
+ "n_buckets": 100,
+ "bug_numbers": [1487271],
+ "description": "Number of forget skippables occurred during a minute"
+ },
+ "FULLSCREEN_TRANSITION_BLACK_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["mozilla-telemetry@upsuper.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 100,
+ "high": 5000,
+ "n_buckets": 50,
+ "bug_numbers": [1271160],
+ "description": "The time spent in the fully-black screen in fullscreen transition"
+ },
+ "FULLSCREEN_CHANGE_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["mozilla-telemetry@upsuper.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 100,
+ "high": 5000,
+ "n_buckets": 50,
+ "bug_numbers": [1271160],
+ "description": "The time content uses to enter/exit fullscreen regardless of fullscreen transition timeout"
+ },
+ "GC_REASON_2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "bug_numbers": [677411, 764184],
+ "description": "Reason (enum value) for initiating a GC"
+ },
+ "GC_IS_COMPARTMENTAL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "bug_numbers": [677411, 1297026],
+ "description": "Is it a zone GC?"
+ },
+ "GC_ZONE_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "jcoppeard@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100,
+ "n_buckets": 20,
+ "bug_numbers": [1790630],
+ "description": "Total number of GC zones"
+ },
+ "GC_ZONES_COLLECTED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "jcoppeard@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100,
+ "n_buckets": 20,
+ "bug_numbers": [1790630],
+ "description": "Number of zones collected in a zone GC"
+ },
+ "GC_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "jcoppeard@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "bug_numbers": [1636419],
+ "description": "Time spent running JS GC (ms)"
+ },
+ "GC_IN_PROGRESS_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "smaug@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "bug_numbers": [1545093],
+ "description": "Time from the beginning of the first slice to the end of the last slice (ms)"
+ },
+ "GC_BUDGET_MS_2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "jcoppeard@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 200,
+ "n_buckets": 50,
+ "description": "Requested GC slice budget (ms)",
+ "bug_numbers": [1637318]
+ },
+ "GC_BUDGET_WAS_INCREASED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "jcoppeard@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "boolean",
+ "description": "Whether the budget was increased due to taking too long or approaching the incremental limit",
+ "bug_numbers": [1697526]
+ },
+ "GC_SLICE_WAS_LONG": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "jcoppeard@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "boolean",
+ "description": "Whether a GC slice took 1.5 times or 5ms more than its budget",
+ "bug_numbers": [1697526]
+ },
+ "GC_ANIMATION_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "jcoppeard@mozilla.com",
+ "sdetar@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "bug_numbers": [1489524],
+ "description": "Time spent running JS GC when animating (ms)"
+ },
+ "GC_MAX_PAUSE_MS_2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "bug_numbers": [1364503],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "description": "Longest GC slice in a GC (ms)"
+ },
+ "GC_PREPARE_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "jcoppeard@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "Time spent running JS GC preparation phase (ms)",
+ "bug_numbers": [1637642]
+ },
+ "GC_MARK_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "bug_numbers": [677411],
+ "description": "Time spent running JS GC mark phase (ms)"
+ },
+ "GC_SWEEP_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "bug_numbers": [677411],
+ "description": "Time spent running JS GC sweep phase (ms)"
+ },
+ "GC_COMPACT_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "bug_numbers": [1247955],
+ "description": "Time spent running JS GC compact phase (ms)"
+ },
+ "GC_MARK_ROOTS_US": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "jcoppeard@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 150000,
+ "n_buckets": 50,
+ "description": "Time spent marking GC roots (us)",
+ "bug_numbers": [1637318]
+ },
+ "GC_MARK_GRAY_MS_2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "jcoppeard@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent marking gray GC objects (ms)",
+ "bug_numbers": [1637318]
+ },
+ "GC_MARK_WEAK_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "jcoppeard@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent marking GC objects held live through weakmaps (ms)",
+ "bug_numbers": [1637642]
+ },
+ "GC_SLICE_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "bug_numbers": [641025],
+ "description": "Time spent running a JS GC slice (ms)"
+ },
+ "GC_SLOW_PHASE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 75,
+ "bug_numbers": [1052728],
+ "description": "The longest phase in any slice that goes over 2x the budget. The phase values are defined in js/src/gc/GenerateStatsPhases.py."
+ },
+ "GC_SLOW_TASK": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "jcoppeard@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 75,
+ "description": "The longest parallel task in any slice that goes over 2x the budget. The phase values are defined in js/src/gc/GenerateStatsPhases.py.",
+ "bug_numbers": [1309651]
+ },
+ "GC_MMU_50": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 20,
+ "bug_numbers": [641025],
+ "description": "Minimum percentage of time spent outside GC over any 50ms window"
+ },
+ "GC_RESET": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "bug_numbers": [641025],
+ "description": "Was an incremental GC canceled?"
+ },
+ "GC_RESET_REASON": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 20,
+ "description": "Reason for cancelling an ongoing GC (see js::gc::AbortReason)",
+ "bug_numbers": [1308116]
+ },
+ "GC_NON_INCREMENTAL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "bug_numbers": [731052],
+ "description": "Was the GC non-incremental?"
+ },
+ "GC_NON_INCREMENTAL_REASON": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 20,
+ "description": "Reason for performing a non-incremental GC (see js::gc::AbortReason)",
+ "bug_numbers": [1308116]
+ },
+ "GC_MINOR_REASON": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "bug_numbers": [1052716],
+ "description": "Reason (enum value) for initiating a minor GC"
+ },
+ "GC_MINOR_REASON_LONG": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "bug_numbers": [1052716],
+ "description": "Reason (enum value) that caused a long (>1ms) minor GC"
+ },
+ "GC_MINOR_US": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 100,
+ "bug_numbers": [1052716],
+ "description": "Time spent running JS minor GC (us)"
+ },
+ "GC_NURSERY_BYTES_2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "pbone@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 16384,
+ "high": 16777216,
+ "n_buckets": 24,
+ "bug_numbers": [1528867],
+ "description": "Size of the GC nursery (bytes)"
+ },
+ "GC_PRETENURE_COUNT_2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "jcoppeard@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 100,
+ "n_buckets": 20,
+ "bug_numbers": [1293262, 1637318],
+ "description": "How many objects groups were selected for pretenuring by a minor GC"
+ },
+ "GC_SLICE_DURING_IDLE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["dev-telemetry-gc-alerts@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 50,
+ "bug_numbers": [1372042],
+ "description": "Percent of GC slice done during idle time"
+ },
+ "GC_BUDGET_OVERRUN": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "jcoppeard@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 100,
+ "bug_numbers": [1381777],
+ "description": "How long a GC slice ran over its budget in microseconds"
+ },
+ "GC_NURSERY_PROMOTION_RATE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "jcoppeard@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 50,
+ "bug_numbers": [1485299],
+ "description": "The percentage of nursery objects that were promoted to tenured heap."
+ },
+ "GC_TENURED_SURVIVAL_RATE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "jcoppeard@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1563755],
+ "description": "The percentage of tenured GC things that survived a collection."
+ },
+ "GC_MARK_RATE_2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "jcoppeard@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 1000,
+ "high": 300000,
+ "n_buckets": 50,
+ "bug_numbers": [1475896, 1637318],
+ "description": "The number of objects marked per ms during GC."
+ },
+ "GC_TIME_BETWEEN_S": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "jcoppeard@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 120,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1556467],
+ "description": "Time spent in between garbage collections for the main runtime (seconds)"
+ },
+ "GC_TIME_BETWEEN_SLICES_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "jcoppeard@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 2000,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1556467],
+ "description": "Time spent in between garbage collections slice for the main runtime (ms)"
+ },
+ "GC_SLICE_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "jcoppeard@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 200,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1556467],
+ "description": "The number of slices in an incremental GC for the main runtime"
+ },
+ "GC_EFFECTIVENESS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "jcoppeard@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "low": 1,
+ "high": 50000,
+ "n_buckets": 100,
+ "bug_numbers": [1580227],
+ "description": "GC 'effectiveness', the amount of memory freed divided by main-thread collection time (MB/s)"
+ },
+ "GC_PARALLEL_MARK": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "jcoppeard@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "bug_numbers": [1861942],
+ "description": "Whether parallel marking was used for this collection"
+ },
+ "GC_PARALLEL_MARK_SPEEDUP": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "jcoppeard@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 50,
+ "high": 800,
+ "n_buckets": 50,
+ "bug_numbers": [1817741],
+ "description": "Ratio of total helper thread time spent marking to main thread time for parallel marking, scaled by 100."
+ },
+ "GC_PARALLEL_MARK_UTILIZATION": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "jcoppeard@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 1,
+ "high": 100,
+ "n_buckets": 50,
+ "bug_numbers": [1817741],
+ "description": "Ratio of helper thread time spent marking to total helper thread time for parallel marking."
+ },
+ "GC_PARALLEL_MARK_INTERRUPTIONS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "jcoppeard@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 1,
+ "high": 500,
+ "n_buckets": 50,
+ "bug_numbers": [1817741],
+ "description": "Number of interruptions/donations per slice during parallel marking."
+ },
+ "GC_TASK_START_DELAY_US": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dev-telemetry-gc-alerts@mozilla.org",
+ "jcoppeard@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 1,
+ "high": 2000,
+ "n_buckets": 50,
+ "bug_numbers": [1817965],
+ "description": "Delay between queuing a GC task and the task starting."
+ },
+ "DESERIALIZE_BYTES": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1717631],
+ "low": 16,
+ "high": 2147483646,
+ "n_buckets": 100,
+ "description": "Size of deserialized data, in bytes",
+ "alert_emails": ["sfink@mozilla.com"]
+ },
+ "DESERIALIZE_ITEMS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1717631],
+ "kind": "exponential",
+ "high": 2147483646,
+ "n_buckets": 50,
+ "description": "Size of deserialized data, in items",
+ "alert_emails": ["sfink@mozilla.com"]
+ },
+ "DESERIALIZE_US": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1717631],
+ "high": 150000000,
+ "n_buckets": 100,
+ "description": "Time spent deserializing structured data",
+ "alert_emails": ["sfink@mozilla.com"]
+ },
+ "GEOLOCATION_ACCURACY_EXPONENTIAL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1507925],
+ "high": 100000,
+ "n_buckets": 50,
+ "description": "Geolocation results' accuracy radius in meters. Smaller radius is more accurate.",
+ "alert_emails": ["shong@mozilla.com"]
+ },
+ "GEOLOCATION_ERROR": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Has seen location error"
+ },
+ "GEOLOCATION_WIN8_SOURCE_IS_MLS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "boolean",
+ "description": "Geolocation on Win8 is either MLS or native"
+ },
+ "GEOLOCATION_OSX_SOURCE_IS_MLS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "boolean",
+ "description": "Geolocation on OS X is either MLS or CoreLocation"
+ },
+ "GPU_PROCESS_LAUNCH_TIME_MS_2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "gfx-telemetry-alerts@mozilla.com",
+ "rhunt@mozilla.com",
+ "dbolter@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1297790, 1317796, 1489524],
+ "kind": "exponential",
+ "high": 64000,
+ "n_buckets": 100,
+ "description": "GPU process launch time in milliseconds"
+ },
+ "GPU_PROCESS_INITIALIZATION_TIME_MS": {
+ "record_in_processes": ["main", "content", "gpu"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "gfx-telemetry-alerts@mozilla.com",
+ "rhunt@mozilla.com",
+ "dbolter@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1324095, 1489524],
+ "kind": "exponential",
+ "high": 64000,
+ "n_buckets": 100,
+ "description": "GPU process initialization (excluding XPCOM and fork time) time in milliseconds"
+ },
+ "MEMORY_RESIDENT_FAST": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 32768,
+ "high": 16777216,
+ "n_buckets": 100,
+ "bug_numbers": [1226196, 1870550],
+ "description": "Resident memory size (KB)",
+ "releaseChannelCollection": "opt-out"
+ },
+ "MEMORY_RESIDENT_PEAK": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "memshrink-telemetry-alerts@mozilla.com",
+ "amccreight@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 32768,
+ "high": 16777216,
+ "n_buckets": 100,
+ "bug_numbers": [1551648, 1870550],
+ "description": "Peak resident memory size (KB)",
+ "releaseChannelCollection": "opt-out"
+ },
+ "MEMORY_TOTAL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": [
+ "memshrink-telemetry-alerts@mozilla.com",
+ "amccreight@mozilla.com"
+ ],
+ "bug_numbers": [1198209, 1511918],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 32768,
+ "high": 16777216,
+ "n_buckets": 100,
+ "description": "Total Memory Across All Processes (KB) (inaccurate WRT shared memory. See MemoryTelemetry.cpp)",
+ "releaseChannelCollection": "opt-out"
+ },
+ "MEMORY_DISTRIBUTION_AMONG_CONTENT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1344174],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "keyed": true,
+ "high": 200,
+ "n_buckets": 100,
+ "description": "Absolute difference of each content process' USS and the mean of USS's, normalized by the mean, in percentage. It will be recorded with the rest of the memory probes when gatherMemory is called, if at least 2 content processes are alive. Example: in case of 4 content processes with USS's: 1G, 500MB, 1G, 1.5G, the reported numbers will be: 0, 50, 0, 50. Which indicates that 2 processes used 50% more or 50% less memory than the avarage and 2 used exactly as much as the avarage."
+ },
+ "MEMORY_UNIQUE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1198209, 1870550],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 32768,
+ "high": 16777216,
+ "n_buckets": 100,
+ "description": "Unique Set Size (KB)",
+ "releaseChannelCollection": "opt-out"
+ },
+ "MEMORY_UNIQUE_CONTENT_STARTUP": {
+ "record_in_processes": ["content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "amccreight@mozilla.com",
+ "memshrink-telemetry-alerts@mozilla.com"
+ ],
+ "bug_numbers": [1494827],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 2048,
+ "high": 131072,
+ "n_buckets": 100,
+ "description": "Unique Set Size of Content Process at Startup (KB)"
+ },
+ "MEMORY_VSIZE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 32768,
+ "high": 16777216,
+ "n_buckets": 100,
+ "description": "Virtual memory size (KB)"
+ },
+ "MEMORY_VSIZE_MAX_CONTIGUOUS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 32768,
+ "high": 16777216,
+ "n_buckets": 100,
+ "description": "Maximum-sized block of contiguous virtual memory (KB)"
+ },
+ "MEMORY_COLLECTION_TIME": {
+ "record_in_processes": ["all"],
+ "products": ["firefox"],
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 1,
+ "high": 60000,
+ "n_buckets": 32,
+ "bug_numbers": [1786864],
+ "description": "Time spent gathering memory telemetry in milliseconds"
+ },
+ "MEMORY_JS_COMPARTMENTS_SYSTEM": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "Total JavaScript compartments used for add-ons and internals."
+ },
+ "MEMORY_JS_COMPARTMENTS_USER": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "Total JavaScript compartments used for web pages"
+ },
+ "MEMORY_JS_REALMS_SYSTEM": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": [
+ "memshrink-telemetry-alerts@mozilla.com",
+ "jdemooij@mozilla.com"
+ ],
+ "bug_numbers": [1518077],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "Total JavaScript realms used for add-ons and internals."
+ },
+ "MEMORY_JS_REALMS_USER": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": [
+ "memshrink-telemetry-alerts@mozilla.com",
+ "jdemooij@mozilla.com"
+ ],
+ "bug_numbers": [1518077],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "Total JavaScript realms used for web pages."
+ },
+ "MEMORY_JS_GC_HEAP": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 1024,
+ "high": 16777216,
+ "n_buckets": 200,
+ "description": "Memory used by the garbage-collected JavaScript heap (KB)"
+ },
+ "MEMORY_STORAGE_SQLITE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 1024,
+ "high": 524288,
+ "n_buckets": 50,
+ "description": "Memory used by SQLite (KB)"
+ },
+ "MEMORY_IMAGES_CONTENT_USED_UNCOMPRESSED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 1024,
+ "high": 1048576,
+ "n_buckets": 50,
+ "description": "Memory used for uncompressed, in-use content images (KB)"
+ },
+ "MEMORY_HEAP_ALLOCATED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 1024,
+ "high": 16777216,
+ "n_buckets": 200,
+ "description": "Heap memory allocated (KB)"
+ },
+ "MEMORY_HEAP_OVERHEAD_FRACTION": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1252375],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 25,
+ "description": "Fraction of committed heap memory that is overhead (percentage)."
+ },
+ "GHOST_WINDOWS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 128,
+ "n_buckets": 32,
+ "releaseChannelCollection": "opt-out",
+ "description": "Number of ghost windows"
+ },
+ "MEMORY_FREE_PURGED_PAGES_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1024,
+ "n_buckets": 10,
+ "description": "Time(ms) to purge dirty heap pages."
+ },
+ "LOW_MEMORY_EVENTS_VIRTUAL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1024,
+ "n_buckets": 21,
+ "description": "Number of low-virtual-memory events fired since last ping",
+ "operating_systems": ["windows"],
+ "bug_numbers": [711490, 1451005]
+ },
+ "LOW_MEMORY_EVENTS_PHYSICAL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1024,
+ "n_buckets": 21,
+ "description": "Number of low-physical-memory events fired since last ping",
+ "operating_systems": ["windows"],
+ "bug_numbers": [711490, 1451005]
+ },
+ "LOW_MEMORY_EVENTS_COMMIT_SPACE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["memshrink-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1024,
+ "n_buckets": 21,
+ "description": "Number of low-commit-space events fired since last ping",
+ "operating_systems": ["windows"],
+ "bug_numbers": [1451005]
+ },
+ "PAGE_FAULTS_HARD": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "low": 8,
+ "high": 65536,
+ "n_buckets": 13,
+ "description": "Hard page faults (since last telemetry ping)",
+ "operating_systems": ["unix"]
+ },
+ "MEMORY_PHC_SLOP": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "memshrink-telemetry-alerts@mozilla.com",
+ "pbone@mozilla.com"
+ ],
+ "bug_numbers": [1829127],
+ "expires_in_version": "128",
+ "kind": "exponential",
+ "low": 4096,
+ "high": 8388608,
+ "n_buckets": 48,
+ "description": "Over-allocation due to PHC's rounding (aka internal fragmentation). Measured in bytes.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "MEMORY_PHC_SLOTS_ALLOCATED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "memshrink-telemetry-alerts@mozilla.com",
+ "pbone@mozilla.com"
+ ],
+ "bug_numbers": [1829127],
+ "expires_in_version": "128",
+ "kind": "exponential",
+ "high": 16384,
+ "n_buckets": 64,
+ "description": "Number of PHC slots currently allocated",
+ "releaseChannelCollection": "opt-out"
+ },
+ "MEMORY_PHC_SLOTS_FREED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "memshrink-telemetry-alerts@mozilla.com",
+ "pbone@mozilla.com"
+ ],
+ "bug_numbers": [1829127],
+ "expires_in_version": "128",
+ "kind": "exponential",
+ "high": 16384,
+ "n_buckets": 64,
+ "description": "Number of PHC slots allocated-then-freed",
+ "releaseChannelCollection": "opt-out"
+ },
+
+ "FONTLIST_INITOTHERFAMILYNAMES": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "Time(ms) spent on reading other family names from all fonts"
+ },
+ "FONTLIST_INITOTHERFAMILYNAMES_NO_DEFERRING": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "Time(ms) spent on reading other family names from all fonts for no timeout case"
+ },
+ "FONTLIST_INITFACENAMELISTS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "Time(ms) spent on reading family names from all fonts"
+ },
+ "FONTLIST_BUNDLEDFONTS_ACTIVATE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1696162],
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "Time(ms) spent activating additional fonts bundled with the product"
+ },
+ "DWRITEFONT_DELAYEDINITFONTLIST_TOTAL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 10,
+ "description": "gfxDWriteFontList::DelayedInitFontList Total (ms)",
+ "operating_systems": ["windows"]
+ },
+ "DWRITEFONT_DELAYEDINITFONTLIST_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 10,
+ "description": "gfxDWriteFontList::DelayedInitFontList Font Family Count",
+ "operating_systems": ["windows"]
+ },
+ "DWRITEFONT_DELAYEDINITFONTLIST_COLLECT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 10,
+ "description": "gfxDWriteFontList::DelayedInitFontList GetSystemFontCollection (ms)",
+ "operating_systems": ["windows"]
+ },
+ "DWRITEFONT_INIT_PROBLEM": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 8,
+ "description": "DirectWrite system fontlist initialization problem (1=GDI interop, 2=system font collection, 3=no fonts)",
+ "operating_systems": ["windows"]
+ },
+ "GDI_INITFONTLIST_TOTAL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 10,
+ "description": "gfxGDIFontList::InitFontList Total (ms)",
+ "operating_systems": ["windows"]
+ },
+ "MAC_INITFONTLIST_TOTAL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 10,
+ "description": "gfxMacPlatformFontList::InitFontList Total (ms)",
+ "operating_systems": ["mac"]
+ },
+ "SYSTEM_FONT_FALLBACK": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 50,
+ "description": "System font fallback (us)"
+ },
+ "SYSTEM_FONT_FALLBACK_FIRST": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 40000,
+ "n_buckets": 20,
+ "description": "System font fallback, first call (ms)"
+ },
+ "SYSTEM_FONT_FALLBACK_SCRIPT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 110,
+ "description": "System font fallback script"
+ },
+ "FONT_CACHE_HIT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "font cache hit"
+ },
+ "BAD_FALLBACK_FONT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "system fallback font can't be used"
+ },
+ "SHUTDOWN_OK": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "alert_emails": ["chutten@mozilla.com"],
+ "bug_numbers": [1421688],
+ "releaseChannelCollection": "opt-out",
+ "kind": "boolean",
+ "description": "Did the browser start after a successful shutdown"
+ },
+ "IMAGE_DECODE_LATENCY_US": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 50,
+ "high": 5000000,
+ "n_buckets": 100,
+ "description": "Time spent decoding an image chunk (us)"
+ },
+ "IMAGE_DECODE_TIME": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 50,
+ "high": 50000000,
+ "n_buckets": 100,
+ "description": "Time spent decoding an image (us)"
+ },
+ "IMAGE_DECODE_ON_DRAW_LATENCY": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 50,
+ "high": 50000000,
+ "n_buckets": 100,
+ "description": "Time from starting a decode to it showing up on the screen (us)"
+ },
+ "IMAGE_DECODE_CHUNKS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 500,
+ "n_buckets": 50,
+ "description": "Number of chunks per decode attempt"
+ },
+ "IMAGE_DECODE_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 500,
+ "n_buckets": 50,
+ "description": "Decode count"
+ },
+ "IMAGE_DECODE_SPEED_JPEG": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 500,
+ "high": 50000000,
+ "n_buckets": 50,
+ "description": "JPEG image decode speed (Kbytes/sec)"
+ },
+ "IMAGE_DECODE_SPEED_GIF": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 500,
+ "high": 50000000,
+ "n_buckets": 50,
+ "description": "GIF image decode speed (Kbytes/sec)"
+ },
+ "IMAGE_DECODE_SPEED_PNG": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 500,
+ "high": 50000000,
+ "n_buckets": 50,
+ "description": "PNG image decode speed (Kbytes/sec)"
+ },
+ "IMAGE_DECODE_SPEED_WEBP": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 500,
+ "high": 50000000,
+ "n_buckets": 50,
+ "description": "WebP image decode speed (Kbytes/sec)",
+ "bug_numbers": [1294490]
+ },
+ "IMAGE_DECODE_SPEED_AVIF": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 500,
+ "high": 50000000,
+ "n_buckets": 50,
+ "description": "AVIF image decode speed (Kbytes/sec)",
+ "bug_numbers": [1294490]
+ },
+ "IMAGE_REQUEST_DISPATCHED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "aosmond@mozilla.com"],
+ "expires_in_version": "62",
+ "kind": "boolean",
+ "description": "Track how many image requests required event dispatching because we were unable to predict the correct scheduler group: true if the request required dispatching. See image/imgRequestProxy.cpp for details.",
+ "bug_numbers": [1359833]
+ },
+ "AVIF_DECODE_RESULT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": [
+ "success",
+ "parse_error",
+ "no_primary_item",
+ "decode_error",
+ "size_overflow",
+ "out_of_memory",
+ "pipe_init_error",
+ "write_buffer_error",
+ "alpha_y_sz_mismatch",
+ "alpha_y_bpc_mismatch",
+ "ispe_mismatch",
+ "render_size_mismatch",
+ "frame_size_changed",
+ "invalid_cicp",
+ "no_samples",
+ "invalid_parse_status",
+ "missing_brand",
+ "ftyp_not_first",
+ "no_image",
+ "multiple_moov",
+ "no_moov",
+ "lsel_no_essential",
+ "a1op_no_essential",
+ "a1lx_essential",
+ "txform_no_essential",
+ "image_item_type",
+ "item_type_missing",
+ "construction_method",
+ "item_loc_not_found",
+ "no_item_data_box",
+ "uncategorized"
+ ],
+ "description": "Decode result of AVIF image",
+ "bug_numbers": [1670827]
+ },
+ "AVIF_AOM_DECODE_ERROR": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": [
+ "error",
+ "mem_error",
+ "abi_mismatch",
+ "incapable",
+ "unsup_bitstream",
+ "unsup_feature",
+ "corrupt_frame",
+ "invalid_param"
+ ],
+ "description": "Error code from aom_codec_decode when decoding AVIF image",
+ "bug_numbers": [1690406]
+ },
+ "AVIF_DECODER": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": ["dav1d", "aom"],
+ "description": "Decoder of AVIF image",
+ "bug_numbers": [1670827]
+ },
+ "AVIF_YUV_COLOR_SPACE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": ["BT601", "BT709", "BT2020", "identity", "unknown"],
+ "description": "YUV color space of AVIF image",
+ "bug_numbers": [1670827]
+ },
+ "AVIF_BIT_DEPTH": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": ["color_8", "color_10", "color_12", "color_16", "unknown"],
+ "description": "Bits per pixel of AVIF image",
+ "bug_numbers": [1670827]
+ },
+ "AVIF_ALPHA": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": ["absent", "present"],
+ "description": "AVIF alpha plane",
+ "bug_numbers": [1696045]
+ },
+ "AVIF_COLR": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": ["nclx", "icc", "absent", "both"],
+ "description": "AVIF colour information type",
+ "bug_numbers": [1696045]
+ },
+ "AVIF_CICP_CP": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": [
+ "RESERVED",
+ "BT709",
+ "UNSPECIFIED",
+ "RESERVED_3",
+ "BT470M",
+ "BT470BG",
+ "BT601",
+ "SMPTE240",
+ "GENERIC_FILM",
+ "BT2020",
+ "XYZ",
+ "SMPTE431",
+ "SMPTE432",
+ "RESERVED_13",
+ "RESERVED_14",
+ "RESERVED_15",
+ "RESERVED_16",
+ "RESERVED_17",
+ "RESERVED_18",
+ "RESERVED_19",
+ "RESERVED_20",
+ "RESERVED_21",
+ "EBU3213",
+ "RESERVED_REST"
+ ],
+ "description": "AVIF CICP colour primaries",
+ "bug_numbers": [1696045]
+ },
+ "AVIF_CICP_TC": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": [
+ "RESERVED",
+ "BT709",
+ "UNSPECIFIED",
+ "RESERVED_3",
+ "BT470M",
+ "BT470BG",
+ "BT601",
+ "SMPTE240",
+ "LINEAR",
+ "LOG_100",
+ "LOG_100_SQRT10",
+ "IEC61966",
+ "BT_1361",
+ "SRGB",
+ "BT2020_10BIT",
+ "BT2020_12BIT",
+ "SMPTE2084",
+ "SMPTE428",
+ "HLG",
+ "RESERVED_REST"
+ ],
+ "description": "AVIF CICP transfer characteristics",
+ "bug_numbers": [1696045]
+ },
+ "AVIF_CICP_MC": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": [
+ "IDENTITY",
+ "BT709",
+ "UNSPECIFIED",
+ "RESERVED",
+ "FCC",
+ "BT470BG",
+ "BT601",
+ "SMPTE240",
+ "YCGCO",
+ "BT2020_NCL",
+ "BT2020_CL",
+ "SMPTE2085",
+ "CHROMAT_NCL",
+ "CHROMAT_CL",
+ "ICTCP",
+ "RESERVED_REST"
+ ],
+ "description": "AVIF CICP matrix coefficients",
+ "bug_numbers": [1696045]
+ },
+ "AVIF_ISPE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": ["valid", "absent", "bitstream_mismatch"],
+ "description": "AVIF spatial extents (image size)",
+ "bug_numbers": [1696045]
+ },
+ "AVIF_PIXI": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": ["valid", "absent", "bitstream_mismatch"],
+ "description": "AVIF pixel information (bits per channel)",
+ "bug_numbers": [1696045]
+ },
+ "AVIF_PASP": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": ["absent", "square", "nonsquare", "invalid"],
+ "description": "AVIF pixel aspect ratio",
+ "bug_numbers": [1745608]
+ },
+ "AVIF_MAJOR_BRAND": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": ["avif", "avis", "other"],
+ "description": "AVIF major brand",
+ "bug_numbers": [1745608]
+ },
+ "AVIF_SEQUENCE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": ["present", "absent"],
+ "description": "AVIF image sequence",
+ "bug_numbers": [1745608]
+ },
+ "AVIF_A1LX": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": ["present", "absent"],
+ "description": "AVIF AV1LayeredImageIndexingProperty (a1lx)",
+ "bug_numbers": [1745608]
+ },
+ "AVIF_A1OP": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": ["present", "absent"],
+ "description": "AVIF OperatingPointSelectorProperty (a1op)",
+ "bug_numbers": [1745608]
+ },
+ "AVIF_CLAP": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": ["present", "absent"],
+ "description": "AVIF CleanApertureBox (clap)",
+ "bug_numbers": [1745608]
+ },
+ "AVIF_GRID": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": ["present", "absent"],
+ "description": "AVIF AVIF grid-based image",
+ "bug_numbers": [1745608]
+ },
+ "AVIF_IPRO": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": ["present", "absent"],
+ "description": "AVIF ItemProtectionBox (ipro)",
+ "bug_numbers": [1745608]
+ },
+ "AVIF_LSEL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": ["present", "absent"],
+ "description": "AVIF LayerSelectorProperty (lsel)",
+ "bug_numbers": [1745608]
+ },
+ "KEYPRESS_PRESENT_LATENCY": {
+ "record_in_processes": ["all"],
+ "products": ["firefox"],
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com", "vchin@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "low": 1,
+ "high": 200000,
+ "n_buckets": 50,
+ "description": "Time between receiving a keypress event on the event loop and compositing its result onto the screen (ms)",
+ "bug_numbers": [1506537, 1580077]
+ },
+ "MOUSEUP_FOLLOWED_BY_CLICK_PRESENT_LATENCY": {
+ "record_in_processes": ["main", "gpu"],
+ "products": ["firefox"],
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com", "sefeng@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "low": 1,
+ "high": 200000,
+ "n_buckets": 50,
+ "description": "Time between receiving a mouseup which follow by a mouseclick on the event loop and compositing its result onto the screen (ms)",
+ "bug_numbers": [1698643]
+ },
+ "SCROLL_PRESENT_LATENCY": {
+ "record_in_processes": ["main", "gpu"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "perf-telemetry-alerts@mozilla.com",
+ "sefeng@mozilla.com",
+ "vchin@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "low": 1,
+ "high": 20000,
+ "n_buckets": 100,
+ "description": "Time between receiving a scroll event on the event loop and compositing its result onto the screen (ms)",
+ "bug_numbers": [1500465, 1604818]
+ },
+ "CANVAS_2D_USED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "2D canvas used"
+ },
+ "CANVAS_WEBGL_ACCL_FAILURE_ID": {
+ "record_in_processes": ["main", "content", "gpu"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "rhunt@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "description": "Track the failure IDs that lead us to reject attempting to create an accelerated context. CANVAS_WEBGL_FAILURE_ID reports the overall WebGL status with the attempt to fallback.",
+ "bug_numbers": [1272808]
+ },
+ "CANVAS_WEBGL_FAILURE_ID": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "rhunt@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "description": "WebGL runtime and dynamic failure IDs. This will record a count for each context creation success or failure. Each failure id is a unique identifier that can be traced back to a particular failure branch or blocklist rule.",
+ "bug_numbers": [1272808]
+ },
+ "CANVAS_WEBGL_SUCCESS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "rhunt@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "WebGL1 creation success",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1247327, 1529352]
+ },
+ "CANVAS_WEBGL_USED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "WebGL canvas used"
+ },
+ "CANVAS_WEBGL2_SUCCESS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "rhunt@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "WebGL2 creation success",
+ "bug_numbers": [1247327]
+ },
+ "CANVAS_FINGERPRINTING_PER_TAB": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "126",
+ "keyed": true,
+ "keys": ["known_text", "unknown"],
+ "kind": "enumerated",
+ "n_values": 8,
+ "releaseChannelCollection": "opt-out",
+ "description": "Type of canvas fingerprinter detected (keyed by known_fingerprinting_text or unknown), 0 = none",
+ "bug_numbers": [1847990]
+ },
+ "FONT_FINGERPRINTING_PER_TAB": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "126",
+ "kind": "boolean",
+ "releaseChannelCollection": "opt-out",
+ "description": "Whether a probable font fingerprinting attempt was detected",
+ "bug_numbers": [1847990]
+ },
+ "TOTAL_CONTENT_PAGE_LOAD_TIME": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com", "bdekoz@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 100,
+ "high": 30000,
+ "n_buckets": 100,
+ "releaseChannelCollection": "opt-out",
+ "description": "HTTP: Total page load time (ms)",
+ "bug_numbers": [1580077]
+ },
+ "HTTP_SUBITEM_OPEN_LATENCY_TIME": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem channel: Page start -> subitem open() (ms)"
+ },
+ "HTTP_SUBITEM_FIRST_BYTE_LATENCY_TIME": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem channel: Page start -> first byte received for subitem reply (ms)"
+ },
+ "HTTP_REQUEST_PER_PAGE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "HTTP: Requests per page (count)"
+ },
+ "HTTP_REQUEST_PER_PAGE_FROM_CACHE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 101,
+ "description": "HTTP: Requests serviced from cache (%)"
+ },
+ "HTTP_REQUEST_PER_CONN": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "HTTP: requests per connection"
+ },
+ "HTTP_KBREAD_PER_CONN2": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "bug_numbers": [1520260],
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 50,
+ "description": "HTTP: KB read per connection"
+ },
+ "HTTP_PAGE_DNS_ISSUE_TIME": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["necko@mozilla.com", "vogosu@mozilla.com"],
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page channel: open() -> DNS request issued (ms)",
+ "bug_numbers": [1580077]
+ },
+ "HTTP_PAGE_DNS_LOOKUP_TIME": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["necko@mozilla.com", "vogosu@mozilla.com"],
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page channel: DNS lookup time (ms)",
+ "bug_numbers": [1580077]
+ },
+ "HTTP_PAGE_TLS_HANDSHAKE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "bug_numbers": [772589, 1580077],
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page channel: After TCP SYN to Ready for HTTP (ms)"
+ },
+ "HTTP_PAGE_TCP_CONNECTION_2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "bug_numbers": [772589, 1580077],
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page channel: TCP SYN to Ready for HTTP (ms)"
+ },
+ "HTTP_PAGE_OPEN_TO_FIRST_SENT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page channel: Open -> first byte of request sent (ms)"
+ },
+ "HTTP_PAGE_FIRST_SENT_TO_LAST_RECEIVED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page channel: First byte of request sent -> last byte of response received (ms)"
+ },
+ "HTTP_PAGE_OPEN_TO_FIRST_RECEIVED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page channel: Open -> first byte of reply received (ms)"
+ },
+ "HTTP_PAGE_OPEN_TO_FIRST_FROM_CACHE_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page channel: Open -> cache read start (ms), [cache2]"
+ },
+ "HTTP_PAGE_CACHE_READ_TIME_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page channel: Cache read time (ms) [cache2]"
+ },
+ "HTTP_PAGE_REVALIDATION": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page channel: Positive cache validation time (ms)"
+ },
+ "HTTP_PAGE_COMPLETE_LOAD_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page channel: Overall load time - all (ms) [cache2]"
+ },
+ "HTTP_PAGE_COMPLETE_LOAD_CACHED_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page channel: Overall load time - cache hits (ms) [cache2]"
+ },
+ "HTTP_PAGE_COMPLETE_LOAD_NET_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP page channel: Overall load time - network (ms) [cache2]"
+ },
+ "HTTP_SUB_DNS_ISSUE_TIME": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem channel: open() -> DNS request issued (ms)"
+ },
+ "HTTP_SUB_DNS_LOOKUP_TIME": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["necko@mozilla.com"],
+ "description": "HTTP subitem channel: DNS lookup time (ms)"
+ },
+ "HTTP_SUB_TLS_HANDSHAKE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [772589],
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem channel: After TCP SYN to Ready for HTTP (ms)"
+ },
+ "HTTP_SUB_TCP_CONNECTION_2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [772589],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem channel: TCP SYN to Ready for HTTP (ms)"
+ },
+ "HTTP_SUB_OPEN_TO_FIRST_SENT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem channel: Open -> first byte of request sent (ms)"
+ },
+ "HTTP_SUB_FIRST_SENT_TO_LAST_RECEIVED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem channel: First byte of request sent -> last byte of response received (ms)"
+ },
+ "HTTP_SUB_OPEN_TO_FIRST_RECEIVED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem channel: Open -> first byte of reply received (ms)"
+ },
+ "HTTP_SUB_OPEN_TO_FIRST_FROM_CACHE_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem channel: Open -> cache read start (ms) [cache2]"
+ },
+ "HTTP_SUB_CACHE_READ_TIME_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem channel: Cache read time (ms) [cache2]"
+ },
+ "HTTP_SUB_REVALIDATION": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem channel: Positive cache validation time (ms)"
+ },
+ "HTTP_SUB_COMPLETE_LOAD_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem channel: Overall load time - all (ms) [cache2]"
+ },
+ "HTTP_SUB_COMPLETE_LOAD_CACHED_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem channel: Overall load time - cache hits (ms) [cache2]"
+ },
+ "HTTP_SUB_COMPLETE_LOAD_NET_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "description": "HTTP subitem channel: Overall load time - network (ms) [cache2]"
+ },
+ "HTTP3_TLS_HANDSHAKE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "bug_numbers": [1675500],
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "description": "HTTP channel (keys: uses_http3_page, uses_http3_sub, supports_http3_page, supports_http3_sub): After TCP SYN to Ready for HTTP (ms)"
+ },
+ "SUP_HTTP3_TCP_CONNECTION": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "bug_numbers": [1675500],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "description": "HTTP channel(keys: supports_http3_page, supports_http3_sub): TCP SYN to Ready for HTTP (ms)"
+ },
+ "HTTP3_OPEN_TO_FIRST_SENT": {
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "bug_numbers": [1675500],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "description": "HTTP channel(keys: uses_http3_page, uses_http3_sub, supports_http3_page, supports_http3_sub): Open -> first byte of request sent (ms)"
+ },
+ "HTTP3_FIRST_SENT_TO_LAST_RECEIVED": {
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "bug_numbers": [1675500],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "description": "HTTP channel (keys: uses_http3_page, uses_http3_sub, supports_http3_page, supports_http3_sub): First byte of request sent -> last byte of response received (ms)"
+ },
+ "HTTP3_OPEN_TO_FIRST_RECEIVED": {
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "bug_numbers": [1675500],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "description": "HTTP channel (keys: uses_http3_page, uses_http3_sub, supports_http3_page, supports_http3_sub): Open -> first byte of reply received (ms)"
+ },
+ "HTTP3_COMPLETE_LOAD": {
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "bug_numbers": [1675500],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "description": "HTTP channel (keys: uses_http3_page, uses_http3_sub, supports_http3_page, supports_http3_sub): Overall load time - network (ms)"
+ },
+ "HTTP3_PERF_PAGE_LOAD_TIME_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "bug_numbers": [1675503],
+ "keyed": true,
+ "description": "Time in milliseconds from navigationStart to loadEventStart for the foreground http or https root content document. This is collected only on page load where the main document uses or suppports HTTP3"
+ },
+ "HTTP3_PERF_FIRST_CONTENTFUL_PAINT_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "bug_numbers": [1675503],
+ "keyed": true,
+ "description": "The time between navigationStart and the first contentful paint of a foreground http or https root content document, in milliseconds. The contentful paint timestamp is taken during display list building and does not include rasterization or compositing of that paint. This is collected only on page load where the main document uses or suppports HTTP3"
+ },
+ "HTTPS_RR_OPEN_TO_FIRST_SENT": {
+ "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"],
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "bug_numbers": [1697480],
+ "expires_in_version": "126",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "keyed": true,
+ "description": "HTTP channel(keys: uses_https_rr_page, uses_https_rr_sub, no_https_rr_page, no_https_rr_sub): Open -> first byte of request sent (ms)"
+ },
+ "H3P_PERF_PAGE_LOAD_TIME_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "bug_numbers": [1743965, 1796398],
+ "keyed": true,
+ "description": "Time in milliseconds from navigationStart to loadEventStart for the foreground http or https root content document. This is collected only on page load where the main document uses HTTP3. It is keyed based on whether a \"priority\" header has been received."
+ },
+ "H3P_PERF_FIRST_CONTENTFUL_PAINT_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "bug_numbers": [1743965, 1796398],
+ "keyed": true,
+ "description": "The time between navigationStart and the first contentful paint of a foreground http or https root content document, in milliseconds. The contentful paint timestamp is taken during display list building and does not include rasterization or compositing of that paint. This is collected only on page load where the main document uses HTTP3. It is keyed based on whether a \"priority\" header has been received."
+ },
+ "HTTP_PROXY_TYPE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 8,
+ "description": "HTTP Proxy Type (none, http, socks)"
+ },
+ "HTTP_TRANSACTION_IS_SSL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1340021],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether an HTTP request occurred over TLS/SSL or not. Recorded during response processing for all requests."
+ },
+ "HTTP_PAGELOAD_IS_SSL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1340021],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether a HTTP page load was over SSL or not. Recorded during response processing for all first-party page loads."
+ },
+ "HTTP_TRANSACTION_USE_ALTSVC": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether a HTTP transaction was routed via Alt-Svc or not."
+ },
+ "HTTP_TRANSACTION_USE_ALTSVC_OE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether a HTTP transaction routed via Alt-Svc was scheme=http"
+ },
+ "HTTP_ALTSVC_ENTRIES_PER_HEADER": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "bug_numbers": [1499149],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "kind": "enumerated",
+ "n_values": 5,
+ "description": "How many alt-svc productions were seen in a single Alt-Svc header"
+ },
+ "HTTP_ALTSVC_MAPPING_CHANGED_TARGET": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "bug_numbers": [1499149],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "kind": "boolean",
+ "description": "Whether or not a new alt-svc mapping would change the target hostname of the existing mapping"
+ },
+ "HTTP_SCHEME_UPGRADE_TYPE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["seceng-telemetry@mozilla.com", "freddyb@mozilla.com"],
+ "bug_numbers": [1340021, 1435733, 1722895],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": [
+ "AlreadyHTTPS",
+ "NoReasonToUpgrade",
+ "PrefBlockedSTS",
+ "STS",
+ "CSP",
+ "BrowserDisplay",
+ "HTTPSOnly",
+ "HTTPSFirst"
+ ],
+ "description": "Was the URL upgraded to HTTPS?"
+ },
+ "HTTP_RESPONSE_STATUS_CODE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["ckerschbaumer@mozilla.com"],
+ "bug_numbers": [1272345, 1296287, 1591131],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 12,
+ "description": "Whether the URL gets redirected? (0=200, 1=301, 2=302, 3=304, 4=307, 5=308, 6=400, 7=401, 8=403, 9=404, 10=500, 11=other)"
+ },
+ "HTTP_NET_VS_CACHE_ONSTART_QSMALL_NORMALPRI_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "84",
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1325322, 1524552, 1616069],
+ "kind": "enumerated",
+ "n_values": 80,
+ "description": "Network vs cache time load (OnStartRequest) difference (ms) for requests with a normal priority and small queue. Cache wins: 41-50 for 1-100ms, 51-59 for 101-1000ms, 60-68 for 1-10s, 69-73 for 11-60s and 74 for > 1m. Network wins: 39-30 for 1-100ms, 29-21 for 101-1000ms, 20-12 for 1-10s, 11-7 for 11-60s and 6 for > 1m."
+ },
+ "HTTP_NET_VS_CACHE_ONSTART_QMED_NORMALPRI_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "84",
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1325322, 1524552, 1616069],
+ "kind": "enumerated",
+ "n_values": 80,
+ "description": "Network vs cache time load (OnStartRequest) difference (ms) for requests with a normal priority and medium queue. Cache wins: 41-50 for 1-100ms, 51-59 for 101-1000ms, 60-68 for 1-10s, 69-73 for 11-60s and 74 for > 1m. Network wins: 39-30 for 1-100ms, 29-21 for 101-1000ms, 20-12 for 1-10s, 11-7 for 11-60s and 6 for > 1m."
+ },
+ "HTTP_NET_VS_CACHE_ONSTART_QBIG_NORMALPRI_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "84",
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1325322, 1524552, 1616069],
+ "kind": "enumerated",
+ "n_values": 80,
+ "description": "Network vs cache time load (OnStartRequest) difference (ms) for requests with a normal priority and large queue. Cache wins: 41-50 for 1-100ms, 51-59 for 101-1000ms, 60-68 for 1-10s, 69-73 for 11-60s and 74 for > 1m. Network wins: 39-30 for 1-100ms, 29-21 for 101-1000ms, 20-12 for 1-10s, 11-7 for 11-60s and 6 for > 1m."
+ },
+ "HTTP_NET_VS_CACHE_ONSTART_QSMALL_HIGHPRI_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "84",
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1325322, 1524552, 1616069],
+ "kind": "enumerated",
+ "n_values": 80,
+ "description": "Network vs cache time load (OnStartRequest) difference (ms) for requests with a high priority and small queue. Cache wins: 41-50 for 1-100ms, 51-59 for 101-1000ms, 60-68 for 1-10s, 69-73 for 11-60s and 74 for > 1m. Network wins: 39-30 for 1-100ms, 29-21 for 101-1000ms, 20-12 for 1-10s, 11-7 for 11-60s and 6 for > 1m."
+ },
+ "HTTP_NET_VS_CACHE_ONSTART_QMED_HIGHPRI_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "84",
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1325322, 1524552, 1616069],
+ "kind": "enumerated",
+ "n_values": 80,
+ "description": "Network vs cache time load (OnStartRequest) difference (ms) for requests with a high priority and medium queue. Cache wins: 41-50 for 1-100ms, 51-59 for 101-1000ms, 60-68 for 1-10s, 69-73 for 11-60s and 74 for > 1m. Network wins: 39-30 for 1-100ms, 29-21 for 101-1000ms, 20-12 for 1-10s, 11-7 for 11-60s and 6 for > 1m."
+ },
+ "HTTP_NET_VS_CACHE_ONSTART_QBIG_HIGHPRI_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "84",
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1325322, 1524552, 1616069],
+ "kind": "enumerated",
+ "n_values": 80,
+ "description": "Network vs cache time load (OnStartRequest) difference (ms) for requests with a high priority and large queue. Cache wins: 41-50 for 1-100ms, 51-59 for 101-1000ms, 60-68 for 1-10s, 69-73 for 11-60s and 74 for > 1m. Network wins: 39-30 for 1-100ms, 29-21 for 101-1000ms, 20-12 for 1-10s, 11-7 for 11-60s and 6 for > 1m."
+ },
+ "HTTP_NET_VS_CACHE_ONSTOP_QSMALL_NORMALPRI_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "84",
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1325322, 1524552, 1616069],
+ "kind": "enumerated",
+ "n_values": 80,
+ "description": "Network vs cache time load (OnStopRequest) difference (ms) for requests with a normal priority and small queue. Cache wins: 41-50 for 1-100ms, 51-59 for 101-1000ms, 60-68 for 1-10s, 69-73 for 11-60s and 74 for > 1m. Network wins: 39-30 for 1-100ms, 29-21 for 101-1000ms, 20-12 for 1-10s, 11-7 for 11-60s and 6 for > 1m."
+ },
+ "HTTP_NET_VS_CACHE_ONSTOP_QMED_NORMALPRI_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "84",
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1325322, 1524552, 1616069],
+ "kind": "enumerated",
+ "n_values": 80,
+ "description": "Network vs cache time load (OnStopRequest) difference (ms) for requests with a normal priority and medium queue. Cache wins: 41-50 for 1-100ms, 51-59 for 101-1000ms, 60-68 for 1-10s, 69-73 for 11-60s and 74 for > 1m. Network wins: 39-30 for 1-100ms, 29-21 for 101-1000ms, 20-12 for 1-10s, 11-7 for 11-60s and 6 for > 1m."
+ },
+ "HTTP_NET_VS_CACHE_ONSTOP_QBIG_NORMALPRI_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "84",
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1325322, 1524552, 1616069],
+ "kind": "enumerated",
+ "n_values": 80,
+ "description": "Network vs cache time load (OnStopRequest) difference (ms) for requests with a normal priority and large queue. Cache wins: 41-50 for 1-100ms, 51-59 for 101-1000ms, 60-68 for 1-10s, 69-73 for 11-60s and 74 for > 1m. Network wins: 39-30 for 1-100ms, 29-21 for 101-1000ms, 20-12 for 1-10s, 11-7 for 11-60s and 6 for > 1m."
+ },
+ "HTTP_NET_VS_CACHE_ONSTOP_QSMALL_HIGHPRI_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "84",
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1325322, 1524552, 1616069],
+ "kind": "enumerated",
+ "n_values": 80,
+ "description": "Network vs cache time load (OnStopRequest) difference (ms) for requests with a high priority and small queue. Cache wins: 41-50 for 1-100ms, 51-59 for 101-1000ms, 60-68 for 1-10s, 69-73 for 11-60s and 74 for > 1m. Network wins: 39-30 for 1-100ms, 29-21 for 101-1000ms, 20-12 for 1-10s, 11-7 for 11-60s and 6 for > 1m."
+ },
+ "HTTP_NET_VS_CACHE_ONSTOP_QMED_HIGHPRI_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "84",
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1325322, 1524552, 1616069],
+ "kind": "enumerated",
+ "n_values": 80,
+ "description": "Network vs cache time load (OnStopRequest) difference (ms) for requests with a high priority and medium queue. Cache wins: 41-50 for 1-100ms, 51-59 for 101-1000ms, 60-68 for 1-10s, 69-73 for 11-60s and 74 for > 1m. Network wins: 39-30 for 1-100ms, 29-21 for 101-1000ms, 20-12 for 1-10s, 11-7 for 11-60s and 6 for > 1m."
+ },
+ "HTTP_NET_VS_CACHE_ONSTOP_QBIG_HIGHPRI_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "84",
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1325322, 1524552, 1616069],
+ "kind": "enumerated",
+ "n_values": 80,
+ "description": "Network vs cache time load (OnStopRequest) difference (ms) for requests with a high priority and large queue. Cache wins: 41-50 for 1-100ms, 51-59 for 101-1000ms, 60-68 for 1-10s, 69-73 for 11-60s and 74 for > 1m. Network wins: 39-30 for 1-100ms, 29-21 for 101-1000ms, 20-12 for 1-10s, 11-7 for 11-60s and 6 for > 1m."
+ },
+ "HTTP_NET_VS_CACHE_ONSTOP_SMALL_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "84",
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1325322, 1524552, 1616069],
+ "kind": "enumerated",
+ "n_values": 80,
+ "description": "Network vs cache time load (OnStopRequest) difference (ms) for cache files with a small size (<256K). Cache wins: 41-50 for 1-100ms, 51-59 for 101-1000ms, 60-68 for 1-10s, 69-73 for 11-60s and 74 for > 1m. Network wins: 39-30 for 1-100ms, 29-21 for 101-1000ms, 20-12 for 1-10s, 11-7 for 11-60s and 6 for > 1m."
+ },
+ "HTTP_NET_VS_CACHE_ONSTOP_LARGE_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "84",
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1325322, 1524552, 1616069],
+ "kind": "enumerated",
+ "n_values": 80,
+ "description": "Network vs cache time load (OnStopRequest) difference (ms) for cache files with a large size (>=256K). Cache wins: 41-50 for 1-100ms, 51-59 for 101-1000ms, 60-68 for 1-10s, 69-73 for 11-60s and 74 for > 1m. Network wins: 39-30 for 1-100ms, 29-21 for 101-1000ms, 20-12 for 1-10s, 11-7 for 11-60s and 6 for > 1m."
+ },
+ "HTTP_NET_VS_CACHE_ONSTART_REVALIDATED_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "84",
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1325322, 1524552, 1616069],
+ "kind": "enumerated",
+ "n_values": 80,
+ "description": "Network vs cache time load (OnStartRequest) difference revalidated cache entries. Cache wins: 41-50 for 1-100ms, 51-59 for 101-1000ms, 60-68 for 1-10s, 69-73 for 11-60s and 74 for > 1m. Network wins: 39-30 for 1-100ms, 29-21 for 101-1000ms, 20-12 for 1-10s, 11-7 for 11-60s and 6 for > 1m."
+ },
+ "HTTP_NET_VS_CACHE_ONSTART_NOTREVALIDATED_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "84",
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1325322, 1524552, 1616069],
+ "kind": "enumerated",
+ "n_values": 80,
+ "description": "Network vs cache time load (OnStartRequest) difference (ms) not revalidated cache entries. Cache wins: 41-50 for 1-100ms, 51-59 for 101-1000ms, 60-68 for 1-10s, 69-73 for 11-60s and 74 for > 1m. Network wins: 39-30 for 1-100ms, 29-21 for 101-1000ms, 20-12 for 1-10s, 11-7 for 11-60s and 6 for > 1m."
+ },
+ "HTTP_NET_VS_CACHE_ONSTOP_REVALIDATED_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "84",
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1325322, 1524552, 1616069],
+ "kind": "enumerated",
+ "n_values": 80,
+ "description": "Network vs cache time load (OnStopRequest) difference (ms) revalidated cache entries. Cache wins: 41-50 for 1-100ms, 51-59 for 101-1000ms, 60-68 for 1-10s, 69-73 for 11-60s and 74 for > 1m. Network wins: 39-30 for 1-100ms, 29-21 for 101-1000ms, 20-12 for 1-10s, 11-7 for 11-60s and 6 for > 1m."
+ },
+ "HTTP_NET_VS_CACHE_ONSTOP_NOTREVALIDATED_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "84",
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1325322, 1524552, 1616069],
+ "kind": "enumerated",
+ "n_values": 80,
+ "description": "Network vs cache time load (OnStopRequest) difference (ms) not revalidated cache entries. Cache wins: 41-50 for 1-100ms, 51-59 for 101-1000ms, 60-68 for 1-10s, 69-73 for 11-60s and 74 for > 1m. Network wins: 39-30 for 1-100ms, 29-21 for 101-1000ms, 20-12 for 1-10s, 11-7 for 11-60s and 6 for > 1m."
+ },
+ "HTTP_ONSTART_SUSPEND_TOTAL_TIME": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1347948],
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 100,
+ "description": "Time in milliseconds that http channel spent suspended between AsyncOpen and OnStartRequest."
+ },
+ "HTTP_UPLOAD_BANDWIDTH_MBPS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "acreskey@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "bug_numbers": [1830087],
+ "keyed": true,
+ "description": "The upload bandwidth for requests larger than 10MB. Measured in megabits per second, Mbps. Keyed by HTTP protocol version."
+ },
+ "NETWORKING_DOWNLOAD_THROUGHPUT_HTTP_1": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "acreskey@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "bug_numbers": [1846798],
+ "description": "The download throughput for http/1.0, http/1.1 requests larger than 10MB. Measured in megabits per second, Mbps."
+ },
+ "NETWORKING_DOWNLOAD_THROUGHPUT_HTTP_2": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "acreskey@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "bug_numbers": [1846798],
+ "description": "The download throughput for http/2 requests larger than 10MB. Measured in megabits per second, Mbps."
+ },
+ "NETWORKING_DOWNLOAD_THROUGHPUT_HTTP_3": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "acreskey@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "bug_numbers": [1846798],
+ "description": "The download throughput for http/3 requests larger than 10MB. Measured in megabits per second, Mbps."
+ },
+ "HTTP_UPLOAD_THROUGHPUT_MBPS_HTTP_1": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "acreskey@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1858256],
+ "description": "The upload throughput for http/1.0, http/1.1 requests larger than 10MB. Measured in megabits per second, Mbps."
+ },
+ "HTTP_UPLOAD_THROUGHPUT_MBPS_HTTP_2": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "acreskey@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1858256],
+ "description": "The upload throughput for http/2 requests larger than 10MB. Measured in megabits per second, Mbps."
+ },
+ "HTTP_UPLOAD_THROUGHPUT_MBPS_HTTP_3": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "acreskey@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1858256],
+ "description": "The upload throughput for http/3 requests larger than 10MB. Measured in megabits per second, Mbps."
+ },
+ "HTTP_UPLOAD_THROUGHPUT_MBPS_HTTP_1_10_50": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1866739, 1858256],
+ "description": "The upload throughput for http/1.0, http/1.1 request size between 10MB and 50MB. Measured in megabits per second, Mbps."
+ },
+ "HTTP_UPLOAD_THROUGHPUT_MBPS_HTTP_1_50_100": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1866739, 1858256],
+ "description": "The upload throughput for http/1.0, http/1.1 request size between 50MB and 100MB. Measured in megabits per second, Mbps."
+ },
+ "HTTP_UPLOAD_THROUGHPUT_MBPS_HTTP_1_100": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1866739, 1858256],
+ "description": "The upload throughput for http/1.0, http/1.1 requests larger than 100MB. Measured in megabits per second, Mbps."
+ },
+ "HTTP_UPLOAD_THROUGHPUT_MBPS_HTTP_2_10_50": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1866739, 1858256],
+ "description": "The upload throughput for http/2 request size between 10MB and 50MB. Measured in megabits per second, Mbps."
+ },
+ "HTTP_UPLOAD_THROUGHPUT_MBPS_HTTP_2_50_100": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1866739, 1858256],
+ "description": "The upload throughput for http/2 request size between 50MB and 100MB. Measured in megabits per second, Mbps."
+ },
+ "HTTP_UPLOAD_THROUGHPUT_MBPS_HTTP_2_100": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1866739, 1858256],
+ "description": "The upload throughput for http/2 requests larger than 100MB. Measured in megabits per second, Mbps."
+ },
+ "HTTP_UPLOAD_THROUGHPUT_MBPS_HTTP_3_10_50": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1866739, 1858256],
+ "description": "The upload throughput for http/3 request size between 10MB and 50MB. Measured in megabits per second, Mbps."
+ },
+ "HTTP_UPLOAD_THROUGHPUT_MBPS_HTTP_3_50_100": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1866739, 1858256],
+ "description": "The upload throughput for http/3 request size between 50MB and 100MB. Measured in megabits per second, Mbps."
+ },
+ "HTTP_UPLOAD_THROUGHPUT_MBPS_HTTP_3_100": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1866739, 1858256],
+ "description": "The upload throughput for http/3 requests larger than 100MB. Measured in megabits per second, Mbps."
+ },
+ "NETWORK_RACE_CACHE_WITH_NETWORK_USAGE_2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1377340],
+ "kind": "categorical",
+ "labels": [
+ "NetworkNoRace",
+ "CacheNoRace",
+ "NetworkRace",
+ "CacheRace",
+ "NetworkDelayedRace",
+ "CacheDelayedRace"
+ ],
+ "description": "Whether we raced network with the cache."
+ },
+ "NETWORK_RACE_CACHE_WITH_NETWORK_SAVED_TIME": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1354407],
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 100,
+ "description": "Time in milliseconds that we saved when we race cache with network."
+ },
+ "NETWORK_RACE_CACHE_WITH_NETWORK_OCEC_ON_START_DIFF": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1354407],
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Time in milliseconds between onStartRequest from the cache and onCacheEntryCheck. Report only when net wins and OCEC is before onStartRequest from net."
+ },
+ "NETWORK_RACE_CACHE_BANDWIDTH_RACE_NETWORK_WIN": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 32,
+ "high": 16777216,
+ "n_buckets": 100,
+ "description": "Amount of bytes received when we decide to race cache with network and network wins.",
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1354405]
+ },
+ "NETWORK_RACE_CACHE_BANDWIDTH_RACE_CACHE_WIN": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 32,
+ "high": 16777216,
+ "n_buckets": 100,
+ "description": "Amount of bytes received when we decide to race cache with network and cache wins.",
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1354405]
+ },
+ "NETWORK_RACE_CACHE_BANDWIDTH_NOT_RACE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 32,
+ "high": 16777216,
+ "n_buckets": 100,
+ "description": "Amount of bytes received when we decide not to race cache with network.",
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1354405]
+ },
+ "NETWORK_RACE_CACHE_VALIDATION": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1377223],
+ "kind": "categorical",
+ "labels": ["NotSent", "CachedContentUsed", "CachedContentNotUsed"],
+ "description": "Stats for validation requests when cache won the race."
+ },
+ "NETWORK_BACK_PRESSURE_SUSPENSION_RATE_V2": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "bug_numbers": [1280629, 1494133, 1524552, 1616069, 1668513, 1700824],
+ "kind": "categorical",
+ "labels": [
+ "Suspended",
+ "NotSuspended",
+ "SuspendedLocal",
+ "NotSuspendedLocal"
+ ],
+ "description": "Collect whether the resource is suspended by back pressure. And split by local-ness."
+ },
+ "NETWORK_BACK_PRESSURE_SUSPENSION_CP_TYPE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "bug_numbers": [1487559, 1524552, 1616069, 1668513, 1700824],
+ "kind": "enumerated",
+ "n_values": 64,
+ "description": "Collect the content policy when the resource is non-local and suspended by back pressure (0-44 from nsContentPolicyType in nsIContentPolicy.idl)."
+ },
+ "NETWORK_BACK_PRESSURE_SUSPENSION_DELAY_TIME_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com", "damjanovic@mozilla.com"],
+ "bug_numbers": [1498434, 1524552, 1616069, 1668513, 1700824],
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 100,
+ "description": "The delay caused by the e10s back pressure suspension(ms)"
+ },
+ "NETWORK_HTTP_REDIRECT_TO_SCHEME": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1413512, 1567462],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "description": "Count of the HTTP redirection that triggered by top-level document or by subresource, keyed by the URL scheme redirected to.",
+ "labels": ["topLevel", "subresource"]
+ },
+ "NETWORK_ASYNC_OPEN_CHILD_TO_TRANSACTION_PENDING_EXP_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com", "acreskey@mozilla.com"],
+ "bug_numbers": [1789468],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "kind": "exponential",
+ "high": 2000,
+ "n_buckets": 50,
+ "description": "The time spent from HttpChannelChild::AsyncOpen to adding the transactionto the nsHttpConnectionMgr, in milliseconds, keyed by the classOfService flags"
+ },
+ "NETWORK_RESPONSE_START_PARENT_TO_CONTENT_EXP_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com", "acreskey@mozilla.com"],
+ "bug_numbers": [1793975],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "kind": "exponential",
+ "high": 2000,
+ "n_buckets": 50,
+ "description": "The time spent relaying response start from the socket thread of the parent process to the content process, in milliseconds, keyed by the classOfService flags"
+ },
+ "NETWORK_DNS_END_TO_CONNECT_START_EXP_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com", "acreskey@mozilla.com"],
+ "bug_numbers": [1791077],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "kind": "exponential",
+ "high": 2000,
+ "n_buckets": 50,
+ "description": "The time spent from dns resolution to connect start, in milliseconds, keyed by protocol version and the classOfService flags"
+ },
+ "NETWORK_RESPONSE_END_PARENT_TO_CONTENT_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com", "acreskey@mozilla.com"],
+ "bug_numbers": [1795775],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "kind": "exponential",
+ "high": 2000,
+ "n_buckets": 50,
+ "description": "The time spent relaying response completion from the socket thread of the parent process to the content process, in milliseconds, keyed by the classOfService flags"
+ },
+ "HTTP_AUTH_DIALOG_STATS_3": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "65",
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "bug_numbers": [1357835],
+ "kind": "enumerated",
+ "n_values": 64,
+ "description": "Stats about what kind of resource requested http authentication. (29=top-level doc, 30=same origin subresources, 31=same origin xhr, 32=non-web-content, (nsIContentPolicy type)=cross-origin subresources per nsIContentPolicy type)"
+ },
+ "HTTP_AUTH_TYPE_STATS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["rbarnes@mozilla.com"],
+ "bug_numbers": [1266571],
+ "expires_in_version": "52",
+ "kind": "enumerated",
+ "n_values": 8,
+ "releaseChannelCollection": "opt-out",
+ "description": "Recorded once for each HTTP 401 response. The value records the type of authentication and the TLS-enabled status. (0=basic/clear, 1=basic/tls, 2=digest/clear, 3=digest/tls, 4=ntlm/clear, 5=ntlm/tls, 6=negotiate/clear, 7=negotiate/tls)"
+ },
+ "HTTP_CHILD_OMT_STATS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1357682],
+ "expires_in_version": "61",
+ "kind": "categorical",
+ "keyed": true,
+ "description": "Stats about success rate of HTTP OMT request in content process, keyed by content policy.",
+ "labels": [
+ "success",
+ "successMainThread",
+ "failListener",
+ "failListenerChain",
+ "notRequested"
+ ]
+ },
+ "TLS_EARLY_DATA_NEGOTIATED": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 3,
+ "description": "Sending TLS early data was possible: 0 - not possible, 1 - possible but not used, 2 - possible and used.",
+ "alert_emails": [
+ "necko@mozilla.com",
+ "ddamjanovic@mozilla.com",
+ "vgosu@mozilla.com"
+ ],
+ "bug_numbers": [1296288, 1654309, 1749881],
+ "releaseChannelCollection": "opt-out"
+ },
+ "TLS_EARLY_DATA_ACCEPTED": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "TLS early data was used and it was accepted (true) or rejected (false) by the remote host.",
+ "alert_emails": [
+ "necko@mozilla.com",
+ "ddamjanovic@mozilla.com",
+ "vgosu@mozilla.com"
+ ],
+ "bug_numbers": [1296288, 1654309, 1749881],
+ "releaseChannelCollection": "opt-out"
+ },
+ "TLS_EARLY_DATA_BYTES_WRITTEN": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 100,
+ "description": "Amount of bytes sent using TLS early data at the start of a TLS connection for a given channel.",
+ "alert_emails": [
+ "necko@mozilla.com",
+ "ddamjanovic@mozilla.com",
+ "vgosu@mozilla.com"
+ ],
+ "bug_numbers": [1296288, 1654309, 1749881],
+ "releaseChannelCollection": "opt-out"
+ },
+ "SSL_HANDSHAKE_VERSION": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1250568, 1340021],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 16,
+ "description": "Negotiated SSL Version (1=tls1, 2=tls1.1, 3=tls1.2, 4=tls1.3)"
+ },
+ "SSL_HANDSHAKE_PRIVACY": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1788290],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 16,
+ "description": "0th bit - TLS13 used? 1th bit - Revocation Privacy, 2nd bit - DNS Privacy, 3rd bit - ECH Privacy"
+ },
+ "SSL_HANDSHAKE_RESULT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1331280, 1340021],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 672,
+ "description": "SSL handshake result, 0=success, 1-255=NSS error offset, 256-511=SEC error offset + 256, 512-639=NSPR error offset + 512, 640-670=PKIX error, 671=unknown err"
+ },
+ "SSL_HANDSHAKE_RESULT_FIRST_TRY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1780014],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 672,
+ "description": "SSL handshake result for first-try connections, 0=success, 1-255=NSS error offset, 256-511=SEC error offset + 256, 512-639=NSPR error offset + 512, 640-670=PKIX error, 671=unknown err"
+ },
+ "SSL_HANDSHAKE_RESULT_CONSERVATIVE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1780014],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 672,
+ "description": "SSL handshake result for conservative mode connections, 0=success, 1-255=NSS error offset, 256-511=SEC error offset + 256, 512-639=NSPR error offset + 512, 640-670=PKIX error, 671=unknown err"
+ },
+ "SSL_HANDSHAKE_RESULT_ECH": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1771479],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 672,
+ "description": "SSL handshake result for connections which used ECH 'Real', 0=success, 1-255=NSS error offset, 256-511=SEC error offset + 256, 512-639=NSPR error offset + 512, 640-670=PKIX error, 671=unknown err"
+ },
+ "SSL_HANDSHAKE_RESULT_ECH_GREASE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1771479],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 672,
+ "description": "SSL handshake result for connections which used ECH GREASE, 0=success, 1-255=NSS error offset, 256-511=SEC error offset + 256, 512-639=NSPR error offset + 512, 640-670=PKIX error, 671=unknown err"
+ },
+ "SSL_TIME_UNTIL_READY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1340021],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 200,
+ "description": "ms of SSL wait time including TCP and proxy tunneling"
+ },
+ "SSL_TIME_UNTIL_READY_FIRST_TRY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1340021],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 200,
+ "description": "ms of SSL wait time including TCP and proxy tunneling for first-try connections"
+ },
+ "SSL_TIME_UNTIL_READY_CONSERVATIVE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1340021],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 200,
+ "description": "ms of SSL wait time including TCP and proxy tunneling for conservative-mode connections"
+ },
+ "SSL_TIME_UNTIL_READY_ECH": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1771479],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 200,
+ "description": "ms of SSL wait time including TCP and proxy tunneling for connections using ECH 'Real'"
+ },
+ "SSL_TIME_UNTIL_READY_ECH_GREASE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1771479],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 200,
+ "description": "ms of SSL wait time including TCP and proxy tunneling for connections using ECH GREASE"
+ },
+ "SSL_TIME_UNTIL_HANDSHAKE_FINISHED_KEYED_BY_KA": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1340021, 1513839],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "keyed": true,
+ "high": 60000,
+ "n_buckets": 200,
+ "description": "ms of SSL wait time for full handshake including TCP and proxy tunneling, keyed by the key exchange algorithm used"
+ },
+ "HTTP3_ECH_OUTCOME": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "seceng-telemetry@mozilla.com",
+ "necko@mozilla.com",
+ "djackson@mozilla.com"
+ ],
+ "bug_numbers": [182287],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 32,
+ "description": "Success / Fail Rates for HTTP3 Keyed by ECH Usage"
+ },
+ "SSL_BYTES_BEFORE_CERT_CALLBACK": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1862062],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 32000,
+ "n_buckets": 64,
+ "description": "plaintext bytes read before a server certificate authenticated"
+ },
+ "SSL_NPN_TYPE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 16,
+ "description": "NPN Results (0=none, 1=negotiated, 2=no-overlap, 3=selected(alpn))"
+ },
+ "SSL_RESUMED_SESSION": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1340021],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "complete TLS connect that used TLS Session Resumption (collected at same time as SSL_TIME_UNTIL_HANDSHAKE_FINISHED)"
+ },
+ "CERT_VALIDATION_HTTP_REQUEST_RESULT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1862062],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 16,
+ "description": "HTTP result of OCSP, etc.. (0=canceled, 1=OK, 2=FAILED, 3=internal-error)"
+ },
+ "CERT_VALIDATION_HTTP_REQUEST_CANCELED_TIME": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1862062],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 200,
+ "description": "ms elapsed time of OCSP etc.. that was canceled"
+ },
+ "CERT_VALIDATION_HTTP_REQUEST_SUCCEEDED_TIME": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1862062],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 200,
+ "description": "ms elapsed time of OCSP etc.. that succeeded"
+ },
+ "CERT_VALIDATION_HTTP_REQUEST_FAILED_TIME": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1862062],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 200,
+ "description": "ms elapsed time of OCSP etc.. that failed"
+ },
+ "SSL_KEY_EXCHANGE_ALGORITHM_FULL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1862062],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 16,
+ "description": "SSL Handshake Key Exchange Algorithm for full handshake (null=0, rsa=1, dh=2, fortezza=3, ecdh=4)"
+ },
+ "SSL_KEY_EXCHANGE_ALGORITHM_RESUMED": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1862062],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 16,
+ "description": "SSL Handshake Key Exchange Algorithm for resumed handshake (null=0, rsa=1, dh=2, fortezza=3, ecdh=4)"
+ },
+ "OCSP_AGE_AT_CRLITE_OVERRIDE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com", "jschanck@mozilla.com"],
+ "bug_numbers": [1794479, 1817101, 1846897],
+ "expires_in_version": "125",
+ "kind": "linear",
+ "releaseChannelCollection": "opt-out",
+ "low": 1,
+ "high": 240,
+ "n_buckets": 20,
+ "description": "When OCSP and CRLite differ, how old is the OCSP response (in hours)?"
+ },
+ "CRLITE_VS_OCSP_RESULT": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com", "dkeeler@mozilla.com"],
+ "bug_numbers": [1675655, 1758827, 1817102, 1846898],
+ "expires_in_version": "125",
+ "kind": "categorical",
+ "releaseChannelCollection": "opt-out",
+ "description": "Does CRLite and OCSP fetching agree when a certificate is revoked?",
+ "labels": [
+ "CRLiteOkOCSPFail",
+ "CRLiteRevOCSPFail",
+ "CRLiteOkOCSPOk",
+ "CRLiteOkOCSPRev",
+ "CRLiteRevOCSPOk",
+ "CRLiteRevOCSPRev",
+ "CRLiteOkOCSPUnk",
+ "CRLiteRevOCSPUnk",
+ "CRLiteOkOCSPSoft",
+ "CRLiteRevOCSPSoft"
+ ]
+ },
+ "CERT_REVOCATION_MECHANISMS": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com", "jschanck@mozilla.com"],
+ "bug_numbers": [1794450, 1817101, 1846897],
+ "expires_in_version": "125",
+ "kind": "categorical",
+ "releaseChannelCollection": "opt-out",
+ "description": "Which revocation checking mechanisms were used?",
+ "labels": [
+ "CRLite",
+ "StapledOCSP",
+ "CachedOCSP",
+ "OCSP",
+ "OneCRL",
+ "ShortValidity"
+ ]
+ },
+ "WEBSOCKETS_HANDSHAKE_TYPE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 16,
+ "description": "Websockets Handshake Results (ws-ok-plain, ws-ok-proxy, ws-failed-plain, ws-failed-proxy, wss-ok-plain, wss-ok-proxy, wss-failed-plain, wss-failed-proxy)"
+ },
+ "HTTP_RESPONSE_VERSION": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 48,
+ "description": "HTTP: Protocol Version Used on Response from nsHttp.h"
+ },
+ "HTTP_09_INFO": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 4,
+ "description": "HTTP 09 Response Breakdown: lowbit subresource, high bit nonstd port",
+ "bug_numbers": [1262572],
+ "alert_emails": ["necko@mozilla.com"]
+ },
+ "SPDY_PARALLEL_STREAMS": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "SPDY: Streams concurrent active per connection"
+ },
+ "SPDY_REQUEST_PER_CONN_3": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "bug_numbers": [1505500, 1642559],
+ "description": "SPDY: Streams created per connection"
+ },
+ "SPDY_SERVER_INITIATED_STREAMS": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 250,
+ "description": "SPDY: Streams recevied per connection"
+ },
+ "SPDY_CHUNK_RECVD": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "SPDY: Recvd Chunk Size (rounded to KB)"
+ },
+ "SPDY_SYN_SIZE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 20,
+ "high": 20000,
+ "n_buckets": 50,
+ "description": "SPDY: SYN Frame Header Size"
+ },
+ "SPDY_SYN_RATIO": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 99,
+ "n_buckets": 20,
+ "description": "SPDY: SYN Frame Header Ratio (lower better)"
+ },
+ "SPDY_SYN_REPLY_SIZE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 16,
+ "high": 20000,
+ "n_buckets": 50,
+ "description": "SPDY: SYN Reply Header Size"
+ },
+ "SPDY_SYN_REPLY_RATIO": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 99,
+ "n_buckets": 20,
+ "description": "SPDY: SYN Reply Header Ratio (lower better)"
+ },
+ "SPDY_NPN_CONNECT": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "SPDY: NPN Negotiated"
+ },
+ "SPDY_NPN_JOIN": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "SPDY: Coalesce Succeeded"
+ },
+ "SPDY_KBREAD_PER_CONN2": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1520260],
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 50,
+ "description": "SPDY: KB read per connection"
+ },
+ "SPDY_SETTINGS_MAX_STREAMS": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "description": "H2: Settings Max Streams parameter"
+ },
+ "SPDY_SETTINGS_IW": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "H2: Settings Initial Window (rounded to KB)"
+ },
+ "SPDY_GOAWAY_LOCAL": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 32,
+ "description": "H2: goaway reason client sent from rfc 7540. 31 is none sent."
+ },
+ "SPDY_GOAWAY_PEER": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 32,
+ "description": "H2: goaway reason from peer from rfc 7540. 31 is none received."
+ },
+ "SPDY_CONTINUED_HEADERS": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1324855],
+ "kind": "exponential",
+ "high": 32000000,
+ "n_buckets": 75,
+ "releaseChannelCollection": "opt-out",
+ "description": "Size of continued H2 headers in bytes."
+ },
+ "HPACK_ELEMENTS_EVICTED_DECOMPRESSOR": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 256,
+ "n_buckets": 50,
+ "description": "HPACK: Number of items removed from dynamic table to make room for 1 new item",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1296280]
+ },
+ "HPACK_BYTES_EVICTED_DECOMPRESSOR": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 8192,
+ "n_buckets": 50,
+ "description": "HPACK: Number of bytes removed from dynamic table to make room for 1 new item",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1296280]
+ },
+ "HPACK_BYTES_EVICTED_RATIO_DECOMPRESSOR": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 256,
+ "n_buckets": 50,
+ "description": "HPACK: Ratio of bytes evicted to bytes added (* 100)",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1296280]
+ },
+ "HPACK_PEAK_COUNT_DECOMPRESSOR": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1024,
+ "n_buckets": 50,
+ "description": "HPACK: peak number of items in the dynamic table",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1296280]
+ },
+ "HPACK_PEAK_SIZE_DECOMPRESSOR": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 16384,
+ "n_buckets": 100,
+ "description": "HPACK: peak size in bytes of the table",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1296280]
+ },
+ "HPACK_ELEMENTS_EVICTED_COMPRESSOR": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 256,
+ "n_buckets": 50,
+ "description": "HPACK: Number of items removed from dynamic table to make room for 1 new item",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1296280]
+ },
+ "HPACK_BYTES_EVICTED_COMPRESSOR": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 8192,
+ "n_buckets": 50,
+ "description": "HPACK: Number of bytes removed from dynamic table to make room for 1 new item",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1296280]
+ },
+ "HPACK_BYTES_EVICTED_RATIO_COMPRESSOR": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 256,
+ "n_buckets": 50,
+ "description": "HPACK: Ratio of bytes evicted to bytes added (* 100)",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1296280]
+ },
+ "HPACK_PEAK_COUNT_COMPRESSOR": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1024,
+ "n_buckets": 50,
+ "description": "HPACK: peak number of items in the dynamic table",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1296280]
+ },
+ "HPACK_PEAK_SIZE_COMPRESSOR": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 16384,
+ "n_buckets": 100,
+ "description": "HPACK: peak size in bytes of the table",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1296280]
+ },
+ "HTTP2_FAIL_BEFORE_SETTINGS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether an HTTP/2 session failed because the peer did not handshake properly",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1050329]
+ },
+ "HTTP_CHANNEL_DISPOSITION": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1341128],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 16,
+ "releaseChannelCollection": "opt-out",
+ "description": "Channel Disposition: 0=Cancel, 1=Disk, 2=NetOK, 3=NetEarlyFail, 4=NetlateFail, +8 for HTTPS"
+ },
+ "HTTP_CHANNEL_DISPOSITION_UPGRADE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "necko@mozilla.com",
+ "seceng-telemetry@mozilla.com",
+ "jkt@mozilla.com"
+ ],
+ "bug_numbers": [1440701],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "description": "Upgrading display content Channel Disposition",
+ "labels": ["cancel", "disk", "netOk", "netEarlyFail", "netLateFail"]
+ },
+ "HTTP_CHANNEL_ONSTART_SUCCESS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Successfully started HTTP channels",
+ "bug_numbers": [1473333, 1587226],
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"]
+ },
+ "HTTP_CHANNEL_PAGE_ONSTART_SUCCESS_TRR3": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 16,
+ "description": "Successfully started HTTP channels for page loading when TRR is enabled; Keyed by TRR domain",
+ "bug_numbers": [1767417],
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"]
+ },
+ "HTTP_CHANNEL_SUB_ONSTART_SUCCESS_TRR3": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 16,
+ "description": "Successfully started HTTP channels for resource loading when TRR is enabled; Keyed by TRR domain",
+ "bug_numbers": [1767417],
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"]
+ },
+ "HTTP_CONNECTION_ENTRY_CACHE_HIT_1": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Fraction of sockets that used a nsConnectionEntry with history - size 300."
+ },
+ "HTTP_CACHE_DISPOSITION_3": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1611185],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": [
+ "Unresolved",
+ "Hit",
+ "HitViaReval",
+ "MissedViaReval",
+ "Missed",
+ "Unknown"
+ ],
+ "keyed": true,
+ "keys": [
+ "ALL",
+ "UNKNOWN",
+ "OTHER",
+ "JAVASCRIPT",
+ "IMAGE",
+ "MEDIA",
+ "STYLESHEET",
+ "WASM"
+ ],
+ "description": "HTTP Cache v2 hit/miss stats keyed by content type."
+ },
+ "HTTP_CACHE_ENTRY_RELOAD_TIME": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 900000,
+ "n_buckets": 50,
+ "description": "Time before we reload an HTTP cache entry again to memory"
+ },
+ "HTTP_CACHE_ENTRY_ALIVE_TIME": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 7200000,
+ "n_buckets": 50,
+ "description": "Time for which an HTTP cache entry is kept warmed in memory"
+ },
+ "HTTP_CACHE_ENTRY_REUSE_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 20,
+ "n_buckets": 19,
+ "description": "Reuse count of an HTTP cache entry warmed in memory"
+ },
+ "HTTP_CACHE_IO_QUEUE_2_OPEN_PRIORITY": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["hbambas@mozilla.com"],
+ "bug_numbers": [1294183],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "HTTP Cache IO queue length"
+ },
+ "HTTP_CACHE_IO_QUEUE_2_READ_PRIORITY": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["hbambas@mozilla.com"],
+ "bug_numbers": [1294183],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "HTTP Cache IO queue length"
+ },
+ "HTTP_CACHE_IO_QUEUE_2_MANAGEMENT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["hbambas@mozilla.com"],
+ "bug_numbers": [1294183],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "HTTP Cache IO queue length"
+ },
+ "HTTP_CACHE_IO_QUEUE_2_OPEN": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["hbambas@mozilla.com"],
+ "bug_numbers": [1294183],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "HTTP Cache IO queue length"
+ },
+ "HTTP_CACHE_IO_QUEUE_2_READ": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["hbambas@mozilla.com"],
+ "bug_numbers": [1294183],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "HTTP Cache IO queue length"
+ },
+ "HTTP_CACHE_IO_QUEUE_2_WRITE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["hbambas@mozilla.com"],
+ "bug_numbers": [1294183],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "HTTP Cache IO queue length"
+ },
+ "HTTP_CACHE_IO_QUEUE_2_WRITE_PRIORITY": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["hbambas@mozilla.com"],
+ "bug_numbers": [1294183],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "HTTP Cache IO queue length"
+ },
+ "HTTP_CACHE_IO_QUEUE_2_INDEX": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["hbambas@mozilla.com"],
+ "bug_numbers": [1294183],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "HTTP Cache IO queue length"
+ },
+ "HTTP_CACHE_IO_QUEUE_2_EVICT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["hbambas@mozilla.com"],
+ "bug_numbers": [1294183],
+ "expires_in_version": "55",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "HTTP Cache IO queue length"
+ },
+ "CACHE_DEVICE_SEARCH_2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time to search cache (ms)"
+ },
+ "TRANSACTION_WAIT_TIME_HTTP": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "bug_numbers": [1605099],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "description": "Time from submission to dispatch of HTTP transaction (ms)"
+ },
+ "TRANSACTION_WAIT_TIME_SPDY": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "bug_numbers": [1605099],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "description": "Time from submission to dispatch of SPDY transaction (ms)"
+ },
+ "TRANSACTION_WAIT_TIME_HTTP3": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "bug_numbers": [1605099],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "description": "Time from submission to dispatch of HTTP/3 transaction (ms)"
+ },
+ "TRANSACTION_WAIT_TIME_HTTP2_SUP_HTTP3": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "bug_numbers": [1675500],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "description": "Time from submission to dispatch of HTTP2 transaction that has received a respone from a server that addretizes HTTP3 support (ms)"
+ },
+ "HTTP_SAW_QUIC_ALT_PROTOCOL_2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "bug_numbers": [1605099],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "enumerated",
+ "n_values": 3,
+ "description": "Does a response has a h3(1), other quic(2) or non(0) alt-protocol advertisement."
+ },
+ "HTTP3_CONNECTION_CLOSE_CODE_3": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "necko@mozilla.com",
+ "ddamjanovic@mozilla.com",
+ "kershaw@mozilla.com"
+ ],
+ "bug_numbers": [1605099, 1688091, 1693738, 1695785],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "enumerated",
+ "n_values": 100,
+ "keyed": true,
+ "description": "Error code when http3 connection is closed. Look at Http3Session for more details."
+ },
+ "HTTP3_0RTT_STATE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "bug_numbers": [1688850],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "enumerated",
+ "n_values": 5,
+ "description": "Collect what is the outcome when 0rtt is used: (0)0RTT_NOT_USED, (1)0RTT_USED_SUCCESS, (2)0RTT_USED_REJECT, (3)0RTT_USED_CONN_ERROR, (4)0RTT_CONN_CLOSED_BY_NECKO."
+ },
+ "HTTP3_0RTT_STATE_DURATION": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "bug_numbers": [1688850],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "keyed": true,
+ "description": "The time a connection was in the zero rtt state, it is keyed by the zero rtt outcome."
+ },
+ "HTTP3_TIME_TO_REUSE_IDLE_CONNECTTION_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50,
+ "keyed": true,
+ "description": "Time to reuse an idle connection in HTTP3(ms); keyed by the transaction reusing an idle connection has succeeded or failed",
+ "bug_numbers": [1655566]
+ },
+ "HTTP3_TIMER_DELAYED": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50,
+ "description": "Time difference between setting timer value and actually firing the timer for calling neqo",
+ "bug_numbers": [1655566]
+ },
+ "HTTP3_REQUEST_PER_CONN": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "bug_numbers": [1652104],
+ "description": "HTTP3: Streams created per connection"
+ },
+ "HTTP3_BLOCKED_BY_STREAM_LIMIT_PER_CONN": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "bug_numbers": [1655566],
+ "description": "HTTP3: number of times a connection is blocked by maximum allowed number of parallel streams per connection"
+ },
+ "HTTP3_TRANS_BLOCKED_BY_STREAM_LIMIT_PER_CONN": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "bug_numbers": [1655566],
+ "description": "HTTP3: number of transactions that are blocked by maximum allowed number of parallel streams per connection"
+ },
+ "HTTP3_TRANS_SENDING_BLOCKED_BY_FLOW_CONTROL_PER_CONN": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "bug_numbers": [1655566],
+ "description": "HTTP3: number of times streams are blocked by the flow control while sending data per connection"
+ },
+ "HTTP3_SENDING_BLOCKED_BY_FLOW_CONTROL_PER_TRANS": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "bug_numbers": [1655566],
+ "description": "HTTP3: number of times when a stream is blocked by the flow control while sendnig data."
+ },
+ "HTTP3_LOSS_RATIO": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 2000,
+ "n_buckets": 100,
+ "bug_numbers": [1677951],
+ "description": "HTTP3: packet loss ratio (multiply by 10000)."
+ },
+ "HTTP3_LATE_ACK_RATIO": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 2000,
+ "n_buckets": 100,
+ "keyed": true,
+ "bug_numbers": [1677951],
+ "description": "HTTP3: spurious retransmissions ratio (spurios_retransmission / packet sent * 10000)."
+ },
+ "HTTP3_LATE_ACK": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "keyed": true,
+ "bug_numbers": [1677951],
+ "description": "HTTP3: the number of spurious retransmissions."
+ },
+ "HTTP3_COUNTS_PTO": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "keyed": true,
+ "bug_numbers": [1677951],
+ "description": "HTTP3: the number of PTOs."
+ },
+ "HTTP3_DROP_DGRAMS": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1677951],
+ "description": "HTTP3: the number of dropped datagrams."
+ },
+ "HTTP3_SAVED_DGRAMS": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1677951],
+ "description": "HTTP3: the number of saved datagrams that are waiting for keys to be available."
+ },
+ "HTTP3_RECEIVED_SENT_DGRAMS": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 100,
+ "keyed": true,
+ "bug_numbers": [1677951],
+ "description": "HTTP3: the number of received/sent packets."
+ },
+ "HTTP3_CHANNEL_ONSTART_SUCCESS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Successfully started HTTP channels when HTTP3 is used",
+ "bug_numbers": [1677987],
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"]
+ },
+ "HTTP_CONTENT_ENCODING": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 6,
+ "description": "encoding removed: 0=unknown, 1=gzip, 2=deflate, 3=brotli"
+ },
+ "CACHE_LM_INCONSISTENT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Cache discovered inconsistent last-modified entry"
+ },
+ "DNT_USAGE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 3,
+ "description": "I want to be tracked, I do NOT want to be tracked, DNT unset"
+ },
+ "DNS_LOOKUP_METHOD2": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 16,
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["necko@mozilla.com"],
+ "description": "DNS Lookup Type (hit, renewal, negative-hit, literal, overflow, network-first, network-shared)"
+ },
+ "DNS_CLEANUP_AGE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1440,
+ "n_buckets": 50,
+ "description": "DNS Cache Entry Age at Removal Time (minutes)"
+ },
+ "DNS_BY_TYPE_CLEANUP_AGE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1440,
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "bug_numbers": [1481251],
+ "releaseChannelCollection": "opt-out",
+ "n_buckets": 50,
+ "description": "DNS Cache Entry Age for by-type queries at Removal Time (minutes)"
+ },
+ "DNS_PREMATURE_EVICTION": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1440,
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1460305],
+ "n_buckets": 50,
+ "description": "DNS Cache Entry Age at Removal Time of non-expired entries (minutes)"
+ },
+ "DNS_BY_TYPE_PREMATURE_EVICTION": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1440,
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "bug_numbers": [1481251],
+ "n_buckets": 50,
+ "description": "DNS Cache Entry Age for by-type queries at Removal Time of non-expired entries (minutes)"
+ },
+ "DNS_LOOKUP_TIME": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["necko@mozilla.com"],
+ "n_buckets": 50,
+ "description": "Time for a successful DNS resolution (msec)"
+ },
+ "DNS_TRR_LOOKUP_TIME3": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["necko@mozilla.com", "vgosu@mozilla.com"],
+ "bug_numbers": [1434852, 1640867, 1646452],
+ "n_buckets": 50,
+ "description": "Time for a completed TRR resolution (msec); Keyed by TRR domain"
+ },
+ "DNS_TRR_PROCESSING_TIME": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1608114],
+ "n_buckets": 50,
+ "description": "Time from the last received byte of the response until we have notified the consumer with a DNS record (msec)"
+ },
+ "DNS_TRR_HTTP_VERSION2": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "keyed": true,
+ "labels": ["h_1", "h_2", "h_3"],
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["nhnt11@mozilla.com", "necko@mozilla.com"],
+ "bug_numbers": [1460314, 1700119],
+ "description": "HTTP version used by DoH endpoint"
+ },
+ "DNS_NATIVE_LOOKUP_TIME": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1434852],
+ "n_buckets": 50,
+ "description": "Time for a completed native name resolution (msec)"
+ },
+ "DNS_BY_TYPE_FAILED_LOOKUP_TIME": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "bug_numbers": [1481251],
+ "n_buckets": 50,
+ "description": "Time for a completed by-type resolution that has a negative answer (msec)."
+ },
+ "DNS_BY_TYPE_SUCCEEDED_LOOKUP_TIME": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "bug_numbers": [1481251],
+ "n_buckets": 50,
+ "description": "Time for a completed by-type resolution that succeeded (msec)."
+ },
+ "DNS_NATIVE_QUEUING": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1470215],
+ "n_buckets": 50,
+ "description": "Time in resolve queue waiting to getaddrinfo (msec)"
+ },
+ "TRR_SKIP_REASON_TRR_FIRST2": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "necko@mozilla.com",
+ "vgosu@mozilla.com",
+ "nhnt11@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 50,
+ "bug_numbers": [1649143, 1699523],
+ "releaseChannelCollection": "opt-out",
+ "description": "When in TRR-first mode, it lists the reason we may have skipped TRR, keyed by the provider."
+ },
+ "TRR_SKIP_REASON_NATIVE_SUCCESS": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "necko@mozilla.com",
+ "vgosu@mozilla.com",
+ "nhnt11@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 50,
+ "bug_numbers": [1649143, 1699523],
+ "releaseChannelCollection": "opt-out",
+ "description": "When in TRR-first mode, if TRR was skipped and native succeeded, it lists the reason we may have skipped TRR, keyed by the provider."
+ },
+ "TRR_SKIP_REASON_NATIVE_FAILED": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "necko@mozilla.com",
+ "vgosu@mozilla.com",
+ "nhnt11@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 50,
+ "bug_numbers": [1699523],
+ "releaseChannelCollection": "opt-out",
+ "description": "When in TRR-first mode, if TRR was skipped and native failed, it lists the reason we may have skipped TRR, keyed by the provider."
+ },
+
+ "TRR_RELEVANT_SKIP_REASON_TRR_FIRST": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "vgosu@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 50,
+ "bug_numbers": [1649143, 1699523, 1742408],
+ "releaseChannelCollection": "opt-out",
+ "description": "When in TRR-first mode, it lists the reason we may have skipped TRR, keyed by the provider. Does not include requests that intentionally skip TRR."
+ },
+ "TRR_RELEVANT_SKIP_REASON_NATIVE_SUCCESS": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "vgosu@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 50,
+ "bug_numbers": [1649143, 1699523, 1742408],
+ "releaseChannelCollection": "opt-out",
+ "description": "When in TRR-first mode, if TRR was skipped and native succeeded, it lists the reason we may have skipped TRR, keyed by the provider. Does not include requests that intentionally skip TRR."
+ },
+ "TRR_RELEVANT_SKIP_REASON_NATIVE_FAILED": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "vgosu@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 50,
+ "bug_numbers": [1699523, 1742408],
+ "releaseChannelCollection": "opt-out",
+ "description": "When in TRR-first mode, if TRR was skipped and native failed, it lists the reason we may have skipped TRR, keyed by the provider. Does not include requests that intentionally skip TRR."
+ },
+ "TRR_SKIP_REASON_STRICT_MODE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "nhnt11@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 50,
+ "bug_numbers": [1737198],
+ "releaseChannelCollection": "opt-out",
+ "description": "When in TRR-first mode, it lists the reason we may have skipped TRR. The key is like `<provider>|<skip reason of first attempt>` or just `<provider>` used when there was no second attempt."
+ },
+ "TRR_SKIP_REASON_RETRY_SUCCESS": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "nhnt11@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 50,
+ "bug_numbers": [1737198],
+ "releaseChannelCollection": "opt-out",
+ "description": "When in TRR-first mode, if TRR failed once and was successfully retried, it lists the reason for the first failure, keyed by the provider."
+ },
+ "TRR_SKIP_REASON_RETRY_FAILED": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "nhnt11@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 50,
+ "bug_numbers": [1737198],
+ "releaseChannelCollection": "opt-out",
+ "description": "When in TRR-first mode, if TRR failed once and retried unsuccessfully, it lists the reason for the first failure, keyed by the provider."
+ },
+ "TRR_RELEVANT_SKIP_REASON_TRR_FIRST_TYPE_REC": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 50,
+ "bug_numbers": [1850367],
+ "releaseChannelCollection": "opt-out",
+ "description": "When in TRR-first mode, it lists the reason we may have skipped TRR for HTTPS RR, keyed by the provider. Does not include requests that intentionally skip TRR."
+ },
+ "TRR_ATTEMPT_COUNT": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "nhnt11@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 10,
+ "bug_numbers": [1737198],
+ "releaseChannelCollection": "opt-out",
+ "description": "Number of times we attempted TRR for a successful lookup in TRR-first mode. Keyed by provider."
+ },
+ "DNS_TRR_FIRST4": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com", "vgosu@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "keyed": true,
+ "labels": ["TRR", "NativeAfterTRR", "Native", "BothFailed"],
+ "bug_numbers": [1497252, 1640867, 1646452],
+ "releaseChannelCollection": "opt-out",
+ "description": "TRR-first mode distribution. 0=Worked, 1=fell back fine after TRR fail, 2=native worked, 3=both failed; Keyed by TRR domain"
+ },
+ "DNS_TRR_DISABLED3": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "keyed": true,
+ "description": "Resolve success rate when in TRR-first and called TRR-disabled (fall-back mode); Keyed by TRR domain",
+ "bug_numbers": [1472659, 1640867, 1646452],
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["necko@mozilla.com", "vgosu@mozilla.com"]
+ },
+ "DNS_TRR_BLACKLISTED3": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "keyed": true,
+ "description": "DNS check for TRR was blocked by blacklist; Keyed by TRR domain",
+ "bug_numbers": [1434852, 1640867, 1646452],
+ "alert_emails": ["necko@mozilla.com", "vgosu@mozilla.com"]
+ },
+ "DNS_TRR_NS_VERFIFIED3": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "keyed": true,
+ "description": "TRR managed to verify NS entry; Keyed by TRR domain",
+ "bug_numbers": [1453825, 1640867, 1646452],
+ "alert_emails": ["necko@mozilla.com", "vgosu@mozilla.com"]
+ },
+ "DNS_TRR_REQUEST_PER_CONN": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1470853],
+ "description": "Number of DOH requests per connection"
+ },
+ "DNS_TRR_SUCCESS3": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "keyed": true,
+ "labels": ["Fine", "Timeout", "Bad"],
+ "description": "How often TRR (Trusted Recursive Resolver) requests are fine, time-out or error. Keyed by TRR domain",
+ "bug_numbers": [1497438, 1640867, 1646452],
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["necko@mozilla.com", "vgosu@mozilla.com"]
+ },
+ "DNS_LOOKUP_ALGORITHM": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": ["nativeOnly", "trrRace", "trrFirst", "trrOnly", "trrShadow"],
+ "bug_numbers": [1434852],
+ "description": "DNS: lookup algorithm"
+ },
+ "DNS_LOOKUP_DISPOSITION3": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com", "vgosu@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "keyed": true,
+ "labels": [
+ "trrOK",
+ "trrFail",
+ "trrAOK",
+ "trrAFail",
+ "trrAAAAOK",
+ "trrAAAAFail",
+ "osOK",
+ "osFail"
+ ],
+ "bug_numbers": [1434852, 1640867, 1646452],
+ "releaseChannelCollection": "opt-out",
+ "description": "DNS: lookup algorithm; Keyed by TRR domain"
+ },
+ "DNS_RENEWAL_TIME": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50,
+ "description": "Time for a renewed DNS OS resolution (msec)"
+ },
+ "DNS_RENEWAL_TIME_FOR_TTL": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50,
+ "description": "Time for a DNS OS resolution (msec) used to get TTL"
+ },
+ "DNS_FAILED_LOOKUP_TIME": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["necko@mozilla.com"],
+ "n_buckets": 50,
+ "description": "Time for an unsuccessful DNS OS resolution (msec)"
+ },
+ "DNS_BLACKLIST_COUNT": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["necko@mozilla.com"],
+ "kind": "linear",
+ "high": 21,
+ "n_buckets": 20,
+ "description": "The number of unusable addresses reported for each record"
+ },
+ "DNS_HTTPSSVC_RECORD_RECEIVING_STAGE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "n_values": 50,
+ "bug_numbers": [1652667],
+ "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"],
+ "kind": "enumerated",
+ "description": "Record at which stage an HTTPSSVC record is received"
+ },
+ "DNS_HTTPSSVC_CONNECTION_FAILED_REASON": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "n_values": 50,
+ "bug_numbers": [1652667],
+ "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"],
+ "kind": "enumerated",
+ "description": "The reason why we failed to connect with an HTTPSSVC record"
+ },
+ "DNS_PERF_FIRST_CONTENTFUL_PAINT_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "acreskey@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "bug_numbers": [1796639],
+ "keyed": true,
+ "description": "The time between navigationStart and the first contentful paint of a foreground http or https root content document, in milliseconds. The contentful paint timestamp is taken during display list building and does not include rasterization or compositing of that paint. Keyed by TRR domain for DoH or 'Native' otherwise"
+ },
+ "DNS_PERF_FIRST_BYTE_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "acreskey@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 100,
+ "bug_numbers": [1803671],
+ "keyed": true,
+ "description": "The time from a top-level document's HTTP channel open to the first byte of the reply is received (ms). Keyed by TRR domain for DoH or 'Native' otherwise"
+ },
+ "REFRESH_DRIVER_TICK": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "rhunt@mozilla.com"],
+ "expires_in_version": "never",
+ "description": "Total time spent ticking the refresh driver in milliseconds",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50
+ },
+ "PAINT_BUILD_DISPLAYLIST_TIME": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "gfx-telemetry-alerts@mozilla.com",
+ "mwoodrow@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "description": "Time spent in building displaylists in milliseconds",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50
+ },
+ "PAINT_RASTERIZE_TIME": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "rhunt@mozilla.com"],
+ "expires_in_version": "never",
+ "description": "Time spent rasterizing each frame in milliseconds",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50
+ },
+ "PREDICTOR_PREDICT_ATTEMPTS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [881804],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "Number of times nsINetworkPredictor::Predict is called and attempts to predict"
+ },
+ "PREDICTOR_LEARN_ATTEMPTS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [881804],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "Number of times nsINetworkPredictor::Learn is called and attempts to learn"
+ },
+ "PREDICTOR_PREDICT_FULL_QUEUE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [881804],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50,
+ "description": "Number of times nsINetworkPredictor::Predict doesn't continue because the queue is full"
+ },
+ "PREDICTOR_LEARN_FULL_QUEUE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [881804],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50,
+ "description": "Number of times nsINetworkPredictor::Learn doesn't continue because the queue is full"
+ },
+ "PREDICTOR_WAIT_TIME": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [881804],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Amount of time a predictor event waits in the queue (ms)"
+ },
+ "PREDICTOR_PREDICT_WORK_TIME": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "bug_numbers": [881804],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Amount of time spent doing the work for predict (ms)",
+ "alert_emails": ["necko@mozilla.com"]
+ },
+ "PREDICTOR_LEARN_WORK_TIME": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [881804],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Amount of time spent doing the work for learn (ms)"
+ },
+ "PREDICTOR_TOTAL_PREDICTIONS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [881804],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "How many actual predictions (preresolves, preconnects, ...) happen"
+ },
+ "PREDICTOR_TOTAL_PREFETCHES": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1016628],
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "How many actual prefetches happen"
+ },
+ "PREDICTOR_PREFETCH_USE_STATUS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1016628, 1312057],
+ "kind": "categorical",
+ "labels": [
+ "Used",
+ "Not200",
+ "Expired",
+ "WouldVary",
+ "WaitedTooLong",
+ "Etag",
+ "Auth",
+ "Redirect"
+ ],
+ "description": "If and why prefetches are used/unused."
+ },
+ "PREDICTOR_PREFETCH_TIME": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1016628],
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "How long it takes from OnStartRequest to OnStopRequest for a prefetch"
+ },
+ "PREDICTOR_TOTAL_PRECONNECTS": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [881804],
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "How many actual preconnects happen"
+ },
+ "PREDICTOR_TOTAL_PRECONNECTS_CREATED": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [881804],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "How many preconnects actually created a speculative socket"
+ },
+ "PREDICTOR_TOTAL_PRECONNECTS_USED": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [881804],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "How many preconnects actually created a used speculative socket"
+ },
+ "PREDICTOR_TOTAL_PRECONNECTS_UNUSED": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [881804],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "How many preconnects needlessly created a speculative socket"
+ },
+ "PREDICTOR_TOTAL_PRERESOLVES": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [881804],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "How many actual preresolves happen"
+ },
+ "PREDICTOR_PREDICTIONS_CALCULATED": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [881804],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "How many prediction calculations are performed"
+ },
+ "PREDICTOR_GLOBAL_DEGRADATION": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [881804],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 50,
+ "description": "The global degradation calculated"
+ },
+ "PREDICTOR_SUBRESOURCE_DEGRADATION": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [881804],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 50,
+ "description": "The degradation calculated for a subresource"
+ },
+ "PREDICTOR_BASE_CONFIDENCE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [881804],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 50,
+ "description": "The base confidence calculated for a subresource"
+ },
+ "PREDICTOR_CONFIDENCE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [881804],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 50,
+ "description": "The final confidence calculated for a subresource"
+ },
+ "PREDICTOR_PREDICT_TIME_TO_ACTION": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "bug_numbers": [881804],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "How long it takes from the time Predict() is called to the time we take action",
+ "alert_emails": ["necko@mozilla.com"]
+ },
+ "PREDICTOR_PREDICT_TIME_TO_INACTION": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [881804],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "How long it takes from the time Predict() is called to the time we figure out there's nothing to do"
+ },
+ "PREDICTOR_PREFETCH_DECISION_REASON": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 15,
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1409542, 1430322, 1312057],
+ "description": "Why the predictor determined a particular resource was eligible for future prefetch (or not). See PrefetchDecisionReason in Predictor.cpp for value meanings"
+ },
+ "PREDICTOR_PREFETCH_IGNORE_REASON": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 15,
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1409542, 1430322, 1312057],
+ "description": "Why the predictor determined a particular resource that was marked eligible for prefetch should not be prefetched. See PrefetchIgnoreReason in Predictor.cpp for value meanings"
+ },
+ "HTTPCONNMGR_TOTAL_SPECULATIVE_CONN": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "How many speculative http connections are created"
+ },
+ "HTTPCONNMGR_USED_SPECULATIVE_CONN": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "How many speculative http connections are actually used"
+ },
+ "HTTPCONNMGR_UNUSED_SPECULATIVE_CONN": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 50,
+ "description": "How many speculative connections are made needlessly"
+ },
+ "TAB_COUNT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gijs@mozilla.com"],
+ "bug_numbers": [1361855, 1488945],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 100,
+ "releaseChannelCollection": "opt-out",
+ "description": "Number of tabs opened across all windows, collected at most every 5 minutes whenever the user interacts with the browser in the following ways: open tab/window, page load."
+ },
+ "TAB_UNLOAD_TO_RELOAD": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["tkikuchi@mozilla.com"],
+ "bug_numbers": [1715858],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "description": "How long (sec) a tab had been unloaded until it was reloaded."
+ },
+ "LOADED_TAB_COUNT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["barret@mozilla.com", "perf-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1634508],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 100,
+ "releaseChannelCollection": "opt-out",
+ "description": "Number of fully loaded (i.e., not pending from session restore) tabs opened across all windows, collected at most every 5 minutes whenever the user interacts with the browser in the following ways: open tab/window, page load, restoring a pending tab."
+ },
+ "TAP_TO_LOAD_IMAGE_SIZE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "50",
+ "kind": "exponential",
+ "high": 32768,
+ "n_buckets": 50,
+ "description": "The size of the image being shown, when using tap-to-load images. (kilobytes)",
+ "bug_numbers": [1208167]
+ },
+ "STS_POLL_AND_EVENTS_CYCLE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "The duraion of a socketThread cycle, including polls and pending events. (ms)"
+ },
+ "STS_POLL_CYCLE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "The duration of poll. (ms)"
+ },
+ "STS_POLL_AND_EVENT_THE_LAST_CYCLE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "The duraion of the socketThread cycle during shutdown, including polls and pending events. (ms)"
+ },
+ "STS_POLL_BLOCK_TIME": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked on poll (ms)."
+ },
+ "PRCONNECT_BLOCKING_TIME_NORMAL": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Connect when we are not shutting down and there has been niether a network nor an offline state change in the last 60s (ms)."
+ },
+ "PRCONNECT_BLOCKING_TIME_SHUTDOWN": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Connect during a shutdown (ms)."
+ },
+ "PRCONNECT_BLOCKING_TIME_CONNECTIVITY_CHANGE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Connect when there has been the connectiviy change in the last 60s (ms)."
+ },
+ "PRCONNECT_BLOCKING_TIME_LINK_CHANGE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Connect when there has been a link change in the last 60s (ms)."
+ },
+ "PRCONNECT_BLOCKING_TIME_OFFLINE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Connect when the offline state has changed in the last 60s (ms)."
+ },
+ "PRCONNECT_FAIL_BLOCKING_TIME_NORMAL": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "bug_numbers": [1257809],
+ "alert_emails": ["ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 100,
+ "description": "Time spent blocked in a failed PR_Connect when we are not shutting down and there has been niether a network nor an offline state change in the last 60s (ms)."
+ },
+ "PRCONNECT_FAIL_BLOCKING_TIME_SHUTDOWN": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "bug_numbers": [1257809],
+ "alert_emails": ["ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 100,
+ "description": "Time spent blocked in a failed PR_Connect during a shutdown (ms)."
+ },
+ "PRCONNECT_FAIL_BLOCKING_TIME_CONNECTIVITY_CHANGE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "bug_numbers": [1257809],
+ "alert_emails": ["ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 100,
+ "description": "Time spent blocked in a failed PR_Connect when there has been the connectiviy change in the last 60s (ms)."
+ },
+ "PRCONNECT_FAIL_BLOCKING_TIME_LINK_CHANGE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "bug_numbers": [1257809],
+ "alert_emails": ["ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 100,
+ "description": "Time spent blocked in a failed PR_Connect when there has been a link change in the last 60s (ms)."
+ },
+ "PRCONNECT_FAIL_BLOCKING_TIME_OFFLINE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "bug_numbers": [1257809],
+ "alert_emails": ["ddamjanovic@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 100,
+ "description": "Time spent blocked in a failed PR_Connect when the offline state has changed in the last 60s (ms)."
+ },
+ "PRCONNECTCONTINUE_BLOCKING_TIME_NORMAL": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_ConnectContinue when we are not shutting down and there has been niether a network nor an offline state change in the last 60s (ms)."
+ },
+ "PRCONNECTCONTINUE_BLOCKING_TIME_SHUTDOWN": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_ConnectContinue during a shutdown (ms)."
+ },
+ "PRCONNECTCONTINUE_BLOCKING_TIME_CONNECTIVITY_CHANGE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_ConnectContinue when there has been the connectivity change in the last 60s (ms)."
+ },
+ "PRCONNECTCONTINUE_BLOCKING_TIME_LINK_CHANGE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_ConnectContinue when there has been a link change in the last 60s (ms)."
+ },
+ "PRCONNECTCONTINUE_BLOCKING_TIME_OFFLINE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_ConnectContinue when the offline state has changed in the last 60s (ms)."
+ },
+ "PRCLOSE_TCP_BLOCKING_TIME_NORMAL": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Close when we are not shutting down and there has been niether a network nor an offline state change in the last 60s (ms)."
+ },
+ "PRCLOSE_TCP_BLOCKING_TIME_SHUTDOWN": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Close during a shutdown (ms)."
+ },
+ "PRCLOSE_TCP_BLOCKING_TIME_CONNECTIVITY_CHANGE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Close when there has been the connectivity change in the last 60s (ms)."
+ },
+ "PRCLOSE_TCP_BLOCKING_TIME_LINK_CHANGE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Close when there has been a link change in the last 60s (ms)."
+ },
+ "PRCLOSE_TCP_BLOCKING_TIME_OFFLINE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Close when the offline state has changed in the last 60s (ms)."
+ },
+ "PRCLOSE_UDP_BLOCKING_TIME_NORMAL": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Close when we are not shutting down and there has been niether a network nor an offline state change in the last 60s (ms)."
+ },
+ "PRCLOSE_UDP_BLOCKING_TIME_SHUTDOWN": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Close during a shutdown (ms)."
+ },
+ "PRCLOSE_UDP_BLOCKING_TIME_CONNECTIVITY_CHANGE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Close when there has been the connectivity change in the last 60s (ms)."
+ },
+ "PRCLOSE_UDP_BLOCKING_TIME_LINK_CHANGE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Close when there has been a link change in the last 60s (ms)."
+ },
+ "PRCLOSE_UDP_BLOCKING_TIME_OFFLINE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "Time spent blocked in PR_Close when the offline state has changed in the last 60s (ms)."
+ },
+ "IPV4_AND_IPV6_ADDRESS_CONNECTIVITY": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 4,
+ "description": "Count the number of 0) successful connections to an ipv4 address, 1) failed connection an ipv4 address, 2) successful connection to an ipv6 address and 3) failed connections to an ipv6 address."
+ },
+ "NETWORK_CONNECTION_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["amarchesini@mozilla.com"],
+ "expires_in_version": "58",
+ "kind": "count",
+ "bug_numbers": [1330255],
+ "description": "Number of the use of navigator.connection."
+ },
+ "FIND_PLUGINS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent scanning filesystem for plugins (ms)"
+ },
+ "PLUGIN_LOAD_METADATA": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 20,
+ "description": "Time spent loading plugin DLL and obtaining metadata (ms)"
+ },
+ "PLUGIN_SHUTDOWN_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 20,
+ "description": "Time spent shutting down plugins (ms)"
+ },
+ "MOZ_SQLITE_COOKIES_OPEN_READAHEAD_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on cookie DB open with readahead (ms)"
+ },
+ "MOZ_SQLITE_COOKIES_BLOCK_MAIN_THREAD_MS_V2": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "alert_emails": ["necko@mozilla.com", "junior@mozilla.com"],
+ "bug_numbers": [870460],
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time spent on blocking main thread by startup cookie database read (ms), only for blocking case"
+ },
+ "MOZ_SQLITE_COOKIES_TIME_TO_BLOCK_MAIN_THREAD_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "alert_emails": ["necko@mozilla.com", "junior@mozilla.com"],
+ "bug_numbers": [1413839],
+ "high": 10000,
+ "n_buckets": 30,
+ "description": "How long (ms) after we finished reading the cookie db until the first cookie request came in (0 implies we blocked the main thread)"
+ },
+ "STARTUP_CACHE_REQUESTS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dothayer@mozilla.com", "plawless@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": ["HitMemory", "HitDisk", "Miss"],
+ "bug_numbers": [1364235, 1590385, 1623098, 1649954, 1689257, 1754639],
+ "releaseChannelCollection": "opt-out",
+ "description": "Record hits and misses to the startup cache, with categories."
+ },
+ "SCRIPT_PRELOADER_REQUESTS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dothayer@mozilla.com", "plawless@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": ["Hit", "HitChild", "Miss"],
+ "bug_numbers": [1364235, 1590385, 1623098, 1649954, 1689257, 1754639],
+ "releaseChannelCollection": "opt-out",
+ "description": "Record hits and misses to the script preloader, with categories."
+ },
+ "SCRIPT_PRELOADER_WAIT_TIME": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dothayer@mozilla.com", "plawless@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 20,
+ "bug_numbers": [1364235, 1590385, 1623098, 1649954, 1689257, 1754639],
+ "releaseChannelCollection": "opt-out",
+ "description": "Time spent waiting for off-thread compiles in the script preloader."
+ },
+ "NETWORK_DISK_CACHE_TRASHRENAME": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 10,
+ "description": "Time spent renaming bad Cache to Cache.Trash (ms)"
+ },
+ "NETWORK_DISK_CACHE_DELETEDIR": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 10,
+ "description": "Time spent deleting disk cache (ms)"
+ },
+ "NETWORK_DISK_CACHE_DELETEDIR_SHUTDOWN": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 10,
+ "description": "Time spent during showdown stopping thread deleting old disk cache (ms)"
+ },
+ "NETWORK_DISK_CACHE_SHUTDOWN": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 10,
+ "description": "Total Time spent (ms) during disk cache showdown"
+ },
+ "NETWORK_DISK_CACHE_SHUTDOWN_V2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 10,
+ "description": "Total Time spent (ms) during disk cache showdown [cache2]"
+ },
+ "NETWORK_DISK_CACHE_SHUTDOWN_CLEAR_PRIVATE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 10,
+ "description": "Time spent (ms) during showdown deleting disk cache for 'clear private data' option"
+ },
+ "NETWORK_DISK_CACHE2_SHUTDOWN_CLEAR_PRIVATE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 10,
+ "description": "Time spent (ms) during showdown deleting disk cache v2 for 'clear private data' option"
+ },
+ "NETWORK_ID2": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1240932, 1395914],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 6,
+ "description": "Network identification (0=None, 1=New Ipv4, 2=Same, 3=New IPv6, 4=New dual)"
+ },
+ "NETWORK_ID_ONLINE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com"],
+ "bug_numbers": [1580130],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "releaseChannelCollection": "opt-out",
+ "labels": ["absent", "present"],
+ "description": "Network ID presence when the network connectivity checker reports that we have connectivity"
+ },
+ "IDLE_NOTIFY_IDLE_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["froydnj@mozilla.com"],
+ "bug_numbers": [731004],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 10,
+ "description": "Time spent checking for and notifying listeners that the user is idle (ms)"
+ },
+ "URLCLASSIFIER_LOOKUP_TIME_2": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "bug_numbers": [1336376, 1531034],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 30,
+ "description": "Time spent per dbservice lookup (ms)"
+ },
+ "URLCLASSIFIER_SHUTDOWN_TIME": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50,
+ "bug_numbers": [1315140, 1531034],
+ "description": "Time spent per dbservice shutdown (ms)"
+ },
+ "URLCLASSIFIER_CL_CHECK_TIME": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 500,
+ "n_buckets": 10,
+ "description": "Time spent per classifier lookup (ms)"
+ },
+ "URLCLASSIFIER_CL_KEYED_UPDATE_TIME": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "kind": "exponential",
+ "low": 20,
+ "high": 120000,
+ "n_buckets": 30,
+ "bug_numbers": [1315893, 1531034],
+ "description": "Time spent per classifier update (ms), keyed by the name of the provider."
+ },
+ "URLCLASSIFIER_ASYNC_CLASSIFYLOCAL_TIME": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "bug_numbers": [1341506, 1531034],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 30,
+ "description": "Time spent per AsyncClassifyLocalWithTables (ms)"
+ },
+ "URLCLASSIFIER_PS_FILELOAD_TIME": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 10,
+ "description": "Time spent loading PrefixSet from file (ms)"
+ },
+ "URLCLASSIFIER_PS_FALLOCATE_TIME": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 10,
+ "description": "Time spent fallocating PrefixSet (ms)"
+ },
+ "URLCLASSIFIER_PS_CONSTRUCT_TIME": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 15,
+ "description": "Time spent constructing PrefixSet from DB (ms)"
+ },
+ "URLCLASSIFIER_VLPS_FILELOAD_TIME": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 10,
+ "bug_numbers": [1283007, 1531034],
+ "description": "Time spent loading Variable-Length PrefixSet from file (ms)"
+ },
+ "URLCLASSIFIER_VLPS_FALLOCATE_TIME": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 10,
+ "bug_numbers": [1283007, 1531034],
+ "description": "Time spent fallocating Variable-Length PrefixSet (ms)"
+ },
+ "URLCLASSIFIER_VLPS_CONSTRUCT_TIME": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 15,
+ "bug_numbers": [1336865, 1531034],
+ "description": "Time spent constructing Variable-Length PrefixSet from file (ms)"
+ },
+ "URLCLASSIFIER_VLPS_LOAD_CORRUPT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "boolean",
+ "bug_numbers": [1305581, 1531034],
+ "description": "Whether or not a variable-length prefix set loaded from disk is corrupted (true = file corrupted)."
+ },
+ "URLCLASSIFIER_VLPS_METADATA_CORRUPT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "boolean",
+ "bug_numbers": [1433636, 1531034],
+ "description": "Whether or not the metadata for a variable-length prefix set loaded from disk is corrupted (true = file corrupted)."
+ },
+ "URLCLASSIFIER_VLPS_LONG_PREFIXES": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "63",
+ "releaseChannelCollection": "opt-out",
+ "kind": "enumerated",
+ "n_values": 32,
+ "bug_numbers": [1322523, 1531034],
+ "description": "Length of the first 20 long prefixes (> 4 bytes) received in a Safe Browsing V4 table during an update."
+ },
+ "URLCLASSIFIER_LC_PREFIXES": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 1500000,
+ "n_buckets": 15,
+ "description": "Size of the prefix cache in entries"
+ },
+ "URLCLASSIFIER_LC_COMPLETIONS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 200,
+ "n_buckets": 10,
+ "description": "Size of the completion cache in entries"
+ },
+ "URLCLASSIFIER_UPDATE_REMOTE_NETWORK_ERROR": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 30,
+ "bug_numbers": [1332780, 1531034],
+ "description": "Network error from SafeBrowsing database updates. (0=sucess, 1=unknown error, 2=already connected, 3=not connected, 4=connection refused,5=net timeout, 6=offline, 7=port access not allowed, 8=net reset, 9=net interrupt, 10=proxy connection refused,11=partial transfer,12=inadequate security,13=unknown host,14=dns lookup queue full,15=unknown proxy host"
+ },
+ "URLCLASSIFIER_UPDATE_REMOTE_STATUS2": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 16,
+ "bug_numbers": [1311910, 1531034],
+ "description": "Server HTTP status code from SafeBrowsing database updates. (0=1xx, 1=200, 2=2xx, 3=204, 4=3xx, 5=400, 6=4xx, 7=403, 8=404, 9=408, 10=413, 11=5xx, 12=502|504|511, 13=503, 14=505, 15=Other). Keyed by provider"
+ },
+ "URLCLASSIFIER_UPDATE_SERVER_RESPONSE_TIME": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "keyed": true,
+ "high": 100000,
+ "n_buckets": 30,
+ "bug_numbers": [1336903, 1531034],
+ "description": "Server response time to update request (ms). Keyed by provider"
+ },
+ "URLCLASSIFIER_UPDATE_TIMEOUT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 4,
+ "bug_numbers": [1336904, 1531034],
+ "description": " Whether or not an update timed out (0 = no timeout, 1 = server respond timeout, 2 = overall timeout). Keyed by provider"
+ },
+ "URLCLASSIFIER_COMPLETE_REMOTE_STATUS2": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 16,
+ "bug_numbers": [1150921, 1311926, 1531034],
+ "description": "Server HTTP status code from remote SafeBrowsing gethash lookups. (0=1xx, 1=200, 2=2xx, 3=204, 4=3xx, 5=400, 6=4xx, 7=403, 8=404, 9=408, 10=413, 11=5xx, 12=502|504|511, 13=503, 14=505, 15=Other). Keyed by provider"
+ },
+ "URLCLASSIFIER_COMPLETION_ERROR": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "enumerated",
+ "n_values": 16,
+ "bug_numbers": [1276826, 1531034],
+ "description": "SafeBrowsing v4 hash completion error (0 = success, 1 = parsing failure, 2 = unknown threat type)"
+ },
+ "URLCLASSIFIER_COMPLETE_TIMEOUT2": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "boolean",
+ "keyed": true,
+ "bug_numbers": [1172688, 1311926, 1531034],
+ "description": "This metric is recorded every time a gethash lookup is performed, `true` is recorded if the lookup times out. Keyed by provider"
+ },
+ "URLCLASSIFIER_COMPLETE_SERVER_RESPONSE_TIME": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "keyed": true,
+ "high": 5000,
+ "n_buckets": 15,
+ "bug_numbers": [1336903, 1531034],
+ "description": "Server response time to getHash request (ms). Keyed by provider"
+ },
+ "URLCLASSIFIER_UPDATE_ERROR": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 16,
+ "bug_numbers": [1311910, 1531034],
+ "description": "Whether or not an error was encountered while processing a Safe Browsing update (0 = success, 1 = unspecified error, 2 = addition of an already existing prefix, 3 = parser got into an infinite loop, 4 = removal index out of bounds, 5 = checksum mismatch, 6 = missing checksum, 7 = update while shutdown, 8 = cannot find table, 9 = build prefix failure, 10 = write disk failure, 11 = protocol parser error). Keyed by provider"
+ },
+ "URLCLASSIFIER_THREATHIT_NETWORK_ERROR": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["tnguyen@mozilla.com, safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "enumerated",
+ "n_values": 30,
+ "bug_numbers": [1351147],
+ "description": "Whether or not an error was encountered while sending a Safe Browsing ThreatHit report. (0=sucess, 1=unknown error, 2=already connected, 3=not connected, 4=connection refused,5=net timeout, 6=offline, 7=port access not allowed, 8=net reset, 9=net interrupt, 10=proxy connection refused, 11=partial transfer, 12=inadequate security, 13=unknown host, 14=dns lookup queue full, 15=unknown proxy host)"
+ },
+ "URLCLASSIFIER_THREATHIT_REMOTE_STATUS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["tnguyen@mozilla.com, safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "enumerated",
+ "n_values": 16,
+ "bug_numbers": [1351147],
+ "description": "Server HTTP status code from Safe Browsing ThreatHit report. (0=1xx, 1=200, 2=2xx, 3=204, 4=3xx, 5=400, 6=4xx, 7=403, 8=404, 9=408, 10=413, 11=5xx, 12=502|504|511, 13=503, 14=505, 15=Other)"
+ },
+ "HTTPS_ONLY_MODE_UPGRADE_TIME_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["julianwels@mozilla.com", "seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1627206],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "low": 50,
+ "high": 300000,
+ "n_buckets": 30,
+ "keyed": true,
+ "keys": [
+ "top_successful",
+ "sub_successful",
+ "top_f_redirectloop",
+ "sub_f_redirectloop",
+ "top_f_timeout",
+ "sub_f_timeout",
+ "top_f_aborted",
+ "sub_f_aborted",
+ "top_f_cxnrefused",
+ "sub_f_cxnrefused",
+ "top_f_ssl_selfsignd",
+ "sub_f_ssl_selfsignd",
+ "top_f_ssl_badcertdm",
+ "sub_f_ssl_badcertdm",
+ "top_f_ssl_unkwnissr",
+ "sub_f_ssl_unkwnissr",
+ "top_f_ssl_other",
+ "sub_f_ssl_other",
+ "top_f_other",
+ "sub_f_other"
+ ],
+ "description": "Time it takes for a request that has been upgraded with HTTPS-Only Mode to complete, broken down by top-level (top) / sub-resource (sub) and status"
+ },
+ "HTTPS_ONLY_MODE_UPGRADE_TYPE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com", "julianwels@mozilla.com"],
+ "bug_numbers": [1675533],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "keyed": true,
+ "description": "What content type did we upgrade, and did it load successfully?"
+ },
+ "PLACES_DATABASE_CORRUPTION_HANDLING_STAGE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["mbonardo@mozilla.com", "fx-search-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 6,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1356812],
+ "description": "PLACES: stage reached when trying to fix a database corruption , see Places::Database::eCorruptDBReplaceStatus"
+ },
+ "PLACES_PAGES_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 1000,
+ "high": 150000,
+ "n_buckets": 20,
+ "releaseChannelCollection": "opt-out",
+ "description": "PLACES: Number of unique pages"
+ },
+ "PLACES_MOST_RECENT_EXPIRED_VISIT_DAYS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["mbonardo@mozilla.com", "fx-search-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 30,
+ "high": 730,
+ "n_buckets": 12,
+ "bug_numbers": [1822848],
+ "releaseChannelCollection": "opt-out",
+ "description": "PLACES: the most recent expired visit in days"
+ },
+ "PLACES_BOOKMARKS_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 100,
+ "high": 8000,
+ "n_buckets": 15,
+ "releaseChannelCollection": "opt-out",
+ "description": "PLACES: Number of bookmarks"
+ },
+ "PLACES_TAGS_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 200,
+ "n_buckets": 10,
+ "description": "PLACES: Number of tags"
+ },
+ "PLACES_KEYWORDS_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 200,
+ "n_buckets": 10,
+ "description": "PLACES: Number of keywords"
+ },
+ "PLACES_BACKUPS_DAYSFROMLAST": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 15,
+ "description": "PLACES: Days from last backup"
+ },
+ "PLACES_BACKUPS_BOOKMARKSTREE_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 50,
+ "high": 2000,
+ "n_buckets": 10,
+ "description": "PLACES: Time to build the bookmarks tree"
+ },
+ "PLACES_BACKUPS_TOJSON_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "low": 50,
+ "high": 2000,
+ "n_buckets": 10,
+ "description": "PLACES: Time to convert and write the backup"
+ },
+ "PLACES_EXPORT_TOHTML_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 50,
+ "high": 2000,
+ "n_buckets": 10,
+ "description": "PLACES: Time to convert and write bookmarks.html"
+ },
+ "PLACES_FAVICON_ICO_SIZES": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 524288,
+ "n_buckets": 100,
+ "description": "PLACES: Size of the ICO favicon files loaded from the web (Bytes)"
+ },
+ "PLACES_FAVICON_PNG_SIZES": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 524288,
+ "n_buckets": 100,
+ "description": "PLACES: Size of the PNG favicon files loaded from the web (Bytes)"
+ },
+ "PLACES_FAVICON_GIF_SIZES": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 524288,
+ "n_buckets": 100,
+ "description": "PLACES: Size of the GIF favicon files loaded from the web (Bytes)"
+ },
+ "PLACES_FAVICON_JPEG_SIZES": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 524288,
+ "n_buckets": 100,
+ "description": "PLACES: Size of the JPEG favicon files loaded from the web (Bytes)"
+ },
+ "PLACES_FAVICON_BMP_SIZES": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 524288,
+ "n_buckets": 100,
+ "description": "PLACES: Size of the BMP favicon files loaded from the web (Bytes)"
+ },
+ "PLACES_FAVICON_SVG_SIZES": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 524288,
+ "n_buckets": 100,
+ "description": "PLACES: Size of the SVG favicon files loaded from the web (Bytes)"
+ },
+ "PLACES_FAVICON_OTHER_SIZES": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 524288,
+ "n_buckets": 100,
+ "description": "PLACES: Size of favicon files without a specific file type probe, loaded from the web (Bytes)"
+ },
+ "LINK_ICON_SIZES_ATTR_USAGE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 4,
+ "alert_emails": ["fx-search-telemetry@mozilla.com"],
+ "bug_numbers": [1053467],
+ "description": "The possible types of the 'sizes' attribute for <link rel=icon>. 0: Attribute not specified, 1: 'any', 2: Integer dimensions, 3: Invalid value."
+ },
+ "LINK_ICON_SIZES_ATTR_DIMENSION": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 513,
+ "n_buckets": 64,
+ "alert_emails": ["fx-search-telemetry@mozilla.com"],
+ "bug_numbers": [1053467],
+ "description": "The width dimension of the 'sizes' attribute for <link rel=icon>."
+ },
+ "PAGE_METADATA_SIZE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 500,
+ "n_buckets": 10,
+ "alert_emails": ["najiang@mozilla.com", "activity-stream@mozilla.com"],
+ "bug_numbers": [1399880],
+ "description": "The size of the description and preview image url for page metadata (Bytes)"
+ },
+ "FENNEC_LOOP_UI_LATENCY": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "55",
+ "alert_emails": ["mobile-platform@mozilla.org"],
+ "kind": "exponential",
+ "low": 10,
+ "high": 10485760,
+ "n_buckets": 22,
+ "description": "Latency in microseconds of UI events in the Android event loop between posting and processing",
+ "bug_numbers": [1322574],
+ "operating_systems": ["android"]
+ },
+ "FENNEC_LOOP_OTHER_LATENCY": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "55",
+ "alert_emails": ["mobile-platform@mozilla.org"],
+ "kind": "exponential",
+ "low": 10,
+ "high": 10485760,
+ "n_buckets": 22,
+ "description": "Latency in microseconds of non-UI events in the Android event loop between posting and processing",
+ "bug_numbers": [1322574],
+ "operating_systems": ["android"]
+ },
+ "PLACES_SORTED_BOOKMARKS_PERC": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 10,
+ "description": "PLACES: Percentage of bookmarks organized in folders"
+ },
+ "PLACES_TAGGED_BOOKMARKS_PERC": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 10,
+ "description": "PLACES: Percentage of tagged bookmarks"
+ },
+ "PLACES_DATABASE_FILESIZE_MB": {
+ "record_in_processes": ["main", "content"],
+ "alert_emails": ["mbonardo@mozilla.com", "fx-search-telemetry@mozilla.com"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 5,
+ "high": 200,
+ "n_buckets": 10,
+ "bug_numbers": [1822848],
+ "releaseChannelCollection": "opt-out",
+ "description": "PLACES: Database filesize (MB)"
+ },
+
+ "PLACES_DATABASE_FAVICONS_FILESIZE_MB": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["mbonardo@mozilla.com", "fx-search-telemetry@mozilla.com"],
+ "bug_numbers": [1346554, 1822848],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 5,
+ "high": 100,
+ "n_buckets": 10,
+ "releaseChannelCollection": "opt-out",
+ "description": "PLACES: Favicons database filesize (MB)"
+ },
+ "PLACES_EXPIRATION_STEPS_TO_CLEAN2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "PLACES: Expiration steps to cleanup the database"
+ },
+ "PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["fx-search-telemetry@mozilla.com", "adw@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 50,
+ "high": 500,
+ "n_buckets": 10,
+ "description": "PLACES: Time for first autocomplete result if > 50ms (ms)"
+ },
+ "PLACES_AUTOCOMPLETE_6_FIRST_RESULTS_TIME_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["fx-search-telemetry@mozilla.com", "adw@mozilla.com"],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 50,
+ "high": 1000,
+ "n_buckets": 30,
+ "bug_numbers": [1489524],
+ "description": "PLACES: Time for the 6 first autocomplete results (ms)"
+ },
+ "HISTORY_LASTVISITED_TREE_QUERY_TIME_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 50,
+ "high": 2000,
+ "n_buckets": 30,
+ "description": "PLACES: Time to load the sidebar history tree sorted by last visit (ms)"
+ },
+ "PLACES_HISTORY_LIBRARY_SEARCH_TIME_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 50,
+ "high": 1000,
+ "n_buckets": 30,
+ "description": "PLACES: Time to search the history library (ms)"
+ },
+ "PLACES_LIBRARY_CUMULATIVE_HISTORY_SEARCHES": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "alert_emails": [
+ "kcochrane@mozilla.com",
+ "firefox-view-engineers@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1815906],
+ "kind": "enumerated",
+ "n_values": 20,
+ "description": "Cumulative no. of History-specific searches performed before selecting a History link in Library."
+ },
+ "PLACES_LIBRARY_CUMULATIVE_BOOKMARK_SEARCHES": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "alert_emails": ["firefox-view-engineers@mozilla.com"],
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1819081],
+ "kind": "enumerated",
+ "n_values": 20,
+ "description": "Cumulative no. of Bookmark-specific searches performed before selecting a bookmark link in Library."
+ },
+ "PLACES_IDLE_FRECENCY_DECAY_TIME_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 50,
+ "high": 10000,
+ "n_buckets": 10,
+ "description": "PLACES: Time to decay all frecencies values on idle (ms)"
+ },
+ "PLACES_IDLE_MAINTENANCE_TIME_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 1000,
+ "high": 30000,
+ "n_buckets": 10,
+ "description": "PLACES: Time to execute maintenance tasks on idle (ms)"
+ },
+ "PLACES_FRECENCY_RECALC_CHUNK_TIME_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["mbonardo@mozilla.com", "fx-search-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 50,
+ "high": 10000,
+ "n_buckets": 10,
+ "bug_numbers": [1811209, 1822848],
+ "releaseChannelCollection": "opt-out",
+ "description": "PLACES: Time to recalculate frecency for a chunk of pages (ms)"
+ },
+ "PLACES_ANNOS_PAGES_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 50,
+ "high": 5000,
+ "n_buckets": 10,
+ "description": "PLACES: Number of pages annotations"
+ },
+ "PLACES_MAINTENANCE_DAYSFROMLAST": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 7,
+ "high": 60,
+ "n_buckets": 10,
+ "description": "PLACES: Days from last maintenance"
+ },
+ "UPDATE_CHECK_NO_UPDATE_EXTERNAL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: count of no updates were found for a background update check (externally initiated)"
+ },
+ "UPDATE_CHECK_NO_UPDATE_NOTIFY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: count of no updates were found for a background update check (timer initiated)"
+ },
+ "UPDATE_CHECK_NO_UPDATE_SUBSEQUENT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [353804],
+ "description": "Update: count of no updates were found for a background update check (timer initiated)"
+ },
+ "UPDATE_CHECK_CODE_EXTERNAL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 50,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: background update check result code except for no updates found (externally initiated)"
+ },
+ "UPDATE_CHECK_CODE_NOTIFY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 50,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: background update check result code except for no updates found (timer initiated)"
+ },
+ "UPDATE_CHECK_CODE_SUBSEQUENT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 50,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [353804],
+ "description": "Update: background update check result code except for no updates found (after we already have an update ready). Possible codes are enumerated by constants starting with CHK_ in toolkit/mozapps/update/UpdateTelemetry.jsm"
+ },
+ "UPDATE_CHECK_EXTENDED_ERROR_EXTERNAL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: keyed count (key names are prefixed with AUS_CHECK_EX_ERR_) of background update check extended error code (externally initiated)"
+ },
+ "UPDATE_CHECK_EXTENDED_ERROR_NOTIFY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: keyed count (key names are prefixed with AUS_CHECK_EX_ERR_) of background update check extended error code (timer initiated)"
+ },
+ "UPDATE_CHECK_EXTENDED_ERROR_SUBSEQUENT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [353804],
+ "description": "Update: keyed count (key names are prefixed with AUS_CHECK_EX_ERR_) of background update check extended error code (timer initiated)"
+ },
+ "UPDATE_INVALID_LASTUPDATETIME_EXTERNAL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: count of systems that have a last update time greater than the current time (externally initiated)"
+ },
+ "UPDATE_INVALID_LASTUPDATETIME_NOTIFY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: count of systems that have a last update time greater than the current time (timer initiated)"
+ },
+ "UPDATE_INVALID_LASTUPDATETIME_SUBSEQUENT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [353804],
+ "description": "Update: count of systems that have a last update time greater than the current time (timer initiated)"
+ },
+ "UPDATE_LAST_NOTIFY_INTERVAL_DAYS_EXTERNAL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "n_buckets": 60,
+ "high": 365,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: interval in days since the last background update check (externally initiated)"
+ },
+ "UPDATE_LAST_NOTIFY_INTERVAL_DAYS_NOTIFY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "n_buckets": 30,
+ "high": 180,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: interval in days since the last background update check (timer initiated)"
+ },
+ "UPDATE_LAST_NOTIFY_INTERVAL_DAYS_SUBSEQUENT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "n_buckets": 30,
+ "high": 180,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [353804],
+ "description": "Update: interval in days since the last background update check (after we already have an update ready)"
+ },
+ "UPDATE_PING_COUNT_EXTERNAL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1151267],
+ "description": "Update: count of systems for this ping for comparison with other pings (externally initiated)"
+ },
+ "UPDATE_PING_COUNT_NOTIFY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1151267],
+ "description": "Update: count of systems for this ping for comparison with other pings (timer initiated)"
+ },
+ "UPDATE_PING_COUNT_SUBSEQUENT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [353804],
+ "description": "Update: count of systems for this ping for comparison with other pings (timer initiated)"
+ },
+ "UPDATE_SERVICE_INSTALLED_EXTERNAL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: whether the service is installed (externally initiated)"
+ },
+ "UPDATE_SERVICE_INSTALLED_NOTIFY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: whether the service is installed (timer initiated)"
+ },
+ "UPDATE_SERVICE_INSTALLED_SUBSEQUENT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [353804],
+ "description": "Update: whether the service is installed (after we already have an update ready)"
+ },
+ "UPDATE_SERVICE_MANUALLY_UNINSTALLED_EXTERNAL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: count of systems that manually uninstalled the service (externally initiated)"
+ },
+ "UPDATE_SERVICE_MANUALLY_UNINSTALLED_NOTIFY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: count of systems that manually uninstalled the service (timer initiated)"
+ },
+ "UPDATE_SERVICE_MANUALLY_UNINSTALLED_SUBSEQUENT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [353804],
+ "description": "Update: count of systems that manually uninstalled the service (timer initiated)"
+ },
+ "UPDATE_UNABLE_TO_APPLY_EXTERNAL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1151267],
+ "description": "Update: count of systems that cannot apply updates (externally initiated)"
+ },
+ "UPDATE_UNABLE_TO_APPLY_NOTIFY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1151267],
+ "description": "Update: count of systems that cannot apply updates (timer initiated)"
+ },
+ "UPDATE_UNABLE_TO_APPLY_SUBSEQUENT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [353804],
+ "description": "Update: count of systems that cannot apply updates (timer initiated)"
+ },
+ "UPDATE_CANNOT_STAGE_EXTERNAL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: count of systems that cannot stage updates (externally initiated)"
+ },
+ "UPDATE_CANNOT_STAGE_NOTIFY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: count of systems that cannot stage updates (timer initiated)"
+ },
+ "UPDATE_CANNOT_STAGE_SUBSEQUENT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [353804],
+ "description": "Update: count of systems that cannot stage updates (timer initiated)"
+ },
+ "UPDATE_PREF_UPDATE_CANCELATIONS_EXTERNAL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: number of sequential update elevation request cancelations greater than 0 (externally initiated)"
+ },
+ "UPDATE_PREF_UPDATE_CANCELATIONS_NOTIFY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: number of sequential update elevation request cancelations greater than 0 (timer initiated)"
+ },
+ "UPDATE_PREF_UPDATE_CANCELATIONS_SUBSEQUENT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [353804],
+ "description": "Update: number of sequential update elevation request cancelations greater than 0 (after we already have an update ready)"
+ },
+ "UPDATE_PREF_SERVICE_ERRORS_EXTERNAL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 30,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: number of sequential update service errors greater than 0 (externally initiated)"
+ },
+ "UPDATE_PREF_SERVICE_ERRORS_NOTIFY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 30,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: number of sequential update service errors greater than 0 (timer initiated)"
+ },
+ "UPDATE_PREF_SERVICE_ERRORS_SUBSEQUENT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 30,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [353804],
+ "description": "Update: number of sequential update service errors greater than 0 (after we already have an update ready)"
+ },
+ "UPDATE_NOT_PREF_UPDATE_AUTO_EXTERNAL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: count of when getAppUpdateAutoEnabled from UpdateUtils.sys.mjs does not return the default value of true (true values are not submitted)"
+ },
+ "UPDATE_NOT_PREF_UPDATE_AUTO_NOTIFY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: count of when getAppUpdateAutoEnabled from UpdateUtils.sys.mjs does not return the default value of true (true values are not submitted)"
+ },
+ "UPDATE_NOT_PREF_UPDATE_AUTO_SUBSEQUENT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [353804],
+ "description": "Update: count of when getAppUpdateAutoEnabled from UpdateUtils.sys.mjs does not return the default value of true (true values are not submitted)"
+ },
+ "UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_EXTERNAL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: count of when the app.update.staging.enabled boolean preference is not the default value of true (true values are not submitted)"
+ },
+ "UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_NOTIFY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: count of when the app.update.staging.enabled boolean preference is not the default value of true (true values are not submitted)"
+ },
+ "UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_SUBSEQUENT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [353804],
+ "description": "Update: count of when the app.update.staging.enabled boolean preference is not the default value of true (true values are not submitted)"
+ },
+ "UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_EXTERNAL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: count of when the app.update.service.enabled boolean preference is not the default value of true (true values are not submitted)"
+ },
+ "UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_NOTIFY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: count of when the app.update.service.enabled boolean preference is not the default value of true (true values are not submitted)"
+ },
+ "UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_SUBSEQUENT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [353804],
+ "description": "Update: count of when the app.update.service.enabled boolean preference is not the default value of true (true values are not submitted)"
+ },
+ "UPDATE_DOWNLOAD_CODE_COMPLETE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 50,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: complete patch type download result code"
+ },
+ "UPDATE_DOWNLOAD_CODE_PARTIAL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 50,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: partial patch type download result code"
+ },
+ "UPDATE_DOWNLOAD_CODE_UNKNOWN": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 50,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: unknown patch type download result code"
+ },
+ "UPDATE_STATE_CODE_COMPLETE_STARTUP": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 20,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: the state of a complete update from update.status on startup"
+ },
+ "UPDATE_STATE_CODE_PARTIAL_STARTUP": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 20,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: the state of a partial patch update from update.status on startup"
+ },
+ "UPDATE_STATE_CODE_UNKNOWN_STARTUP": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 20,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: the state of an unknown patch update from update.status on startup"
+ },
+ "UPDATE_STATE_CODE_COMPLETE_STAGE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 20,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: the state of a complete patch update from update.status after staging"
+ },
+ "UPDATE_STATE_CODE_PARTIAL_STAGE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 20,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: the state of a partial patch update from update.status after staging"
+ },
+ "UPDATE_STATE_CODE_UNKNOWN_STAGE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 20,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: the state of an unknown patch update from update.status after staging"
+ },
+ "UPDATE_STATUS_ERROR_CODE_COMPLETE_STARTUP": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: the status error code for a failed complete patch update from update.status on startup"
+ },
+ "UPDATE_STATUS_ERROR_CODE_PARTIAL_STARTUP": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: the status error code for a failed partial patch update from update.status on startup"
+ },
+ "UPDATE_STATUS_ERROR_CODE_UNKNOWN_STARTUP": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: the status error code for a failed unknown patch update from update.status on startup"
+ },
+ "UPDATE_STATUS_ERROR_CODE_COMPLETE_STAGE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: the status error code for a failed complete patch update from update.status after staging"
+ },
+ "UPDATE_STATUS_ERROR_CODE_PARTIAL_STAGE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: the status error code for a failed partial patch update from update.status after staging"
+ },
+ "UPDATE_STATUS_ERROR_CODE_UNKNOWN_STAGE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: the status error code for a failed unknown patch update from update.status after staging"
+ },
+ "UPDATE_WIZ_LAST_PAGE_CODE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 30,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1137447],
+ "description": "Update: the update wizard page displayed when the UI was closed (mapped in toolkit/mozapps/update/UpdateTelemetry.jsm)"
+ },
+ "UPDATE_NOTIFICATION_SHOWN": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "bug_numbers": [893505, 1521427, 1553982],
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: the application update doorhanger type that was displayed.",
+ "labels": ["restart", "available", "manual", "unsupported", "otherinstance"]
+ },
+ "UPDATE_NOTIFICATION_BADGE_SHOWN": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "bug_numbers": [893505, 1365204, 1521427, 1553982],
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: the application update badge type that was displayed.",
+ "labels": ["restart", "available", "manual", "unsupported", "otherinstance"]
+ },
+ "UPDATE_NOTIFICATION_DISMISSED": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "bug_numbers": [893505, 1521427, 1553982],
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: the dismiss action was executed for this application update doorhanger type.",
+ "labels": ["restart", "available", "manual", "unsupported", "otherinstance"]
+ },
+ "UPDATE_NOTIFICATION_MAIN_ACTION_DOORHANGER": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "bug_numbers": [893505, 1521427, 1553982],
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: the main update action was initiated for this application update doorhanger type.",
+ "labels": ["restart", "available", "manual", "unsupported", "otherinstance"]
+ },
+ "UPDATE_NOTIFICATION_MAIN_ACTION_MENU": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "bug_numbers": [893505, 1521427, 1553982],
+ "releaseChannelCollection": "opt-out",
+ "description": "Update: the update action was initiated from the PanelUI application update menu item.",
+ "labels": ["restart", "available", "manual", "unsupported", "otherinstance"]
+ },
+ "UPDATE_CAN_USE_BITS_EXTERNAL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": [
+ "CanUseBits",
+ "NoBits_NotWindows",
+ "NoBits_FeatureOff",
+ "NoBits_Pref",
+ "NoBits_Proxy",
+ "NoBits_OtherUser"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1343669, 1540193, 1520321],
+ "description": "Update: Whether BITS could be used to download updates (externally initiated)"
+ },
+ "UPDATE_CAN_USE_BITS_NOTIFY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": [
+ "CanUseBits",
+ "NoBits_NotWindows",
+ "NoBits_FeatureOff",
+ "NoBits_Pref",
+ "NoBits_Proxy",
+ "NoBits_OtherUser"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1343669, 1540193, 1520321],
+ "description": "Update: Whether BITS could be used to download updates (timer initiated)"
+ },
+ "UPDATE_CAN_USE_BITS_SUBSEQUENT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": [
+ "CanUseBits",
+ "NoBits_NotWindows",
+ "NoBits_FeatureOff",
+ "NoBits_Pref",
+ "NoBits_Proxy",
+ "NoBits_OtherUser"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1343669, 1540193, 1520321, 353804],
+ "description": "Update: Whether BITS could be used to download updates (after we already have an update ready)"
+ },
+ "UPDATE_BITS_RESULT_COMPLETE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 99,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1343669, 1540193, 1520321],
+ "description": "Update: Result code from downloading a complete update via BITS",
+ "operating_systems": ["windows"]
+ },
+ "UPDATE_BITS_RESULT_PARTIAL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "bytesized@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 99,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1343669, 1540193, 1520321],
+ "description": "Update: Result code from downloading a partial update via BITS",
+ "operating_systems": ["windows"]
+ },
+ "UPDATE_LANGPACK_OVERTIME": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "application-update-telemetry-alerts@mozilla.com",
+ "dtownsend@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 600,
+ "n_buckets": 22,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1656994],
+ "description": "Update: How much longer langpacks took to download than the app update in seconds."
+ },
+ "THUNDERBIRD_GLODA_SIZE_MB": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 1000,
+ "n_buckets": 40,
+ "description": "Gloda: size of global-messages-db.sqlite (MB)"
+ },
+ "THUNDERBIRD_INDEXING_RATE_MSG_PER_S": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 20,
+ "description": "Gloda: indexing rate (message/s)"
+ },
+ "FX_TAB_CLOSE_TIME_ANIM_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["mconley@mozilla.com"],
+ "bug_numbers": [1340842, 1488952],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Firefox: Time taken from the point of closing a tab (with animation), to the browser element being removed from the DOM. (ms)."
+ },
+ "FX_TAB_CLOSE_TIME_NO_ANIM_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["mconley@mozilla.com"],
+ "bug_numbers": [1340842, 1714255, 1730041, 1754640],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Firefox: Time taken from the point of closing a tab (without animation) to the browser element being removed from the DOM. (ms)."
+ },
+ "FX_TAB_CLOSE_PERMIT_UNLOAD_TIME_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["mconley@mozilla.com"],
+ "bug_numbers": [1340842, 1714255, 1730041, 1754640],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Firefox: Time taken to run permitUnload on a browser during tab close to see whether or not we're allowed to close the tab (ms)."
+ },
+ "FX_REFRESH_DRIVER_CHROME_FRAME_DELAY_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "perf-telemetry-alerts@mozilla.com",
+ "gfx-telemetry-alerts@mozilla.com",
+ "rhunt@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "bug_numbers": [1220699],
+ "description": "Delay in ms between the target and the actual handling time of the frame at refresh driver in the chrome process."
+ },
+ "FX_REFRESH_DRIVER_CONTENT_FRAME_DELAY_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "perf-telemetry-alerts@mozilla.com",
+ "gfx-telemetry-alerts@mozilla.com",
+ "rhunt@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "bug_numbers": [1221674],
+ "description": "Delay in ms between the target and the actual handling time of the frame at refresh driver in the content process."
+ },
+ "FX_REFRESH_DRIVER_SYNC_SCROLL_FRAME_DELAY_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "perf-telemetry-alerts@mozilla.com",
+ "gfx-telemetry-alerts@mozilla.com",
+ "rhunt@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "bug_numbers": [1228147],
+ "description": "Delay in ms between the target and the actual handling time of the frame at refresh driver while scrolling synchronously."
+ },
+ "FX_TAB_SWITCH_UPDATE_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "perf-telemetry-alerts@mozilla.com",
+ "mconley@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 20,
+ "bug_numbers": [1489524],
+ "description": "Firefox: Time in ms spent updating UI in response to a tab switch"
+ },
+ "FX_TAB_SWITCH_TOTAL_E10S_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["mconley@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 20,
+ "bug_numbers": [1156592, 1489524],
+ "releaseChannelCollection": "opt-out",
+ "description": "Firefox: Time in ms between tab selection and tab content paint in e10s windows"
+ },
+ "FX_TAB_SWITCH_COMPOSITE_E10S_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["mwoodrow@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 20,
+ "bug_numbers": [1481704, 1529352, 1580077],
+ "releaseChannelCollection": "opt-out",
+ "description": "Firefox: Time in ms between tab selection and first composite of the tab content in e10s windows"
+ },
+ "FX_TAB_SWITCH_SPINNER_VISIBLE_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["mconley@mozilla.com", "dothayer@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 20,
+ "bug_numbers": [1156592],
+ "releaseChannelCollection": "opt-out",
+ "description": "Firefox: If the spinner interstitial displays during tab switching, records the time in ms the graphic is visible"
+ },
+ "FX_TAB_SWITCH_SPINNER_VISIBLE_LONG_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["mconley@mozilla.com", "dothayer@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 1000,
+ "high": 64000,
+ "n_buckets": 7,
+ "bug_numbers": [1301104],
+ "releaseChannelCollection": "opt-out",
+ "description": "Firefox: If the spinner interstitial displays during tab switching, records the time in ms the graphic is visible. This probe is similar to FX_TAB_SWITCH_SPINNER_VISIBLE_MS, but is for truly degenerate cases."
+ },
+ "FX_TAB_SWITCH_SPINNER_VISIBLE_TRIGGER": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["dothayer@mozilla.com"],
+ "bug_numbers": [1442068],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": [
+ "none",
+ "preActions",
+ "postActions",
+ "onLoadTimeout",
+ "onLayersReady",
+ "onSizeModeOrOcc",
+ "onEndSwapDocShells"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "Diagnostic probe to aid in categorizing tab switch spinners. Records what most recently set the loadTimer to null if a spinner was displayed."
+ },
+ "FX_NUMBER_OF_UNIQUE_SITE_ORIGINS_ALL_TABS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100,
+ "n_buckets": 50,
+ "description": "When a document is loaded, report the number of unique site origins across the browser(all tabs) if it has been at least 5 minutes since last time we collect this data",
+ "bug_numbers": [1589700],
+ "alert_emails": ["sefeng@mozilla.com", "perf-telemetry-alerts@mozilla.com"],
+ "releaseChannelCollection": "opt-out"
+ },
+ "FX_NUMBER_OF_UNIQUE_SITE_ORIGINS_PER_DOCUMENT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100,
+ "n_buckets": 50,
+ "description": "When a document is unloaded, report the highest number of site origins loaded simultaneously in that document.",
+ "bug_numbers": [1603185],
+ "alert_emails": ["barret@mozilla.com", "perf-telemetry-alerts@mozilla.com"],
+ "releaseChannelCollection": "opt-out"
+ },
+ "FX_TAB_SWITCH_REQUEST_TAB_WARMING_STATE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "expires_in_version": "88",
+ "alert_emails": ["mconley@mozilla.com"],
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": ["disqualified", "notWarmed", "stillLoading", "loaded"],
+ "bug_numbers": [1385453],
+ "description": "Firefox: When a tab is selected, records whether or not the tab was speculatively 'warmed up' to improve tab switch time."
+ },
+ "FX_TAB_CLICK_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 20,
+ "description": "Firefox: Time in ms spent on switching tabs in response to a tab click"
+ },
+ "FX_BOOKMARKS_TOOLBAR_INIT_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "alert_emails": ["mak@mozilla.com"],
+ "kind": "exponential",
+ "low": 50,
+ "high": 5000,
+ "n_buckets": 10,
+ "bug_numbers": [723165],
+ "description": "Firefox: Time to initialize the bookmarks toolbar view (ms)"
+ },
+ "FX_NEW_WINDOW_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "alert_emails": ["mconley@mozilla.com"],
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 20,
+ "bug_numbers": [1489524],
+ "description": "Firefox: Time taken to open a new browser window (ms)"
+ },
+ "FX_PAGE_LOAD_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "65",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 20,
+ "description": "Firefox: Time taken to load a page (ms). This includes all static contents, no dynamic content. Loading of about: pages is not counted.",
+ "bug_numbers": [790213, 1504247],
+ "alert_emails": ["tdsmith@mozilla.com"]
+ },
+ "FX_PAGE_LOAD_MS_2": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 10000,
+ "n_buckets": 200,
+ "description": "Firefox: Time taken to load a page (ms). This includes all static contents, no dynamic content. Page reloads and loading of about: pages are not included.",
+ "bug_numbers": [1504247, 1549519],
+ "alert_emails": [
+ "tdsmith@mozilla.com",
+ "perf-telemetry-alerts@mozilla.com",
+ "product-metrics-telemetry-alerts@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out"
+ },
+ "FX_PAGE_RELOAD_NORMAL_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 10000,
+ "n_buckets": 200,
+ "description": "Firefox: Time taken to reload a page (ms) when using the LOAD_RELOAD_NORMAL flag",
+ "bug_numbers": [1549519],
+ "alert_emails": [
+ "sefeng@mozilla.com",
+ "perf-telemetry-alerts@mozilla.com",
+ "product-metrics-telemetry-alerts@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out"
+ },
+ "FX_PAGE_RELOAD_SKIP_CACHE_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 10000,
+ "n_buckets": 200,
+ "description": "Firefox: Time taken to reload a page (ms) when caches are skipped",
+ "bug_numbers": [1549519],
+ "alert_emails": [
+ "sefeng@mozilla.com",
+ "perf-telemetry-alerts@mozilla.com",
+ "product-metrics-telemetry-alerts@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out"
+ },
+ "FX_SCHEDULE_PRESSURE_IDLE_SAMPLE_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "61",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 20,
+ "bug_numbers": [1406414],
+ "alert_emails": ["jaws@mozilla.com"],
+ "description": "Firefox: Time taken to get an idle callback while loading a page (ms). Loading of about: pages is not counted."
+ },
+ "FX_TOTAL_TOP_VISITS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "boolean",
+ "description": "Count the number of times a new top page was starting to load"
+ },
+ "FX_THUMBNAILS_CAPTURE_TIME_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 500,
+ "n_buckets": 15,
+ "description": "THUMBNAILS: Time (ms) it takes to capture a thumbnail"
+ },
+ "FX_THUMBNAILS_STORE_TIME_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 500,
+ "n_buckets": 15,
+ "description": "THUMBNAILS: Time (ms) it takes to store a thumbnail in the cache"
+ },
+ "FX_THUMBNAILS_HIT_OR_MISS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "THUMBNAILS: Thumbnail found"
+ },
+ "FX_MIGRATION_ENTRY_POINT_CATEGORICAL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "bug_numbers": [1822692],
+ "alert_emails": [
+ "mconley@mozilla.com",
+ "gijs@mozilla.com",
+ "mak@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": [
+ "unknown",
+ "firstrun",
+ "fxrefresh",
+ "places",
+ "passwords",
+ "newtab",
+ "file_menu",
+ "help_menu",
+ "bookmarks_toolbar",
+ "preferences"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "Where the migration wizard was entered from."
+ },
+ "FX_MIGRATION_SOURCE_BROWSER": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "bug_numbers": [731025, 1523179, 1643431, 1678204],
+ "alert_emails": ["gijs@mozilla.com", "mak@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 15,
+ "releaseChannelCollection": "opt-out",
+ "description": "The browser that data is pulled from. The values correspond to the internal browser ID (see MigrationUtils.jsm)"
+ },
+ "FX_MIGRATION_ERRORS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "bug_numbers": [731025, 1584261, 1643431, 1678204],
+ "alert_emails": ["gijs@mozilla.com", "mak@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 12,
+ "releaseChannelCollection": "opt-out",
+ "description": "Errors encountered during migration in buckets defined by the datatype, keyed by the string description of the browser."
+ },
+ "FX_MIGRATION_USAGE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "bug_numbers": [731025, 1584261, 1643431, 1678204],
+ "alert_emails": ["gijs@mozilla.com", "mak@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 12,
+ "releaseChannelCollection": "opt-out",
+ "description": "Usage of migration for each datatype when migration is run through the post-firstrun flow which allows individual datatypes, keyed by the string description of the browser."
+ },
+ "FX_MIGRATION_BOOKMARKS_IMPORT_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "bug_numbers": [1289436, 1643431, 1678204],
+ "alert_emails": ["gijs@mozilla.com", "mak@mozilla.com"],
+ "expires_in_version": "92",
+ "kind": "exponential",
+ "n_buckets": 70,
+ "high": 100000,
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "description": "How long it took to import bookmarks from another browser, keyed by the name of the browser."
+ },
+ "FX_MIGRATION_HISTORY_IMPORT_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "bug_numbers": [1289436, 1643431, 1678204],
+ "alert_emails": ["gijs@mozilla.com", "mak@mozilla.com"],
+ "expires_in_version": "92",
+ "kind": "exponential",
+ "n_buckets": 70,
+ "high": 100000,
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "description": "How long it took to import history from another browser, keyed by the name of the browser."
+ },
+ "FX_MIGRATION_LOGINS_IMPORT_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "bug_numbers": [1289436, 1584261, 1643431, 1678204],
+ "alert_emails": [
+ "gijs@mozilla.com",
+ "mak@mozilla.com",
+ "passwords-dev@mozilla.org"
+ ],
+ "expires_in_version": "92",
+ "kind": "exponential",
+ "n_buckets": 70,
+ "high": 100000,
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "description": "How long it took to import logins (passwords) from another browser, keyed by the name of the browser. The time for users to unlock Keychain on macOS is included in this time."
+ },
+ "FX_MIGRATION_BOOKMARKS_JANK_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "bug_numbers": [1338522],
+ "alert_emails": ["dao@mozilla.com"],
+ "expires_in_version": "65",
+ "kind": "exponential",
+ "n_buckets": 20,
+ "high": 60000,
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "description": "Accumulated timer delay (variance between when the timer was expected to fire and when it actually fired) in milliseconds as an indicator for decreased main-thread responsiveness while importing bookmarks from another browser, keyed by the name of the browser (see gAvailableMigratorKeys in MigrationUtils.jsm). The import is happening on a background thread and should ideally not affect the UI noticeably."
+ },
+ "FX_MIGRATION_HISTORY_JANK_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "bug_numbers": [1338522],
+ "alert_emails": ["dao@mozilla.com"],
+ "expires_in_version": "65",
+ "kind": "exponential",
+ "n_buckets": 20,
+ "high": 60000,
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "description": "Accumulated timer delay (variance between when the timer was expected to fire and when it actually fired) in milliseconds as an indicator for decreased main-thread responsiveness while importing history from another browser, keyed by the name of the browser (see gAvailableMigratorKeys in MigrationUtils.jsm). The import is happening on a background thread and should ideally not affect the UI noticeably."
+ },
+ "FX_MIGRATION_LOGINS_JANK_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "bug_numbers": [1338522, 1584261, 1643431, 1678204],
+ "alert_emails": ["dao@mozilla.com", "passwords-dev@mozilla.org"],
+ "expires_in_version": "92",
+ "kind": "exponential",
+ "n_buckets": 20,
+ "high": 60000,
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "description": "Accumulated timer delay (variance between when the timer was expected to fire and when it actually fired) in milliseconds as an indicator for decreased main-thread responsiveness while importing logins / passwords from another browser, keyed by the name of the browser (see gAvailableMigratorKeys in MigrationUtils.jsm). The import is happening on a background thread and should ideally not affect the UI noticeably. The time with the blocking Keychain dialog on macOS can skew this data."
+ },
+ "FX_MIGRATION_BOOKMARKS_QUANTITY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "bug_numbers": [1279501, 1643431, 1678204],
+ "alert_emails": ["gijs@mozilla.com", "mak@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "n_buckets": 20,
+ "high": 1000,
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "description": "How many bookmarks we imported from another browser, keyed by the name of the browser."
+ },
+ "FX_MIGRATION_HISTORY_QUANTITY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "bug_numbers": [1279501, 1643431, 1678204],
+ "alert_emails": ["gijs@mozilla.com", "mak@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "n_buckets": 40,
+ "high": 10000,
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "description": "How many history visits we imported from another browser, keyed by the name of the browser."
+ },
+ "FX_MIGRATION_LOGINS_QUANTITY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "bug_numbers": [1279501, 1584261, 1643431, 1678204],
+ "alert_emails": [
+ "gijs@mozilla.com",
+ "mak@mozilla.com",
+ "passwords-dev@mozilla.org"
+ ],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "n_buckets": 20,
+ "high": 1000,
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "description": "How many logins (passwords) we imported from another browser, keyed by the name of the browser."
+ },
+ "FX_MIGRATION_CARDS_QUANTITY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "bug_numbers": [1834545],
+ "alert_emails": ["mconley@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "n_buckets": 20,
+ "high": 1000,
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "description": "How many credit card entries we imported from another browser, keyed by the name of the browser."
+ },
+ "FX_MIGRATION_EXTENSIONS_QUANTITY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "bug_numbers": [1834545],
+ "alert_emails": ["mconley@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "n_buckets": 20,
+ "high": 1000,
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "description": "How many extensions were matched to be imported from another browser, keyed by the name of the browser."
+ },
+ "PWMGR_IMPORT_LOGINS_FROM_FILE_CATEGORICAL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "bug_numbers": [1688653, 1749882, 1777397, 1850888],
+ "alert_emails": ["passwords-dev@mozilla.org"],
+ "expires_in_version": "144",
+ "kind": "categorical",
+ "labels": ["added", "modified", "error", "no_change"],
+ "releaseChannelCollection": "opt-out",
+ "description": "Results of login import from a CSV/TSV file, by category"
+ },
+ "FX_ABOUTHOME_CACHE_CONSTRUCTION": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["mconley@mozilla.com"],
+ "bug_numbers": [
+ 1622263, 1683101, 1714258, 1730042, 1754641, 1781978, 1811151, 1841926
+ ],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "description": "The length of time (in milliseconds) that it takes for the cache worker to generate the cache and return it to the main thread",
+ "releaseChannelCollection": "opt-out"
+ },
+ "INPUT_EVENT_RESPONSE_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "perf-telemetry-alerts@mozilla.com",
+ "chutten@mozilla.com"
+ ],
+ "bug_numbers": [1235908, 1511919],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time (ms) from the Input event being created to the end of it being handled",
+ "releaseChannelCollection": "opt-out"
+ },
+ "INPUT_EVENT_RESPONSE_COALESCED_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "perf-telemetry-alerts@mozilla.com",
+ "chutten@mozilla.com",
+ "gfritzsche@mozilla.com"
+ ],
+ "bug_numbers": [1357457, 1489524],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "description": "Time (ms) from the Input event being created to the end of it being handled, but with overlapping events coalesced."
+ },
+ "INPUT_EVENT_RESPONSE_STARTUP_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["wpan@mozilla.com"],
+ "bug_numbers": [1373814],
+ "expires_in_version": "61",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "description": "Time (ms) from the Input event being created to the end of it being handled, but with overlapping events coalesced, which happens before the process is ready for interaction."
+ },
+ "INPUT_EVENT_RESPONSE_POST_STARTUP_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["wpan@mozilla.com"],
+ "bug_numbers": [1373814],
+ "expires_in_version": "61",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "description": "Time (ms) from the Input event being created to the end of it being handled, but with overlapping events coalesced, which happens after the process is ready for interaction."
+ },
+ "LOAD_INPUT_EVENT_RESPONSE_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1298101],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time (ms) from the Input event being created to the end of it being handled for events handling during page load only"
+ },
+ "EVENTLOOP_UI_ACTIVITY_EXP_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1198196],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50,
+ "description": "Widget: Time it takes for the message before a UI message (ms)"
+ },
+ "FX_SESSION_RESTORE_STARTUP_INIT_SESSION_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "session-restore-telemetry-alerts@mozilla.com",
+ "mdeboer@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 20,
+ "bug_numbers": [1489524],
+ "description": "Session restore: Time it takes to prepare the data structures for restoring a session (ms)"
+ },
+ "FX_SESSION_RESTORE_STARTUP_ONLOAD_INITIAL_WINDOW_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "session-restore-telemetry-alerts@mozilla.com",
+ "mdeboer@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 20,
+ "bug_numbers": [1489524],
+ "description": "Session restore: Time it takes to finish restoration once we have first opened a window (ms)"
+ },
+ "FX_SESSION_RESTORE_COLLECT_ALL_WINDOWS_DATA_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 10,
+ "description": "Session restore: Time to collect all window data (ms)"
+ },
+ "FX_SESSION_RESTORE_COLLECT_DATA_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 10,
+ "description": "Session restore: Time to collect all window and tab data (ms)"
+ },
+ "FX_SESSION_RESTORE_CONTENT_COLLECT_DATA_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "session-restore-telemetry-alerts@mozilla.com",
+ "mdeboer@mozilla.com"
+ ],
+ "expires_in_version": "60",
+ "kind": "exponential",
+ "keyed": true,
+ "high": 30000,
+ "n_buckets": 20,
+ "bug_numbers": [1360916],
+ "description": "Session restore: Duration of data collection in the content process (ms). Possible keys currently are: historychange, scroll, formdata, pageStyle, disallow, storage, storagechange."
+ },
+ "FX_SESSION_RESTORE_COLLECT_SESSION_HISTORY_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "session-restore-telemetry-alerts@mozilla.com",
+ "farre@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 20,
+ "bug_numbers": [1810704],
+ "description": "Session restore: Duration of session history collection in the parent process (ms)."
+ },
+ "FX_SESSION_RESTORE_PRIVACY_LEVEL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "60",
+ "kind": "enumerated",
+ "n_values": 3,
+ "bug_numbers": [1360810],
+ "description": "Session restore: The privacy level chosen by the user (0: Collect data from all sites, 1: Collect data only from non-HTTPS sites, 2: Collect no data)"
+ },
+ "FX_SESSION_RESTORE_SERIALIZE_DATA_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 10,
+ "description": "Session restore: Time to JSON serialize session data (ms)"
+ },
+ "FX_SESSION_RESTORE_READ_FILE_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Session restore: Time to read the session data from the file on disk (ms)"
+ },
+ "FX_SESSION_RESTORE_WRITE_FILE_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 9000,
+ "n_buckets": 20,
+ "description": "Session restore: Time to write the session data to the file on disk (ms)"
+ },
+ "FX_SESSION_RESTORE_FILE_SIZE_BYTES": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 50000000,
+ "n_buckets": 30,
+ "description": "Session restore: The size of file sessionstore.js (bytes)"
+ },
+ "FX_SESSION_RESTORE_CORRUPT_FILE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "default",
+ "kind": "boolean",
+ "description": "Session restore: Whether the file read on startup contained parse-able JSON"
+ },
+ "FX_SESSION_RESTORE_ALL_FILES_CORRUPT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "default",
+ "kind": "boolean",
+ "description": "Session restore: Whether none of the backup files contained parse-able JSON"
+ },
+ "FX_SESSION_RESTORE_RESTORE_WINDOW_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "session-restore-telemetry-alerts@mozilla.com",
+ "mdeboer@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "bug_numbers": [1489524],
+ "description": "Session restore: Time spent blocking the main thread while restoring a window state (ms)"
+ },
+ "FX_SESSION_RESTORE_SEND_UPDATE_CAUSED_OOM": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "default",
+ "kind": "count",
+ "description": "Count of messages sent by SessionRestore from child frames to the parent and that cannot be transmitted as they eat up too much memory."
+ },
+ "FX_SESSION_RESTORE_AUTO_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "low": 100,
+ "high": 100000,
+ "n_buckets": 20,
+ "description": "Session restore: If the browser is setup to auto-restore tabs, this probe measures the time elapsed between the instant we start Session Restore and the instant we have finished restoring tabs eagerly. At this stage, the tabs that are restored on demand are not restored yet."
+ },
+ "FX_SESSION_RESTORE_MANUAL_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["session-restore-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "low": 100,
+ "high": 100000,
+ "n_buckets": 20,
+ "description": "Session restore: If a session is restored by the user clicking on 'Restore Session', this probe measures the time elapsed between the instant the user has clicked and the instant we have finished restoring tabs eagerly. At this stage, the tabs that are restored on demand are not restored yet."
+ },
+ "FX_SESSION_RESTORE_NUMBER_OF_TABS_RESTORED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 500,
+ "n_buckets": 20,
+ "description": "Session restore: Number of tabs in the session that has just been restored."
+ },
+ "FX_SESSION_RESTORE_NUMBER_OF_WINDOWS_RESTORED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "enumerated",
+ "n_values": 50,
+ "description": "Session restore: Number of windows in the session that has just been restored."
+ },
+ "FX_SESSION_RESTORE_NUMBER_OF_EAGER_TABS_RESTORED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "enumerated",
+ "n_values": 50,
+ "description": "Session restore: Number of tabs restored eagerly in the session that has just been restored."
+ },
+ "FX_SESSION_RESTORE_CLOSED_TABS_NOT_SAVED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "expires_in_version": "127",
+ "alert_emails": ["firefox-view-engineers@mozilla.com"],
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1848459],
+ "kind": "enumerated",
+ "n_values": 25,
+ "description": "Session restore: Number of closed tabs that are NOT saved due to lack of open tabs worth saving on window close."
+ },
+ "FX_TABLETMODE_PAGE_LOAD": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "47",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 30,
+ "keyed": true,
+ "description": "Number of toplevel location changes in tablet and desktop mode (only used on win10 where tablet mode is available)"
+ },
+ "FX_URLBAR_MERINO_LATENCY_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["fx-search-telemetry@mozilla.com", "adw@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1727799],
+ "description": "Firefox: Latency of the Merino source for quick suggest (ms)"
+ },
+ "FX_URLBAR_MERINO_LATENCY_WEATHER_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["fx-search-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1804536],
+ "description": "Firefox: Latency of Merino weather suggestions (ms)"
+ },
+ "FX_URLBAR_MERINO_RESPONSE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["fx-search-telemetry@mozilla.com", "adw@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": [
+ "success",
+ "timeout",
+ "network_error",
+ "http_error",
+ "no_suggestion"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1737923],
+ "description": "Firefox: Records whether each Merino fetch was successful or not. 'success' (0): The fetch completed without any error before the timeout elapsed. 'timeout' (1): The timeout elapsed before the fetch completed or otherwise failed. 'network_error' (2): The fetch failed due to a network error before the timeout elapsed. e.g., the user's network or the Merino server was down. 'http_error' (3): The fetch completed before the timeout elapsed but the server returned an error."
+ },
+ "FX_URLBAR_MERINO_RESPONSE_WEATHER": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["fx-search-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": [
+ "success",
+ "timeout",
+ "network_error",
+ "http_error",
+ "no_suggestion"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1804536],
+ "description": "For weather suggestions, this histogram is updated in addition to FX_URLBAR_MERINO_RESPONSE. The categories are the same."
+ },
+ "FX_URLBAR_QUICK_SUGGEST_REMOTE_SETTINGS_LATENCY_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["fx-search-telemetry@mozilla.com", "adw@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1737651],
+ "description": "Firefox: Latency of the remote settings source for quick suggest (ms)"
+ },
+ "FX_URLBAR_ZERO_PREFIX_DWELL_TIME_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["fx-search-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1806765],
+ "description": "Firefox: Records how long the user is exposed to the zero-prefix results panel (ms)"
+ },
+ "FX_URLBAR_SELECTED_RESULT_METHOD": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dzeber@mozilla.com", "fx-search-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": [
+ "enter",
+ "enterSelection",
+ "click",
+ "arrowEnterSelection",
+ "tabEnterSelection",
+ "rightClickEnter"
+ ],
+ "bug_numbers": [1334615],
+ "description": "The input method the user used to select a result in the urlbar. 'enter' => The user hit the Enter key on the heuristic result at index 0. 'enterSelection' => The user chose a non-heuristic result (in exotic ways) and then hit the Enter key. 'click' => The user clicked a result with the mouse. 'arrowEnterSelection' => The user chose a non-heuristic result using arrow keys and then hit the Enter key. 'tabEnterSelection' => The user chose a non-heuristic result using tab at least once and then hit the Enter key. 'rightClickEnter' => The user chose a non-heuristic result using right click and then hit the Enter key."
+ },
+ "FX_SEARCHBAR_SELECTED_RESULT_METHOD": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dzeber@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": ["enter", "enterSelection", "click"],
+ "bug_numbers": [1334615],
+ "description": "The input method the user used to select a result in the searchbar. 'enter' => The user hit the Enter key without choosing a result in the popup. 'enterSelection' => The user chose a result and then hit the Enter key. 'click' => The user clicked a result with the mouse."
+ },
+ "FX_URLBAR_PROVIDER_CLIPBOARD_READ_TIME_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["fx-search-telemetry@mozilla.com"],
+ "expires_in_version": "128",
+ "kind": "exponential",
+ "low": 16,
+ "high": 5000,
+ "n_buckets": 8,
+ "bug_numbers": [1866710],
+ "releaseChannelCollection": "opt-out",
+ "description": "Time taken to read data from the clipboard (ms)"
+ },
+ "INNERWINDOWS_WITH_MUTATION_LISTENERS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Deleted or to-be-reused innerwindow which has had mutation event listeners."
+ },
+ "HTMLEDITORS_WITH_BEFOREINPUT_LISTENERS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["mnakano.birchill@mozilla.com"],
+ "bug_numbers": [1668134, 1665530],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "releaseChannelCollection": "opt-out",
+ "description": "Number of HTML editors whose window or a node in it has had beforeinput event listeners"
+ },
+ "HTMLEDITORS_OVERRIDDEN_BY_BEFOREINPUT_LISTENERS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["mnakano.birchill@mozilla.com"],
+ "bug_numbers": [1668134, 1665530],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "releaseChannelCollection": "opt-out",
+ "description": "Number of HTML editors whose dispatching beforeinput events are canceled"
+ },
+ "HTMLEDITORS_WITH_MUTATION_LISTENERS_WITHOUT_BEFOREINPUT_LISTENERS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["mnakano.birchill@mozilla.com"],
+ "bug_numbers": [1668134, 1665530],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "releaseChannelCollection": "opt-out",
+ "description": "Number of HTML editors whose window or a node in it has had mutation event listeners, but has not had beforeinput event listeners"
+ },
+ "HTMLEDITORS_WITH_MUTATION_OBSERVERS_WITHOUT_BEFOREINPUT_LISTENERS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["mnakano.birchill@mozilla.com"],
+ "bug_numbers": [1668134, 1665530],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "releaseChannelCollection": "opt-out",
+ "description": "Number of HTML editors a node in the window has been observed by mutation observers, but has not had beforeinput event listeners"
+ },
+ "ENCODING_OVERRIDE_SITUATION_TEXT": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["hsivonen@mozilla.com"],
+ "bug_numbers": [
+ 840476, 977573, 1513473, 1554592, 1603047, 1648464, 1686463, 1706838,
+ 1712928
+ ],
+ "expires_in_version": "106",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": [
+ "UnlabeledAscii",
+ "UnlabeledNonUtf8TLD",
+ "UnlabeledNonUtf8",
+ "UnlabeledUtf8",
+ "ChannelNonUtf8",
+ "ChannelUtf8",
+ "Bug"
+ ],
+ "description": "Labeling status of top-level page when overriding encoding for plain text"
+ },
+ "ENCODING_OVERRIDE_SITUATION_HTML": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["hsivonen@mozilla.com"],
+ "bug_numbers": [
+ 840476, 977573, 1513473, 1554592, 1603047, 1648464, 1686463, 1706838,
+ 1712928
+ ],
+ "expires_in_version": "106",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": [
+ "UnlabeledAscii",
+ "UnlabeledNonUtf8TLD",
+ "UnlabeledNonUtf8",
+ "UnlabeledUtf8",
+ "ChannelNonUtf8",
+ "ChannelUtf8",
+ "InternalNonUtf8",
+ "InternalUtf8",
+ "LocalLabeled",
+ "Bug"
+ ],
+ "description": "Labeling status of top-level page when overriding encoding for HTML"
+ },
+ "ENCODING_DETECTION_OUTCOME_TEXT": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["hsivonen@mozilla.com"],
+ "bug_numbers": [1686463, 1702248, 1706838, 1712928],
+ "expires_in_version": "106",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": [
+ "UtfInitial",
+ "UtfFinal",
+ "TldInitial",
+ "TldFinal",
+ "ContentInitial",
+ "ContentFinal",
+ "GenericInitial",
+ "GenericFinal",
+ "TldFinalA",
+ "ContentFinalA",
+ "GenericFinalA"
+ ],
+ "description": "Type of automatic encoding detection outcome for text/plain excluding ASCII-only"
+ },
+ "ENCODING_DETECTION_OUTCOME_HTML": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["hsivonen@mozilla.com"],
+ "bug_numbers": [1686463, 1702248, 1706838, 1712928],
+ "expires_in_version": "106",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": [
+ "UtfInitial",
+ "UtfFinal",
+ "TldInitial",
+ "TldFinal",
+ "ContentInitial",
+ "ContentFinal",
+ "GenericInitial",
+ "GenericFinal",
+ "TldFinalA",
+ "ContentFinalA",
+ "GenericFinalA"
+ ],
+ "description": "Type of automatic encoding detection outcome for text/html excluding ASCII-only"
+ },
+ "LONG_REFLOW_INTERRUPTIBLE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Long running reflow, interruptible or not"
+ },
+ "XMLHTTPREQUEST_ASYNC_OR_SYNC": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Type of XMLHttpRequest, async or sync"
+ },
+ "LOCALDOMSTORAGE_SHUTDOWN_DATABASE_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time to flush and close the localStorage database (ms)"
+ },
+ "LOCALDOMSTORAGE_PRELOAD_PENDING_ON_FIRST_ACCESS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "boolean",
+ "description": "True when we had to wait for a pending preload on first access to localStorage data, false otherwise"
+ },
+ "LOCALDOMSTORAGE_GETALLKEYS_BLOCKING_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time to block before we return a list of all keys in domain's LocalStorage (ms)"
+ },
+ "LOCALDOMSTORAGE_GETKEY_BLOCKING_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time to block before we return a key name in domain's LocalStorage (ms)"
+ },
+ "LOCALDOMSTORAGE_GETLENGTH_BLOCKING_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time to block before we return number of keys in domain's LocalStorage (ms)"
+ },
+ "LOCALDOMSTORAGE_GETVALUE_BLOCKING_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time to block before we return a value for a key in LocalStorage (ms)"
+ },
+ "LOCALDOMSTORAGE_SETVALUE_BLOCKING_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time to block before we set a single key's value in LocalStorage (ms)"
+ },
+ "LOCALDOMSTORAGE_REMOVEKEY_BLOCKING_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time to block before we remove a single key from LocalStorage (ms)"
+ },
+ "LOCALDOMSTORAGE_CLEAR_BLOCKING_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time to block before we clear LocalStorage for all domains (ms)"
+ },
+ "LOCALDOMSTORAGE_UNLOAD_BLOCKING_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time to fetch LocalStorage data before we can clean the cache (ms)"
+ },
+ "LOCALDOMSTORAGE_SESSIONONLY_PRELOAD_BLOCKING_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "40",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time to fetch LocalStorage data before we can expose them as session only data (ms)"
+ },
+ "LOGIN_REPUTATION_LOGIN_WHITELIST_RESULT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "enumerated",
+ "bug_numbers": [1422671],
+ "n_values": 3,
+ "description": "Login reputation login whitelist result (0=UNSPECIFIED, 1=SAFE, 2=ERROR)"
+ },
+ "LOGIN_REPUTATION_LOGIN_WHITELIST_LOOKUP_TIME": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "bug_numbers": [1422671],
+ "high": 5000,
+ "n_buckets": 30,
+ "description": "Time spent per login reputation service lookup local whitelist (ms)"
+ },
+ "RANGE_CHECKSUM_ERRORS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Number of histograms with range checksum errors"
+ },
+ "BUCKET_ORDER_ERRORS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Number of histograms with bucket order errors"
+ },
+ "TOTAL_COUNT_HIGH_ERRORS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Number of histograms with total count high errors"
+ },
+ "TOTAL_COUNT_LOW_ERRORS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Number of histograms with total count low errors"
+ },
+ "TELEMETRY_ARCHIVE_DIRECTORIES_COUNT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 13,
+ "n_buckets": 12,
+ "bug_numbers": [1162538],
+ "description": "Number of directories in the archive at scan"
+ },
+ "TELEMETRY_ARCHIVE_OLDEST_DIRECTORY_AGE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 13,
+ "n_buckets": 12,
+ "bug_numbers": [1162538],
+ "description": "The age of the oldest Telemetry archive directory in months"
+ },
+ "TELEMETRY_ARCHIVE_SCAN_PING_COUNT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 100,
+ "bug_numbers": [1162538],
+ "description": "Number of Telemetry pings in the archive at scan"
+ },
+ "TELEMETRY_ARCHIVE_SESSION_PING_COUNT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1162538],
+ "description": "Number of Telemetry pings added to the archive during the session"
+ },
+ "TELEMETRY_ARCHIVE_SIZE_MB": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 300,
+ "n_buckets": 60,
+ "bug_numbers": [1162538],
+ "description": "The size of the Telemetry archive (MB)"
+ },
+ "TELEMETRY_ARCHIVE_EVICTED_OVER_QUOTA": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 100,
+ "bug_numbers": [1162538],
+ "description": "Number of Telemetry pings evicted from the archive during cleanup, because they were over the quota"
+ },
+ "TELEMETRY_ARCHIVE_EVICTED_OLD_DIRS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 13,
+ "n_buckets": 12,
+ "bug_numbers": [1162538],
+ "description": "Number of Telemetry directories evicted from the archive during cleanup, because they were too old"
+ },
+ "TELEMETRY_ARCHIVE_EVICTING_DIRS_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 300000,
+ "n_buckets": 20,
+ "bug_numbers": [1162538],
+ "description": "Time (ms) it takes for evicting old directories"
+ },
+ "TELEMETRY_ARCHIVE_CHECKING_OVER_QUOTA_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 300000,
+ "n_buckets": 20,
+ "bug_numbers": [1162538],
+ "description": "Time (ms) it takes for checking if the archive is over-quota"
+ },
+ "TELEMETRY_ARCHIVE_EVICTING_OVER_QUOTA_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 300000,
+ "n_buckets": 20,
+ "bug_numbers": [1162538],
+ "description": "Time (ms) it takes for evicting over-quota pings"
+ },
+ "TELEMETRY_PENDING_LOAD_FAILURE_READ": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Number of pending Telemetry pings that failed to load from the disk"
+ },
+ "TELEMETRY_PENDING_LOAD_FAILURE_PARSE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Number of pending Telemetry pings that failed to parse once loaded from the disk"
+ },
+ "TELEMETRY_PENDING_PINGS_SIZE_MB": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 17,
+ "n_buckets": 16,
+ "description": "The size of the Telemetry pending pings directory (MB). The special value 17 is used to indicate over quota pings."
+ },
+ "TELEMETRY_PENDING_PINGS_AGE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 365,
+ "n_buckets": 30,
+ "description": "The age, in days, of the pending pings."
+ },
+ "TELEMETRY_PENDING_PINGS_EVICTED_OVER_QUOTA": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 100,
+ "description": "Number of Telemetry pings evicted from the pending pings directory during cleanup, because they were over the quota"
+ },
+ "TELEMETRY_PENDING_EVICTING_OVER_QUOTA_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 300000,
+ "n_buckets": 20,
+ "description": "Time (ms) it takes for evicting over-quota pending pings"
+ },
+ "TELEMETRY_PENDING_CHECKING_OVER_QUOTA_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 300000,
+ "n_buckets": 20,
+ "description": "Time (ms) it takes for checking if the pending pings are over-quota"
+ },
+ "TELEMETRY_PING_SIZE_EXCEEDED_SEND": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Number of Telemetry pings discarded before sending because they exceeded the maximum size"
+ },
+ "TELEMETRY_PING_SIZE_EXCEEDED_PENDING": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Number of Telemetry pending pings discarded because they exceeded the maximum size"
+ },
+ "TELEMETRY_PING_SIZE_EXCEEDED_ARCHIVED": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Number of archived Telemetry pings discarded because they exceeded the maximum size"
+ },
+ "TELEMETRY_PING_SUBMISSION_WAITING_CLIENTID": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1233986],
+ "description": "The number of pings that were submitted and had to wait for a client id (i.e. before it was cached or loaded from disk)"
+ },
+ "TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 30,
+ "n_buckets": 29,
+ "description": "The size (MB) of the Telemetry pending pings exceeding the maximum file size"
+ },
+ "TELEMETRY_DISCARDED_ARCHIVED_PINGS_SIZE_MB": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 30,
+ "n_buckets": 29,
+ "description": "The size (MB) of the Telemetry archived, compressed, pings exceeding the maximum file size"
+ },
+ "TELEMETRY_DISCARDED_SEND_PINGS_SIZE_MB": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 30,
+ "n_buckets": 29,
+ "description": "The size (MB) of the ping data submitted to Telemetry exceeding the maximum size"
+ },
+ "TELEMETRY_COMPRESS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time taken to compress telemetry object (ms)"
+ },
+ "TELEMETRY_SEND_SUCCESS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "bug_numbers": [1318284],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 120000,
+ "n_buckets": 20,
+ "description": "Time needed (in ms) for a successful send of a Telemetry ping to the servers and getting a reply back."
+ },
+ "TELEMETRY_SEND_FAILURE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "bug_numbers": [1318284],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 120000,
+ "n_buckets": 20,
+ "description": "Time needed (in ms) for a failed send of a Telemetry ping to the servers and getting a reply back."
+ },
+ "TELEMETRY_SEND_FAILURE_TYPE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com", "chutten@mozilla.com"],
+ "bug_numbers": [1367110, 1501303],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": [
+ "eOK",
+ "eRequest",
+ "eUnreachable",
+ "eChannelOpen",
+ "eRedirect",
+ "abort",
+ "timeout",
+ "eTooLate",
+ "eTerminated"
+ ],
+ "description": "Counts of the different ways sending a Telemetry ping can fail."
+ },
+ "TELEMETRY_SEND_FAILURE_TYPE_PER_PING": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com", "chutten@mozilla.com"],
+ "bug_numbers": [1438896],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "keyed": true,
+ "labels": [
+ "eOK",
+ "eRequest",
+ "eUnreachable",
+ "eChannelOpen",
+ "eRedirect",
+ "abort",
+ "timeout",
+ "eTooLate",
+ "eTerminated"
+ ],
+ "description": "Counts of the different ways sending a Telemetry ping can fail per ping type."
+ },
+ "TELEMETRY_STRINGIFY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 10,
+ "description": "Time to stringify telemetry object (ms)"
+ },
+ "TELEMETRY_SUCCESS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Successful telemetry submission"
+ },
+ "TELEMETRY_INVALID_PING_TYPE_SUBMITTED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "description": "Count of individual invalid ping types that were submitted to Telemetry."
+ },
+ "TELEMETRY_INVALID_PAYLOAD_SUBMITTED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "bug_numbers": [1292226],
+ "kind": "count",
+ "description": "Count of individual invalid payloads that were submitted to Telemetry."
+ },
+ "TELEMETRY_PING_EVICTED_FOR_SERVER_ERRORS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Number of Telemetry ping files evicted due to server errors (4XX HTTP code received)"
+ },
+ "TELEMETRY_SESSIONDATA_FAILED_LOAD": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Set if Telemetry failed to load the session data from disk."
+ },
+ "TELEMETRY_SESSIONDATA_FAILED_PARSE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Set if Telemetry failed to parse the session data loaded from disk."
+ },
+ "TELEMETRY_SESSIONDATA_FAILED_VALIDATION": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Set if Telemetry failed to validate the session data loaded from disk."
+ },
+ "TELEMETRY_SESSIONDATA_FAILED_SAVE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Set if Telemetry failed to save the session data to disk."
+ },
+ "TELEMETRY_ASSEMBLE_PAYLOAD_EXCEPTION": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "bug_numbers": [1250640],
+ "expires_in_version": "53",
+ "kind": "count",
+ "description": "Count of exceptions in TelemetrySession.getSessionPayload()."
+ },
+ "TELEMETRY_EVENT_PING_SENT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com", "chutten@mozilla.com"],
+ "bug_numbers": [1460595],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": ["periodic", "max", "shutdown"],
+ "description": "Number of 'event' pings sent, by reason"
+ },
+ "TELEMETRY_EVENT_REGISTRATION_ERROR": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com", "chutten@mozilla.com"],
+ "bug_numbers": [1480204],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": ["Other", "Name", "Category", "Method", "Object", "ExtraKeys"],
+ "description": "Number of event registration failures, by field causing the failure"
+ },
+ "TELEMETRY_EVENT_RECORDING_ERROR": {
+ "record_in_processes": ["all"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com", "chutten@mozilla.com"],
+ "bug_numbers": [1480204],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": ["UnknownEvent", "Expired", "ExtraKey", "Value", "Extra"],
+ "description": "Number of event recording failures, by type of failure"
+ },
+ "TELEMETRY_TEST_FLAG": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_COUNT2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1288745],
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_COUNT_INIT_NO_RECORD": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "a testing histogram; not meant to be touched - initially not recording"
+ },
+ "TELEMETRY_TEST_CATEGORICAL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "bug_numbers": [1188888],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": ["CommonLabel", "Label2", "Label3"],
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_CATEGORICAL_OPTOUT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "geckoview_streaming", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "bug_numbers": [1188888],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": ["CommonLabel", "Label4", "Label5", "Label6"],
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_CATEGORICAL_NVALUES": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "bug_numbers": [1188888],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "n_values": 70,
+ "labels": ["CommonLabel", "Label7", "Label8"],
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_CATEGORICAL_WIN_ONLY": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "bug_numbers": [1539907],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "n_values": 70,
+ "labels": ["CommonLabel", "Label7", "Label8"],
+ "description": "a testing histogram used as a compile-time check; not meant to be touched",
+ "operating_systems": ["windows"]
+ },
+ "TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "description": "a testing histogram; not meant to be touched - initially not recording"
+ },
+ "TELEMETRY_TEST_KEYED_FLAG": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "keyed": true,
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_KEYED_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_KEYED_KEYS": {
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "bug_numbers": [1343855],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "keyed": true,
+ "keys": ["testkey", "CommonKey", "thirdKey"],
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_KEYED_BOOLEAN": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "keyed": true,
+ "bug_numbers": [1299144],
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_KEYED_EXPONENTIAL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 1,
+ "high": 40000,
+ "n_buckets": 10,
+ "keyed": true,
+ "bug_numbers": [1347216],
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_KEYED_LINEAR": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 1,
+ "high": 250000,
+ "n_buckets": 10,
+ "keyed": true,
+ "bug_numbers": [1347216],
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_KEYED_CATEGORICAL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "keyed": true,
+ "labels": ["CommonLabel", "Label2", "Label3"],
+ "bug_numbers": [1347216],
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_RELEASE_OPTOUT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "releaseChannelCollection": "opt-out",
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_RELEASE_OPTIN": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "releaseChannelCollection": "opt-in",
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_KEYED_RELEASE_OPTIN": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "keyed": true,
+ "releaseChannelCollection": "opt-in",
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_EXPONENTIAL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 1,
+ "high": 2147483646,
+ "n_buckets": 10,
+ "bug_numbers": [1288745, 1685406],
+ "releaseChannelCollection": "opt-out",
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_LINEAR": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 1,
+ "high": 2147483646,
+ "n_buckets": 10,
+ "bug_numbers": [1288745, 1685406],
+ "releaseChannelCollection": "opt-out",
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_MIRROR_FOR_CUSTOM": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["chutten@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 1,
+ "high": 2147483646,
+ "n_buckets": 10,
+ "bug_numbers": [1716340],
+ "releaseChannelCollection": "opt-out",
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_MIRROR_FOR_TIMING": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["chutten@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 1,
+ "high": 2147483646,
+ "n_buckets": 10,
+ "bug_numbers": [1768636],
+ "releaseChannelCollection": "opt-out",
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_BOOLEAN": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "bug_numbers": [1288745],
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_EXPIRED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird", "geckoview_streaming"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "4",
+ "kind": "linear",
+ "high": 10,
+ "n_buckets": 3,
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_EXPIRED_KEYED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "4",
+ "kind": "linear",
+ "keyed": true,
+ "low": 1,
+ "high": 2147483646,
+ "n_buckets": 10,
+ "bug_numbers": [1468809],
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_CONTENT_PROCESS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 1,
+ "high": 10000,
+ "n_buckets": 10,
+ "bug_numbers": [1335343],
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_KEYED_CONTENT_PROCESS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 1,
+ "high": 10000,
+ "n_buckets": 10,
+ "keyed": true,
+ "bug_numbers": [1335343],
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_FLAG_CONTENT_PROCESS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "bug_numbers": [1335343],
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_FLAG_MAIN_PROCESS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "bug_numbers": [1335343],
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_ALL_PROCESSES": {
+ "record_in_processes": ["all"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 1,
+ "high": 10000,
+ "n_buckets": 10,
+ "bug_numbers": [1335343],
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_ALL_CHILD_PROCESSES": {
+ "record_in_processes": ["all_childs"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 1,
+ "high": 10000,
+ "n_buckets": 10,
+ "bug_numbers": [1335343],
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_DEFAULT_PRODUCTS": {
+ "record_in_processes": ["all"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 1,
+ "high": 10000,
+ "n_buckets": 10,
+ "bug_numbers": [1452552],
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_DESKTOP_ONLY": {
+ "record_in_processes": ["all"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 1,
+ "high": 10000,
+ "n_buckets": 10,
+ "bug_numbers": [1452552],
+ "description": "a testing histogram; not meant to be touched",
+ "products": ["firefox", "thunderbird"]
+ },
+ "TELEMETRY_TEST_MULTIPRODUCT": {
+ "record_in_processes": ["all"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 1,
+ "high": 10000,
+ "n_buckets": 10,
+ "bug_numbers": [1452552],
+ "description": "a testing histogram; not meant to be touched",
+ "products": ["firefox", "fennec", "thunderbird"]
+ },
+ "TELEMETRY_TEST_MOBILE_ONLY": {
+ "record_in_processes": ["all"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 1,
+ "high": 10000,
+ "n_buckets": 10,
+ "bug_numbers": [1452552],
+ "description": "a testing histogram; not meant to be touched",
+ "products": ["fennec"]
+ },
+ "TELEMETRY_TEST_OS_ANDROID_ONLY": {
+ "record_in_processes": ["all"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 1,
+ "high": 10000,
+ "n_buckets": 10,
+ "bug_numbers": [1477213],
+ "description": "a testing histogram; not meant to be touched",
+ "operating_systems": ["android"]
+ },
+ "TELEMETRY_TEST_OS_WIN_ONLY": {
+ "record_in_processes": ["all"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 1,
+ "high": 10000,
+ "n_buckets": 10,
+ "bug_numbers": [1477213],
+ "description": "a testing histogram; not meant to be touched",
+ "operating_systems": ["windows"]
+ },
+ "TELEMETRY_TEST_OS_LINUX_ONLY": {
+ "record_in_processes": ["all"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 1,
+ "high": 10000,
+ "n_buckets": 10,
+ "bug_numbers": [1477213],
+ "description": "a testing histogram; not meant to be touched",
+ "operating_systems": ["linux"]
+ },
+ "TELEMETRY_TEST_OS_MAC_ONLY": {
+ "record_in_processes": ["all"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 1,
+ "high": 10000,
+ "n_buckets": 10,
+ "bug_numbers": [1477213],
+ "description": "a testing histogram; not meant to be touched",
+ "operating_systems": ["mac"]
+ },
+ "TELEMETRY_TEST_MAIN_ONLY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 1,
+ "high": 2147483646,
+ "n_buckets": 10,
+ "bug_numbers": [1498164],
+ "description": "a testing histogram; not meant to be touched",
+ "record_into_store": ["main"]
+ },
+ "TELEMETRY_TEST_SYNC_ONLY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 1,
+ "high": 2147483646,
+ "n_buckets": 10,
+ "bug_numbers": [1498164],
+ "description": "a testing histogram; not meant to be touched",
+ "record_into_store": ["sync"]
+ },
+ "TELEMETRY_TEST_MULTIPLE_STORES": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 1,
+ "high": 2147483646,
+ "n_buckets": 10,
+ "bug_numbers": [1498164],
+ "description": "a testing histogram; not meant to be touched",
+ "record_into_store": ["main", "sync"]
+ },
+ "TELEMETRY_TEST_KEYED_SYNC_ONLY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "keyed": true,
+ "low": 1,
+ "high": 2147483646,
+ "n_buckets": 10,
+ "bug_numbers": [1498164],
+ "description": "a testing histogram; not meant to be touched",
+ "record_into_store": ["sync"]
+ },
+ "TELEMETRY_TEST_KEYED_MULTIPLE_STORES": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "keyed": true,
+ "low": 1,
+ "high": 2147483646,
+ "n_buckets": 10,
+ "bug_numbers": [1498164],
+ "description": "a testing histogram; not meant to be touched",
+ "record_into_store": ["main", "sync"]
+ },
+ "TELEMETRY_TEST_STREAMING": {
+ "record_in_processes": ["main"],
+ "products": ["geckoview_streaming"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 1,
+ "high": 2147483646,
+ "n_buckets": 10,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1566366],
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "TELEMETRY_TEST_STREAMING_2": {
+ "record_in_processes": ["main"],
+ "products": ["geckoview_streaming"],
+ "alert_emails": ["telemetry-client-dev@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 1,
+ "high": 2147483646,
+ "n_buckets": 10,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1566366],
+ "description": "a testing histogram; not meant to be touched"
+ },
+ "STARTUP_CRASH_DETECTED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Whether there was a crash during the last startup"
+ },
+ "SAFE_MODE_USAGE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 3,
+ "description": "Whether the user is in safe mode (No, Yes, Forced)"
+ },
+ "SCRIPT_BLOCK_INCORRECT_MIME_3": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["ckerschbaumer@mozilla.com"],
+ "bug_numbers": [1288361, 1299267, 1399990, 1510225, 1581559],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": [
+ "unknown",
+ "javaScript",
+ "image",
+ "audio",
+ "video",
+ "text_plain",
+ "text_csv",
+ "text_xml",
+ "app_octet_stream",
+ "app_xml",
+ "app_json",
+ "text_json",
+ "text_html",
+ "empty",
+ "serviceworker_load",
+ "worker_load",
+ "importScript_load",
+ "script_load",
+ "same_origin",
+ "CORS_origin",
+ "cross_origin",
+ "worklet_load"
+ ],
+ "description": "Whether the script load has a MIME type of ...? (unknown, javaScript, image, audio, video, text_plain, text_csv, text_xml, app_octet_stream, app_xml, app_json, text_json, text_html, empty). Whether the script load is from ...? (serviceworker_load, worker_load, importSript_load, script_load). Whether the script load is of ...? (same_origin, CORS_origin, cross_origin)"
+ },
+ "NEWTAB_PAGE_ENABLED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "boolean",
+ "description": "New tab page is enabled."
+ },
+ "NEWTAB_PAGE_PINNED_SITES_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "enumerated",
+ "n_values": 9,
+ "description": "Number of pinned sites on the new tab page."
+ },
+ "NEWTAB_PAGE_BLOCKED_SITES_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 100,
+ "n_buckets": 10,
+ "description": "Number of sites blocked from the new tab page."
+ },
+ "MS_MESSAGE_REQUEST_TIME_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["user-journey@mozilla.com", "aoprea@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 2000,
+ "n_buckets": 20,
+ "bug_numbers": [1600335],
+ "description": "Firefox: Time in ms spent selecting and matching messages to user profile."
+ },
+ "BROWSERPROVIDER_XUL_IMPORT_BOOKMARKS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 20,
+ "description": "Number of bookmarks in the original XUL places database",
+ "operating_systems": ["android"]
+ },
+ "SECURITY_UI": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "seceng-telemetry@mozilla.com",
+ "fxprivacyandsecurity@mozilla.com"
+ ],
+ "bug_numbers": [767676],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "description": "Security-related UI events (addons, form submission, TLS certs, Safe Browsing, updates and geolocation). See /security/manager/ssl/nsISecurityUITelemetry.idl for the specific values."
+ },
+ "STORAGE_ACCESS_API_UI": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["pbz@mozilla.com", "tihuang@mozilla.com"],
+ "bug_numbers": [1513309, 1588844, 1643432, 1689262, 1730043, 1832670],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": [
+ "Request",
+ "AllowAutomatically",
+ "Deny",
+ "Allow",
+ "AllowOnAnySite"
+ ],
+ "description": "Storage Access API UI events.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "IPC_TRANSACTION_CANCEL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["billm@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "True when an IPC transaction is canceled"
+ },
+ "IPC_SAME_PROCESS_MESSAGE_COPY_OOM_KB": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "50",
+ "kind": "exponential",
+ "low": 100,
+ "high": 10000000,
+ "n_buckets": 10,
+ "description": "Whenever the same-process MessageManager cannot be sent through sendAsyncMessage as it would cause an OOM, the size of the message content, in kb."
+ },
+ "SEARCH_COUNTS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["fx-search-telemetry@mozilla.com", "rev-data@mozilla.com"],
+ "bug_numbers": [
+ 1089670, 1475571, 1482158, 1499193, 1545172, 1572500, 1654680
+ ],
+ "description": "Records search counts for search access points. The format is: <engine-name>.<search-access-point>. For the urlbar when in search mode, the format is <engine name>.urlbar-searchmode. For the urlbar when an internal @engine shortcut is used, the format is: <engine-name>.alias.",
+ "record_into_store": ["main", "account-ecosystem"]
+ },
+ "SEARCH_SERVICE_INIT2_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["fx-search-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "bug_numbers": [1832509],
+ "description": "Time (ms) it takes to initialize the search service"
+ },
+ "SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["fx-search-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "n_buckets": 30,
+ "high": 100000,
+ "description": "Time (ms) it takes to fetch the country code"
+ },
+ "SEARCH_SERVICE_COUNTRY_FETCH_RESULT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["fx-search-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 8,
+ "description": "Result of XHR request fetching the country-code. 0=SUCCESS, 1=SUCCESS_WITHOUT_DATA, 2=XHRTIMEOUT, 3=ERROR (rest reserved for finer-grained error codes later)"
+ },
+ "SEARCH_SERVICE_US_COUNTRY_MISMATCHED_TIMEZONE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["fx-search-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Set if the fetched country-code indicates US but the time-zone heuristic doesn't"
+ },
+ "SEARCH_SERVICE_US_TIMEZONE_MISMATCHED_COUNTRY": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["fx-search-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Set if the time-zone heuristic indicates US but the fetched country code doesn't"
+ },
+ "SEARCH_SERVICE_US_COUNTRY_MISMATCHED_PLATFORM_OSX": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["fx-search-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "If we are on OSX and either the OSX countryCode or the geoip countryCode indicates we are in the US, set to false if they both do or true otherwise"
+ },
+ "SEARCH_SERVICE_NONUS_COUNTRY_MISMATCHED_PLATFORM_OSX": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["fx-search-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "If we are on OSX and neither the OSX countryCode nor the geoip countryCode indicates we are in the US, set to false if they both agree on the value or true otherwise"
+ },
+ "SEARCH_SERVICE_US_COUNTRY_MISMATCHED_PLATFORM_WIN": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["fx-search-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "If we are on Windows and either the Windows countryCode or the geoip countryCode indicates we are in the US, set to false if they both do or true otherwise"
+ },
+ "SEARCH_SERVICE_NONUS_COUNTRY_MISMATCHED_PLATFORM_WIN": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "alert_emails": ["fx-search-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "If we are on Windows and neither the Windows countryCode nor the geoip countryCode indicates we are in the US, set to false if they both agree on the value or true otherwise"
+ },
+ "SEARCH_SUGGESTIONS_LATENCY_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["fx-search-telemetry@mozilla.com", "adw@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 50,
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1743885],
+ "description": "Records the latencies (ms) of search suggestions fetches per search engine. Keys in this histogram are search engine IDs for built-in search engines and 'other' for non-built-in search engines."
+ },
+ "TOUCH_ENABLED_DEVICE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [795307, 1390269],
+ "alert_emails": ["jimm@mozilla.com"],
+ "description": "Boolean indicating if a touch input device is detected.",
+ "operating_systems": ["windows"]
+ },
+ "COMPONENTS_SHIM_ACCESSED_BY_CONTENT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "Whether content ever accesed the Components shim in this session"
+ },
+ "SSL_SUCCESFUL_CERT_VALIDATION_TIME_MOZILLAPKIX": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1862062],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50,
+ "description": "Time spent on a successful cert verification in mozilla::pkix mode (ms)"
+ },
+ "SSL_INITIAL_FAILED_CERT_VALIDATION_TIME_MOZILLAPKIX": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1862062],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50,
+ "description": "Time spent on an initially failed cert verification in mozilla::pkix mode (ms)"
+ },
+ "CRASH_STORE_COMPRESSED_BYTES": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 202,
+ "description": "Size (in bytes) of the compressed crash store JSON file."
+ },
+ "POPUP_NOTIFICATION_STATS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["MattN+telemetry@mozilla.com", "jhofmann@mozilla.com"],
+ "bug_numbers": [1207089, 1536454, 1584554],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 40,
+ "description": "(Bug 1207089) Usage of popup notifications, keyed by ID (0 = Offered, 1..4 = Action (3 is unused), 5 = Click outside (unused), 6 = Leave page, 7 = Use 'X' (unused), 8 = Not now (unused), 10 = Open submenu, 11 = Learn more. Add 20 if happened after reopen.)"
+ },
+ "POPUP_NOTIFICATION_MAIN_ACTION_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["MattN+telemetry@mozilla.com", "jhofmann@mozilla.com"],
+ "bug_numbers": [1207089, 1584554],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "keyed": true,
+ "low": 100,
+ "high": 600000,
+ "n_buckets": 40,
+ "description": "(Bug 1207089) Time in ms between initially requesting a popup notification and triggering the main action, keyed by ID"
+ },
+ "POPUP_NOTIFICATION_DISMISSAL_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["MattN+telemetry@mozilla.com", "jhofmann@mozilla.com"],
+ "bug_numbers": [1207089, 1584554],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "keyed": true,
+ "low": 200,
+ "high": 20000,
+ "n_buckets": 50,
+ "description": "(Bug 1207089) Time in ms between displaying a popup notification and dismissing it without an action the first time, keyed by ID"
+ },
+ "PRINT_BACKGROUND_TASK_TIME_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["jwatt@jwatt.org"],
+ "expires_in_version": "95",
+ "description": "Milliseconds taken on the background thread only for print background tasks. Keys describe the task, usually the information being retrieved, current possible keys are: DefaultSettings, MarginsForPaper, Printers, NamedPrinter, NamedOrDefaultPrinter, SupportsDuplex, SupportsColor, SupportsCollation, PaperList.",
+ "keyed": true,
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1660686]
+ },
+ "PRINT_BACKGROUND_TASK_ROUND_TRIP_TIME_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["jwatt@jwatt.org"],
+ "expires_in_version": "95",
+ "description": "Milliseconds taken for print background tasks from spawning to background thread back to before resolving or rejecting on the main thread. See PRINT_BACKGROUND_TASK_TIME_MS for key description.",
+ "keyed": true,
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1660686]
+ },
+ "PRINT_INIT_TO_PLATFORM_SENT_SETTINGS_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["jwatt@jwatt.org", "emilio@mozilla.com"],
+ "expires_in_version": "112",
+ "description": "Milliseconds taken from when a print is initiated (typically by the user) to when the platform code is sent the print settings to create the initial print preview document.",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1663733, 1714261, 1745270, 1777398]
+ },
+ "PRINT_INIT_TO_PREVIEW_DOC_SHOWN_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["jwatt@jwatt.org", "emilio@mozilla.com"],
+ "expires_in_version": "112",
+ "description": "Milliseconds taken from when a print is initiated (typically by the user) to when the initial print preview document is shown.",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1663733, 1714261, 1745270, 1777398]
+ },
+ "DEVTOOLS_COLD_TOOLBOX_OPEN_DELAY_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "bug_numbers": [1405584, 1566392, 1643433],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 100,
+ "keyed": true,
+ "description": "Time taken (in ms) to open the first DevTools toolbox. This is keyed by tool ID being opened [inspector, webconsole, jsdebugger, styleeditor, performance, memory, netmonitor, storage, dom]."
+ },
+ "DEVTOOLS_WARM_TOOLBOX_OPEN_DELAY_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "bug_numbers": [1405584, 1566392, 1643433],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 100,
+ "keyed": true,
+ "description": "Time taken (in ms) to open all but first DevTools toolbox. This is keyed by tool ID being opened [inspector, webconsole, jsdebugger, styleeditor, performance, memory, netmonitor, storage, dom]."
+ },
+ "DEVTOOLS_TOOLBOX_PAGE_RELOAD_DELAY_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "bug_numbers": [1405585, 1566392, 1643433],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 120000,
+ "n_buckets": 100,
+ "keyed": true,
+ "description": "Time taken (in ms) to update DevTools panel when reloading a page. This is keyed by tool ID being currently opened [inspector, webconsole, jsdebugger, styleeditor, performance, memory, netmonitor, storage, dom]."
+ },
+ "DEVTOOLS_INSPECTOR_NEW_ROOT_TO_RELOAD_DELAY_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "bug_numbers": [1405585, 1566392, 1643433],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 120000,
+ "n_buckets": 100,
+ "description": "Time taken (in ms) to update the inspector during a page reload, starting from new-root event."
+ },
+ "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE_LOCAL_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 1000,
+ "description": "The time (in milliseconds) that it took to display a selected source to the user."
+ },
+ "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE_REMOTE_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 1000,
+ "description": "The time (in milliseconds) that it took to display a selected source to the user."
+ },
+ "DEVTOOLS_DEBUGGER_LOAD_SOURCE_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "bug_numbers": [1429047, 1436039, 1557116],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 20000,
+ "n_buckets": 100,
+ "description": "The time (in milliseconds) that it took to load a source for the user."
+ },
+ "MEDIA_GMP_UPDATE_XML_FETCH_RESULT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": [
+ "cert_pinning_ok",
+ "cert_pinning_fail",
+ "content_sig_ok",
+ "content_sig_fail"
+ ],
+ "description": "Reports if a Firefox succeeded or failed in fetching the GMP update.xml from balrog, and which method was used to verify the download.",
+ "bug_numbers": [1714621]
+ },
+ "MEDIA_GMP_UPDATE_CONTENT_PROCESS_HAS_H264": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "105",
+ "kind": "enumerated",
+ "n_values": 5,
+ "bug_numbers": [1767334, 1768033],
+ "description": "Whether H264 encoding is an available capability for being sent to the content processes. 0=no h264, 1=h264 avaiable, 2=no h264 1 directory added, 3=no h264 >1 directories added, 4=directory adds in prorgress"
+ },
+ "MEDIA_MKV_CANPLAY_REQUESTED": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "118",
+ "kind": "boolean",
+ "description": "Reports a true value when a page requests canPlayType for a matroska media type.",
+ "bug_numbers": [1429986, 1570634, 1606206, 1689266, 1754643, 1813150]
+ },
+ "MEDIA_HLS_CANPLAY_REQUESTED": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "92",
+ "kind": "boolean",
+ "description": "Reports a true value when a page requests canPlayType for a HLS media type.",
+ "bug_numbers": [1628048, 1672573]
+ },
+ "MEDIA_HLS_CANPLAY_SUPPORTED": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "92",
+ "kind": "boolean",
+ "description": "Reports a true value when a canPlayType request supports HLS.",
+ "bug_numbers": [1628048, 1672573]
+ },
+ "MEDIA_MP4_PARSE_SAMPLE_DESCRIPTION_ENTRIES_HAVE_MULTIPLE_CODECS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "bug_numbers": [1513651],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Records if multiple codecs are present in a track's sample description entries. Recorded each time we process a track's metadata while parsing mp4s."
+ },
+ "MEDIA_MP4_PARSE_SAMPLE_DESCRIPTION_ENTRIES_HAVE_MULTIPLE_CRYPTO": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "bug_numbers": [1513651],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Records if multiple sets of crypto info are present in a track's sample description entries. Recorded each time we process a track's metadata while parsing mp4s."
+ },
+ "MEDIA_MP4_PARSE_NUM_SAMPLE_DESCRIPTION_ENTRIES": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "bug_numbers": [1513651],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 5,
+ "description": "Counts the number of entries in the sample description box (stsd) for a track in an mp4. Recorded each time we process a track's metadata while parsing mp4s."
+ },
+ "MEDIA_AUDIO_INIT_FAILURE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": ["first", "other"],
+ "description": "Failure occurs when initializing the audio stream",
+ "bug_numbers": [1671714]
+ },
+ "MEDIA_AUDIO_BACKEND": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": [
+ "unknown",
+ "audiounit",
+ "audiounit_rust",
+ "aaudio",
+ "opensl",
+ "wasapi",
+ "winmm",
+ "alsa",
+ "jack",
+ "oss",
+ "pulse",
+ "pulse_rust",
+ "sndio",
+ "sunaudio"
+ ],
+ "description": "The operating system audio backend",
+ "bug_numbers": [1671714, 1679745]
+ },
+ "MEDIA_SNIFFER_MP4_BRAND_PATTERN": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": [
+ "ftyp_mp4",
+ "ftyp_avc",
+ "ftyp_3gp4",
+ "ftyp_3gp",
+ "ftyp_M4V",
+ "ftyp_M4A",
+ "ftyp_M4P",
+ "ftyp_qt",
+ "ftyp_iso",
+ "ftyp_mmp4",
+ "ftyp_avif",
+ "ftyp_crx"
+ ],
+ "description": "ISOBMFF brand pattern used to identify MP4 in media sniffer",
+ "bug_numbers": [1725190]
+ },
+ "WEBRTC_DTLS_CIPHER": {
+ "record_in_processes": ["content", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["webrtc-dtls-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1484024, 1570634, 1607283, 1645077, 1714303],
+ "expires_in_version": "100",
+ "kind": "enumerated",
+ "n_values": 14,
+ "description": "The DTLS cipher (as integer) negotiated for a RTCPeerConnection. See TransportLayerDtls::RecordCipherTelemetry for the meaning of the values"
+ },
+ "WEBRTC_SRTP_CIPHER": {
+ "record_in_processes": ["content", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["webrtc-dtls-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1491511, 1570634, 1607283, 1645077, 1714303],
+ "expires_in_version": "100",
+ "kind": "categorical",
+ "labels": [
+ "Unknown",
+ "Aes128CmHmacSha1_80",
+ "Aes128CmHmacSha1_32",
+ "AeadAes128Gcm",
+ "AeadAes256Gcm"
+ ],
+ "description": "The SRTP cipher (as label) negotiated for a RTCPeerConnection."
+ },
+ "WEBRTC_AVSYNC_WHEN_AUDIO_LAGS_VIDEO_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "The delay (in milliseconds) when audio is behind video. Zero delay is counted. Measured every second of a call."
+ },
+ "WEBRTC_AVSYNC_WHEN_VIDEO_LAGS_AUDIO_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 1000,
+ "description": "The delay (in milliseconds) when video is behind audio. Zero delay is not counted. Measured every second of a call."
+ },
+ "WEBRTC_VIDEO_QUALITY_INBOUND_BANDWIDTH_KBITS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 1000,
+ "description": "Locally measured data rate of inbound video (kbit/s). Computed every second of a call."
+ },
+ "WEBRTC_AUDIO_QUALITY_INBOUND_BANDWIDTH_KBITS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 1000,
+ "description": "Locally measured data rate on inbound audio (kbit/s). Computed every second of a call."
+ },
+ "WEBRTC_VIDEO_QUALITY_OUTBOUND_BANDWIDTH_KBITS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 1000,
+ "description": "Data rate deduced from RTCP from remote recipient of outbound video (kbit/s). Computed every second of a call (for easy comparison)."
+ },
+ "WEBRTC_AUDIO_QUALITY_OUTBOUND_BANDWIDTH_KBITS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000000,
+ "n_buckets": 1000,
+ "description": "Data rate deduced from RTCP from remote recipient of outbound audio (kbit/s). Computed every second of a call (for easy comparison)."
+ },
+ "WEBRTC_VIDEO_QUALITY_INBOUND_PACKETLOSS_RATE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Locally measured packet loss on inbound video (permille). Sampled every second of a call."
+ },
+ "WEBRTC_AUDIO_QUALITY_INBOUND_PACKETLOSS_RATE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Locally measured packet loss on inbound audio (permille). Sampled every second of a call."
+ },
+ "WEBRTC_VIDEO_QUALITY_OUTBOUND_PACKETLOSS_RATE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "RTCP-reported packet loss by remote recipient of outbound video (permille). Sampled every second of a call (for easy comparison)."
+ },
+ "WEBRTC_AUDIO_QUALITY_OUTBOUND_PACKETLOSS_RATE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "RTCP-reported packet loss by remote recipient of outbound audio (permille). Sampled every second of a call (for easy comparison)."
+ },
+ "WEBRTC_VIDEO_QUALITY_INBOUND_JITTER": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "description": "Locally measured jitter on inbound video (ms). Sampled every second of a call."
+ },
+ "WEBRTC_AUDIO_QUALITY_INBOUND_JITTER": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 1000,
+ "description": "Locally measured jitter on inbound audio (ms). Sampled every second of a call."
+ },
+ "WEBRTC_VIDEO_QUALITY_OUTBOUND_JITTER": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 1000,
+ "description": "RTCP-reported jitter by remote recipient of outbound video (ms). Sampled every second of a call (for easy comparison)."
+ },
+ "WEBRTC_AUDIO_QUALITY_OUTBOUND_JITTER": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 1000,
+ "description": "RTCP-reported jitter by remote recipient of outbound audio (ms). Sampled every second of a call (for easy comparison)."
+ },
+ "WEBRTC_VIDEO_ERROR_RECOVERY_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 500,
+ "description": "Time to recover from a video error in ms"
+ },
+ "WEBRTC_VIDEO_RECOVERY_BEFORE_ERROR_PER_MIN": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 200,
+ "description": "Number of losses recovered before error per min"
+ },
+ "WEBRTC_VIDEO_RECOVERY_AFTER_ERROR_PER_MIN": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 200,
+ "description": "Number of losses recovered after error per min"
+ },
+ "WEBRTC_VIDEO_DECODE_ERROR_TIME_PERMILLE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 100,
+ "description": "Percentage*10 (permille) of call decoding with errors or frozen due to errors"
+ },
+ "WEBRTC_VIDEO_QUALITY_OUTBOUND_RTT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 1000,
+ "description": "Roundtrip time of outbound video (ms). Sampled every second of a call."
+ },
+ "WEBRTC_AUDIO_QUALITY_OUTBOUND_RTT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 1000,
+ "description": "Roundtrip time of outbound audio (ms). Sampled every second of a call."
+ },
+ "WEBRTC_VIDEO_ENCODER_BITRATE_AVG_PER_CALL_KBPS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "description": "Video encoder's average bitrate (in kbits/s) over an entire call"
+ },
+ "WEBRTC_VIDEO_ENCODER_BITRATE_STD_DEV_PER_CALL_KBPS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "description": "Standard deviation from video encoder's average bitrate (in kbits/s) over an entire call"
+ },
+ "WEBRTC_VIDEO_ENCODER_FRAMERATE_AVG_PER_CALL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 200,
+ "n_buckets": 50,
+ "description": "Video encoder's average framerate (in fps) over an entire call"
+ },
+ "WEBRTC_VIDEO_ENCODER_FRAMERATE_10X_STD_DEV_PER_CALL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 200,
+ "n_buckets": 50,
+ "description": "Standard deviation from video encoder's average framerate (in 1/10 fps) over an entire call"
+ },
+ "WEBRTC_VIDEO_ENCODER_DROPPED_FRAMES_PER_CALL_FPM": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "description": "Video encoder's number of frames dropped (in frames/min) over an entire call"
+ },
+ "WEBRTC_VIDEO_DECODER_BITRATE_AVG_PER_CALL_KBPS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "description": "Video decoder's average bitrate (in kbits/s) over an entire call"
+ },
+ "WEBRTC_VIDEO_DECODER_BITRATE_STD_DEV_PER_CALL_KBPS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "description": "Standard deviation from video decoder's average bitrate (in kbits/s) over an entire call"
+ },
+ "WEBRTC_VIDEO_DECODER_FRAMERATE_AVG_PER_CALL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 200,
+ "n_buckets": 50,
+ "description": "Video decoder's average framerate (in fps) over an entire call"
+ },
+ "WEBRTC_VIDEO_DECODER_FRAMERATE_10X_STD_DEV_PER_CALL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 200,
+ "n_buckets": 50,
+ "description": "Standard deviation from video decoder's average framerate (in 1/10 fps) over an entire call"
+ },
+ "WEBRTC_VIDEO_DECODER_DISCARDED_PACKETS_PER_CALL_PPM": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "description": "Video decoder's number of discarded packets (in packets/min) over an entire call"
+ },
+ "WEBRTC_CALL_DURATION": {
+ "bug_numbers": [1571015, 1586271],
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 1000,
+ "description": "The length of time (in seconds) that a call lasted.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "WEBRTC_AV_CALL_DURATION": {
+ "bug_numbers": [1695378],
+ "record_in_processes": ["content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "description": "The length of time (in seconds) that a call with audio or video in it lasted.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "WEBRTC_CALL_COUNT_3": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [875097, 1261063, 1654248],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "The number of successfully connected calls made during a session (discounting re-negotiations)."
+ },
+ "WEBRTC_GET_USER_MEDIA_TYPE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 8,
+ "description": "Type for media in getUserMedia calls (0=Camera, 1=Screen, 2=Application, 3=Window, 4=Browser, 5=Microphone, 6=AudioCapture, 7=Other)"
+ },
+ "WEBRTC_LOAD_STATE_RELAXED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 25,
+ "description": "Percentage of time spent in the Relaxed load state in calls over 30 seconds."
+ },
+ "WEBRTC_LOAD_STATE_RELAXED_SHORT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 25,
+ "description": "Percentage of time spent in the Relaxed load state in calls 5-30 seconds."
+ },
+ "WEBRTC_LOAD_STATE_NORMAL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 25,
+ "description": "Percentage of time spent in the Normal load state in calls over 30 seconds."
+ },
+ "WEBRTC_LOAD_STATE_NORMAL_SHORT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 25,
+ "description": "Percentage of time spent in the Normal load state in calls over 5-30 seconds."
+ },
+ "WEBRTC_LOAD_STATE_STRESSED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 25,
+ "description": "Percentage of time spent in the Stressed load state in calls over 30 seconds."
+ },
+ "WEBRTC_LOAD_STATE_STRESSED_SHORT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 25,
+ "description": "Percentage of time spent in the Stressed load state in calls 5-30 seconds."
+ },
+ "WEBRTC_RENEGOTIATIONS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 21,
+ "n_buckets": 20,
+ "description": "Number of Renegotiations during each call"
+ },
+ "WEBRTC_MAX_VIDEO_SEND_TRACK": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 10,
+ "n_buckets": 9,
+ "description": "Number of Video tracks sent simultaneously"
+ },
+ "WEBRTC_MAX_VIDEO_RECEIVE_TRACK": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 20,
+ "n_buckets": 19,
+ "description": "Number of Video tracks received simultaneously"
+ },
+ "WEBRTC_MAX_AUDIO_SEND_TRACK": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 20,
+ "n_buckets": 19,
+ "description": "Number of Audio tracks sent simultaneously"
+ },
+ "WEBRTC_MAX_AUDIO_RECEIVE_TRACK": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 30,
+ "n_buckets": 29,
+ "description": "Number of Audio tracks received simultaneously"
+ },
+ "WEBRTC_DATACHANNEL_NEGOTIATED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Was DataChannels negotiated"
+ },
+ "WEBRTC_CALL_TYPE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 8,
+ "description": "Type of call: (Bitmask) Audio = 1, Video = 2, DataChannels = 4"
+ },
+ "WEBRTC_SOFTWARE_H264_ENABLED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "bug_numbers": [1766509],
+ "releaseChannelCollection": "opt-out",
+ "description": "Whether hardware H264 is enabled"
+ },
+ "WEBRTC_HAS_H264_HARDWARE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "bug_numbers": [1766509],
+ "releaseChannelCollection": "opt-out",
+ "description": "Whether the user has H264 hardware"
+ },
+ "WEBRTC_HARDWARE_H264_ENABLED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "bug_numbers": [1766509],
+ "releaseChannelCollection": "opt-out",
+ "description": "Whether software H264 is enabled"
+ },
+ "WEBRTC_H264_ENABLED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "bug_numbers": [1766509],
+ "releaseChannelCollection": "opt-out",
+ "description": "Whether any kind of H264 is enabled"
+ },
+ "WEBRTC_GMP_INIT_SUCCESS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["webrtc-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "bug_numbers": [1774222],
+ "releaseChannelCollection": "opt-out",
+ "description": "Counts of GMP codec initialization success and failure"
+ },
+ "MEDIA_RECORDER_RECORDING_DURATION": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "112",
+ "kind": "exponential",
+ "high": 3600,
+ "n_buckets": 100,
+ "bug_numbers": [1400757, 1429765, 1480589, 1532391, 1714303, 1754645],
+ "description": "The length of time (in seconds) that a recording lasted. Recorded when a recorder stops"
+ },
+ "DEVTOOLS_ENTRY_POINT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "bug_numbers": [1378863],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "apoirot@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": [
+ "KeyShortcut",
+ "SystemMenu",
+ "HamburgerMenu",
+ "ContextMenu",
+ "CommandLine",
+ "SessionRestore",
+ "SlowScript"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "Records how the user is triggering Developer Tools startup."
+ },
+ "DEVTOOLS_TOOLBOX_OPENED_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools toolbox has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_OPTIONS_OPENED_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools options panel has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_WEBCONSOLE_OPENED_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Web Console has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_BROWSERCONSOLE_OPENED_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Browser Console has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_INSPECTOR_OPENED_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Inspector has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_RULEVIEW_OPENED_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Rule View has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_COMPUTEDVIEW_OPENED_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Computed View has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_LAYOUTVIEW_OPENED_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1347552],
+ "description": "Number of times the DevTools Layout View has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_FONTINSPECTOR_OPENED_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Font Inspector has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_ANIMATIONINSPECTOR_OPENED_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Animation Inspector has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_JSDEBUGGER_OPENED_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Debugger has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_JSBROWSERDEBUGGER_OPENED_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Browser Debugger has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_STYLEEDITOR_OPENED_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Style Editor has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_JSPROFILER_OPENED_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools JS Profiler has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_MEMORY_OPENED_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Memory Tool has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_NETMONITOR_OPENED_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Network Monitor has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_STORAGE_OPENED_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Storage Inspector has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_DOM_OPENED_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1343501],
+ "description": "Number of times the DevTools DOM Inspector has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_RESPONSIVE_OPENED_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Responsive Design Mode tool has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_EYEDROPPER_OPENED_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Eyedropper tool has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_MENU_EYEDROPPER_OPENED_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Eyedropper has been opened via the DevTools menu.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_PICKER_EYEDROPPER_OPENED_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times the DevTools Eyedropper has been opened via the color picker.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_ABOUTDEBUGGING_OPENED_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "jan@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985, 1204601],
+ "description": "Number of times about:debugging has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_COMPATIBILITYVIEW_OPENED_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1639454],
+ "description": "Number of times the DevTools Compatibility View has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_CUSTOM_OPENED_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1247985],
+ "description": "Number of times a custom developer tool has been opened.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_ACCESSIBILITY_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 86400,
+ "n_buckets": 100,
+ "bug_numbers": [1447302, 1503568, 1587985],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "yzenevich@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has the accessibility panel been active (seconds)."
+ },
+ "DEVTOOLS_ACCESSIBILITY_PICKER_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 86400,
+ "n_buckets": 100,
+ "bug_numbers": [1447302, 1503568, 1587985],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "yzenevich@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has the picker tool in accessibility panel been active (seconds)."
+ },
+ "DEVTOOLS_ACCESSIBILITY_SERVICE_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 86400,
+ "n_buckets": 100,
+ "bug_numbers": [1447302, 1503568, 1587985],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "yzenevich@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has the platform accessibility been active (seconds) in accessibility panel."
+ },
+ "DEVTOOLS_APPLICATION_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "66",
+ "kind": "exponential",
+ "high": 86400,
+ "n_buckets": 100,
+ "bug_numbers": [1451734],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "jdescottes@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has the application panel been active (seconds)."
+ },
+ "DEVTOOLS_FLEXBOX_HIGHLIGHTER_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 86400,
+ "n_buckets": 100,
+ "bug_numbers": [1509907],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "mbalfanz@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has the flexbox highlighter been active (seconds)."
+ },
+ "DEVTOOLS_GRID_HIGHLIGHTER_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 86400,
+ "n_buckets": 100,
+ "bug_numbers": [1513969],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "mbalfanz@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has the grid highlighter been active (seconds)."
+ },
+ "DEVTOOLS_TOOLBOX_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "bug_numbers": [1446496],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "jryans@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has the toolbox been active (seconds)"
+ },
+ "DEVTOOLS_OPTIONS_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "bug_numbers": [1446496],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "jryans@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has the options panel been active (seconds)"
+ },
+ "DEVTOOLS_WEBCONSOLE_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "bug_numbers": [1446496],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "jryans@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has the web console been active (seconds)"
+ },
+ "DEVTOOLS_BROWSERCONSOLE_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "bug_numbers": [1446496],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "jryans@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has the browser console been active (seconds)"
+ },
+ "DEVTOOLS_INSPECTOR_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "bug_numbers": [1446496],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "jryans@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has the inspector been active (seconds)"
+ },
+ "DEVTOOLS_RULEVIEW_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "bug_numbers": [1446496],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "jryans@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has the rule view been active (seconds)"
+ },
+ "DEVTOOLS_CHANGESVIEW_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "bug_numbers": [1509890],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "mbalfanz@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has the Changes panel been active (seconds)"
+ },
+ "DEVTOOLS_COMPUTEDVIEW_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "bug_numbers": [1446496],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "jryans@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has the computed view been active (seconds)"
+ },
+ "DEVTOOLS_LAYOUTVIEW_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "bug_numbers": [1347552, 1446496],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "jryans@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has the layout view been active (seconds)"
+ },
+ "DEVTOOLS_FONTINSPECTOR_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "bug_numbers": [1446496],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "jryans@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has the font inspector been active (seconds)"
+ },
+ "DEVTOOLS_ANIMATIONINSPECTOR_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "bug_numbers": [1446496],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "jryans@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has the animation inspector been active (seconds)"
+ },
+ "DEVTOOLS_JSDEBUGGER_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "bug_numbers": [1446496],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "jryans@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has the JS debugger been active (seconds)"
+ },
+ "DEVTOOLS_JSBROWSERDEBUGGER_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "bug_numbers": [1446496],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "jryans@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has the JS browser debugger been active (seconds)"
+ },
+ "DEVTOOLS_STYLEEDITOR_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "bug_numbers": [1446496],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "jryans@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has the style editor been active (seconds)"
+ },
+ "DEVTOOLS_JSPROFILER_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "bug_numbers": [1446496],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "jryans@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has the JS profiler been active (seconds)"
+ },
+ "DEVTOOLS_MEMORY_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "bug_numbers": [1446496],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "jryans@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has the Memory Tool been active (seconds)"
+ },
+ "DEVTOOLS_NETMONITOR_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "bug_numbers": [1446496],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "jryans@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has the network monitor been active (seconds)"
+ },
+ "DEVTOOLS_STORAGE_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "bug_numbers": [1446496],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "jryans@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has the storage inspector been active (seconds)"
+ },
+ "DEVTOOLS_DOM_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "bug_numbers": [1343501, 1446496],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "jryans@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has the DOM inspector been active (seconds)"
+ },
+ "DEVTOOLS_RESPONSIVE_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "bug_numbers": [1242057, 1446496],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "jryans@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has the responsive view been active (seconds)"
+ },
+ "DEVTOOLS_ABOUTDEBUGGING_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "bug_numbers": [1446496],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "jryans@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has about:debugging been active? (seconds) (bug 1204601)"
+ },
+ "DEVTOOLS_COMPATIBILITYVIEW_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "bug_numbers": [1639454],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has the compatibility view been active (seconds)"
+ },
+ "DEVTOOLS_CUSTOM_TIME_ACTIVE_SECONDS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000000,
+ "n_buckets": 100,
+ "bug_numbers": [1446496],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "jryans@mozilla.com"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "How long has a custom developer tool been active (seconds)"
+ },
+ "DEVTOOLS_SAVE_HEAP_SNAPSHOT_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 1000,
+ "description": "The time (in milliseconds) that it took to save a heap snapshot in mozilla::devtools::ChromeUtils::SaveHeapSnapshot."
+ },
+ "DEVTOOLS_READ_HEAP_SNAPSHOT_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 1000,
+ "description": "The time (in milliseconds) that it took to read a heap snapshot in mozilla::devtools::ChromeUtils::ReadHeapSnapshot."
+ },
+ "DEVTOOLS_HEAP_SNAPSHOT_NODE_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 10000000,
+ "n_buckets": 10000,
+ "description": "The number of nodes serialized into a heap snapshot."
+ },
+ "DEVTOOLS_HEAP_SNAPSHOT_EDGE_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 10000000,
+ "n_buckets": 10000,
+ "description": "The number of edges serialized into a heap snapshot."
+ },
+ "DEVTOOLS_JAVASCRIPT_ERROR_DISPLAYED": {
+ "alert_emails": ["nicolas.b.pierron@mozilla.com"],
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "bug_numbers": [1255133, 1378449],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "keyed": true,
+ "description": "Records the key-identifiers of error messages when they are displayed in the webconsole of the debugger. Nothing should be recorded unless they are visible on the user screen."
+ },
+ "DEVTOOLS_TOOLBOX_HOST": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "bug_numbers": [1205845, 1389995],
+ "n_values": 9,
+ "releaseChannelCollection": "opt-out",
+ "description": "Records DevTools toolbox host each time the toolbox is opened and when the host is changed (0:Bottom, 1:RIGHT, 2:WINDOW, 3:CUSTOM, 4:LEFT, 5:PAGE, 9:Unknown)."
+ },
+ "DEVTOOLS_NUMBER_OF_CSS_GRIDS_IN_A_PAGE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dev-developer-tools@lists.mozilla.org"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 30,
+ "n_buckets": 29,
+ "bug_numbers": [1373483],
+ "description": "On page load, record the number of CSS Grid elements present on a page when the DevTools is open",
+ "releaseChannelCollection": "opt-out"
+ },
+ "DEVTOOLS_FONTEDITOR_FONT_TYPE_DISPLAYED": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "dev-developer-tools@lists.mozilla.org",
+ "mbalfanz@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "bug_numbers": [1523656],
+ "description": "Indicates if the font editor displayed its UI for a non-variable font or a variable font",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": ["variable", "nonvariable"]
+ },
+ "BROWSER_IS_USER_DEFAULT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [855928, 1641713],
+ "description": "Whether Firefox is the system default browser on startup. A true value is also recorded here, and a false value is recorded to BROWSER_SET_DEFAULT_ERROR, if a user clicked 'Use Firefox as my default browser' on an in-product prompt. (Note that on Windows 8+ the latter action opens the right settings dialog but does not actually change the default browser without further user action.) On Windows, 'system default browser' is operationalized as whether Firefox is the default HTTP protocol handler."
+ },
+ "BROWSER_IS_USER_DEFAULT_ERROR": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "releaseChannelCollection": "opt-out",
+ "description": "True if the browser was unable to determine if the browser was set as default."
+ },
+ "BROWSER_SET_DEFAULT_DIALOG_PROMPT_RAWCOUNT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 250,
+ "n_buckets": 15,
+ "releaseChannelCollection": "opt-out",
+ "description": "The number of times that a profile has seen the 'Set Default Browser' dialog."
+ },
+ "BROWSER_SET_DEFAULT_ALWAYS_CHECK": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "releaseChannelCollection": "opt-out",
+ "description": "True if the profile has `browser.shell.checkDefaultBrowser` set to true."
+ },
+ "BROWSER_SET_DEFAULT_RESULT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 4,
+ "releaseChannelCollection": "opt-out",
+ "description": "Result of the Set Default Browser dialog. After Firefox 89 the these values are: (0=Use Firefox + 'Don't ask again' checked, 1=Use Firefox + 'Don't ask again' unchecked, 2=Not Now + 'Don't ask again' checked, 3=Not Now + 'Don't ask again' unchecked). Before Firefox 89 these values were: (0=Use Firefox + 'Always perform check' unchecked, 1=Use Firefox + 'Always perform check' checked, 2=Not Now + 'Always perform check' unchecked, 3=Not Now + 'Always perform check' checked)."
+ },
+ "BROWSER_SET_DEFAULT_ERROR": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "releaseChannelCollection": "opt-out",
+ "description": "True if the browser was unable to set Firefox as the default browser"
+ },
+ "BROWSER_SET_DEFAULT_PDF_HANDLER_USER_CHOICE_RESULT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "operating_systems": ["windows"],
+ "expires_in_version": "126",
+ "kind": "categorical",
+ "labels": [
+ "Success",
+ "ErrProgID",
+ "ErrHash",
+ "ErrLaunchExe",
+ "ErrExeTimeout",
+ "ErrExeProgID",
+ "ErrExeHash",
+ "ErrExeRejected",
+ "ErrExeOther",
+ "ErrOther",
+ "ErrBuild"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1805509],
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "description": "Result of each attempt to set the default browser with SetDefaultExtensionHandlersUserChoice() for pdf extension"
+ },
+ "BROWSER_SET_DEFAULT_USER_CHOICE_RESULT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "operating_systems": ["windows"],
+ "expires_in_version": "126",
+ "kind": "categorical",
+ "labels": [
+ "Success",
+ "ErrProgID",
+ "ErrHash",
+ "ErrLaunchExe",
+ "ErrExeTimeout",
+ "ErrExeProgID",
+ "ErrExeHash",
+ "ErrExeRejected",
+ "ErrExeOther",
+ "ErrOther",
+ "ErrBuild"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1703578, 1736631, 1791928],
+ "alert_emails": ["application-update-telemetry-alerts@mozilla.com"],
+ "description": "Result of each attempt to set the default browser with SetDefaultBrowserUserChoice()"
+ },
+ "BROWSER_IS_ASSIST_DEFAULT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "The result of the default browser check for assist intent."
+ },
+ "BROWSER_ATTRIBUTION_ERRORS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": [
+ "read_error",
+ "decode_error",
+ "write_error",
+ "quarantine_error",
+ "empty_error",
+ "null_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 an extended attributed on the .app bundle.",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1621402, 1525076, 1874944],
+ "alert_emails": ["aoprea@mozilla.com"],
+ "operating_systems": ["mac", "windows"]
+ },
+ "MIXED_CONTENT_PAGE_LOAD": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 4,
+ "description": "Accumulates type of content per page load (0=no mixed or non-secure page, 1=mixed passive, 2=mixed active, 3=mixed passive and mixed active)"
+ },
+ "MIXED_CONTENT_UNBLOCK_COUNTER": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 3,
+ "description": "A simple counter of daily mixed-content unblock operations and top documents loaded"
+ },
+ "MIXED_CONTENT_HSTS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "How often would blocked mixed content be allowed if HSTS upgrades were allowed? 0=display/no-HSTS, 1=display/HSTS, 2=active/no-HSTS, 3=active/HSTS"
+ },
+ "MIXED_CONTENT_DOWNLOADS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com", "sstreich@mozilla.com"],
+ "bug_numbers": [1646768],
+ "expires_in_version": "90",
+ "kind": "boolean",
+ "description": "Accumulates how many downloads are mixed-content (True = The download is MixedContent, False= is not MixedContent)"
+ },
+ "MIXED_CONTENT_IMAGES": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "bug_numbers": [1819336],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "releaseChannelCollection": "opt-out",
+ "labels": [
+ "ImgUpSuccess",
+ "ImgUpFailure",
+ "ImgNoUpSuccess",
+ "ImgNoUpFailure"
+ ],
+ "description": "How often image loads fail in regular mode and in upgrading mode?"
+ },
+ "MIXED_CONTENT_VIDEO": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "bug_numbers": [1819336],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "releaseChannelCollection": "opt-out",
+ "labels": [
+ "VideoUpSuccess",
+ "VideoUpFailure",
+ "VideoNoUpSuccess",
+ "VideoNoUpFailure"
+ ],
+ "description": "How often video loads fail in regular mode and in upgrading mode?"
+ },
+ "MIXED_CONTENT_AUDIO": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "bug_numbers": [1819336],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "releaseChannelCollection": "opt-out",
+ "labels": [
+ "AudioUpSuccess",
+ "AudioUpFailure",
+ "AudioNoUpSuccess",
+ "AudioNoUpFailure"
+ ],
+ "description": "How often audio loads fail in regular mode and in upgrading mode?"
+ },
+ "CONTENT_SIGNATURE_VERIFICATION_STATUS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1258647, 1862062],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 20,
+ "description": "What was the result of the content signature verification? 0=valid, 1=invalid, 2=noCertChain, 3=createContextFailedWithOtherError, 4=expiredCert, 5=certNotValidYet, 6=buildCertChainFailed, 7=eeCertForWrongHost, 8=extractKeyError, 9=vfyContextError"
+ },
+ "CONTENT_SIGNATURE_VERIFICATION_ERRORS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1435713, 1862062],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "keyed": true,
+ "labels": [
+ "err0",
+ "err1",
+ "err2",
+ "err3",
+ "err4",
+ "err5",
+ "err6",
+ "err7",
+ "err8",
+ "err9"
+ ],
+ "description": "Result of the content signature verification keyed by application (certificate fingerprint). 0=valid, 1=invalid, 2=noCertChain, 3=createContextFailedWithOtherError, 4=expiredCert, 5=certNotValidYet, 6=buildCertChainFailed, 7=eeCertForWrongHost, 8=extractKeyError, 9=vfyContextError"
+ },
+ "NTLM_MODULE_USED_2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 8,
+ "description": "The module used for the NTLM protocol (Windows_API, Kerberos, Samba_auth or Generic) and whether or not the authentication was used to connect to a proxy server. This data is collected only once per session (at first NTLM authentification) ; fixed version."
+ },
+ "FX_THUMBNAILS_BG_QUEUE_SIZE_ON_CAPTURE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 100,
+ "n_buckets": 15,
+ "description": "BACKGROUND THUMBNAILS: Size of capture queue when a capture request is received"
+ },
+ "FX_THUMBNAILS_BG_CAPTURE_QUEUE_TIME_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 300000,
+ "n_buckets": 20,
+ "description": "BACKGROUND THUMBNAILS: Time the capture request spent in the queue before being serviced (ms)"
+ },
+ "FX_THUMBNAILS_BG_CAPTURE_SERVICE_TIME_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 20,
+ "description": "BACKGROUND THUMBNAILS: Time the capture took once it started and successfully completed (ms)"
+ },
+ "FX_THUMBNAILS_BG_CAPTURE_DONE_REASON_2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "BACKGROUND THUMBNAILS: Reason the capture completed (see TEL_CAPTURE_DONE_* constants in BackgroundPageThumbs.jsm)"
+ },
+ "FX_THUMBNAILS_BG_CAPTURE_PAGE_LOAD_TIME_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 20,
+ "description": "BACKGROUND THUMBNAILS: Time the capture's page load took (ms)"
+ },
+ "FX_THUMBNAILS_BG_CAPTURE_CANVAS_DRAW_TIME_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 500,
+ "n_buckets": 15,
+ "description": "BACKGROUND THUMBNAILS: Time it took to draw the capture's window to canvas (ms)"
+ },
+ "NETWORK_CACHE_V2_MISS_TIME_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "hbambas@mozilla.com",
+ "mnovotny@mozilla.com",
+ "necko@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "bug_numbers": [1489524],
+ "description": "Time spent to find out a cache entry file is missing"
+ },
+ "NETWORK_CACHE_V2_HIT_TIME_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "hbambas@mozilla.com",
+ "mnovotny@mozilla.com",
+ "necko@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "bug_numbers": [1489524, 1580077],
+ "description": "Time spent to open an existing file"
+ },
+ "NETWORK_CACHE_V1_TRUNCATE_TIME_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent to reopen an entry with OPEN_TRUNCATE"
+ },
+ "NETWORK_CACHE_V1_MISS_TIME_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent to find out a cache entry is missing"
+ },
+ "NETWORK_CACHE_V1_HIT_TIME_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent to open an existing cache entry"
+ },
+ "NETWORK_CACHE_V2_OUTPUT_STREAM_STATUS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 7,
+ "description": "Final status of the CacheFileOutputStream (0=ok, 1=other error, 2=out of memory, 3=disk full, 4=file corrupted, 5=file not found, 6=binding aborted)"
+ },
+ "NETWORK_CACHE_V2_INPUT_STREAM_STATUS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 7,
+ "description": "Final status of the CacheFileInputStream (0=ok, 1=other error, 2=out of memory, 3=disk full, 4=file corrupted, 5=file not found, 6=binding aborted)"
+ },
+ "NETWORK_CACHE_HIT_MISS_STAT_PER_CACHE_SIZE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 40,
+ "description": "Hit/Miss count split by cache size in file count (0=Hit 0-5000, 1=Miss 0-5000, 2=Hit 5001-10000, ...)"
+ },
+ "NETWORK_CACHE_HIT_RATE_PER_CACHE_SIZE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 400,
+ "description": "Hit rate for a specific cache size in file count. The hit rate is split into 20 buckets, the lower limit of the range in percents is 5*n/20. The cache size is divided into 20 ranges of length 5000, the lower limit of the range is 5000*(n%20)"
+ },
+ "NETWORK_CACHE_METADATA_FIRST_READ_TIME_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent to read the first part of the metadata from the cache entry file."
+ },
+ "NETWORK_CACHE_METADATA_SECOND_READ_TIME_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Time spent to read the missing part of the metadata from the cache entry file."
+ },
+ "NETWORK_CACHE_METADATA_SIZE_2": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1133739, 1495336],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 1024,
+ "high": 66560,
+ "n_buckets": 66,
+ "description": "Size of the metadata in bytes parsed from the disk."
+ },
+ "NETWORK_CACHE_SIZE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1548472],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 4000,
+ "n_buckets": 100,
+ "keyed": true,
+ "keys": ["SMARTSIZE", "USERDEFINEDSIZE"],
+ "description": "Cache size in megabytes keyed by cache capacity calculation type. Numbers are sampled periodically, every time 2GB of data is written to the cache."
+ },
+ "NETWORK_CACHE_ENTRY_COUNT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1548472],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 100,
+ "keyed": true,
+ "keys": ["SMARTSIZE", "USERDEFINEDSIZE"],
+ "description": "Number of entries in the cache keyed by cache capacity calculation type. Numbers are sampled periodically, every time 2GB of data is written to the cache."
+ },
+ "NETWORK_CACHE_SIZE_SHARE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1548472],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 99,
+ "keyed": true,
+ "keys": [
+ "UNKNOWN",
+ "OTHER",
+ "JAVASCRIPT",
+ "IMAGE",
+ "MEDIA",
+ "STYLESHEET",
+ "WASM"
+ ],
+ "description": "Percentage of the cache size occupied by given content type. Numbers are sampled periodically, every time 2GB of data is written to the cache."
+ },
+ "NETWORK_CACHE_ENTRY_COUNT_SHARE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com", "mnovotny@mozilla.com"],
+ "bug_numbers": [1548472],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 99,
+ "keyed": true,
+ "keys": [
+ "UNKNOWN",
+ "OTHER",
+ "JAVASCRIPT",
+ "IMAGE",
+ "MEDIA",
+ "STYLESHEET",
+ "WASM"
+ ],
+ "description": "Percentage of the entries with the given content type. Numbers are sampled periodically, every time 2GB of data is written to the cache."
+ },
+ "NETWORK_COOKIE_UNICODE_BYTE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "vgosu@mozilla.com"],
+ "bug_numbers": [1797231, 1813469, 1850889],
+ "expires_in_version": "130",
+ "kind": "categorical",
+ "releaseChannelCollection": "opt-out",
+ "description": "Records whether a cookie contains unexpected characters",
+ "labels": ["none", "unicodeName", "unicodeValue"]
+ },
+ "SSL_TLS13_INTOLERANCE_REASON_PRE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1250568],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 64,
+ "description": "Potential TLS 1.3 intolerance, before considering historical info (see tlsIntoleranceTelemetryBucket() in nsNSSIOLayer.cpp)."
+ },
+ "SSL_TLS13_INTOLERANCE_REASON_POST": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1250568],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 64,
+ "description": "Potential TLS 1.3 intolerance, after considering historical info (see tlsIntoleranceTelemetryBucket() in nsNSSIOLayer.cpp)."
+ },
+ "SSL_TLS12_INTOLERANCE_REASON_PRE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 64,
+ "description": "Potential TLS 1.2 intolerance, before considering historical info (see tlsIntoleranceTelemetryBucket() in nsNSSIOLayer.cpp)."
+ },
+ "SSL_TLS12_INTOLERANCE_REASON_POST": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 64,
+ "description": "Potential TLS 1.2 intolerance, after considering historical info (see tlsIntoleranceTelemetryBucket() in nsNSSIOLayer.cpp)."
+ },
+ "SSL_TLS11_INTOLERANCE_REASON_PRE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 64,
+ "description": "Potential TLS 1.1 intolerance, before considering historical info (see tlsIntoleranceTelemetryBucket() in nsNSSIOLayer.cpp)."
+ },
+ "SSL_TLS11_INTOLERANCE_REASON_POST": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 64,
+ "description": "Potential TLS 1.1 intolerance, after considering historical info (see tlsIntoleranceTelemetryBucket() in nsNSSIOLayer.cpp)."
+ },
+ "SSL_TLS10_INTOLERANCE_REASON_PRE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 64,
+ "description": "Potential TLS 1.0 intolerance, before considering historical info (see tlsIntoleranceTelemetryBucket() in nsNSSIOLayer.cpp)."
+ },
+ "SSL_TLS10_INTOLERANCE_REASON_POST": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 64,
+ "description": "Potential TLS 1.0 intolerance, after considering historical info (see tlsIntoleranceTelemetryBucket() in nsNSSIOLayer.cpp)."
+ },
+ "SSL_VERSION_FALLBACK_INAPPROPRIATE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 64,
+ "description": "TLS/SSL version intolerance was falsely detected, server rejected handshake (see tlsIntoleranceTelemetryBucket() in nsNSSIOLayer.cpp)."
+ },
+ "TLS_CIPHER_SUITE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1862062],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 64,
+ "description": "Negotiated cipher suite in TLS handshake (see key in AccumulateCipherSuite in nsNSSCallbacks.cpp)"
+ },
+ "SSL_KEA_RSA_KEY_SIZE_FULL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1862062],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 24,
+ "description": "RSA KEA (TLS_RSA_*) key size in full handshake"
+ },
+ "SSL_KEA_DHE_KEY_SIZE_FULL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1862062],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 24,
+ "description": "DHE KEA (TLS_DHE_*) key size in full handshake"
+ },
+ "SSL_KEA_ECDHE_CURVE_FULL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1862062],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 36,
+ "description": "ECDHE KEA (TLS_ECDHE_*) curve (23=P-256, 24=P-384, 25=P-521, 29=Curve25519) in full handshake"
+ },
+ "SSL_AUTH_ALGORITHM_FULL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1862062],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 16,
+ "description": "SSL Authentication Algorithm (null=0, rsa(KEA)=1, ecdsa=4, rsa(sign)=7) in full handshake"
+ },
+ "SSL_AUTH_RSA_KEY_SIZE_FULL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1862062],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 24,
+ "description": "RSA signature key size for TLS_*_RSA_* in full handshake"
+ },
+ "SSL_AUTH_ECDSA_CURVE_FULL": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1862062],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 36,
+ "description": "ECDSA signature curve for TLS_*_ECDSA_* in full handshake (23=P-256, 24=P-384, 25=P-521)"
+ },
+ "SSL_REASONS_FOR_NOT_FALSE_STARTING": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1862062],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 512,
+ "description": "Bitmask of reasons we did not false start when libssl would have let us (see key in nsNSSCallbacks.cpp)"
+ },
+ "SSL_HANDSHAKE_TYPE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1862062],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 8,
+ "description": "Type of handshake (1=resumption, 2=false started, 3=chose not to false start, 4=not allowed to false start)"
+ },
+ "SSL_OCSP_STAPLING": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1862062],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 8,
+ "description": "Status of OCSP stapling on this handshake (1=present, good; 2=none; 3=present, expired; 4=present, other error)"
+ },
+ "SSL_CERT_ERROR_OVERRIDES": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1862062],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 24,
+ "description": "Was a certificate error overridden on this handshake? What was it? (0=unknown error (indicating bug), 1=no, >1=a specific error)"
+ },
+ "SSL_CERT_VERIFICATION_ERRORS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "jhofmann@mozilla.com",
+ "rtestard@mozilla.com",
+ "seceng@mozilla.org"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "bug_numbers": [1503572],
+ "releaseChannelCollection": "opt-out",
+ "description": "If certificate verification failed in a TLS handshake, what was the error? (see MapCertErrorToProbeValue in security/manager/ssl/SSLServerCertVerification.cpp and the values in security/pkix/include/pkix/Result.h)"
+ },
+ "SSL_CT_POLICY_COMPLIANT_CONNECTIONS_BY_CA": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 256,
+ "bug_numbers": [1320567],
+ "releaseChannelCollection": "opt-out",
+ "description": "Number of successfully established TLS connections compliant with the Certificate Transparency Policy, by CA. See https://searchfox.org/mozilla-central/source/security/manager/ssl/RootHashes.inc for names of CAs. Bucket zero holds CAs not present in the list."
+ },
+ "SSL_CT_POLICY_NON_COMPLIANT_CONNECTIONS_BY_CA": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 256,
+ "bug_numbers": [1320567],
+ "releaseChannelCollection": "opt-out",
+ "description": "Number of successfully established TLS connections NOT compliant with the Certificate Transparency Policy, by CA. See https://searchfox.org/mozilla-central/source/security/manager/ssl/RootHashes.inc for names of CAs. Bucket zero holds CAs not present in the list."
+ },
+ "SSL_PERMANENT_CERT_ERROR_OVERRIDES": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1862062],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1024,
+ "n_buckets": 10,
+ "description": "How many permanent certificate overrides a user has stored."
+ },
+ "SSL_SCTS_ORIGIN": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 10,
+ "bug_numbers": [1293231],
+ "releaseChannelCollection": "opt-out",
+ "description": "Origin of Signed Certificate Timestamps received (1=Embedded, 2=TLS handshake extension, 3=Stapled OCSP response)"
+ },
+ "SSL_SCTS_PER_CONNECTION": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 10,
+ "bug_numbers": [1293231],
+ "releaseChannelCollection": "opt-out",
+ "description": "Histogram of Signed Certificate Timestamps per SSL connection, from all sources (embedded / OCSP Stapling / TLS handshake). Bucket 0 counts the cases when no SCTs were received, or none were extracted due to parsing errors."
+ },
+ "SSL_SCTS_VERIFICATION_STATUS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 10,
+ "bug_numbers": [1293231],
+ "releaseChannelCollection": "opt-out",
+ "description": "Verification status of Signed Certificate Timestamps received (0=Decoding error, 1=Valid SCT, 2=SCT from unknown log, 3=Invalid SCT signature, 4=SCT timestamp is in the future, 5=Valid SCT from a disqualified log)"
+ },
+ "CERT_OCSP_ENABLED": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Is OCSP fetching enabled? (pref security.OCSP.enabled)"
+ },
+ "CERT_OCSP_REQUIRED": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Is OCSP required when the cert has an OCSP URI? (pref security.OCSP.require)"
+ },
+ "CERT_EV_STATUS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1254653, 1862062],
+ "releaseChannelCollection": "opt-out",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "EV status of a certificate, recorded on each TLS connection. 0=invalid, 1=DV, 2=EV"
+ },
+ "CERT_VALIDATION_SUCCESS_BY_CA": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "alert_emails": ["jjones@mozilla.com", "seceng-telemetry@mozilla.com"],
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1364159, 1369747, 1441550],
+ "kind": "enumerated",
+ "n_values": 256,
+ "description": "Successful SSL server cert validations by CA (see RootHashes.inc for names of CAs)"
+ },
+ "CERT_PINNING_FAILURES_BY_CA": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["pinning@mozilla.org"],
+ "bug_numbers": [1862062],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 256,
+ "description": "Pinning failures by CA (see RootHashes.inc for names of CAs)"
+ },
+ "CERT_PINNING_RESULTS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["pinning@mozilla.org"],
+ "bug_numbers": [1862062],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Certificate pinning results (0 = failure, 1 = success)"
+ },
+ "CERT_PINNING_TEST_RESULTS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["pinning@mozilla.org"],
+ "bug_numbers": [1862062],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Certificate pinning test results (0 = failure, 1 = success)"
+ },
+ "CERT_PINNING_MOZ_RESULTS_BY_HOST": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["dkeeler@mozilla.com", "pinning@mozilla.org"],
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1007844, 1521940],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 512,
+ "description": "Certificate pinning results by host for Mozilla operational sites"
+ },
+ "CERT_PINNING_MOZ_TEST_RESULTS_BY_HOST": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["dkeeler@mozilla.com", "pinning@mozilla.org"],
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1007844, 1521940],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 512,
+ "description": "Certificate pinning test results by host for Mozilla operational sites"
+ },
+ "CERT_CHAIN_KEY_SIZE_STATUS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1862062],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 4,
+ "description": "Does enforcing a larger minimum RSA key size cause verification failures? 1 = no, 2 = yes, 3 = another error prevented finding a verified chain"
+ },
+ "WEAVE_START_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 10,
+ "description": "The number of times a sync started in this session"
+ },
+ "WEAVE_COMPLETE_SUCCESS_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 10,
+ "description": "The number of times a sync successfully completed in this session"
+ },
+ "WEAVE_WIPE_SERVER_SUCCEEDED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "55",
+ "alert_emails": ["sync-dev@mozilla.org"],
+ "kind": "boolean",
+ "bug_numbers": [1241699],
+ "description": "Stores 1 if a wipeServer call succeeded, and 0 if it failed."
+ },
+ "WEBCRYPTO_EXTRACTABLE_IMPORT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether an imported key was marked as extractable"
+ },
+ "WEBCRYPTO_EXTRACTABLE_GENERATE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether a generated key was marked as extractable"
+ },
+ "WEBCRYPTO_EXTRACTABLE_ENC": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether a key used in an encrypt/decrypt operation was marked as extractable"
+ },
+ "WEBCRYPTO_EXTRACTABLE_SIG": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether a key used in a sign/verify operation was marked as extractable"
+ },
+ "WEBCRYPTO_RESOLVED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether a promise created by WebCrypto was resolved (vs rejected)"
+ },
+ "WEBCRYPTO_METHOD": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 20,
+ "description": "Methods invoked under window.crypto.subtle (0=encrypt, 1=decrypt, 2=sign, 3=verify, 4=digest, 5=generateKey, 6=deriveKey, 7=deriveBits, 8=importKey, 9=exportKey, 10=wrapKey, 11=unwrapKey)"
+ },
+ "WEBCRYPTO_ALG": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 30,
+ "description": "Algorithms used with WebCrypto (see table in WebCryptoTask.cpp)"
+ },
+ "MASTER_PASSWORD_ENABLED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "flag",
+ "description": "If a primary-password is enabled for this profile"
+ },
+ "DISPLAY_SCALING": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 500,
+ "n_buckets": 100,
+ "bug_numbers": [1121966],
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "description": "Scaling percentage for the display where the first window is opened"
+ },
+ "SHUTDOWN_PHASE_DURATION_TICKS_QUIT_APPLICATION": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 65,
+ "n_buckets": 10,
+ "bug_numbers": [1689953],
+ "alert_emails": ["dothayer@mozilla.com, jstutte@mozilla.com"],
+ "description": "Duration of shutdown phase quit-application, as measured by the shutdown terminator in ticks of 100ms"
+ },
+ "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_CHANGE_NET_TEARDOWN": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 65,
+ "n_buckets": 10,
+ "bug_numbers": [1689953],
+ "alert_emails": ["dothayer@mozilla.com, jstutte@mozilla.com"],
+ "description": "Duration of shutdown phase profile-change-net-teardown, as measured by the shutdown terminator in ticks of 100ms"
+ },
+ "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_CHANGE_TEARDOWN": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 65,
+ "n_buckets": 10,
+ "bug_numbers": [1689953],
+ "alert_emails": ["dothayer@mozilla.com, jstutte@mozilla.com"],
+ "description": "Duration of shutdown phase profile-change-teardown, as measured by the shutdown terminator in ticks of 100ms"
+ },
+ "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_BEFORE_CHANGE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 65,
+ "n_buckets": 10,
+ "bug_numbers": [1689953],
+ "alert_emails": ["dothayer@mozilla.com, jstutte@mozilla.com"],
+ "description": "Duration of shutdown phase profile-before-change, as measured by the shutdown terminator in ticks of 100ms"
+ },
+ "SHUTDOWN_PHASE_DURATION_TICKS_PROFILE_BEFORE_CHANGE_QM": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 65,
+ "n_buckets": 10,
+ "bug_numbers": [1689953],
+ "alert_emails": ["dothayer@mozilla.com, jstutte@mozilla.com"],
+ "description": "Duration of shutdown phase profile-before-change-qm, as measured by the shutdown terminator in ticks of 100ms"
+ },
+ "SHUTDOWN_PHASE_DURATION_TICKS_XPCOM_WILL_SHUTDOWN": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 65,
+ "n_buckets": 10,
+ "bug_numbers": [1689953],
+ "alert_emails": ["dothayer@mozilla.com, jstutte@mozilla.com"],
+ "description": "Duration of shutdown phase xpcom-will-shutdown, as measured by the shutdown terminator in ticks of 100ms"
+ },
+ "SHUTDOWN_PHASE_DURATION_TICKS_XPCOM_SHUTDOWN": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 65,
+ "n_buckets": 10,
+ "bug_numbers": [1689953],
+ "alert_emails": ["dothayer@mozilla.com, jstutte@mozilla.com"],
+ "description": "Duration of shutdown phase xpcom-shutdown, as measured by the shutdown terminator in ticks of 100ms"
+ },
+ "TAP_TO_LOAD_ENABLED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "50",
+ "kind": "enumerated",
+ "n_values": 3,
+ "description": "Whether or not a user has tap-to-load enabled.",
+ "bug_numbers": [1208167]
+ },
+ "ZOOMED_VIEW_ENABLED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "60",
+ "kind": "boolean",
+ "description": "Whether or not a user has the zoomed view (a.k.a. \"Magnify small areas\") enabled.",
+ "alert_emails": ["mobile-frontend@mozilla.com"],
+ "bug_numbers": [1235061]
+ },
+ "PAGE_LOAD_ERROR": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "keyed": true,
+ "keys": ["top", "frame"],
+ "n_values": 45,
+ "labels": [
+ "UNKNOWN_PROTOCOL",
+ "FILE_NOT_FOUND",
+ "FILE_ACCESS_DENIED",
+ "UNKNOWN_HOST",
+ "CONNECTION_REFUSED",
+ "PROXY_BAD_GATEWAY",
+ "NET_INTERRUPT",
+ "NET_TIMEOUT",
+ "P_GATEWAY_TIMEOUT",
+ "CSP_FRAME_ANCEST",
+ "CSP_FORM_ACTION",
+ "CSP_NAVIGATE_TO",
+ "XFO_VIOLATION",
+ "PHISHING_URI",
+ "MALWARE_URI",
+ "UNWANTED_URI",
+ "HARMFUL_URI",
+ "CONTENT_CRASHED",
+ "FRAME_CRASHED",
+ "BUILDID_MISMATCH",
+ "NET_RESET",
+ "MALFORMED_URI",
+ "REDIRECT_LOOP",
+ "UNKNOWN_SOCKET",
+ "DOCUMENT_N_CACHED",
+ "OFFLINE",
+ "DOC_PRINTMODE",
+ "PORT_ACCESS",
+ "UNKNOWN_PROXY_HOST",
+ "PROXY_CONNECTION",
+ "PROXY_FORBIDDEN",
+ "P_NOT_IMPLEMENTED",
+ "PROXY_AUTH",
+ "PROXY_TOO_MANY",
+ "CONTENT_ENCODING",
+ "REMOTE_XUL",
+ "UNSAFE_CONTENT",
+ "CORRUPTED_CONTENT",
+ "INTERCEPTION_FAIL",
+ "INADEQUATE_SEC",
+ "BLOCKED_BY_POLICY",
+ "HTTP2_SENT_GOAWAY",
+ "HTTP3_PROTOCOL",
+ "BINDING_FAILED",
+ "otherError"
+ ],
+ "description": "Page load errors. Match values with type of error in nsDocShellTelemetryUtils.cpp",
+ "alert_emails": ["seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1636962]
+ },
+ "COOKIE_BEHAVIOR": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "enumerated",
+ "n_values": 15,
+ "description": "Records the cookie behavior pref at startup.",
+ "alert_emails": [
+ "jhofmann@mozilla.com",
+ "pdol@mozilla.com",
+ "seceng-telemetry@mozilla.com"
+ ],
+ "bug_numbers": [1484251, 1561384]
+ },
+ "TRACKING_PROTECTION_ENABLED": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "boolean",
+ "description": "True if tracking protection is enabled globally at startup.",
+ "alert_emails": ["pdol@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "bug_numbers": [1058133]
+ },
+ "TRACKING_PROTECTION_PBM_DISABLED": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "boolean",
+ "description": "True if tracking protection in Private Browsing mode is disabled at startup.",
+ "alert_emails": ["pdol@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "bug_numbers": [1200944]
+ },
+ "FINGERPRINTERS_BLOCKED_COUNT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": ["pageLoad", "blocked", "allowed"],
+ "description": "A count of the status of fingerprinter blocking per top level page load. ('pageLoad' = There was a page load, 'blocked' = at least one fingerprinter was blocked, 'allowed' = at least one fingerprinter was detected and allowed.) Note, pageLoad is used as a baseline measurement.",
+ "alert_emails": ["aedelstein@mozilla.com", "seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1522919]
+ },
+ "CRYPTOMINERS_BLOCKED_COUNT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": ["pageLoad", "blocked", "allowed"],
+ "description": "A count of the status of cryptominer blocking per top level page load. ('pageLoad' = There was a page load, 'blocked' = at least one cryptominer was blocked, 'allowed' = at least one cryptominer was detected and allowed.) Note, pageLoad is used as a baseline measurement.",
+ "alert_emails": ["aedelstein@mozilla.com", "seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1522919]
+ },
+ "FENNEC_TRACKING_PROTECTION_STATE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "enumerated",
+ "n_values": 5,
+ "description": "The state of the user-visible tracking protection setting (0 = Disabled, 1 = Enabled in PB, 2 = Enabled)",
+ "alert_emails": ["pdol@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "bug_numbers": [1228090]
+ },
+ "TRACKING_PROTECTION_SHIELD": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 4,
+ "releaseChannelCollection": "opt-out",
+ "description": "Status of the shield icon for each top-level pageload (outside of Private Browsing mode) when tracking protection is enabled (0 = there was a page load, this is used as a baseline, 1 = shield crossed out because CB was disabled on this page by the user, 2 = shield shown because some content was blocked)",
+ "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+ "bug_numbers": [1058133, 1659762]
+ },
+ "TRACKING_PROTECTION_EVENTS": {
+ "record_in_processes": ["main", "content"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 3,
+ "description": "(Fennec Only) A value of 0 is sent when the security UI changes on pages loaded outside of Private Browsing mode, a value of 1 is sent when users manually disable TP on that page, and 2 is sent when users manually re-enable TP on that page.",
+ "alert_emails": ["safebrowsing-telemetry@mozilla.org"],
+ "bug_numbers": [1058133],
+ "products": ["fennec"]
+ },
+ "STORAGE_ACCESS_GRANTED_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": [
+ "StorageGranted",
+ "StorageAccessAPI",
+ "OpenerAfterUI",
+ "Opener",
+ "Redirect"
+ ],
+ "description": "A count of the granted storage access. ('StorageGranted' = There was a storage access granted, 'StorageAccessAPI' = the storage access was granted by the storage access API, 'OpenerAfterUserInteraction' = the storage access was granted due to the heuristic 'OpenerAfterUserInteraction', 'Opener' = the storage access was granted due to the heuristic 'Opener', 'Redirect' = the storage access was granted due to the heuristic 'Redirect'.) Note, StorageGranted is used as a baseline measurement.",
+ "alert_emails": [
+ "tihuang@mozilla.com",
+ "aedelstein@mozilla.com",
+ "senglehardt@mozilla.com",
+ "seceng-telemetry@mozilla.com"
+ ],
+ "bug_numbers": [1668199]
+ },
+ "STORAGE_ACCESS_REMAINING_DAYS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "enumerated",
+ "n_values": 60,
+ "bug_numbers": [1668199],
+ "alert_emails": [
+ "tihuang@mozilla.com",
+ "aedelstein@mozilla.com",
+ "senglehardt@mozilla.com",
+ "seceng-telemetry@mozilla.com"
+ ],
+ "description": "The time remaining until expiration per storage access permissions, in days (one record per permission, not averaged)."
+ },
+ "QUERY_STRIPPING_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": [
+ "Navigation",
+ "Redirect",
+ "StripForNavigation",
+ "StripForRedirect"
+ ],
+ "description": "A count of the query stripping. ('Navigation' = There was a top-level loading via navigation, 'Redirect' = There was a top-level loading via redirect, 'StripForNavigation' = There was a stripping happened for a top-level navigation, 'StripForRedirect' = There was a stripping happened for a top-level redirect.",
+ "alert_emails": [
+ "tihuang@mozilla.com",
+ "pbz@mozilla.com",
+ "seceng-telemetry@mozilla.com"
+ ],
+ "bug_numbers": [1706616]
+ },
+ "QUERY_STRIPPING_PARAM_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "low": 1,
+ "high": 100,
+ "n_buckets": 25,
+ "description": "If query params get stripped, how many per navigation/redirect.",
+ "alert_emails": [
+ "tihuang@mozilla.com",
+ "pbz@mozilla.com",
+ "seceng-telemetry@mozilla.com"
+ ],
+ "bug_numbers": [1762374]
+ },
+ "QUERY_STRIPPING_COUNT_BY_PARAM": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": [
+ "param_mc_eid",
+ "param_oly_anon_id",
+ "param_oly_enc_id",
+ "param___s",
+ "param_vero_id",
+ "param__hsenc",
+ "param_mkt_tok",
+ "param_fbclid",
+ "param_gclid",
+ "param_dclid",
+ "param_msclkid",
+ "param__openstat",
+ "param_yclid",
+ "param_wickedid",
+ "param_twclid",
+ "param___hssc",
+ "param___hstc",
+ "param___hsfp",
+ "param_hsctatracking",
+ "param_wbraid",
+ "param_gbraid",
+ "param_ysclid"
+ ],
+ "description": "Counts how often specific query parameters are stripped.",
+ "alert_emails": [
+ "tihuang@mozilla.com",
+ "pbz@mozilla.com",
+ "seceng-telemetry@mozilla.com"
+ ],
+ "bug_numbers": [1762374, 1830058]
+ },
+ "EMAIL_TRACKER_COUNT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": [
+ "base_normal",
+ "content_normal",
+ "base_email_webapp",
+ "content_email_webapp"
+ ],
+ "description": "A count of the classified email trackers. ('base_normal' = The classified email tracker is in base category and loaded under a normal site, 'content_normal' = The classified email tracker is in content category and loaded under a normal site, 'base_email_webapp' = The classified email tracker is in base category and loaded under a known email webapp, 'content_email_webapp' = The classified email tracker is in content category and loaded under a known email webapp.)",
+ "alert_emails": ["tihuang@mozilla.com", "seceng-telemetry@mozilla.com"],
+ "bug_numbers": [1773701]
+ },
+ "EMAIL_TRACKER_EMBEDDED_PER_TAB": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 100,
+ "keyed": true,
+ "keys": [
+ "base_normal",
+ "content_normal",
+ "all_normal",
+ "base_emailapp",
+ "content_emailapp",
+ "all_emailapp"
+ ],
+ "alert_emails": ["tihuang@mozilla.com", "seceng-telemetry@mozilla.com"],
+ "description": "A count of the embedded third-party email trackers for known email web apps. ('base_normal' = The count of loaded base email trackers under normal sites, 'conten_normal' = The count of loaded content email trackers under normal sites, 'all_normal' = The count of all loaded email trackers under normal sites, 'base_emailapp' = The count of loaded base email trackers under email webapps, 'content_emailapp' = The count of loaded content email trackers under email webapps, 'all_emailapp' = The count of all loaded email trackers under email webapps.)",
+ "bug_numbers": [1781249]
+ },
+ "SERVICE_WORKER_LAUNCH_TIME_2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "bug_numbers": [1328398, 1693074],
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 25,
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["sw-telemetry@mozilla.com", "echuang@mozilla.com"],
+ "description": "Time (ms) for spawning a service worker in the remote content process."
+ },
+ "SERVICE_WORKER_ISOLATED_LAUNCH_TIME": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "bug_numbers": [1740335],
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 25,
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": [
+ "sw-telemetry@mozilla.com",
+ "rjesup@mozilla.com",
+ "echuang@mozilla.com"
+ ],
+ "description": "Time (ms) for spawning a service worker in an isolated content process."
+ },
+ "SERVICE_WORKER_REGISTRATION_LOADING": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 20,
+ "description": "Tracking how ServiceWorkerRegistrar loads data before the first content is shown. File bugs in Core::DOM in case of a Telemetry regression."
+ },
+ "SERVICE_WORKER_FETCH_INTERCEPTION_DURATION_MS_2": {
+ "expires_in_version": "never",
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "kind": "exponential",
+ "bug_numbers": [1359610, 1433916, 1693074],
+ "high": 60000,
+ "keyed": true,
+ "n_buckets": 25,
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["sw-telemetry@mozilla.com"],
+ "description": "Time delta (ms) between when a network request is intercepted in the parent process and the service worker provides a response."
+ },
+ "SERVICE_WORKER_FETCH_EVENT_DISPATCH_MS_2": {
+ "expires_in_version": "never",
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "bug_numbers": [1359610, 1433916, 1693074],
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 25,
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["sw-telemetry@mozilla.com", "echuang@mozilla.com"],
+ "description": "Time (in ms) measured between when the fetch event is dispatched by the Service Worker and before we execute the event listeners."
+ },
+
+ "SERVICE_WORKER_FETCH_EVENT_FINISH_SYNTHESIZED_RESPONSE_MS_2": {
+ "expires_in_version": "never",
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "bug_numbers": [1359610, 1433916, 1693074],
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 25,
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["sw-telemetry@mozilla.com", "echuang@mozilla.com"],
+ "description": "Time (in ms) measured between when the respondWith promise resolves and when we provide the response through the intercepted channel."
+ },
+
+ "SERVICE_WORKER_FETCH_EVENT_CHANNEL_RESET_MS_2": {
+ "expires_in_version": "never",
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "bug_numbers": [1359610, 1433916, 1693074],
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 25,
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["sw-telemetry@mozilla.com", "echuang@mozilla.com"],
+ "description": "Time (in ms) measured between when the fetch handler finished executing and when we reset the network channel."
+ },
+
+ "SERVICE_WORKER_DESTROYED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1202706],
+ "alert_emails": ["tdsmith@mozilla.com", "compatibility@lists.mozilla.org"],
+ "description": "Number of service workers destroyed; used in conjunction with use counter histograms"
+ },
+ "SUBPROCESS_ABNORMAL_ABORT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "description": "Counts of plugin/content process abnormal shutdown, whether or not a crash report was available."
+ },
+ "SUBPROCESS_CRASHES_WITH_DUMP": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "description": "Counts of plugin and content process crashes which are reported with a crash dump."
+ },
+ "SUBPROCESS_LAUNCH_FAILURE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["haftandilian@mozilla.com"],
+ "expires_in_version": "never",
+ "bug_numbers": [1275430],
+ "kind": "count",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "description": "Counts the number of times launching a subprocess fails. Counts are by subprocess-type using the GeckoProcessType enum."
+ },
+ "PROCESS_CRASH_SUBMIT_ATTEMPT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "keys": [
+ "main-crash",
+ "main-hang",
+ "content-crash",
+ "content-hang",
+ "plugin-crash",
+ "plugin-hang",
+ "gmplugin-crash",
+ "gmplugin-hang",
+ "gpu-crash",
+ "gpu-hang",
+ "vr-crash",
+ "vr-hang",
+ "rdd-crash",
+ "rdd-hang",
+ "socket-crash",
+ "socket-hang",
+ "utility-crash",
+ "utility-hang",
+ "sandboxbroker-crash",
+ "sandboxbroker-hang",
+ "forkserver-crash",
+ "forkserver-hang"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "An attempt to submit a crash. Keyed on the CrashManager Crash.type."
+ },
+ "PROCESS_CRASH_SUBMIT_SUCCESS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "description": "The submission status when main/plugin/content crashes are submitted. 1 is success, 0 is failure. Keyed on the CrashManager Crash.type."
+ },
+ "VIDEO_PLAY_TIME_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "description": "Total time spent playing video in milliseconds.",
+ "kind": "exponential",
+ "high": 14400000,
+ "n_buckets": 100,
+ "bug_numbers": [1261955, 1127646, 1570634, 1567604, 1685399],
+ "releaseChannelCollection": "opt-out"
+ },
+ "MEDIA_PLAY_TIME_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "description": "Total time spent playing media in milliseconds, keyed by its content (Audio and video, audio only, video only, audio and video but audio inaudible, audio and video but audio muted).",
+ "keyed": true,
+ "kind": "exponential",
+ "high": 14400000,
+ "n_buckets": 100,
+ "bug_numbers": [1678373],
+ "releaseChannelCollection": "opt-out"
+ },
+ "AUDIBLE_PLAY_TIME_PERCENT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "description": "Percentage of time spent playing media with an audible audio track, regardless of the volume. Keyed by audio-only or video presence.",
+ "keyed": true,
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 50,
+ "bug_numbers": [1678373],
+ "releaseChannelCollection": "opt-out"
+ },
+ "MUTED_PLAY_TIME_PERCENT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "description": "Percentage of time spent playing media that has an audio track, with the audio muted. Keyed by audio-only or video presence.",
+ "keyed": true,
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 50,
+ "bug_numbers": [1678373],
+ "releaseChannelCollection": "opt-out"
+ },
+ "VIDEO_VISIBLE_PLAY_TIME_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "description": "Total time spent playing visible video in milliseconds.",
+ "keyed": true,
+ "kind": "exponential",
+ "high": 7200000,
+ "n_buckets": 100,
+ "bug_numbers": [1723799],
+ "releaseChannelCollection": "opt-out"
+ },
+ "VIDEO_HIDDEN_PLAY_TIME_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "description": "Total time spent playing video while element is hidden, in milliseconds.",
+ "kind": "exponential",
+ "high": 7200000,
+ "n_buckets": 100,
+ "bug_numbers": [1285419, 1570634, 1685399],
+ "releaseChannelCollection": "opt-out"
+ },
+ "VIDEO_HIDDEN_PLAY_TIME_PERCENTAGE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "description": "Percentage of total time spent playing video while element is hidden. Keyed by audio presence and by height ranges (boundaries: 240. 480, 576, 720, 1080, 2160), e.g.: 'V,0<h<=240', 'AV,h>2160'; and 'All' will accumulate all percentages. This is reported whenever the decoder stops. (eg. pausing media, encountering an error, changing to a new resource, page goes to the bf-cached or page gets discarded)",
+ "keyed": true,
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 50,
+ "bug_numbers": [1287987, 1570634, 1685399],
+ "releaseChannelCollection": "opt-out"
+ },
+ "VIDEO_HDR_PLAY_TIME_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com", "bwerth@mozilla.com"],
+ "expires_in_version": "never",
+ "description": "Total time spent playing HDR video in milliseconds.",
+ "kind": "exponential",
+ "high": 14400000,
+ "n_buckets": 100,
+ "bug_numbers": [1777264],
+ "releaseChannelCollection": "opt-out"
+ },
+ "VIDEO_ENCRYPTED_PLAY_TIME_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "description": "Total time spent playing encrypted video in milliseconds.",
+ "kind": "exponential",
+ "high": 7200000,
+ "n_buckets": 100,
+ "bug_numbers": [1662212, 1685399],
+ "releaseChannelCollection": "opt-out"
+ },
+ "VIDEO_CLEARKEY_PLAY_TIME_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "description": "Total time spent playing Clearkey encrypted video in milliseconds.",
+ "kind": "exponential",
+ "high": 7200000,
+ "n_buckets": 100,
+ "bug_numbers": [1662212, 1685399],
+ "releaseChannelCollection": "opt-out"
+ },
+ "VIDEO_WIDEVINE_PLAY_TIME_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "description": "Total time spent playing Widevine encrypted video in milliseconds.",
+ "kind": "exponential",
+ "high": 7200000,
+ "n_buckets": 100,
+ "bug_numbers": [1662212, 1685399],
+ "releaseChannelCollection": "opt-out"
+ },
+ "VIDEO_INFERRED_DECODE_SUSPEND_PERCENTAGE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "110",
+ "description": "Percentage of total time spent *not* fully decoding video while element is hidden (simulated, even when feature is not enabled). Keyed by audio presence and by height ranges (boundaries: 240. 480, 576, 720, 1080, 2160), e.g.: 'V,0<h<=240', 'AV,h>2160'; and 'All' will accumulate all percentages.",
+ "keyed": true,
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 50,
+ "bug_numbers": [1293145, 1570634, 1606206, 1685399, 1714303, 1754647],
+ "releaseChannelCollection": "opt-out"
+ },
+ "VIDEO_INTER_KEYFRAME_AVERAGE_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "110",
+ "description": "Average interval between video keyframes in played videos, in milliseconds. Keyed by audio presence and by height ranges (boundaries: 240. 480, 576, 720, 1080, 2160), e.g.: 'V,0<h<=240', 'AV,h>2160'; and 'All' will accumulate all percentages.",
+ "keyed": true,
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 100,
+ "bug_numbers": [1289668, 1570634, 1606206, 1685399, 1754648]
+ },
+ "VIDEO_INTER_KEYFRAME_MAX_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "92",
+ "description": "Maximum interval between video keyframes in played videos, in milliseconds; '0' means only 1 keyframe found. Keyed by audio presence and by height ranges (boundaries: 240. 480, 576, 720, 1080, 2160), e.g.: 'V,0<h<=240', 'AV,h>2160'; and 'All' will accumulate all percentages. This is reported whenever the decoder stops. (eg. pausing media, encountering an error, changing to a new resource, page goes to the bf-cached or page gets discarded)",
+ "keyed": true,
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 100,
+ "bug_numbers": [1289668, 1570634, 1606206, 1685399]
+ },
+ "VIDEO_SUSPEND_RECOVERY_TIME_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "100",
+ "description": "Time taken for a video to resume after decoding was suspended, in milliseconds. Keyed by audio presence, hw acceleration, and by height ranges (boundaries: 240. 480, 720, 1080, 2160), e.g.: 'V,0-240', 'AV(hw),2160+'; and 'All' will accumulate all percentages.",
+ "keyed": true,
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "bug_numbers": [1294349, 1570634, 1606206, 1689267]
+ },
+ "MEDIA_CODEC_USED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "keyed": true,
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "Count of use of audio/video codecs in HTMLMediaElements and WebAudio. Those with 'resource' prefix are approximate; report based on HTTP ContentType or sniffing. Those with 'webaudio' prefix are for WebAudio."
+ },
+ "MSE_SOURCE_BUFFER_TYPE": {
+ "products": ["firefox"],
+ "record_in_processes": ["main", "content"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "bug_numbers": [1699249, 1848211],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": [
+ "VideoHevc",
+ "VideoWebm",
+ "AudioWebm",
+ "VideoMp4",
+ "AudioMp4",
+ "VideoMp2t",
+ "AudioMp2t",
+ "AudioMpeg",
+ "AudioAac"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "Count the actual or possible (because we don't support some of them for now) usage amount for each media mime type used in MSE. We collect this data in MediaSource's `AddSourceBuffer()` and `IsTypeSupported()`."
+ },
+ "FX_SANITIZE_TOTAL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["mbonardo@mozilla.com", "jhofmann@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 20,
+ "description": "Sanitize: Total time it takes to sanitize (ms)"
+ },
+ "FX_SANITIZE_CACHE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["mbonardo@mozilla.com", "jhofmann@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 20,
+ "description": "Sanitize: Time it takes to sanitize the cache (ms)"
+ },
+ "FX_SANITIZE_COOKIES_2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["mbonardo@mozilla.com", "jhofmann@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 20,
+ "description": "Sanitize: Time it takes to sanitize firefox cookies (ms). A subset of FX_SANITIZE_COOKIES."
+ },
+ "FX_SANITIZE_HISTORY": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["mbonardo@mozilla.com", "jhofmann@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 20,
+ "description": "Sanitize: Time it takes to sanitize history (ms)"
+ },
+ "FX_SANITIZE_FORMDATA": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["mbonardo@mozilla.com", "jhofmann@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 20,
+ "description": "Sanitize: Time it takes to sanitize stored form data (ms)"
+ },
+ "FX_SANITIZE_DOWNLOADS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["mbonardo@mozilla.com", "jhofmann@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 20,
+ "description": "Sanitize: Time it takes to sanitize recent downloads (ms)"
+ },
+ "FX_SANITIZE_SESSIONS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["mbonardo@mozilla.com", "jhofmann@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 20,
+ "description": "Sanitize: Time it takes to sanitize saved sessions (ms)"
+ },
+ "FX_SANITIZE_SITESETTINGS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["mbonardo@mozilla.com", "jhofmann@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 20,
+ "description": "Sanitize: Time it takes to sanitize site-specific settings (ms)"
+ },
+ "FX_SANITIZE_OPENWINDOWS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["mbonardo@mozilla.com", "jhofmann@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 30000,
+ "n_buckets": 20,
+ "description": "Sanitize: Time it takes to sanitize the open windows list (ms). On Android, this is the time it takes to close all open tabs (ms)."
+ },
+ "PWMGR_BLOCKLIST_NUM_SITES": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100,
+ "n_buckets": 10,
+ "description": "The number of sites for which the user has explicitly rejected saving logins",
+ "alert_emails": ["loines@mozilla.com"],
+ "bug_numbers": [1454733, 1545172],
+ "releaseChannelCollection": "opt-out",
+ "record_into_store": ["main"]
+ },
+ "PWMGR_FORM_AUTOFILL_RESULT": {
+ "record_in_processes": ["content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["seceng-telemetry@mozilla.com", "loines@mozilla.com"],
+ "bug_numbers": [1340021, 1545172],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 20,
+ "description": "The result of auto-filling a login form. See http://mzl.la/1Mbs6jL for bucket descriptions.",
+ "record_into_store": ["main"]
+ },
+ "PWMGR_LOGIN_LAST_USED_DAYS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 750,
+ "n_buckets": 40,
+ "description": "Time in days each saved login was last used",
+ "alert_emails": ["loines@mozilla.com"],
+ "bug_numbers": [1454733, 1545172],
+ "releaseChannelCollection": "opt-out",
+ "record_into_store": ["main"]
+ },
+ "PWMGR_LOGIN_PAGE_SAFETY": {
+ "record_in_processes": ["content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["seceng-telemetry@mozilla.com", "loines@mozilla.com"],
+ "bug_numbers": [1340021, 1545172],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 8,
+ "description": "The safety of a page where we see a password field. (0: safe page & safe submit; 1: safe page & unsafe submit; 2: safe page & unknown submit; 3: unsafe page & safe submit; 4: unsafe page & unsafe submit; 5: unsafe page & unknown submit)",
+ "record_into_store": ["main"]
+ },
+ "PWMGR_NUM_IMPROVED_GENERATED_PASSWORDS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "The number of passwords generated when 'signon.improvedPasswordRules.enabled' is true. 0 = Password generated by default rules, 1 = Password generated by custom rules",
+ "alert_emails": ["tgiles@mozilla.com", "passwords-dev@mozilla.com"],
+ "bug_numbers": [1686071]
+ },
+ "PWMGR_NUM_PASSWORDS_PER_HOSTNAME": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 21,
+ "n_buckets": 20,
+ "description": "The number of passwords per hostname",
+ "alert_emails": ["loines@mozilla.com"],
+ "bug_numbers": [1454733, 1545172],
+ "releaseChannelCollection": "opt-out",
+ "record_into_store": ["main"]
+ },
+ "PWMGR_NUM_FORM_HAS_POSSIBLE_USERNAME_EVENT_PER_DOC": {
+ "record_in_processes": ["content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "130",
+ "kind": "linear",
+ "high": 21,
+ "n_buckets": 20,
+ "bug_numbers": [1708455, 1791929, 1822724],
+ "description": "The number of documents that receive >= bucket number 'DOMFormHasPossibleUsernameEvent' event",
+ "alert_emails": ["passwords-dev@mozilla.org"],
+ "releaseChannelCollection": "opt-out"
+ },
+ "PWMGR_NUM_SAVED_PASSWORDS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 750,
+ "n_buckets": 50,
+ "description": "Total number of saved logins, including those that cannot be decrypted",
+ "alert_emails": ["loines@mozilla.com"],
+ "bug_numbers": [1454733, 1545172],
+ "releaseChannelCollection": "opt-out",
+ "record_into_store": ["main"]
+ },
+ "PWMGR_NUM_HTTPAUTH_PASSWORDS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 750,
+ "n_buckets": 50,
+ "description": "Number of HTTP Auth logins"
+ },
+ "PWMGR_PROMPT_REMEMBER_ACTION": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 5,
+ "description": "Action taken by user through prompt for creating a login. (0=Prompt displayed [always recorded], 1=Add login, 2=Don't save now, 3=Never save)",
+ "alert_emails": ["loines@mozilla.com"],
+ "bug_numbers": [1454733, 1545172],
+ "releaseChannelCollection": "opt-out",
+ "record_into_store": ["main"]
+ },
+ "PWMGR_PROMPT_UPDATE_ACTION": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 5,
+ "description": "Action taken by user through prompt for modifying a login. (0=Prompt displayed [always recorded], 1=Update login, 2=Don't update, 3=Remove saved login)",
+ "alert_emails": ["loines@mozilla.com"],
+ "bug_numbers": [1454733, 1545172, 1226236],
+ "releaseChannelCollection": "opt-out",
+ "record_into_store": ["main"]
+ },
+ "PWMGR_SAVING_ENABLED": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Number of users who have password saving on globally",
+ "alert_emails": ["loines@mozilla.com"],
+ "bug_numbers": [1454733, 1545172],
+ "releaseChannelCollection": "opt-out",
+ "record_into_store": ["main"]
+ },
+ "PWMGR_IS_USERNAME_ONLY_FORM": {
+ "record_in_processes": ["content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether a <form> is a username-only form. (0=Form has a possible username field but is not considered as a username-only form, 1=Form is considered as a username-only form)",
+ "bug_numbers": [1708455],
+ "alert_emails": ["passwords-dev@mozilla.org"],
+ "releaseChannelCollection": "opt-out"
+ },
+ "PWMGR_SIGNUP_FORM_DETECTION_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 70,
+ "bug_numbers": [1815449],
+ "alert_emails": ["passwords-dev@mozilla.org"],
+ "releaseChannelCollection": "opt-out",
+ "description": "Elapsed time to detect whether a <form> element is a signup form."
+ },
+ "PWMGR_USERNAME_PRESENT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec", "thunderbird"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Whether a saved login has a username"
+ },
+ "SLOW_SCRIPT_NOTICE_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Count slow script notices"
+ },
+ "SLOW_SCRIPT_PAGE_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "bug_numbers": [1251667],
+ "description": "The number of pages that trigger slow script notices"
+ },
+ "SLOW_SCRIPT_NOTIFY_DELAY": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50,
+ "bug_numbers": [1271978],
+ "description": "The difference between the js slow script timeout for content set in prefs and the actual time we waited before displaying the notification (msec)."
+ },
+ "SERVICE_WORKER_SPAWN_ATTEMPTS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "50",
+ "kind": "count",
+ "description": "Count attempts to spawn a ServiceWorker for a domain. File bugs in Core::DOM in case of a Telemetry regression."
+ },
+ "SERVICE_WORKER_WAS_SPAWNED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "50",
+ "kind": "count",
+ "description": "Count ServiceWorkers that really did get a thread created for them. File bugs in Core::DOM in case of a Telemetry regression."
+ },
+ "SERVICE_WORKER_SPAWN_GETS_QUEUED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["amarchesini@mozilla.com"],
+ "bug_numbers": [1286895],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Tracking whether a ServiceWorker spawn gets queued due to hitting max workers per domain limit. File bugs in Core::DOM in case of a Telemetry regression."
+ },
+ "SHARED_WORKER_SPAWN_GETS_QUEUED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["amarchesini@mozilla.com"],
+ "bug_numbers": [1286895],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Tracking whether a SharedWorker spawn gets queued due to hitting max workers per domain limit. File bugs in Core::DOM in case of a Telemetry regression."
+ },
+ "SHARED_WORKER_DESTROYED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1202706],
+ "alert_emails": ["tdsmith@mozilla.com", "compatibility@lists.mozilla.org"],
+ "description": "Number of shared workers destroyed; used in conjunction with use counter histograms"
+ },
+ "DEDICATED_WORKER_SPAWN_GETS_QUEUED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["amarchesini@mozilla.com"],
+ "bug_numbers": [1286895],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Tracking whether a DedicatedWorker spawn gets queued due to hitting max workers per domain limit. File bugs in Core::DOM in case of a Telemetry regression."
+ },
+ "DEDICATED_WORKER_DESTROYED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1202706],
+ "alert_emails": ["tdsmith@mozilla.com", "compatibility@lists.mozilla.org"],
+ "description": "Number of dedicated workers destroyed; used in conjunction with use counter histograms"
+ },
+ "SERVICE_WORKER_CONTROLLED_DOCUMENTS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "50",
+ "kind": "count",
+ "description": "Count whenever a document is controlled. File bugs in Core::DOM in case of a Telemetry regression."
+ },
+ "SERVICE_WORKER_UPDATED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "50",
+ "kind": "count",
+ "description": "Count ServiceWorkers scripts that are updated. File bugs in Core::DOM in case of a Telemetry regression."
+ },
+ "SERVICE_WORKER_RUNNING": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 31,
+ "n_buckets": 30,
+ "keyed": true,
+ "keys": ["All", "Fetch"],
+ "bug_numbers": [1740335],
+ "alert_emails": [
+ "sw-telemetry@mozilla.com",
+ "rjesup@mozilla.com",
+ "echuang@mozilla.com"
+ ],
+ "description": "The sampled number of simultaneous running ServiceWorkers in a session, sampled once each 10 seconds (lazily on state change)"
+ },
+ "GRAPHICS_SANITY_TEST": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "rhunt@mozilla.com"],
+ "kind": "enumerated",
+ "n_values": 20,
+ "releaseChannelCollection": "opt-out",
+ "description": "Reports results from the graphics sanity test to track which drivers are having problems (0=TEST_PASSED, 1=TEST_FAILED_RENDER, 2=TEST_FAILED_VIDEO, 3=TEST_CRASHED)"
+ },
+ "READER_MODE_PARSE_RESULT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "alert_emails": ["firefox-dev@mozilla.org", "gijs@mozilla.com"],
+ "kind": "enumerated",
+ "n_values": 5,
+ "description": "The result of trying to parse a document to show in reader view (0=Success, 1=Error too many elements, 2=Error in worker, 3=Error no article)"
+ },
+ "READER_MODE_DOWNLOAD_RESULT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "alert_emails": ["firefox-dev@mozilla.org", "gijs@mozilla.com"],
+ "kind": "enumerated",
+ "n_values": 5,
+ "description": "The result of trying to download a document to show in reader view (0=Success, 1=Error XHR, 2=Error no document)"
+ },
+ "PERMISSIONS_SQL_CORRUPTED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Record the permissions.sqlite init failure"
+ },
+ "DEFECTIVE_PERMISSIONS_SQL_REMOVED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Record the removal of defective permissions.sqlite"
+ },
+ "GRAPHICS_DRIVER_STARTUP_TEST": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "rhunt@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 20,
+ "releaseChannelCollection": "opt-out",
+ "description": "Reports whether or not graphics drivers crashed during startup."
+ },
+ "GRAPHICS_SANITY_TEST_REASON": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "rhunt@mozilla.com"],
+ "expires_in_version": "70",
+ "kind": "enumerated",
+ "n_values": 20,
+ "releaseChannelCollection": "opt-out",
+ "description": "Reports why a graphics sanity test was run. 0=First Run, 1=App Updated, 2=Device Change, 3=Driver Change, 4=Advanced Layers Config Change."
+ },
+ "TRANSLATION_OPPORTUNITIES": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "boolean",
+ "description": "A number of successful and failed attempts to translate a document"
+ },
+ "TRANSLATION_OPPORTUNITIES_BY_LANGUAGE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "boolean",
+ "keyed": true,
+ "description": "A number of successful and failed attempts to translate a document grouped by language"
+ },
+ "TRANSLATED_PAGES": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "count",
+ "description": "A number of sucessfully translated pages"
+ },
+ "TRANSLATED_PAGES_BY_LANGUAGE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "count",
+ "keyed": true,
+ "description": "A number of sucessfully translated pages by language"
+ },
+ "TRANSLATED_CHARACTERS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "exponential",
+ "high": 10240,
+ "n_buckets": 50,
+ "description": "A number of sucessfully translated characters"
+ },
+ "DENIED_TRANSLATION_OFFERS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "count",
+ "description": "A number of tranlation offers the user denied"
+ },
+ "AUTO_REJECTED_TRANSLATION_OFFERS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "count",
+ "description": "A number of auto-rejected tranlation offers"
+ },
+ "REQUESTS_OF_ORIGINAL_CONTENT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "count",
+ "description": "A number of times the user requested to see the original content of a translated page"
+ },
+ "CHANGES_OF_TARGET_LANGUAGE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "count",
+ "description": "A number of times when the target language was changed by the user"
+ },
+ "CHANGES_OF_DETECTED_LANGUAGE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "boolean",
+ "description": "A number of changes of detected language before (true) or after (false) translating a page for the first time."
+ },
+ "SHOULD_TRANSLATION_UI_APPEAR": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "flag",
+ "description": "Tracks situations when the user opts for displaying translation UI"
+ },
+ "SHOULD_AUTO_DETECT_LANGUAGE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "default",
+ "kind": "flag",
+ "description": "Tracks situations when the user opts for auto-detecting the language of a page"
+ },
+ "WEAVE_DEVICE_COUNT_DESKTOP": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["sync-dev@mozilla.org"],
+ "bug_numbers": [1232050],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 10,
+ "releaseChannelCollection": "opt-out",
+ "description": "Number of desktop devices (including this device) associated with this Sync account. Recorded each time Sync successfully completes the 'clients' engine."
+ },
+ "WEAVE_DEVICE_COUNT_MOBILE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["sync-dev@mozilla.org"],
+ "bug_numbers": [1232050],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 10,
+ "releaseChannelCollection": "opt-out",
+ "description": "Number of mobile devices associated with this Sync account. Recorded each time Sync successfully completes the 'clients' engine."
+ },
+ "CONTENT_DOCUMENTS_DESTROYED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1204994, 1569672],
+ "alert_emails": ["tdsmith@mozilla.com", "compatibility@lists.mozilla.org"],
+ "description": "Number of content documents destroyed; used in conjunction with use counter histograms"
+ },
+ "TOP_LEVEL_CONTENT_DOCUMENTS_DESTROYED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1204994, 1569672],
+ "alert_emails": ["tdsmith@mozilla.com", "compatibility@lists.mozilla.org"],
+ "description": "Number of top-level content documents destroyed; used in conjunction with use counter histograms"
+ },
+ "PUSH_API_NOTIFY": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["push@mozilla.com"],
+ "bug_numbers": [1429286],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Number of push messages that were successfully decrypted and delivered to a ServiceWorker."
+ },
+ "D3D11_SYNC_HANDLE_FAILURE": {
+ "record_in_processes": ["main", "content", "gpu"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "gfx-telemetry-alerts@mozilla.com",
+ "bschouten@mozilla.com"
+ ],
+ "expires_in_version": "66",
+ "releaseChannelCollection": "opt-out",
+ "kind": "count",
+ "description": "Number of times the D3D11 compositor failed to get a texture sync handle."
+ },
+ "GFX_CONTENT_FAILED_TO_ACQUIRE_DEVICE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "rhunt@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 6,
+ "description": "Failed to create a gfx content device. 0=content d3d11, 1=image d3d11, 2=d2d1."
+ },
+ "GFX_CRASH": {
+ "record_in_processes": ["main", "content", "gpu"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "rhunt@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "releaseChannelCollection": "opt-out",
+ "description": "Graphics Crash Reason (...)"
+ },
+ "WEBFONT_DOWNLOAD_TIME": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["jdaggett@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50,
+ "description": "Time to download a webfont (ms)"
+ },
+ "WEBFONT_DOWNLOAD_TIME_AFTER_START": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "perf-telemetry-alerts@mozilla.com",
+ "necko@mozilla.com",
+ "bdekoz@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 50,
+ "releaseChannelCollection": "opt-out",
+ "description": "Time after navigationStart webfont download completed (ms)",
+ "bug_numbers": [1580077]
+ },
+ "WEBFONT_FONTTYPE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["jdaggett@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "Font format type (woff/woff2/ttf/...)"
+ },
+ "WEBFONT_SRCTYPE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["jdaggett@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 5,
+ "description": "Font src type loaded (1 = local, 2 = url, 3 = data)"
+ },
+ "WEBFONT_PER_PAGE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["jdaggett@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "description": "Number of fonts loaded at page load"
+ },
+ "WEBFONT_SIZE_PER_PAGE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["jdaggett@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 50,
+ "description": "Size of all fonts loaded at page load (kb)"
+ },
+ "WEBFONT_SIZE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["jdaggett@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 50,
+ "description": "Size of font loaded (kb)"
+ },
+ "WEBFONT_COMPRESSION_WOFF": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["jdaggett@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 50,
+ "description": "Compression ratio of WOFF data (%)"
+ },
+ "WEBFONT_COMPRESSION_WOFF2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["jdaggett@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 50,
+ "description": "Compression ratio of WOFF2 data (%)"
+ },
+ "ALERTS_SERVICE_DND_ENABLED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["firefox-dev@mozilla.org"],
+ "bug_numbers": [1219030],
+ "expires_in_version": "50",
+ "kind": "boolean",
+ "description": "XUL-only: whether the user has toggled do not disturb."
+ },
+ "PLUGIN_DRAWING_MODEL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "rhunt@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "bug_numbers": [1229961],
+ "n_values": 12,
+ "releaseChannelCollection": "opt-out",
+ "description": "Plugin drawing model. 0 when windowed, otherwise NPDrawingModel + 1."
+ },
+ "JS_PAGELOAD_EXECUTION_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dpalmeiro@mozilla.com",
+ "perf-telemetry-alerts@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 100,
+ "bug_numbers": [1709139],
+ "description": "Time spent during page load executing Javascript in ms."
+ },
+ "JS_PAGELOAD_DELAZIFICATION_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dpalmeiro@mozilla.com",
+ "perf-telemetry-alerts@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "bug_numbers": [1709139],
+ "description": "Time spent during page load delazifying Javascript in ms."
+ },
+ "JS_PAGELOAD_XDR_ENCODING_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dpalmeiro@mozilla.com",
+ "perf-telemetry-alerts@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "bug_numbers": [1709139],
+ "description": "Time spent during page load XDR encoding Javascript in ms."
+ },
+ "JS_PAGELOAD_BASELINE_COMPILE_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dpalmeiro@mozilla.com",
+ "perf-telemetry-alerts@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "bug_numbers": [1709139],
+ "description": "Time spent during page load baseline compiling Javascript in ms."
+ },
+ "JS_PAGELOAD_GC_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dpalmeiro@mozilla.com",
+ "perf-telemetry-alerts@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "bug_numbers": [1709139],
+ "description": "Time spent during page load in the GC in ms."
+ },
+ "JS_PAGELOAD_PROTECT_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dpalmeiro@mozilla.com",
+ "perf-telemetry-alerts@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "bug_numbers": [1709139],
+ "description": "Time spent during page load protecting JIT executable memory."
+ },
+ "JS_PAGELOAD_PARSE_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dpalmeiro@mozilla.com",
+ "perf-telemetry-alerts@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "bug_numbers": [1709139],
+ "description": "Time spent during page load syntax parsing JS scripts on the main thread in ms."
+ },
+ "DOM_SCRIPT_KIND": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["jcoppeard@mozilla.com"],
+ "expires_in_version": "63",
+ "kind": "categorical",
+ "bug_numbers": [1430145],
+ "labels": ["ClassicScript", "ModuleScript"],
+ "description": "Record the kind of every script loaded in a document."
+ },
+ "DOM_SCRIPT_LOADING_SOURCE": {
+ "record_in_processes": ["content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["nicolas.b.pierron@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "bug_numbers": [1362114],
+ "labels": ["Inline", "SourceFallback", "Source", "AltData"],
+ "description": "Record the input from which the bytes are coming from, for each script in a document."
+ },
+ "DOM_SCRIPT_PRELOAD_RESULT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["jcoppeard@mozilla.com"],
+ "expires_in_version": "63",
+ "kind": "categorical",
+ "bug_numbers": [1430145],
+ "labels": [
+ "Used",
+ "RejectedByPolicy",
+ "RequestMismatch",
+ "LoadError",
+ "NotUsed"
+ ],
+ "description": "Whether a preloaded script was used or the reason it was not used."
+ },
+ "VIDEO_DROPPED_FRAMES_PROPORTION": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 50,
+ "bug_numbers": [1238433, 1570634],
+ "description": "Percentage of frames decoded frames dropped between starting and stopping a video"
+ },
+ "VIDEO_DROPPED_FRAMES_PROPORTION_EXPONENTIAL": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "bug_numbers": [1238433, 1570634, 1752207, 1755363],
+ "description": "Ratio of dropped frames per total decoded frames scaled by 10,000 between starting and stopping a video"
+ },
+ "VIDEO_DROPPED_DECODED_FRAMES_PROPORTION_EXPONENTIAL": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "bug_numbers": [1761994],
+ "description": "Ratio of decoder dropped frames per total decoded frames scaled by 10,000 between starting and stopping a video"
+ },
+ "VIDEO_DROPPED_SINK_FRAMES_PROPORTION_EXPONENTIAL": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "bug_numbers": [1761994],
+ "description": "Ratio of sink dropped frames per total decoded frames scaled by 10,000 between starting and stopping a video"
+ },
+ "VIDEO_DROPPED_COMPOSITOR_FRAMES_PROPORTION_EXPONENTIAL": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "bug_numbers": [1761994],
+ "description": "Ratio of compositor dropped frames per total decoded frames scaled by 10,000 between starting and stopping a video"
+ },
+ "MEDIA_DECODER_BACKEND_USED": {
+ "record_in_processes": ["main", "content", "gpu"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "rhunt@mozilla.com"],
+ "bug_numbers": [1259695],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 10,
+ "description": "Media decoder backend (0=WMF Software, 1=DXVA2D3D9, 2=DXVA2D3D11)"
+ },
+ "IPC_MESSAGE_SIZE2": {
+ "record_in_processes": ["main", "content", "gpu"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["hchang@mozilla.com"],
+ "bug_numbers": [1353159],
+ "expires_in_version": "60",
+ "kind": "exponential",
+ "high": 8000000,
+ "n_buckets": 50,
+ "keyed": false,
+ "description": "Measures the size of all IPC messages sent that are >= 4096 bytes."
+ },
+ "IPC_REPLY_SIZE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["wmccloskey@mozilla.com"],
+ "bug_numbers": [1264820],
+ "expires_in_version": "55",
+ "kind": "exponential",
+ "high": 8000000,
+ "n_buckets": 50,
+ "keyed": true,
+ "description": "Measures the size of IPC messages by message name"
+ },
+ "IPC_SYNC_MAIN_LATENCY_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["jya@mozilla.com", "drno@ohlmeier.org"],
+ "bug_numbers": [1333489],
+ "expires_in_version": "63",
+ "kind": "exponential",
+ "low": 32,
+ "high": 750,
+ "n_buckets": 40,
+ "keyed": true,
+ "description": "Measures the number of milliseconds we spend waiting for sync IPC messages to finish sending, keyed by message name. Note: only messages that wait for more than 500 microseconds and block the main thread are included in this probe."
+ },
+ "SANDBOX_HAS_SECCOMP_BPF": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gcp@mozilla.com"],
+ "bug_numbers": [1098428],
+ "expires_in_version": "55",
+ "kind": "boolean",
+ "operating_systems": ["linux"],
+ "description": "Whether the system has seccomp-bpf capability"
+ },
+ "SANDBOX_HAS_SECCOMP_TSYNC": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gcp@mozilla.com"],
+ "bug_numbers": [1098428, 1370578],
+ "expires_in_version": "61",
+ "kind": "boolean",
+ "operating_systems": ["linux"],
+ "description": "Whether the system has seccomp-bpf thread-sync capability"
+ },
+ "SANDBOX_HAS_USER_NAMESPACES": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gcp@mozilla.com", "jld@mozilla.com"],
+ "bug_numbers": [1098428, 1370578, 1461546, 1464220],
+ "expires_in_version": "70",
+ "releaseChannelCollection": "opt-out",
+ "kind": "boolean",
+ "operating_systems": ["linux"],
+ "description": "Whether our process succedeed in creating a user namespace"
+ },
+ "SANDBOX_HAS_USER_NAMESPACES_PRIVILEGED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gcp@mozilla.com"],
+ "bug_numbers": [1098428, 1370578],
+ "expires_in_version": "61",
+ "kind": "boolean",
+ "operating_systems": ["linux"],
+ "description": "Whether the system has the capability to create privileged user namespaces"
+ },
+ "SANDBOX_MEDIA_ENABLED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gcp@mozilla.com"],
+ "bug_numbers": [1098428],
+ "expires_in_version": "55",
+ "kind": "boolean",
+ "operating_systems": ["linux"],
+ "description": "Whether the sandbox is enabled for media/GMP plugins"
+ },
+ "SANDBOX_CONTENT_ENABLED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gcp@mozilla.com"],
+ "bug_numbers": [1098428],
+ "expires_in_version": "55",
+ "kind": "boolean",
+ "operating_systems": ["linux"],
+ "description": "Whether the sandbox is enabled for the content process"
+ },
+ "SANDBOX_REJECTED_SYSCALLS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["jld@mozilla.com", "gcp@mozilla.com"],
+ "bug_numbers": [1286865],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "count",
+ "keyed": true,
+ "operating_systems": ["linux"],
+ "description": "System calls blocked by a seccomp-bpf sandbox policy; limited to syscalls where we would crash on Nightly. The key is generally the architecture and syscall ID but in some cases we include non-personally-identifying information from the syscall arguments; see the function SubmitToTelemetry in security/sandbox/linux/reporter/SandboxReporter.cpp for details."
+ },
+ "SANDBOX_FAILED_LAUNCH_KEYED": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["bowen@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 50,
+ "bug_numbers": [1368600],
+ "operating_systems": ["windows"],
+ "description": "Error code when a Windows sandboxed process fails to launch, keyed by process type and Windows error code. See https://searchfox.org/mozilla-central/search?q=ResultCode++path%3Asandbox_types.h&redirect=true for definitions of the error codes."
+ },
+ "SYNC_WORKER_OPERATION": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["amarchesini@mozilla.com", "khuey@mozilla.com"],
+ "bug_numbers": [1267904],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 20,
+ "keyed": true,
+ "description": "Tracking how long a Worker thread is blocked when a sync operation is executed on the main-thread."
+ },
+ "SUBPROCESS_KILL_HARD": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["wmccloskey@mozilla.com"],
+ "bug_numbers": [1269961],
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "description": "Counts the number of times a subprocess was forcibly killed, and the reason."
+ },
+ "FX_CONTENT_CRASH_DUMP_UNAVAILABLE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["wmccloskey@mozilla.com"],
+ "bug_numbers": [1269961],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "Counts the number of times that about:tabcrashed was unable to find a crash dump."
+ },
+ "FX_CONTENT_CRASH_NOT_SUBMITTED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["wmccloskey@mozilla.com"],
+ "bug_numbers": [1269961],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "description": "Counts the number of times that about:tabcrashed was unloaded without submitting."
+ },
+ "D3D11_COMPOSITING_FAILURE_ID": {
+ "record_in_processes": ["main", "content", "gpu"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "rhunt@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "description": "D3D11 compositor runtime and dynamic failure IDs. This will record a count for each context creation success or failure. Each failure id is a unique identifier that can be traced back to a particular failure branch or blocklist rule.",
+ "bug_numbers": [1002846]
+ },
+ "OPENGL_COMPOSITING_FAILURE_ID": {
+ "record_in_processes": ["main", "content", "gpu"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "rhunt@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "keyed": true,
+ "description": "OpenGL compositor runtime and dynamic failure IDs. This will record a count for each context creation success or failure. Each failure id is a unique identifier that can be traced back to a particular failure branch or blocklist rule.",
+ "bug_numbers": [1002846]
+ },
+ "BLINK_FILESYSTEM_USED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["amarchesini@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "bug_numbers": [1272501],
+ "releaseChannelCollection": "opt-out",
+ "description": "Webkit/Blink filesystem used"
+ },
+ "WEBKIT_DIRECTORY_USED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["amarchesini@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "bug_numbers": [1272501],
+ "releaseChannelCollection": "opt-out",
+ "description": "HTMLInputElement.webkitdirectory attribute used"
+ },
+ "TIME_TO_FIRST_CLICK_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1307675, 1332511],
+ "expires_in_version": "58",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "description": "Time in milliseconds from the first non-blank paint to the creation time of the next click event per top-level content browsing context."
+ },
+ "TIME_TO_FIRST_KEY_INPUT_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1307675, 1332511],
+ "expires_in_version": "58",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "description": "Time in milliseconds from the first non-blank paint to the creation time of the next key event per top-level content browsing context."
+ },
+ "TIME_TO_FIRST_MOUSE_MOVE_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1307675, 1332511],
+ "expires_in_version": "58",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "description": "Time in milliseconds from the first non-blank paint to the creation time of the next mouse move event per top-level content browsing context."
+ },
+ "TIME_TO_FIRST_SCROLL_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1307675, 1332511],
+ "expires_in_version": "58",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "description": "Time in milliseconds from the first non-blank paint to the creation time of the next scroll event per top-level content browsing context."
+ },
+ "TIME_TO_FIRST_INTERACTION_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1332511, 1489524],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "description": "Time in milliseconds from the first non-blank paint to the creation time of the next click, key, mouse or scroll event per top-level content browsing context."
+ },
+ "CONTENT_PAINT_TIME": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "gfx-telemetry-alerts@mozilla.com",
+ "mwoodrow@mozilla.com",
+ "dbolter@mozilla.com"
+ ],
+ "bug_numbers": [1309442, 1489524, 1584109],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "Time spent in the paint pipeline for content in milliseconds."
+ },
+ "CONTENT_FULL_PAINT_TIME": {
+ "record_in_processes": ["main", "content", "gpu"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "gfx-telemetry-alerts@mozilla.com",
+ "jmuizelaar@mozilla.com",
+ "dbolter@mozilla.com"
+ ],
+ "bug_numbers": [1505858, 1584109],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "Time spent in the paint pipeline until it's ready for composition in milliseconds."
+ },
+ "CONTENT_FRAME_TIME": {
+ "record_in_processes": ["main", "gpu"],
+ "products": ["firefox"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "jnicol@mozilla.com"],
+ "bug_numbers": [1470528, 1509536, 1584109],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 50,
+ "description": "The time, in percentage of a vsync interval, spent from beginning a paint in the content process until that frame is presented in the compositor."
+ },
+ "CONTENT_FRAME_TIME_VSYNC": {
+ "record_in_processes": ["main", "gpu"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "gfx-telemetry-alerts@mozilla.com",
+ "mwoodrow@mozilla.com"
+ ],
+ "bug_numbers": [1517355, 1584109],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "linear",
+ "low": 8,
+ "high": 792,
+ "n_buckets": 100,
+ "description": "The time, in percentage of a vsync interval, spent from the vsync that started a paint in the content process until that frame is presented in the compositor."
+ },
+ "CONTENT_FRAME_TIME_WITH_SVG": {
+ "record_in_processes": ["main", "gpu"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "gfx-telemetry-alerts@mozilla.com",
+ "mwoodrow@mozilla.com"
+ ],
+ "bug_numbers": [1483549, 1509536, 1584109],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 50,
+ "description": "The time, in percentage of a vsync interval, spent from beginning a paint in the content process until that frame is presented in the compositor, for frames that contained an SVG to be drawn by WebRender"
+ },
+ "CONTENT_FRAME_TIME_WITHOUT_RESOURCE_UPLOAD": {
+ "record_in_processes": ["main", "gpu"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "gfx-telemetry-alerts@mozilla.com",
+ "mwoodrow@mozilla.com"
+ ],
+ "bug_numbers": [1503405, 1584109],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 50,
+ "description": "The time, in percentage of a vsync interval, spent from beginning a paint in the content process until that frame is presented in the compositor by WebRender, excluding time spent uploading resources"
+ },
+ "CONTENT_FRAME_TIME_WITHOUT_UPLOAD": {
+ "record_in_processes": ["main", "gpu"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "gfx-telemetry-alerts@mozilla.com",
+ "mwoodrow@mozilla.com"
+ ],
+ "bug_numbers": [1503405, 1584109],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 50,
+ "description": "The time, in percentage of a vsync interval, spent from beginning a paint in the content process until that frame is presented in the compositor by WebRender, excluding time spent uploading any content"
+ },
+ "CONTENT_FRAME_TIME_REASON": {
+ "record_in_processes": ["main", "gpu"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "gfx-telemetry-alerts@mozilla.com",
+ "mwoodrow@mozilla.com"
+ ],
+ "bug_numbers": [1510853, 1584109],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "description": "The reason that CONTENT_FRAME_TIME recorded a slow (>200) result, if any.",
+ "labels": [
+ "OnTime",
+ "NoVsync",
+ "MissedComposite",
+ "SlowComposite",
+ "MissedCompositeMid",
+ "MissedCompositeLong",
+ "MissedCompositeLow",
+ "NoVsyncNoId"
+ ]
+ },
+ "CONTENT_SMALL_PAINT_PHASE_WEIGHT_PARTIAL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "gfx-telemetry-alerts@mozilla.com",
+ "mmynttinen@mozilla.com"
+ ],
+ "bug_numbers": [1430897, 1518134],
+ "expires_in_version": "never",
+ "keyed": true,
+ "keys": ["dl", "wrdl", "sb", "fb"],
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 12,
+ "description": "Percentages of times for phases in a normal content paint relative to the time spent in the entire normal paint. (\"dl\" = Display list, \"wrdl\" = WebRender display list, \"sb\" = Scene building, \"fb\" = Frame building)"
+ },
+ "CONTENT_LARGE_PAINT_PHASE_WEIGHT_PARTIAL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "gfx-telemetry-alerts@mozilla.com",
+ "mmynttinen@mozilla.com"
+ ],
+ "bug_numbers": [1309442, 1518134],
+ "expires_in_version": "never",
+ "keyed": true,
+ "keys": ["dl", "wrdl", "sb", "fb"],
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 12,
+ "description": "Percentages of times for phases in an expensive content paint relative to the time spent in the entire expensive paint. (\"dl\" = Display list, \"wrdl\" = WebRender display list, \"sb\" = Scene building, \"fb\" = Frame building)"
+ },
+ "CONTENT_SMALL_PAINT_PHASE_WEIGHT_FULL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "gfx-telemetry-alerts@mozilla.com",
+ "mmynttinen@mozilla.com"
+ ],
+ "bug_numbers": [1430897, 1518134],
+ "expires_in_version": "never",
+ "keyed": true,
+ "keys": ["dl", "wrdl", "sb", "fb"],
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 12,
+ "description": "Percentages of times for phases in a normal content paint relative to the time spent in the entire normal paint. (\"dl\" = Display list, \"wrdl\" = WebRender display list, \"sb\" = Scene building, \"fb\" = Frame building)"
+ },
+ "CONTENT_LARGE_PAINT_PHASE_WEIGHT_FULL": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "gfx-telemetry-alerts@mozilla.com",
+ "mmynttinen@mozilla.com"
+ ],
+ "bug_numbers": [1309442, 1518134],
+ "expires_in_version": "never",
+ "keyed": true,
+ "keys": ["dl", "wrdl", "sb", "fb"],
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 12,
+ "description": "Percentages of times for phases in an expensive content paint relative to the time spent in the entire expensive paint. (\"dl\" = Display list, \"wrdl\" = WebRender display list, \"sb\" = Scene building, \"fb\" = Frame building)"
+ },
+ "GPU_WAIT_TIME_MS": {
+ "record_in_processes": ["main", "gpu"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "gfx-telemetry-alerts@mozilla.com",
+ "jmuizelaar@mozilla.com"
+ ],
+ "bug_numbers": [1524090, 1656130],
+ "expires_in_version": "86",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 50,
+ "description": "The number of milliseconds we spend waiting for the GPU"
+ },
+ "NARRATE_CONTENT_BY_LANGUAGE_2": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["eisaacson@mozilla.com"],
+ "bug_numbers": [1308030, 1324868, 1532821],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "72",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 4,
+ "description": "Number of Narrate initialization attempts and successes broken up by content's language (ISO 639-1 code) (0 = initialization attempt, 1 = successfully initialized)"
+ },
+ "NARRATE_CONTENT_SPEAKTIME_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["eisaacson@mozilla.com"],
+ "bug_numbers": [1308030, 1532821],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "72",
+ "kind": "linear",
+ "high": 300000,
+ "n_buckets": 30,
+ "description": "Time in MS that content is narrated in 10 second increments up to 5 minutes"
+ },
+ "TABCHILD_PAINT_TIME": {
+ "record_in_processes": ["content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "mconley@mozilla.com"],
+ "bug_numbers": [1313686],
+ "expires_in_version": "65",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "Time spent painting the contents of a remote browser (ms).",
+ "releaseChannelCollection": "opt-out"
+ },
+ "TIME_TO_NON_BLANK_PAINT_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 100,
+ "bug_numbers": [1307242, 1489524, 1580077],
+ "description": "The time between navigation start and the first non-blank paint of a foreground root content document, in milliseconds. This only records documents that were in an active docshell throughout the whole time between navigation start and non-blank paint. The non-blank paint timestamp is taken during display list building and does not include rasterization or compositing of that paint."
+ },
+ "TIME_TO_NON_BLANK_PAINT_NETOPT_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com", "hbambas@mozilla.com"],
+ "expires_in_version": "63",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 100,
+ "bug_numbers": [1411632],
+ "description": "The time between navigation start and the first non-blank paint of a foreground root content document, in milliseconds. This only records documents that were in an active docshell throughout the whole time between navigation start and non-blank paint. The non-blank paint timestamp is taken during display list building and does not include rasterization or compositing of that paint. This probe only accumulates when there were active-tab network load optimizations happening during the interval."
+ },
+ "TIME_TO_NON_BLANK_PAINT_NO_NETOPT_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com", "hbambas@mozilla.com"],
+ "expires_in_version": "63",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 100,
+ "bug_numbers": [1411632],
+ "description": "The time between navigation start and the first non-blank paint of a foreground root content document, in milliseconds. This only records documents that were in an active docshell throughout the whole time between navigation start and non-blank paint. The non-blank paint timestamp is taken during display list building and does not include rasterization or compositing of that paint. This probe only accumulates when there were no active-tab network load optimizations happening during the interval."
+ },
+ "TIME_TO_FIRST_CONTENTFUL_PAINT_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 100,
+ "bug_numbers": [1515132],
+ "description": "The time between navigation start and the first contentful paint of a foreground root content document, in milliseconds. This only records documents that were in an active docshell throughout the whole time between navigation start and contentful paint. The contentful paint timestamp is taken during display list building and does not include rasterization or compositing of that paint."
+ },
+ "TAB_AUDIO_INDICATOR_USED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "100",
+ "kind": "enumerated",
+ "n_values": 6,
+ "bug_numbers": [1314220, 1525374, 1714303],
+ "description": "The total usage amount of the operations of tab audio indicator, mute=0 , unmuted=1, unblockByVisitingTab=2, unblockingByClickingIcon=3",
+ "releaseChannelCollection": "opt-out"
+ },
+ "INPUT_EVENT_QUEUED_CLICK_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["bschouten@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1341531],
+ "description": "Time (ms) for the mouse up/down event to dispatch, but before handlers executing."
+ },
+ "INPUT_EVENT_QUEUED_KEYBOARD_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["bschouten@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1341531],
+ "description": "Time (ms) for the keyboard event to dispatch, but before handlers executing."
+ },
+ "INPUT_EVENT_QUEUED_APZ_TOUCH_MOVE_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["wpan@mozilla.com"],
+ "expires_in_version": "60",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1341531],
+ "description": "Time (ms) for the APZ handled touch move event to dispatch, but before handlers executing."
+ },
+ "INPUT_EVENT_QUEUED_APZ_MOUSE_MOVE_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["wpan@mozilla.com"],
+ "expires_in_version": "60",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1341531],
+ "description": "Time (ms) for the APZ handled mouse move event to dispatch, but before handlers executing."
+ },
+ "INPUT_EVENT_QUEUED_APZ_WHEEL_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["wpan@mozilla.com"],
+ "expires_in_version": "60",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1341531],
+ "description": "Time (ms) for the APZ handled wheel event to dispatch, but before handlers executing."
+ },
+ "IPC_SYNC_MESSAGE_MANAGER_LATENCY_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["nika@thelayzells.com"],
+ "bug_numbers": [1348113],
+ "expires_in_version": "70",
+ "kind": "exponential",
+ "low": 32,
+ "high": 750,
+ "n_buckets": 40,
+ "keyed": true,
+ "description": "Measures the number of milliseconds we spend waiting for sync message manager IPC messages to finish sending, keyed by message name. Note: only messages that wait for more than 500 microseconds are included in this probe."
+ },
+ "TIME_TO_DOM_LOADING_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "hbambas@mozilla.com",
+ "vgosu@mozilla.com",
+ "necko@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "bug_numbers": [1344893, 1489524],
+ "description": "Time in milliseconds from navigationStart to domLoading."
+ },
+ "TIME_TO_DOM_INTERACTIVE_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "hbambas@mozilla.com",
+ "vgosu@mozilla.com",
+ "necko@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "bug_numbers": [1344893, 1489524, 1580077],
+ "description": "Time in milliseconds from navigationStart to domInteractive."
+ },
+ "TIME_TO_DOM_CONTENT_LOADED_START_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["bdekoz@mozilla.com", "perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "bug_numbers": [1344893, 1580077],
+ "description": "Time in milliseconds from navigationStart to domContentLoadedEventStart."
+ },
+ "TIME_TO_DOM_CONTENT_LOADED_START_ACTIVE_NETOPT_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com", "hbambas@mozilla.com"],
+ "expires_in_version": "63",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "bug_numbers": [1411632],
+ "description": "Time in milliseconds from navigationStart to domContentLoadedEventStart for all-the-time active page for which some of the network loading optimization happened."
+ },
+ "TIME_TO_DOM_CONTENT_LOADED_START_ACTIVE_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com", "hbambas@mozilla.com"],
+ "expires_in_version": "63",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "bug_numbers": [1411632],
+ "description": "Time in milliseconds from navigationStart to domContentLoadedEventStart for all-the-time active page for which none of the network loading optimization happened."
+ },
+ "TIME_TO_DOM_CONTENT_LOADED_END_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "bug_numbers": [1344893, 1489524, 1580077],
+ "description": "Time in milliseconds from navigationStart to domContentLoadedEventEnd."
+ },
+ "TIME_TO_DOM_COMPLETE_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "hbambas@mozilla.com",
+ "vgosu@mozilla.com",
+ "necko@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "bug_numbers": [1344893, 1489524, 1580077],
+ "description": "Time in milliseconds from navigationStart to domComplete."
+ },
+ "TIME_TO_LOAD_EVENT_START_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["bdekoz@mozilla.com", "perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "bug_numbers": [1344893, 1580077],
+ "description": "Time in milliseconds from navigationStart to loadEventStart."
+ },
+ "TIME_TO_LOAD_EVENT_START_ACTIVE_NETOPT_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com", "hbambas@mozilla.com"],
+ "expires_in_version": "63",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "bug_numbers": [1411632],
+ "description": "Time in milliseconds from navigationStart to loadEventStart for all-the-time active page for which some of the network loading optimization happened."
+ },
+ "TIME_TO_LOAD_EVENT_START_ACTIVE_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["necko@mozilla.com", "hbambas@mozilla.com"],
+ "expires_in_version": "63",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "bug_numbers": [1411632],
+ "description": "Time in milliseconds from navigationStart to loadEventStart for all-the-time active page for which none of the network loading optimization happened."
+ },
+ "TIME_TO_LOAD_EVENT_END_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "bug_numbers": [1344893, 1489524, 1580077],
+ "description": "Time in milliseconds from navigationStart to loadEventEnd."
+ },
+ "TIME_TO_RESPONSE_START_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "bug_numbers": [1344893, 1489524, 1580077],
+ "description": "Time in milliseconds from navigationStart to responseStart."
+ },
+ "PERF_PAGE_LOAD_TIME_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dpalmeiro@mozilla.com",
+ "perf-telemetry-alerts@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "bug_numbers": [1671729],
+ "description": "Time in milliseconds from navigationStart to loadEventStart for the foreground http or https root content document."
+ },
+ "PERF_PAGE_LOAD_TIME_FROM_RESPONSESTART_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dpalmeiro@mozilla.com",
+ "perf-telemetry-alerts@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "bug_numbers": [1671729],
+ "description": "Time in milliseconds from responseStart to loadEventStart for the foreground http or https root content document."
+ },
+ "PERF_FIRST_CONTENTFUL_PAINT_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dpalmeiro@mozilla.com",
+ "perf-telemetry-alerts@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "bug_numbers": [1671729],
+ "description": "The time between navigationStart and the first contentful paint of a foreground http or https root content document, in milliseconds. The contentful paint timestamp is taken during display list building and does not include rasterization or compositing of that paint."
+ },
+ "PERF_FIRST_CONTENTFUL_PAINT_FROM_RESPONSESTART_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dpalmeiro@mozilla.com",
+ "perf-telemetry-alerts@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "bug_numbers": [1671729],
+ "description": "The time between responseStart and the first contentful paint of a foreground http or https root content document, in milliseconds. The contentful paint timestamp is taken during display list building and does not include rasterization or compositing of that paint."
+ },
+ "PERF_LARGEST_CONTENTFUL_PAINT_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "bug_numbers": [1862939],
+ "description": "The time between navigationStart and the largest contentful paint of a foreground http or https root content document, in milliseconds. The contentful paint timestamp is taken during display list building and does not include rasterization or compositing of that paint."
+ },
+ "PERF_LARGEST_CONTENTFUL_PAINT_FROM_RESPONSE_START_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["perf-telemetry-alerts@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "bug_numbers": [1862939],
+ "description": "The time between responseStart and the largest contentful paint of a foreground http or https root content document, in milliseconds. The contentful paint timestamp is taken during display list building and does not include rasterization or compositing of that paint."
+ },
+ "PERF_REQUEST_ANIMATION_CALLBACK_NON_PAGELOAD_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dpalmeiro@mozilla.com",
+ "perf-telemetry-alerts@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1671729],
+ "description": "Time spent in milliseconds calling all request animation frame callbacks for a document after it has reached readystate complete."
+ },
+ "PERF_REQUEST_ANIMATION_CALLBACK_PAGELOAD_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "dpalmeiro@mozilla.com",
+ "perf-telemetry-alerts@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1671729],
+ "description": "Time spent in milliseconds calling all request animation frame callbacks for a document before it has reached readystate complete."
+ },
+ "INPUT_EVENT_HANDLED_MOUSE_UP_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["wpan@mozilla.com"],
+ "expires_in_version": "60",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1345540],
+ "description": "Time (ms) for the mouse up event spent in handlers."
+ },
+ "INPUT_EVENT_HANDLED_MOUSE_DOWN_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["wpan@mozilla.com"],
+ "expires_in_version": "60",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1345540],
+ "description": "Time (ms) for the mouse down event spent in handlers."
+ },
+ "INPUT_EVENT_HANDLED_KEYBOARD_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["wpan@mozilla.com"],
+ "expires_in_version": "60",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1345540],
+ "description": "Time (ms) for the keyboard event spent in handlers."
+ },
+ "INPUT_EVENT_HANDLED_APZ_TOUCH_MOVE_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["wpan@mozilla.com"],
+ "expires_in_version": "60",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1345540],
+ "description": "Time (ms) for the APZ handled touch move event spent in handlers."
+ },
+ "INPUT_EVENT_HANDLED_APZ_MOUSE_MOVE_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["wpan@mozilla.com"],
+ "expires_in_version": "60",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1345540],
+ "description": "Time (ms) for the APZ handled mouse move event spent in handlers."
+ },
+ "INPUT_EVENT_HANDLED_APZ_WHEEL_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["wpan@mozilla.com"],
+ "expires_in_version": "60",
+ "kind": "exponential",
+ "high": 5000,
+ "n_buckets": 100,
+ "bug_numbers": [1345540],
+ "description": "Time (ms) for the APZ handled wheel event spent in handlers."
+ },
+ "IPC_SYNC_RECEIVE_MS": {
+ "record_in_processes": ["main", "content", "gpu"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["nika@thelayzells.com"],
+ "bug_numbers": [1365719],
+ "expires_in_version": "70",
+ "kind": "exponential",
+ "low": 32,
+ "high": 750,
+ "n_buckets": 40,
+ "keyed": true,
+ "description": "Measures the number of milliseconds we spend processing sync IPC messages in the receiving process, keyed by message name. Note: only messages that take over 500 microseconds are included in this probe."
+ },
+ "WEBEXT_BACKGROUND_PAGE_LOAD_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+ "bug_numbers": [1353172, 1489524],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "high": 60000,
+ "n_buckets": 100,
+ "description": "The amount of time it takes to load a WebExtensions background page, from when the build function is called to when the page has finished processing the onload event."
+ },
+ "WEBEXT_BACKGROUND_PAGE_LOAD_MS_BY_ADDONID": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+ "bug_numbers": [
+ 1483002, 1513556, 1578225, 1623315, 1666980, 1706839, 1745271, 1777402,
+ 1811155, 1861303
+ ],
+ "expires_in_version": "132",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "high": 60000,
+ "n_buckets": 100,
+ "description": "The amount of time it takes to load a WebExtensions background page, from when the build function is called to when the page has finished processing the onload event, keyed by addon id.",
+ "keyed": true
+ },
+ "WEBEXT_BROWSERACTION_POPUP_OPEN_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+ "bug_numbers": [
+ 1297167, 1513556, 1578225, 1623315, 1666980, 1706839, 1745271, 1777402,
+ 1811155, 1861303
+ ],
+ "expires_in_version": "132",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "high": 50000,
+ "n_buckets": 100,
+ "description": "The amount of time it takes for a BrowserAction popup to open."
+ },
+ "WEBEXT_BROWSERACTION_POPUP_OPEN_MS_BY_ADDONID": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+ "bug_numbers": [
+ 1483002, 1513556, 1578225, 1623315, 1666980, 1706839, 1745271, 1777402,
+ 1811155, 1861303
+ ],
+ "expires_in_version": "132",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "high": 50000,
+ "n_buckets": 100,
+ "description": "The amount of time it takes for a BrowserAction popup to open, keyed by addon id.",
+ "keyed": true
+ },
+ "WEBEXT_BROWSERACTION_POPUP_PRELOAD_RESULT_COUNT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+ "bug_numbers": [
+ 1297167, 1513556, 1578225, 1623315, 1666980, 1706839, 1745271, 1777402,
+ 1811155, 1861303
+ ],
+ "expires_in_version": "132",
+ "kind": "categorical",
+ "labels": ["popupShown", "clearAfterHover", "clearAfterMousedown"],
+ "releaseChannelCollection": "opt-out",
+ "description": "The number of times a browserAction popup is preloaded and results in one of the categories."
+ },
+ "WEBEXT_BROWSERACTION_POPUP_PRELOAD_RESULT_COUNT_BY_ADDONID": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+ "bug_numbers": [
+ 1483002, 1513556, 1578225, 1623315, 1666980, 1706839, 1745271, 1777402,
+ 1811155, 1861303
+ ],
+ "expires_in_version": "132",
+ "kind": "categorical",
+ "labels": ["popupShown", "clearAfterHover", "clearAfterMousedown"],
+ "releaseChannelCollection": "opt-out",
+ "description": "The number of times a browserAction popup is preloaded and results in one of the categories, keyed by addon id.",
+ "keyed": true
+ },
+ "WEBEXT_DNR_EVALUATE_RULES_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["addons-dev-internal@mozilla.com"],
+ "bug_numbers": [1803363, 1850890],
+ "expires_in_version": "126",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "high": 100000,
+ "n_buckets": 100,
+ "description": "Amount of time it takes to evaluate DNR rules."
+ },
+ "WEBEXT_DNR_STARTUPCACHE_READ_BYTES": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["addons-dev-internal@mozilla.com"],
+ "bug_numbers": [1803363, 1850890],
+ "expires_in_version": "126",
+ "kind": "exponential",
+ "high": 50000000,
+ "n_buckets": 50,
+ "description": "Amount of data read from the DNR startup cache file."
+ },
+ "WEBEXT_DNR_STARTUPCACHE_READ_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["addons-dev-internal@mozilla.com"],
+ "bug_numbers": [1803363, 1850890],
+ "expires_in_version": "126",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "high": 100000,
+ "n_buckets": 100,
+ "description": "Amount of time it takes to read data from the DNR startup cache file."
+ },
+ "WEBEXT_DNR_STARTUPCACHE_WRITE_BYTES": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["addons-dev-internal@mozilla.com"],
+ "bug_numbers": [1803363, 1850890],
+ "expires_in_version": "126",
+ "kind": "exponential",
+ "high": 50000000,
+ "n_buckets": 50,
+ "description": "Amount of data written to the DNR startup cache file."
+ },
+ "WEBEXT_DNR_STARTUPCACHE_WRITE_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["addons-dev-internal@mozilla.com"],
+ "bug_numbers": [1803363, 1850890],
+ "expires_in_version": "126",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "high": 100000,
+ "n_buckets": 100,
+ "description": "Amount of time it takes to write data to the DNR startup cache file."
+ },
+ "WEBEXT_DNR_VALIDATE_RULES_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["addons-dev-internal@mozilla.com"],
+ "bug_numbers": [1803363, 1850890],
+ "expires_in_version": "126",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "high": 100000,
+ "n_buckets": 100,
+ "description": "Amount of time it takes to validate DNR rules."
+ },
+ "WEBEXT_EVENTPAGE_RUNNING_TIME_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+ "bug_numbers": [1787940],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "high": 60000,
+ "n_buckets": 100,
+ "description": "The amount of time (keyed by addon id) that an event page has been running before being suspended, or the entire addon shutdown."
+ },
+ "WEBEXT_EVENTPAGE_RUNNING_TIME_MS_BY_ADDONID": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+ "bug_numbers": [1787940, 1817103, 1861303],
+ "expires_in_version": "132",
+ "kind": "exponential",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "high": 60000,
+ "n_buckets": 100,
+ "description": "The amount of time (keyed by addon id) that an event page has been running before being suspended, or the entire addon shutdown."
+ },
+ "WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+ "bug_numbers": [1787940, 1817103, 1861303],
+ "expires_in_version": "132",
+ "kind": "categorical",
+ "labels": [
+ "suspend",
+ "reset_other",
+ "reset_event",
+ "reset_listeners",
+ "reset_nativeapp",
+ "reset_streamfilter",
+ "reset_parentapicall"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "The number of times an event page hit the idle timeout and results in one of the categories, keyed by addon id."
+ },
+ "WEBEXT_EVENTPAGE_IDLE_RESULT_COUNT_BY_ADDONID": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+ "bug_numbers": [1787940, 1817103, 1861303],
+ "expires_in_version": "132",
+ "kind": "categorical",
+ "labels": [
+ "suspend",
+ "reset_other",
+ "reset_event",
+ "reset_listeners",
+ "reset_nativeapp",
+ "reset_streamfilter",
+ "reset_parentapicall"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "The number of times an event page hit the idle timeout and results in one of the categories, keyed by addon id.",
+ "keyed": true
+ },
+ "WEBEXT_CONTENT_SCRIPT_INJECTION_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+ "bug_numbers": [1356323, 1489524],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "high": 50000,
+ "n_buckets": 100,
+ "description": "The amount of time it takes for content scripts from a WebExtension to be injected into a window."
+ },
+ "WEBEXT_CONTENT_SCRIPT_INJECTION_MS_BY_ADDONID": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+ "bug_numbers": [
+ 1483002, 1513556, 1578225, 1623315, 1666980, 1706839, 1745271, 1777402,
+ 1811155, 1861303
+ ],
+ "expires_in_version": "132",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "high": 50000,
+ "n_buckets": 100,
+ "description": "The amount of time it takes for content scripts from a WebExtension to be injected into a window, keyed by addon id.",
+ "keyed": true
+ },
+ "WEBEXT_EXTENSION_STARTUP_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+ "bug_numbers": [1353171, 1489524],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "high": 50000,
+ "n_buckets": 100,
+ "description": "The amount of time it takes for a WebExtension to start up, from when the startup function is called to when the startup promise resolves."
+ },
+ "WEBEXT_EXTENSION_STARTUP_MS_BY_ADDONID": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+ "bug_numbers": [
+ 1483002, 1513556, 1578225, 1623315, 1666980, 1706839, 1745271, 1777402,
+ 1811155, 1861303
+ ],
+ "expires_in_version": "132",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "high": 50000,
+ "n_buckets": 100,
+ "description": "The amount of time it takes for a WebExtension to start up, from when the startup function is called to when the startup promise resolves, keyed by addon id.",
+ "keyed": true
+ },
+ "WEBEXT_PAGEACTION_POPUP_OPEN_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+ "bug_numbers": [
+ 1297167, 1513556, 1578225, 1623315, 1666980, 1706839, 1745271, 1777402,
+ 1811155, 1861303
+ ],
+ "expires_in_version": "132",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "high": 50000,
+ "n_buckets": 100,
+ "description": "The amount of time it takes for a PageAction popup to open."
+ },
+ "WEBEXT_PAGEACTION_POPUP_OPEN_MS_BY_ADDONID": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+ "bug_numbers": [
+ 1483002, 1513556, 1578225, 1623315, 1666980, 1706839, 1745271, 1777402,
+ 1811155, 1861303
+ ],
+ "expires_in_version": "132",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "high": 50000,
+ "n_buckets": 100,
+ "description": "The amount of time it takes for a PageAction popup to open, keyed by addon id.",
+ "keyed": true
+ },
+ "WEBEXT_STORAGE_LOCAL_GET_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+ "bug_numbers": [
+ 1371398, 1513556, 1578225, 1623315, 1666980, 1706839, 1745271, 1777402,
+ 1811155, 1861303
+ ],
+ "expires_in_version": "132",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "high": 50000,
+ "n_buckets": 100,
+ "description": "The amount of time it takes to perform a get via storage.local using the JSONFile backend."
+ },
+ "WEBEXT_STORAGE_LOCAL_GET_MS_BY_ADDONID": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+ "bug_numbers": [
+ 1483002, 1513556, 1578225, 1623315, 1666980, 1706839, 1745271, 1777402,
+ 1811155, 1861303
+ ],
+ "expires_in_version": "132",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "high": 50000,
+ "n_buckets": 100,
+ "description": "The amount of time it takes to perform a get via storage.local using the JSONFile backend, keyed by addon id.",
+ "keyed": true
+ },
+ "WEBEXT_STORAGE_LOCAL_SET_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+ "bug_numbers": [
+ 1371398, 1513556, 1578225, 1623315, 1666980, 1706839, 1745271, 1777402,
+ 1811155, 1861303
+ ],
+ "expires_in_version": "132",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "high": 50000,
+ "n_buckets": 100,
+ "description": "The amount of time it takes to perform a set via storage.local using the JSONFile backend."
+ },
+ "WEBEXT_STORAGE_LOCAL_SET_MS_BY_ADDONID": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+ "bug_numbers": [
+ 1483002, 1513556, 1578225, 1623315, 1666980, 1706839, 1745271, 1777402,
+ 1811155, 1861303
+ ],
+ "expires_in_version": "132",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "high": 50000,
+ "n_buckets": 100,
+ "description": "The amount of time it takes to perform a set via storage.local using the JSONFile backend, keyed by addon id.",
+ "keyed": true
+ },
+ "WEBEXT_STORAGE_LOCAL_IDB_GET_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+ "bug_numbers": [
+ 1465120, 1513556, 1578225, 1623315, 1666980, 1706839, 1745271, 1777402,
+ 1811155, 1861303
+ ],
+ "expires_in_version": "132",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "high": 50000,
+ "n_buckets": 100,
+ "description": "The amount of time it takes to perform a get via storage.local using the IndexedDB backend."
+ },
+ "WEBEXT_STORAGE_LOCAL_IDB_GET_MS_BY_ADDONID": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+ "bug_numbers": [
+ 1483002, 1513556, 1578225, 1623315, 1666980, 1706839, 1745271, 1777402,
+ 1811155, 1861303
+ ],
+ "expires_in_version": "132",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "high": 50000,
+ "n_buckets": 100,
+ "description": "The amount of time it takes to perform a get via storage.local using the IndexedDB backend, keyed by addon id.",
+ "keyed": true
+ },
+ "WEBEXT_STORAGE_LOCAL_IDB_SET_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+ "bug_numbers": [
+ 1465120, 1513556, 1578225, 1623315, 1666980, 1706839, 1745271, 1777402,
+ 1811155, 1861303
+ ],
+ "expires_in_version": "132",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "high": 50000,
+ "n_buckets": 100,
+ "description": "The amount of time it takes to perform a set via storage.local using the IndexedDB backend."
+ },
+ "WEBEXT_STORAGE_LOCAL_IDB_SET_MS_BY_ADDONID": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+ "bug_numbers": [
+ 1483002, 1513556, 1578225, 1623315, 1666980, 1706839, 1745271, 1777402,
+ 1811155, 1861303
+ ],
+ "expires_in_version": "132",
+ "kind": "exponential",
+ "releaseChannelCollection": "opt-out",
+ "high": 50000,
+ "n_buckets": 100,
+ "description": "The amount of time it takes to perform a set via storage.local using the IndexedDB backend, keyed by addon id.",
+ "keyed": true
+ },
+ "WEBEXT_STORAGE_LOCAL_IDB_MIGRATE_RESULT_COUNT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "bug_numbers": [
+ 1465129, 1513556, 1578225, 1623315, 1666980, 1706839, 1745271, 1777402,
+ 1811155, 1861303
+ ],
+ "alert_emails": ["addons-dev-internal@mozilla.com", "lgreco@mozilla.com"],
+ "expires_in_version": "132",
+ "kind": "categorical",
+ "labels": ["success", "failure"],
+ "releaseChannelCollection": "opt-out",
+ "description": "The number of times a storage.local backend data migration has been completed and results in one of the categories."
+ },
+ "PLACES_SEARCHBAR_FILTER_TYPE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "alert_emails": ["pguruprasad@mozilla.com", "rfambro@mozilla.com"],
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1801290],
+ "kind": "categorical",
+ "labels": ["visited", "lastvisited", "dayandsite", "site", "day"],
+ "description": "The type of search filters used for the sidebar search."
+ },
+ "PLACES_SEARCHBAR_CUMULATIVE_SEARCHES": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "alert_emails": ["pguruprasad@mozilla.com", "rfambro@mozilla.com"],
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1801290],
+ "kind": "enumerated",
+ "n_values": 20,
+ "description": "Cumulative no. of searches performed before selecting a link."
+ },
+ "PLACES_SEARCHBAR_CUMULATIVE_FILTER_COUNT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "alert_emails": ["pguruprasad@mozilla.com", "rfambro@mozilla.com"],
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1801290],
+ "kind": "enumerated",
+ "n_values": 20,
+ "description": "Cumulative no. of search filters applied performed before selecting a link."
+ },
+ "PLACES_BOOKMARKS_SEARCHBAR_CUMULATIVE_SEARCHES": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "alert_emails": ["firefox-view-engineers@mozilla.com"],
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1819081],
+ "kind": "enumerated",
+ "n_values": 20,
+ "description": "Cumulative no. bookmark of searches performed before selecting a link."
+ },
+ "HOVER_UNTIL_UNSELECTED_TAB_OPENED": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["alwu@mozilla.com"],
+ "expires_in_version": "60",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "bug_numbers": [1274919],
+ "description": "Measure the time how long the cursor is hovering before opening the unselcted tab. Only record the data if someone requests for sending unselected tab hover msg.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "WEBVR_USERS_VIEW_IN": {
+ "record_in_processes": ["main", "gpu", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dmu@mozilla.com"],
+ "bug_numbers": [1306156],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 3,
+ "releaseChannelCollection": "opt-out",
+ "description": "The amount of sessions who view VR content in: 2D=0, Oculus=1, OpenVR=2."
+ },
+ "WEBVR_TIME_SPENT_VIEWING_IN_2D": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dmu@mozilla.com"],
+ "bug_numbers": [1306156],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1200000,
+ "n_buckets": 100,
+ "releaseChannelCollection": "opt-out",
+ "description": "The amount of time spent(ms) of a session for viewing VR content in 2D."
+ },
+ "WEBVR_TIME_SPENT_VIEWING_IN_OCULUS": {
+ "record_in_processes": ["main", "gpu"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dmu@mozilla.com"],
+ "bug_numbers": [1306156],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1200000,
+ "n_buckets": 100,
+ "releaseChannelCollection": "opt-out",
+ "description": "The amount of time spent(ms) of a session for viewing content in Oculus."
+ },
+ "WEBVR_TIME_SPENT_VIEWING_IN_OPENVR": {
+ "record_in_processes": ["main", "gpu"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dmu@mozilla.com"],
+ "bug_numbers": [1306156],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1200000,
+ "n_buckets": 100,
+ "releaseChannelCollection": "opt-out",
+ "description": "The amount of time spent(ms) of a session for viewing content in OpenVR."
+ },
+ "WEBVR_DROPPED_FRAMES_IN_OCULUS": {
+ "record_in_processes": ["main", "gpu"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dmu@mozilla.com"],
+ "bug_numbers": [1392476],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 200,
+ "n_buckets": 100,
+ "releaseChannelCollection": "opt-out",
+ "description": "The average number of dropped frames per sec in Oculus, sent when user exits WebVR content."
+ },
+ "WEBVR_DROPPED_FRAMES_IN_OPENVR": {
+ "record_in_processes": ["main", "gpu"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["dmu@mozilla.com"],
+ "bug_numbers": [1392476],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "high": 200,
+ "n_buckets": 100,
+ "releaseChannelCollection": "opt-out",
+ "description": "The average number of dropped frames per sec in OpenVR, sent when user exits WebVR content."
+ },
+ "WEBXR_API_MODE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["fxr-telemetry@mozilla.com", "dmu@mozilla.com"],
+ "bug_numbers": [1635872],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "releaseChannelCollection": "opt-out",
+ "labels": ["WebXR", "WebVR"],
+ "description": "Determine what API users use to enter the web immersive mode."
+ },
+ "URLCLASSIFIER_UI_EVENTS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["seceng-telemetry@mozilla.com", "dlee@mozilla.com"],
+ "bug_numbers": [1375277, 1531034, 1636962],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 64,
+ "description": "URL CLassifier-related (aka Safe Browsing) UI events. See nsIUrlClassifierUITelemetry.idl for the specific values."
+ },
+ "GFX_OMTP_PAINT_TIME": {
+ "record_in_processes": ["content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "rhunt@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "description": "Amount of time in tenths of a millisecond from the beginning of the first async paint until all async paints are finished.",
+ "bug_numbers": [1483245, 1518669]
+ },
+ "GFX_OMTP_PAINT_WAIT_TIME": {
+ "record_in_processes": ["content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "rhunt@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 200,
+ "n_buckets": 50,
+ "description": "Amount of time in milliseconds the main thread spends waiting for the paint thread to complete, if the time was greater than 200us.",
+ "bug_numbers": [1386968, 1518669]
+ },
+ "GFX_OMTP_PAINT_TASK_COUNT": {
+ "record_in_processes": ["content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "rhunt@mozilla.com"],
+ "expires_in_version": "70",
+ "kind": "exponential",
+ "high": 100,
+ "n_buckets": 25,
+ "description": "The amount of async paint tasks queued to the paint thread during a layer transaction.",
+ "bug_numbers": [1483245]
+ },
+ "GFX_MACOS_VIDEO_LOW_POWER": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["bwerth@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1737682],
+ "description": "MacOS video low power state achieved when enqueueing a video frame.",
+ "kind": "categorical",
+ "labels": [
+ "NotVideo",
+ "LowPower",
+ "FailMultipleVideo",
+ "FailWindowed",
+ "FailOverlaid",
+ "FailBacking",
+ "FailMacOSVersion",
+ "FailPref",
+ "FailSurface",
+ "FailEnqueue"
+ ]
+ },
+ "PERMISSION_REQUEST_ORIGIN_SCHEME": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["jhofmann@mozilla.com"],
+ "bug_numbers": [1345077, 1494589],
+ "expires_in_version": "70",
+ "kind": "enumerated",
+ "n_values": 10,
+ "keyed": true,
+ "description": "Permission requests (showing a permission prompt) by origin scheme (0=other,1=http,2=https)."
+ },
+ "PERMISSION_REQUEST_HANDLING_USER_INPUT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["jhofmann@mozilla.com"],
+ "bug_numbers": [1345077, 1494589],
+ "expires_in_version": "70",
+ "kind": "boolean",
+ "keyed": true,
+ "description": "Permission requests (showing a permission prompt) by whether they were requested from code handling a user input event."
+ },
+ "QUIRKS_MODE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["emilio@mozilla.com"],
+ "bug_numbers": [1505117],
+ "expires_in_version": "70",
+ "kind": "categorical",
+ "releaseChannelCollection": "opt-out",
+ "labels": ["FullStandards", "AlmostStandards", "NavQuirks"],
+ "description": "HTML document compat mode (quirks mode)"
+ },
+ "BFCACHE_COMBO": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["sefeng@mozilla.com"],
+ "bug_numbers": [1531917],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "releaseChannelCollection": "opt-out",
+ "labels": [
+ "BFCache_Success",
+ "Success_Not_Toplevel",
+ "Unload",
+ "Unload_Req",
+ "Req",
+ "Unload_Req_Peer",
+ "Unload_Req_Peer_MSE",
+ "Unload_Req_MSE",
+ "SPD_Unload_Req_Peer",
+ "Remote_Subframes",
+ "Beforeunload",
+ "Other"
+ ],
+ "description": "The common combinations of BFCacheStatus when we determine whether the page can be BFCached or not; If it's BFCached, we record BFCache_Success; Success_Not_Toplevel represents the number of not the only top level document in BrowsingContextGroup's documents that are BFCached, and BFCache_Success includes Success_Not_Toplevel; If it's not and it falls under common failure reasons combinations, we record the corresponding combination; Otherwise, we record Other to indicate this is not a common failure"
+ },
+ "BFCACHE_PAGE_RESTORED": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["sefeng@mozilla.com"],
+ "bug_numbers": [1531917, 1551935],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "releaseChannelCollection": "opt-out",
+ "description": "Whether bfcache is used when loading a page from session history"
+ },
+ "HIDDEN_VIEWPORT_OVERFLOW_TYPE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["mozilla-telemetry@upsuper.org", "botond@mozilla.com"],
+ "bug_numbers": [1423013, 1423017, 1513089],
+ "expires_in_version": "70",
+ "kind": "categorical",
+ "releaseChannelCollection": "opt-out",
+ "labels": ["NoOverflow", "Desktop", "ButNotMinScaleSize", "MinScaleSize"],
+ "description": "How common are different types of out-of-reach viewport overflow?"
+ },
+ "WR_GPU_WAIT_TIME": {
+ "record_in_processes": ["main", "gpu"],
+ "products": ["firefox"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1772216],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "The time spent waiting for the GPU to complete previously issued drawing commands in milliseconds"
+ },
+ "WR_RENDERER_TIME": {
+ "record_in_processes": ["main", "gpu"],
+ "products": ["firefox"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1772223],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "releaseChannelCollection": "opt-out",
+ "n_buckets": 50,
+ "description": "The time spent by WebRender in the Renderer in milliseconds."
+ },
+ "WR_RENDERER_TIME_NO_SC_MS": {
+ "record_in_processes": ["main", "gpu"],
+ "products": ["firefox"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1784286],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "releaseChannelCollection": "opt-out",
+ "n_buckets": 50,
+ "description": "The time spent by WebRender in the Renderer in milliseconds for frames with no shader compilation."
+ },
+ "WR_SCENEBUILD_TIME": {
+ "record_in_processes": ["main", "gpu"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "gfx-telemetry-alerts@mozilla.com",
+ "jmuizelaar@mozilla.com"
+ ],
+ "bug_numbers": [1470901, 1584109],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "WebRender scene build time in milliseconds"
+ },
+ "WR_SCENESWAP_TIME": {
+ "record_in_processes": ["main", "gpu"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "gfx-telemetry-alerts@mozilla.com",
+ "jmuizelaar@mozilla.com"
+ ],
+ "bug_numbers": [1470901, 1584109],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "WebRender scene swap time in miliseconds"
+ },
+ "WR_FRAMEBUILD_TIME": {
+ "record_in_processes": ["main", "gpu"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "gfx-telemetry-alerts@mozilla.com",
+ "jmuizelaar@mozilla.com"
+ ],
+ "bug_numbers": [1470901, 1584109],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "WebRender render time in milliseconds"
+ },
+ "WR_RASTERIZE_GLYPHS_TIME": {
+ "record_in_processes": ["main", "gpu"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "gfx-telemetry-alerts@mozilla.com",
+ "jmuizelaar@mozilla.com"
+ ],
+ "bug_numbers": [],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 500,
+ "n_buckets": 50,
+ "description": "Time taken by WebRender to rasterize a set of glyphs"
+ },
+ "WR_RASTERIZE_BLOBS_TIME": {
+ "record_in_processes": ["main", "gpu"],
+ "products": ["firefox"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1770974],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 500,
+ "n_buckets": 50,
+ "description": "Time taken by WebRender to rasterize blobs"
+ },
+ "WR_TEXTURE_CACHE_UPDATE_TIME": {
+ "record_in_processes": ["main", "gpu"],
+ "products": ["firefox"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1772223],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "Time taken by WebRender to update the texture cache in milliseconds."
+ },
+ "WR_TIME_TO_FRAME_BUILD_MS": {
+ "record_in_processes": ["main", "gpu"],
+ "products": ["firefox"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1778395],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "Time elapsed (in milliseconds) between the construction of a transaction and the associated frame build beginning."
+ },
+ "WR_TIME_TO_RENDER_START_MS": {
+ "record_in_processes": ["main", "gpu"],
+ "products": ["firefox"],
+ "alert_emails": ["gfx-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1778395],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "Time elapsed (in milliseconds) between the construction of a frame and the start of rendering."
+ },
+ "WEB_AUDIO_BECOMES_AUDIBLE_TIME": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "100",
+ "kind": "linear",
+ "high": 60,
+ "n_buckets": 10,
+ "bug_numbers": [1490074, 1570634, 1606206, 1652196, 1700828, 1714303],
+ "description": "How long the AudioContext would become audible since it was created, time unit is seconds.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "AUTOPLAY_DEFAULT_SETTING_CHANGE": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "100",
+ "kind": "categorical",
+ "labels": ["allow", "blockAudible", "blockAll"],
+ "bug_numbers": [1567302, 1606206, 1689267],
+ "description": "counts the number of times a user changed the default autoplay behavior to each setting during a subsession.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "WEB_AUDIO_AUTOPLAY": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "expires_in_version": "96",
+ "kind": "categorical",
+ "labels": ["NeverBlocked", "AllowedAfterBlocked", "NeverAllowed"],
+ "bug_numbers": [1512277, 1652196, 1700828],
+ "description": "The number of times an AudioContext is allowed to start after being blocked, or the number of AudioContexts attempting to start which were never blocked or never allowed.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "TABS_AUDIO_COMPETITION": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["media-alerts@mozilla.com"],
+ "bug_numbers": [1683788],
+ "expires_in_version": "91",
+ "kind": "categorical",
+ "labels": ["None", "Ignored", "ManagedFocusByUser", "ManagedFocusByGecko"],
+ "description": "This probe would be reported whenever (1) media starts without encountering audio competition (2) encountering audio competition but users ignore it (keep all media continually playing) (3) encountering audio competition and users manually handle it (4) encountering audio competition and Gecko handled it. The value of each state would be the total happening count for specific situation during the whole session.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "QM_QUOTA_INFO_LOAD_TIME_V0": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "bug_numbers": [1609625, 1683102],
+ "kind": "exponential",
+ "keyed": true,
+ "high": 180000,
+ "n_buckets": 60,
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["ttung@mozilla.com", "storage-telemetry@mozilla.com"],
+ "description": "Time (ms) for the QuotaManager to load quota information. Keyed by conditions during quota info loading, see RecordTimeDeltaHelper::Run in https://searchfox.org/mozilla-central/source/dom/quota/ActorsParent.cpp"
+ },
+ "QM_FIRST_INITIALIZATION_ATTEMPT": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "bug_numbers": [1592934],
+ "kind": "boolean",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["ttung@mozilla.com", "storage-telemetry@mozilla.com"],
+ "description": "True if the first initialization attempt succeeded, keyed by the initialization type."
+ },
+ "QM_SHUTDOWN_TIME_V0": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "bug_numbers": [1824075],
+ "kind": "exponential",
+ "keyed": true,
+ "high": 60000,
+ "n_buckets": 60,
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["jvarga@mozilla.com", "storage-telemetry@mozilla.com"],
+ "description": "Time (ms) for the QuotaManager to shutdown. Keyed by conditions during shutdown, see RecordTimeDeltaHelper::Run in https://searchfox.org/mozilla-central/source/dom/quota/ActorsParent.cpp"
+ },
+ "GV_PAGE_LOAD_PROGRESS_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["geckoview-team@mozilla.com", "esawin@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 100,
+ "bug_numbers": [1499418, 1580077],
+ "description": "GeckoView: The time between page load progress start (0) and completion (100) in ms."
+ },
+ "GV_PAGE_LOAD_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": ["geckoview-team@mozilla.com", "esawin@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 100,
+ "bug_numbers": [1499418, 1584109],
+ "description": "GeckoView: Time taken to load a page in ms. This includes all static contents, no dynamic content. Loading of about: pages is not counted. Back back navigation (sometimes via BFCache) is included which is a source of bimodality due to the <50ms load times."
+ },
+ "GV_PAGE_RELOAD_MS": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "geckoview-team@mozilla.com",
+ "sefeng@mozilla.com",
+ "perf-telemetry-alerts@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 100000,
+ "n_buckets": 100,
+ "bug_numbers": [1549519, 1580077],
+ "description": "GeckoView: Time taken to reload a page in ms. This includes all static contents, no dynamic content. Loading of about: pages is not counted."
+ },
+ "HTTP_TRAFFIC_ANALYSIS_3": {
+ "record_in_processes": ["main"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": [
+ "arthur@mozilla.com",
+ "tanvi@mozilla.com",
+ "tihuang@mozilla.com"
+ ],
+ "bug_numbers": [
+ 1533363, 1546796, 1546906, 1550756, 1572666, 1590124, 1621800, 1668604,
+ 1706842
+ ],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "n_values": 50,
+ "labels": [
+ "Y0_N1Sys",
+ "Y1_N1",
+ "Y2_N3Oth",
+ "Y3_N3BasicLead",
+ "Y4_N3BasicBg",
+ "Y5_N3BasicOth",
+ "Y6_N3ContentLead",
+ "Y7_N3ContentBg",
+ "Y8_N3ContentOth",
+ "Y9_N3FpLead",
+ "Y10_N3FpBg",
+ "Y11_N3FpOth",
+ "Y12_P1Sys",
+ "Y13_P1",
+ "Y14_P3Oth",
+ "Y15_P3BasicLead",
+ "Y16_P3BasicBg",
+ "Y17_P3BasicOth",
+ "Y18_P3ContentLead",
+ "Y19_P3ContentBg",
+ "Y20_P3ContentOth",
+ "Y21_P3FpLead",
+ "Y22_P3FpBg",
+ "Y23_P3FpOth"
+ ],
+ "keyed": true,
+ "keys": ["Connection", "Transaction"],
+ "releaseChannelCollection": "opt-out",
+ "description": "Numbers of HTTP transactions and connections by type. There are some categories separated by labels based on 5 parameters: 1. normal browsing or private browsing 2. system principal or not 3. first party or third party 4. class of service (Leader/Background/Others) 5. tracking classification (Basic/Content/FingerprintingContent)."
+ },
+ "THREAD_WAKEUP": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["opettay@mozilla.com"],
+ "bug_numbers": [1554292, 1566485, 1606208],
+ "expires_in_version": "85",
+ "kind": "exponential",
+ "high": 60000,
+ "n_buckets": 100,
+ "keyed": true,
+ "description": "How many milliseconds it took before 100 wakeups for the thread was done because of new Runnables."
+ },
+ "PRESSHELL_REQS_PER_STYLE_FLUSH": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["layout-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1571612, 1616147],
+ "expires_in_version": "82",
+ "kind": "exponential",
+ "high": 256,
+ "n_buckets": 32,
+ "description": "The number of style flush requests pending per style flush."
+ },
+ "PRESSHELL_REQS_PER_LAYOUT_FLUSH": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["layout-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1571612, 1616147],
+ "expires_in_version": "82",
+ "keyed": true,
+ "keys": ["Style", "Layout"],
+ "kind": "exponential",
+ "high": 256,
+ "n_buckets": 32,
+ "description": "The number of style flush requests and layout flush requests pending per layout flush. Both values are recorded because flushing a pending layout flush also flushes style."
+ },
+ "PRESSHELL_FLUSHES_PER_TICK": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["layout-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1571612, 1616147],
+ "expires_in_version": "82",
+ "keyed": true,
+ "keys": ["Style", "Layout"],
+ "kind": "exponential",
+ "high": 256,
+ "n_buckets": 32,
+ "description": "The number of style and layout flushes that occur per Refresh Driver tick."
+ },
+ "IDB_CUSTOM_OPEN_WITH_OPTIONS_COUNT": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "bug_numbers": [1566758],
+ "alert_emails": ["sgiesecke@mozilla.com"],
+ "expires_in_version": "74",
+ "kind": "categorical",
+ "labels": [
+ "system",
+ "content_file",
+ "content_http_https",
+ "content_moz_ext",
+ "content_about",
+ "content_other",
+ "expanded",
+ "other"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "Tracking the use of the custom IDBFactory.open overload accepting an options dictionary in one of the categories."
+ },
+ "PRESSHELL_LAYOUT_TOTAL_MS_PER_TICK": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "alert_emails": ["layout-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [1578319, 1616147],
+ "expires_in_version": "82",
+ "keyed": true,
+ "keys": [
+ "Restyle",
+ "ReflowOther",
+ "ReflowFlex",
+ "ReflowGrid",
+ "ReflowTable",
+ "ReflowText"
+ ],
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "Time in milliseconds spent in the layout system per Refresh Driver tick."
+ },
+ "COOKIE_PURGING_ORIGINS_PURGED": {
+ "expires_in_version": "never",
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "kind": "exponential",
+ "high": 500,
+ "n_buckets": 30,
+ "bug_numbers": [1630053, 1656134, 1689271],
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": [
+ "jhofmann@mozilla.com",
+ "senglehardt@mozilla.com",
+ "aedelstein@mozilla.com",
+ "seceng-telemetry@mozilla.com"
+ ],
+ "description": "The number of origins that were purged during one run of cookie purging."
+ },
+ "COOKIE_PURGING_TRACKERS_WITH_USER_INTERACTION": {
+ "expires_in_version": "never",
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "kind": "exponential",
+ "high": 500,
+ "n_buckets": 30,
+ "bug_numbers": [1630053, 1656134, 1689271],
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": [
+ "jhofmann@mozilla.com",
+ "senglehardt@mozilla.com",
+ "aedelstein@mozilla.com",
+ "seceng-telemetry@mozilla.com"
+ ],
+ "description": "The number of trackers that had user interaction at the time of purging."
+ },
+ "COOKIE_PURGING_TRACKERS_USER_INTERACTION_REMAINING_DAYS": {
+ "expires_in_version": "never",
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 60,
+ "bug_numbers": [1630053, 1656134, 1689271],
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": [
+ "jhofmann@mozilla.com",
+ "senglehardt@mozilla.com",
+ "aedelstein@mozilla.com",
+ "seceng-telemetry@mozilla.com"
+ ],
+ "description": "The time remaining until expiration per user interaction permissions of trackers at the time of purging, in days (one record per tracker, not averaged)."
+ },
+ "COOKIE_PURGING_DURATION_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "jhofmann@mozilla.com",
+ "senglehardt@mozilla.com",
+ "aedelstein@mozilla.com",
+ "seceng-telemetry@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "bug_numbers": [1630053, 1656134, 1689271],
+ "kind": "exponential",
+ "high": 600000,
+ "releaseChannelCollection": "opt-out",
+ "n_buckets": 30,
+ "description": "Time spent per daily cookie purging activity, in milliseconds. This measures the time for all batch iterations of that day."
+ },
+ "COOKIE_PURGING_INTERVAL_HOURS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": [
+ "jhofmann@mozilla.com",
+ "senglehardt@mozilla.com",
+ "aedelstein@mozilla.com",
+ "seceng-telemetry@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "bug_numbers": [1630053, 1656134, 1689271],
+ "kind": "linear",
+ "high": 168,
+ "releaseChannelCollection": "opt-out",
+ "n_buckets": 56,
+ "description": "How much time (in hours) passed between the current cookie purging activity and the one before that (cookie purging is run on 'daily idle')"
+ },
+ "REFERRER_POLICY_COUNT": {
+ "products": ["firefox"],
+ "record_in_processes": ["main"],
+ "alert_emails": [
+ "tihuang@mozilla.com",
+ "jhofmann@mozilla.com",
+ "ckerschbaumer@mozilla.com",
+ "seceng-telemetry@mozilla.com"
+ ],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 18,
+ "bug_numbers": [1720869],
+ "description": "The counter of each referrer policy which has been computed for a referrer. The buckets (0-8) are for same-site requests and buckets (9-17) are for cross-site requests. Note that the index matches to the order in ReferrerPolicy.webidl",
+ "releaseChannelCollection": "opt-out"
+ },
+ "APZ_ZOOM_ACTIVITY": {
+ "products": ["firefox"],
+ "record_in_processes": ["content"],
+ "alert_emails": ["botond@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "bug_numbers": [1627708, 1672576],
+ "description": "Whether non-RDM page had user-triggered zooming activity",
+ "releaseChannelCollection": "opt-out"
+ },
+ "APZ_ZOOM_PINCHSOURCE": {
+ "products": ["firefox"],
+ "record_in_processes": ["main", "gpu"],
+ "alert_emails": ["botond@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 5,
+ "bug_numbers": [1627708, 1672576],
+ "description": "Input device that triggered the zoom gesture (InputData::PinchGestureSource)",
+ "releaseChannelCollection": "opt-out"
+ },
+ "AUTOFILL_PROFILE_NUM_USES": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "How many times each saved autofill profile(address or credit card) has ever been autofilled. Rewritten after each autofill; always contains one entry per each count of uses. Example: If user has 3 cards, and he used 2 of them 5 times, and one of them 3 times, the value will be: `{3: 1, 5: 2}.",
+ "alert_emails": ["autofill@lists.mozilla.org", "passwords-dev@mozilla.org"],
+ "bug_numbers": [1653073, 1656355],
+ "releaseChannelCollection": "opt-out",
+ "keyed": true,
+ "keys": ["credit_card", "address"]
+ },
+ "CREDITCARD_NUM_USES": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "description": "How many times each saved credit card has ever been autofilled. Rewritten after each autofill; always contains one entry per each count of uses. Example: If user has 3 cards, and he used 2 of them 5 times, and one of them 3 times, the value will be: `{3: 1, 5: 2}.",
+ "alert_emails": ["jmathies@mozilla.com", "chsiang@mozilla.com"],
+ "bug_numbers": [1653073, 1656355],
+ "releaseChannelCollection": "opt-out"
+ },
+ "SQLITE_STORE_OPEN": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["dtownsend@mozilla.com", "mbonardo@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1635489],
+ "description": "The result of attempting to open a sqlite file.",
+ "labels": [
+ "success",
+ "failure",
+ "access",
+ "diskio",
+ "corrupt",
+ "busy",
+ "misuse",
+ "diskspace"
+ ]
+ },
+ "SQLITE_STORE_QUERY": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["dtownsend@mozilla.com", "mbonardo@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "keyed": true,
+ "releaseChannelCollection": "opt-out",
+ "bug_numbers": [1635489],
+ "description": "The result of attempting to query a sqlite file.",
+ "labels": [
+ "success",
+ "failure",
+ "access",
+ "diskio",
+ "corrupt",
+ "busy",
+ "misuse",
+ "diskspace"
+ ]
+ },
+ "REL_PRELOAD_MISS_RATIO": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"],
+ "bug_numbers": [1658532, 1583604, 1666981, 1700831, 1736687, 1777403],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": [
+ "TYPE_SCRIPT_USED",
+ "TYPE_SCRIPT_UNUSED",
+ "TYPE_STYLE_USED",
+ "TYPE_STYLE_UNUSED",
+ "TYPE_IMAGE_USED",
+ "TYPE_IMAGE_UNUSED",
+ "TYPE_FONT_USED",
+ "TYPE_FONT_UNUSED",
+ "TYPE_FETCH_USED",
+ "TYPE_FETCH_UNUSED"
+ ],
+ "releaseChannelCollection": "opt-out",
+ "description": "Ratio of used and unused resources preloaded with link rel=preload tag or response header, broken down by supported resource type."
+ },
+ "HTTP_TRANSACTION_RESTART_REASON": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "n_values": 50,
+ "bug_numbers": [1667775],
+ "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"],
+ "kind": "enumerated",
+ "description": "The reason why a HTTP transaction is restarted."
+ },
+ "HTTP_CONNECTION_CLOSE_REASON": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "keyed": true,
+ "n_values": 50,
+ "bug_numbers": [1784261],
+ "releaseChannelCollection": "opt-out",
+ "description": "Reason for HTTP connection closure, keyed by properties like HTTP version and connection state."
+ },
+ "TRANSACTION_ECH_RETRY_WITH_ECH_COUNT": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "bug_numbers": [1671886],
+ "description": "When echConfig is enabled, number of times a transaction is retried because of SSL_ERROR_ECH_RETRY_WITH_ECH"
+ },
+ "TRANSACTION_ECH_RETRY_WITHOUT_ECH_COUNT": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "bug_numbers": [1671886],
+ "description": "When echConfig is enabled, number of times a transaction is retried because of SSL_ERROR_ECH_RETRY_WITHOUT_ECH"
+ },
+ "TRANSACTION_ECH_RETRY_ECH_FAILED_COUNT": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "bug_numbers": [1671886],
+ "description": "When echConfig is enabled, number of times a transaction is retried because of SSL_ERROR_ECH_FAILED"
+ },
+ "TRANSACTION_ECH_RETRY_OTHERS_COUNT": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 50,
+ "bug_numbers": [1671886],
+ "description": "When echConfig is enabled, number of times a transaction is retried because of other error"
+ },
+ "ECHCONFIG_SUCCESS_RATE": {
+ "record_in_processes": ["main", "socket"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": [
+ "EchConfigSucceeded",
+ "EchConfigFailed",
+ "NoEchConfigSucceeded",
+ "NoEchConfigFailed"
+ ],
+ "description": "TLS handshake with and without EchConfig success rate.",
+ "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"],
+ "bug_numbers": [1682555]
+ },
+ "TEXT_RECOGNITION_API_PERFORMANCE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["gtatum@mozilla.com", "nordzilla@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 120000,
+ "n_buckets": 20,
+ "bug_numbers": [1783261],
+ "description": "The milliseconds of time the text recognition results took to display, including the UI time and OS response time."
+ },
+ "TEXT_RECOGNITION_INTERACTION_TIMING": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["gtatum@mozilla.com", "nordzilla@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 120000,
+ "n_buckets": 20,
+ "bug_numbers": [1783261],
+ "description": "The milliseconds of time that a user viewed the text results."
+ },
+ "TEXT_RECOGNITION_TEXT_LENGTH": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["gtatum@mozilla.com", "nordzilla@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 20,
+ "bug_numbers": [1783261],
+ "description": "Measures the length of the text that was recognized, in code units."
+ },
+ "DOWNLOADS_USER_ACTION_ON_BLOCKED_DOWNLOAD": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["dlee@mozilla.com", "safebrowsing-telemetry@mozilla.org"],
+ "bug_numbers": [1722066],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "enumerated",
+ "n_values": 3,
+ "keyed": true,
+ "description": "Users action on blocked download, keyed by verdict type (0=Number of blocked downloads, 1=Confirm block, 2=Confirm unblock)"
+ },
+ "EH_NUM_OF_HINTS_PER_PAGE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "bug_numbers": [1743630, 1796398],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 30,
+ "n_buckets": 20,
+ "description": "Number of 103 responses received for a top level load."
+ },
+ "EH_FINAL_RESPONSE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "bug_numbers": [1743630, 1796398],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "categorical",
+ "labels": ["R2xx", "R3xx", "R4xx", "Other", "Cancel"],
+ "description": "The final response status code after one or more 103 responses."
+ },
+ "EH_TIME_TO_FINAL_RESPONSE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "ddamjanovic@mozilla.com"],
+ "bug_numbers": [1743630, 1796398],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 3000,
+ "n_buckets": 100,
+ "description": "Collect the time elapsed between receiving a 103 response and the final response for a page load."
+ },
+ "EH_STATE_OF_PRELOAD_REQUEST": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "n_values": 50,
+ "bug_numbers": [1803747],
+ "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"],
+ "kind": "enumerated",
+ "description": "The usage of a early hint preload request"
+ },
+ "EH_PERF_PAGE_LOAD_TIME_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "bug_numbers": [1808323],
+ "keyed": true,
+ "description": "Time in milliseconds from navigationStart to loadEventStart for the foreground http or https root content document. This is collected only on page load where the main document uses or suppports Early Hints preload."
+ },
+ "EH_PERF_FIRST_CONTENTFUL_PAINT_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "bug_numbers": [1808323],
+ "keyed": true,
+ "description": "The time between navigationStart and the first contentful paint of a foreground http or https root content document, in milliseconds. The contentful paint timestamp is taken during display list building and does not include rasterization or compositing of that paint. This is collected only on page load where the main document uses or supports Early Hints preload."
+ },
+ "EH_PERF_CHANNEL_COMPLETION_TIME": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["necko@mozilla.com", "kershaw@mozilla.com"],
+ "expires_in_version": "130",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 50000,
+ "n_buckets": 100,
+ "bug_numbers": [1838406],
+ "keyed": true,
+ "description": "The time between AsyncOpen and OnStopRequest for Early Hints preload request."
+ },
+ "FX_PICTURE_IN_PICTURE_WINDOW_OPEN_DURATION": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["mconley@mozilla.com"],
+ "bug_numbers": [1560590, 1683100, 1756703],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "description": "The length of time (in seconds) that a Picture-in-Picture window was open.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "FX_PICTURE_IN_PICTURE_BACKGROUND_TAB_PLAYING_DURATION": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["mconley@mozilla.com"],
+ "bug_numbers": [1817042],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "description": "The length of time (in seconds) that a video was in Picture-in-Picture mode in a background tab.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "FX_PICTURE_IN_PICTURE_FOREGROUND_TAB_PLAYING_DURATION": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["mconley@mozilla.com"],
+ "bug_numbers": [1817042],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 100,
+ "description": "The length of time (in seconds) that a video was in Picture-in-Picture mode in a foreground tab.",
+ "releaseChannelCollection": "opt-out"
+ },
+ "ORB_JAVASCRIPT_VALIDATION_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["farre@mozilla.com"],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "keyed": true,
+ "keys": ["javascript", "json", "other", "failure"],
+ "high": 10000,
+ "n_buckets": 50,
+ "bug_numbers": [1804638],
+ "description": "Time (in ms) that it takes for a ORB JavaScript validator to complete a validation, including IPC to the validator actor."
+ },
+ "ORB_RECEIVE_DATA_FOR_VALIDATION_MS": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["farre@mozilla.com"],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "keyed": true,
+ "keys": ["javascript", "json", "other", "failure"],
+ "high": 10000,
+ "n_buckets": 50,
+ "bug_numbers": [1804638],
+ "description": "Time (in ms) that it takes to receive data for ORB JavaScript validation, including IPC to the validator actor."
+ },
+ "ORB_DID_EVER_BLOCK_RESPONSE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["farre@mozilla.com"],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "bug_numbers": [1812051],
+ "description": "If any opaque response was blocked for a given top-level window context."
+ },
+ "ORB_BLOCK_REASON": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["sefeng@mozilla.com", "afarre@mozilla.com"],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": [
+ "MIME_NEVER_SNIFFED",
+ "RESP_206_BLCLISTED",
+ "NOSNIFF_BLC_OR_TEXTP",
+ "RESP_206_NO_FIRST",
+ "AFTER_SNIFF_MEDIA",
+ "AFTER_SNIFF_NOSNIFF",
+ "AFTER_SNIFF_STA_CODE",
+ "AFTER_SNIFF_CT_FAIL",
+ "MEDIA_NOT_INITIAL",
+ "MEDIA_INCORRECT_RESP",
+ "JS_VALIDATION_FAILED"
+ ],
+ "bug_numbers": [1833216],
+ "description": "The reason of why this request was blocked by ORB"
+ },
+ "ORB_BLOCK_INITIATOR": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "alert_emails": ["sefeng@mozilla.com", "afarre@mozilla.com"],
+ "releaseChannelCollection": "opt-out",
+ "expires_in_version": "never",
+ "kind": "categorical",
+ "labels": [
+ "INVALID",
+ "OTHER",
+ "FILTERED_FETCH",
+ "BLOCKED_FETCH",
+ "SCRIPT",
+ "IMAGE",
+ "STYLESHEET",
+ "XMLHTTPREQUEST",
+ "DTD",
+ "FONT",
+ "MEDIA",
+ "CSP_REPORT",
+ "XSLT",
+ "IMAGESET",
+ "WEB_MANIFEST",
+ "SPECULATIVE",
+ "UA_FONT",
+ "PROXIED_WEBRTC_MEDIA",
+ "PING",
+ "BEACON",
+ "WEB_TRANSPORT",
+ "EXCLUDED"
+ ],
+ "bug_numbers": [1833216],
+ "description": "The initiator of this ORB blocked request."
+ },
+ "EVENT_LONGTASK": {
+ "record_in_processes": ["main", "content"],
+ "products": ["firefox", "fennec"],
+ "expires_in_version": "never",
+ "alert_emails": ["rjesup@mozilla.com", "perf-telemetry-alerts@mozilla.com"],
+ "bug_numbers": [12345678],
+ "kind": "exponential",
+ "high": 10000,
+ "n_buckets": 50,
+ "keyed": true,
+ "description": "LongTask events for parent process (keys: event name): time the event ran in ms"
+ },
+ "STRIP_ON_SHARE_PARAMS_REMOVED": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "linear",
+ "high": 100,
+ "n_buckets": 50,
+ "alert_emails": ["amadan@mozilla.com", "pbz@mozilla.com"],
+ "bug_numbers": [1833105],
+ "description": "The number of query parameters removed, if any, when strip on share is used"
+ },
+ "STRIP_ON_SHARE_LENGTH_DECREASE": {
+ "record_in_processes": ["main"],
+ "products": ["firefox"],
+ "expires_in_version": "never",
+ "releaseChannelCollection": "opt-out",
+ "kind": "exponential",
+ "high": 1000,
+ "n_buckets": 100,
+ "alert_emails": ["amadan@mozilla.com", "pbz@mozilla.com"],
+ "bug_numbers": [1833105],
+ "description": "The decrease in the length of the URL when strip on share is used, only if at least one parameter is stripped"
+ },
+ "SCREENWAKELOCK_HELD_DURATION_MS": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["vhilla@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 240000,
+ "high": 28800000,
+ "n_buckets": 50,
+ "bug_numbers": [1589554],
+ "description": "How long a screen wake lock was held in ms"
+ },
+ "SCREENWAKELOCK_RELEASE_BATTERY_LEVEL_DISCHARGING": {
+ "record_in_processes": ["content"],
+ "products": ["firefox"],
+ "alert_emails": ["vhilla@mozilla.com"],
+ "expires_in_version": "never",
+ "kind": "linear",
+ "low": 1,
+ "high": 100,
+ "n_buckets": 50,
+ "bug_numbers": [1589554],
+ "description": "Battery level when discharging and the wake lock was released"
+ }
+}
diff --git a/toolkit/components/telemetry/Makefile.in b/toolkit/components/telemetry/Makefile.in
new file mode 100644
index 0000000000..d61114f9d8
--- /dev/null
+++ b/toolkit/components/telemetry/Makefile.in
@@ -0,0 +1,13 @@
+#
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+include $(topsrcdir)/config/rules.mk
+
+# This is so hacky. Waiting on bug 988938.
+addondir = $(srcdir)/tests/addons
+testdir = $(topobjdir)/_tests/xpcshell/toolkit/components/telemetry/tests/unit
+
+misc:: $(call mkdir_deps,$(testdir))
+ $(foreach dir,$(wildcard $(addondir)/*),$(call py_action,zip $(notdir $(dir)).xpi,-C $(dir) $(testdir)/$(notdir $(dir)).xpi \*))
diff --git a/toolkit/components/telemetry/Processes.yaml b/toolkit/components/telemetry/Processes.yaml
new file mode 100644
index 0000000000..d5187cbc5a
--- /dev/null
+++ b/toolkit/components/telemetry/Processes.yaml
@@ -0,0 +1,40 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# This lists the known child processes we collect Telemetry for.
+# The entries are keyed with the names used in Telemetry internally, the same name that is used
+# in the main pings payload, i.e. "payload/processes/<process name>". See:
+# https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/data/main-ping.html#processes
+#
+# For now this is only used to inform the data pipeline about new processes, but will be used to
+# generate headers with C++ data later (enums, strings, ...).
+parent:
+ gecko_enum: GeckoProcessType_Default
+ description: This is the main process. It is also known as the parent or chrome process.
+content:
+ gecko_enum: GeckoProcessType_Content
+ description: This is for processes web content is rendered in.
+extension:
+ gecko_enum: GeckoProcessType_Content
+ description: >
+ This is the WebExtension process. It is a re-used content process, with the data submitted
+ separately to avoid skewing other content process Telemetry.
+gpu:
+ gecko_enum: GeckoProcessType_GPU
+ description: This is the compositor or GPU process.
+dynamic:
+ gecko_enum: GeckoProcessType_Default
+ description: >
+ This is not a real process, it is used to logically group add-on probes.
+ It contains data of any probes registered at runtime by add-ons.
+rdd:
+ gecko_enum: GeckoProcessType_RDD
+ description: >
+ Remote Media Decoder process in charge of running some video or audio decoders.
+socket:
+ gecko_enum: GeckoProcessType_Socket
+ description: This is the process that handles networking requests.
+utility:
+ gecko_enum: GeckoProcessType_Utility
+ description: This is a process that will hold arbitrary IPC actors requiring specific sandboxing.
diff --git a/toolkit/components/telemetry/Scalars.yaml b/toolkit/components/telemetry/Scalars.yaml
new file mode 100644
index 0000000000..66a8b787e7
--- /dev/null
+++ b/toolkit/components/telemetry/Scalars.yaml
@@ -0,0 +1,9785 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# This file contains a definition of the scalar probes that are recorded in Telemetry.
+# They are submitted with the "main" pings and can be inspected in about:telemetry.
+
+# The following section contains the accessibility scalars.
+a11y:
+ always_underline_links:
+ bug_numbers:
+ - 1835194
+ description: >
+ Bool tracking if the user has always underline links enabled in firefox.
+ expires: never
+ kind: boolean
+ notification_emails:
+ - accessibility@mozilla.com
+ - mreschenberg@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ release_channel_collection: opt-out
+ keyed: false
+
+ use_system_colors:
+ bug_numbers:
+ - 1835194
+ description: >
+ Bool tracking if the user has system colors enabled in firefox.
+ expires: never
+ kind: boolean
+ notification_emails:
+ - accessibility@mozilla.com
+ - mreschenberg@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ release_channel_collection: opt-out
+ keyed: false
+
+ HCM_background:
+ bug_numbers:
+ - 1694717
+ description: >
+ Unsigned int tracking the user's prefered background color \
+ (logged when HCM is enabled).
+ expires: never
+ kind: uint
+ notification_emails:
+ - accessibility@mozilla.com
+ - mreschenberg@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+ release_channel_collection: opt-out
+ keyed: false
+
+ HCM_foreground:
+ bug_numbers:
+ - 1694717
+ description: >
+ Unsigned int tracking the user's prefered foreground color \
+ (logged when HCM is enabled).
+ expires: never
+ kind: uint
+ notification_emails:
+ - accessibility@mozilla.com
+ - mreschenberg@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+ release_channel_collection: opt-out
+ keyed: false
+
+ backplate:
+ bug_numbers:
+ - 1539212
+ description: >
+ Boolean tracking if the user has the backplate preference enabled or disabled.
+ expires: never
+ kind: boolean
+ notification_emails:
+ - accessibility@mozilla.com
+ - mreschenberg@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+ release_channel_collection: opt-out
+ keyed: false
+
+ instantiators:
+ bug_numbers:
+ - 1323069
+ - 1462238
+ description: >
+ The leaf name and version number of the binary for the process responsible
+ for remotely instantiating a11y.
+ expires: never
+ kind: string
+ notification_emails:
+ - accessibility@mozilla.com
+ - jteh@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+ release_channel_collection: opt-out
+ keyed: false
+
+ invert_colors:
+ bug_numbers:
+ - 1794626
+ description: >
+ Boolean tracking if the user has an invert colors OS
+ setting enabled.
+ expires: never
+ kind: boolean
+ notification_emails:
+ - accessibility@mozilla.com
+ - mreschenberg@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ release_channel_collection: opt-out
+ keyed: false
+
+ theme:
+ bug_numbers:
+ - 1022528
+ description: >
+ OS high contrast or other accessibility theme is enabled.
+ The result is split into keys which represent the values of
+ browser.display.document_color_use: "default", "always", or "never".
+ keyed: true
+ expires: never
+ kind: boolean
+ notification_emails:
+ - accessibility@mozilla.com
+ - eisaacson@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+# The following section contains the browser engagement scalars.
+browser.engagement:
+ max_concurrent_tab_count:
+ bug_numbers:
+ - 1271304
+ description: >
+ The count of maximum number of tabs open during a subsession,
+ across all windows, including tabs in private windows and restored
+ at startup.
+ expires: never
+ kind: uint
+ notification_emails:
+ - rweiss@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ tab_open_event_count:
+ bug_numbers:
+ - 1271304
+ description: >
+ The count of tab open events per subsession, across all windows, after the
+ session has been restored. This includes tab open events from private windows
+ and from manual session restorations (i.e. after crashes and from about:home).
+ expires: never
+ kind: uint
+ notification_emails:
+ - rweiss@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ max_concurrent_window_count:
+ bug_numbers:
+ - 1271304
+ description: >
+ The count of maximum number of browser windows open during a subsession. This
+ includes private windows and the ones opened when starting the browser.
+ expires: never
+ kind: uint
+ notification_emails:
+ - rweiss@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ window_open_event_count:
+ bug_numbers:
+ - 1271304
+ description: >
+ The count of browser window open events per subsession, after the session
+ has been restored. The count includes private windows and the ones from manual
+ session restorations (i.e. after crashes and from about:home).
+ expires: never
+ kind: uint
+ notification_emails:
+ - rweiss@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ max_concurrent_tab_pinned_count:
+ bug_numbers:
+ - 1505535
+ description: >
+ The count of maximum number of pinned tabs open during a subsession. This
+ includes private windows and the ones opened when starting the browser.
+ Starting Firefox 85 this includes number of restored pinned tabs at
+ startup.
+ expires: never
+ kind: uint
+ notification_emails:
+ - bmiroglio@mozilla.com
+ - aoprea@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ tab_pinned_event_count:
+ bug_numbers:
+ - 1505535
+ description: >
+ The count of tab pinned events per subsession, across all windows, after the
+ session has been restored. This includes tab pinned events from private windows.
+ expires: never
+ kind: uint
+ notification_emails:
+ - bmiroglio@mozilla.com
+ - aoprea@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ tab_unload_count:
+ bug_numbers:
+ - 1715858
+ description: >
+ The count of tab unload events by TabUnloader due to a low-memory situation.
+ expires: never
+ kind: uint
+ notification_emails:
+ - tkikuchi@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ tab_reload_count:
+ bug_numbers:
+ - 1715858
+ description: >
+ The count of tab reload events by the user after unloaded.
+ expires: never
+ kind: uint
+ notification_emails:
+ - tkikuchi@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ total_uri_count:
+ bug_numbers:
+ - 1271313
+ - 1529232
+ description: >
+ The count of the total non-unique http(s) URIs visited in a subsession, including
+ page reloads, after the session has been restored. URIs on minimized or background
+ tabs may also be counted towards this. Private browsing is not included in this
+ count.
+ expires: never
+ kind: uint
+ notification_emails:
+ - rweiss@mozilla.com
+ - loines@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+ record_into_store:
+ - 'main'
+ - 'account-ecosystem'
+
+ total_uri_count_normal_and_private_mode:
+ bug_numbers:
+ - 1535169
+ description: >
+ The count of the total non-unique http(s) URIs visited in a subsession, including
+ page reloads, after the session has been restored. URIs on minimized or background
+ tabs may also be counted towards this. Private browsing is included in this count.
+ expires: never
+ kind: uint
+ notification_emails:
+ - rweiss@mozilla.com
+ - loines@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+ record_into_store:
+ - 'main'
+ - 'account-ecosystem'
+
+ unfiltered_uri_count:
+ bug_numbers:
+ - 1304647
+ description: >
+ The count of the total non-unique URIs visited in a subsession, not restricted to
+ a specific protocol, including page reloads and about:* pages (other than initial
+ pages such as about:blank, ...), after the session has been restored. This does
+ not include background page requests and URIs from embedded pages or private browsing.
+ expires: never
+ kind: uint
+ notification_emails:
+ - bcolloran@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ unique_domains_count:
+ bug_numbers:
+ - 1271310
+ description: >
+ The count of the unique domains visited in a subsession, after the session
+ has been restored. Subdomains under eTLD are aggregated after the first level
+ (i.e. test.example.com and other.example.com are only counted once).
+ This does not include background page requests and domains from embedded pages
+ or private browsing. The count is limited to 100 unique domains.
+ expires: never
+ kind: uint
+ notification_emails:
+ - rweiss@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ active_ticks:
+ bug_numbers:
+ - 1376942
+ - 1545172
+ description: >
+ The count of the number of five-second intervals ('ticks') the user
+ was considered 'active' in a subsession. Session activity involves keyboard or mouse
+ interaction with the application. It does not take into account whether or not the window
+ has focus or is in the foreground, only if it is receiving these interaction events.
+ expires: never
+ kind: uint
+ notification_emails:
+ - bcolloran@mozilla.com
+ - loines@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - 'main'
+ record_into_store:
+ - 'main'
+ - 'account-ecosystem'
+
+ profile_count:
+ bug_numbers:
+ - 1647422
+ - 1356265
+ description: >
+ The count of the browser profiles on the current system. This counts profiles that have been
+ used across all OS user accounts on machine since this probe was added. The value persists
+ across installs. A value of 0 is reported if there is an error determining the correct count.
+ expires: never
+ kind: uint
+ notification_emails:
+ - rtestard@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ operating_systems:
+ - windows
+
+ bookmarks_toolbar_bookmark_added:
+ bug_numbers:
+ - 1665391
+ description: >
+ The count of bookmarks added to the bookmarks toolbar. This counts bookmarks created on the
+ bookmarks toolbar and bookmarks moved to the bookmarks toolbar.
+ expires: never
+ kind: uint
+ notification_emails:
+ - rtestard@mozilla.com
+ - jaws@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ bookmarks_toolbar_bookmark_opened:
+ bug_numbers:
+ - 1665391
+ description: >
+ The count of bookmarks opened from the Bookmarks Toolbar. This counts bookmarks opened on the
+ toolbar and bookmarks opened from the 'symlinked' Other Bookmarks folder on the
+ Bookmarks Toolbar. It does not count Bookmarks Toolbar bookmarks opened via the Library,
+ Bookmarks Menu, or other UI since the goal is to measure interactions that pass through the toolbar.
+ expires: never
+ kind: uint
+ notification_emails:
+ - rtestard@mozilla.com
+ - jaws@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ sessionrestore_interstitial:
+ bug_numbers:
+ - 1671271
+ description: >
+ Whether we show about:sessionrestore or not. Split by key, which indicates
+ why we showed the item or not. Specifically, 'deferred_restore' means we
+ didn't autorestore and thus didn't show the interstitial; 'autorestore'
+ indicates we autorestored without showing the interstitial.
+ 'shown_old_session', 'shown_only_about_welcomeback',
+ 'shown_many_crashes', 'shown_many_crashes_old_session' all indicate we did
+ show the interstitial because of a crash, and the text after 'shown'
+ indicates what secondary reason there was to show this page (as we don't
+ show it for 'just' one crash).
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - gijs@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ session_time_including_suspend:
+ bug_numbers:
+ - 1205985
+ description: >
+ The duration of the session in milliseconds, including the time the device
+ was suspended.
+ expires: never
+ kind: uint
+ notification_emails:
+ - padenot@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ session_time_excluding_suspend:
+ bug_numbers:
+ - 1205985
+ description: >
+ The duration of the session in milliseconds, excluding the time the device
+ was suspended.
+ expires: never
+ kind: uint
+ notification_emails:
+ - padenot@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+
+# The following section contains the browser engagement scalars.
+browser.engagement.navigation:
+ urlbar:
+ bug_numbers:
+ - 1271313
+ description: >
+ The count URI loads triggered in a subsession from the urlbar (awesomebar),
+ broken down by the originating action.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ urlbar_handoff:
+ bug_numbers:
+ - 1732429
+ description: >
+ The count URI loads triggered in a subsession from the urlbar via handoff,
+ broken down by the originating action.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ urlbar_persisted:
+ bug_numbers:
+ - 1779471
+ description: >
+ The count of URI loads triggered in a subsession from the urlbar after the
+ search term was shown in the urlbar, broken down by the originating action.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ urlbar_searchmode:
+ bug_numbers:
+ - 1654680
+ description: >
+ The count URI loads triggered in a subsession from the urlbar's search
+ mode, broken down by the originating action.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ searchbar:
+ bug_numbers:
+ - 1271313
+ description: >
+ The count URI loads triggered in a subsession from the searchbar,
+ broken down by the originating action.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ about_home:
+ bug_numbers:
+ - 1271313
+ description: >
+ The count URI loads triggered in a subsession from about:home,
+ broken down by the originating action.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ about_newtab:
+ bug_numbers:
+ - 1271313
+ description: >
+ The count URI loads triggered in a subsession from about:newtab,
+ broken down by the originating action.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ contextmenu:
+ bug_numbers:
+ - 1271313
+ description: >
+ The count URI loads triggered in a subsession from the contextmenu,
+ broken down by the originating action.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ webextension:
+ bug_numbers:
+ - 1492233
+ description: >
+ The count URI loads triggered in a subsession from a webextension.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+browser.startup:
+ abouthome_cache_result:
+ bug_numbers:
+ - 1622263
+ - 1614351
+ - 1683101
+ - 1714258
+ - 1730042
+ - 1754641
+ - 1781978
+ - 1811151
+ - 1841926
+ description: >
+ How the about:home startup cache functioned on startup.
+
+ 0: Result value was never set (error case)
+ 1: Cache did not exist
+ 2: Cache page stream was corrupt / inaccessible
+ 3: Cache script stream was corrupt / inaccessible
+ 4: Cache was invalidated by a version bump
+ 5: Cache was valid, but read too late to be useful.
+ 6: Cache was valid and used.
+ 7: Cache is disabled.
+ 8: User did not load about:home on its own by default.
+ 9: Cache is disabled because about:newtab preloading is disabled.
+
+ expires: never
+ kind: uint
+ notification_emails:
+ - mconley@mozilla.com
+ - perf-telemetry-alerts@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ abouthome_cache_shutdownwrite:
+ bug_numbers:
+ - 1622263
+ - 1614351
+ - 1683101
+ - 1714258
+ - 1730042
+ - 1754641
+ - 1781978
+ - 1811151
+ - 1841926
+ description: >
+ True if the about:home startup cache was written via the AsyncShutdown
+ blocker.
+
+ expires: never
+ kind: boolean
+ notification_emails:
+ - mconley@mozilla.com
+ - perf-telemetry-alerts@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+# The following section contains the browser usage scalars.
+browser.usage:
+ graphite:
+ bug_numbers:
+ - 1331915
+ description: >
+ The number of times a graphite2 font has been loaded.
+ expires: "65"
+ kind: uint
+ notification_emails:
+ - tom@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'all'
+
+ plugin_instantiated:
+ bug_numbers:
+ - 1381591
+ description: >-
+ The number of plugin instances that were created.
+ expires: never # Jan-2021 but we don't have a version number for that
+ kind: uint
+ notification_emails:
+ - bsmedberg@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+ - 'content'
+
+browser.ui:
+ toolbar_widgets:
+ bug_numbers:
+ - 1620358
+ description: >
+ The widgets in the toolbars.
+ See https://firefox-source-docs.mozilla.org/browser/BrowserUsageTelemetry.html
+ expires: never
+ kind: boolean
+ keyed: true
+ notification_emails:
+ - shong@mozilla.com
+ - dtownsend@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ customized_widgets:
+ bug_numbers:
+ - 1620358
+ description: >
+ Records when widgets are added, removed or moved in the UI.
+ See https://firefox-source-docs.mozilla.org/browser/BrowserUsageTelemetry.html
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - shong@mozilla.com
+ - dtownsend@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+browser.ui.interaction:
+ all_tabs_panel_dragstart_tab_event_count:
+ bug_numbers:
+ - 1804722
+ description: >
+ Records a count of how many times a drag event started for a tab within the All
+ Tabs panel.
+ expires: never
+ kind: uint
+ notification_emails:
+ - shong@mozilla.com
+ - mconley@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ all_tabs_panel_entrypoint:
+ bug_numbers:
+ - 1804722
+ description: >
+ Records a count of how many times the All Tabs Panel was opened, keyed on a string
+ describing the entrypoint.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - shong@mozilla.com
+ - mconley@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ keyboard:
+ bug_numbers:
+ - 1620358
+ description: >
+ Records a count of interactions with keyboard shortcuts.
+ See https://firefox-source-docs.mozilla.org/browser/BrowserUsageTelemetry.html
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - shong@mozilla.com
+ - dtownsend@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ menu_bar:
+ bug_numbers:
+ - 1620358
+ description: >
+ Records a count of interactions with items in the menu bar.
+ See https://firefox-source-docs.mozilla.org/browser/BrowserUsageTelemetry.html
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - shong@mozilla.com
+ - dtownsend@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ tabs_bar:
+ bug_numbers:
+ - 1620358
+ description: >
+ Records a count of interactions with items in the tab bar.
+ See https://firefox-source-docs.mozilla.org/browser/BrowserUsageTelemetry.html
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - shong@mozilla.com
+ - dtownsend@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ nav_bar:
+ bug_numbers:
+ - 1620358
+ description: >
+ Records a count of interactions with items in the nav bar.
+ See https://firefox-source-docs.mozilla.org/browser/BrowserUsageTelemetry.html
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - shong@mozilla.com
+ - dtownsend@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ bookmarks_bar:
+ bug_numbers:
+ - 1620358
+ description: >
+ Records a count of interactions with items in the bookmarks bar.
+ See https://firefox-source-docs.mozilla.org/browser/BrowserUsageTelemetry.html
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - shong@mozilla.com
+ - dtownsend@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ app_menu:
+ bug_numbers:
+ - 1620358
+ description: >
+ Records a count of interactions with items in the app menu.
+ See https://firefox-source-docs.mozilla.org/browser/BrowserUsageTelemetry.html
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - shong@mozilla.com
+ - dtownsend@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ tabs_context:
+ bug_numbers:
+ - 1620358
+ description: >
+ Records a count of interactions with items in the tab context menu.
+ See https://firefox-source-docs.mozilla.org/browser/BrowserUsageTelemetry.html
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - shong@mozilla.com
+ - dtownsend@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ tabs_context_entrypoint:
+ bug_numbers:
+ - 1804722
+ description: >
+ Records a count for each entrypoint (the container of the trigger node) when an
+ item from the tabs context menu is selected.
+ See https://firefox-source-docs.mozilla.org/browser/BrowserUsageTelemetry.html
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - shong@mozilla.com
+ - mconley@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ content_context:
+ bug_numbers:
+ - 1620358
+ description: >
+ Records a count of interactions with items in the content context menu.
+ See https://firefox-source-docs.mozilla.org/browser/BrowserUsageTelemetry.html
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - shong@mozilla.com
+ - dtownsend@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ overflow_menu:
+ bug_numbers:
+ - 1620358
+ description: >
+ Records a count of interactions with items in the overflow menu.
+ See https://firefox-source-docs.mozilla.org/browser/BrowserUsageTelemetry.html
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - shong@mozilla.com
+ - dtownsend@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ unified_extensions_area:
+ bug_numbers:
+ - 1800114
+ description: >
+ Records a count of interactions with items in the Unified Extensions area.
+ See https://firefox-source-docs.mozilla.org/browser/BrowserUsageTelemetry.html
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - dtownsend@mozilla.com
+ - mconley@mozilla.com
+ - wdurand@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ pinned_overflow_menu:
+ bug_numbers:
+ - 1620358
+ description: >
+ Records a count of interactions with items in the pinned area of the
+ overflow menu.
+ See https://firefox-source-docs.mozilla.org/browser/BrowserUsageTelemetry.html
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - shong@mozilla.com
+ - dtownsend@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ pageaction_urlbar:
+ bug_numbers:
+ - 1620358
+ description: >
+ Records a count of interactions with page action items in the url bar.
+ See https://firefox-source-docs.mozilla.org/browser/BrowserUsageTelemetry.html
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - shong@mozilla.com
+ - dtownsend@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ pageaction_panel:
+ bug_numbers:
+ - 1620358
+ description: >
+ Records a count of interactions with page action items in the panel.
+ See https://firefox-source-docs.mozilla.org/browser/BrowserUsageTelemetry.html
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - shong@mozilla.com
+ - dtownsend@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ preferences_paneHome:
+ bug_numbers:
+ - 1620358
+ description: >
+ Records the items interacted with in the Home section of preferences.
+ See https://firefox-source-docs.mozilla.org/browser/BrowserUsageTelemetry.html
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - shong@mozilla.com
+ - dtownsend@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ preferences_paneGeneral:
+ bug_numbers:
+ - 1620358
+ description: >
+ Records the items interacted with in the General section of preferences.
+ See https://firefox-source-docs.mozilla.org/browser/BrowserUsageTelemetry.html
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - shong@mozilla.com
+ - dtownsend@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ preferences_panePrivacy:
+ bug_numbers:
+ - 1620358
+ description: >
+ Records the items interacted with in the Privacy section of preferences.
+ See https://firefox-source-docs.mozilla.org/browser/BrowserUsageTelemetry.html
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - shong@mozilla.com
+ - dtownsend@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ preferences_paneSearch:
+ bug_numbers:
+ - 1620358
+ description: >
+ Records the items interacted with in the Search section of preferences.
+ See https://firefox-source-docs.mozilla.org/browser/BrowserUsageTelemetry.html
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - shong@mozilla.com
+ - dtownsend@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ preferences_paneSearchResults:
+ bug_numbers:
+ - 1620358
+ description: >
+ Records the items interacted with in the Search results section of
+ preferences.
+ See https://firefox-source-docs.mozilla.org/browser/BrowserUsageTelemetry.html
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - shong@mozilla.com
+ - dtownsend@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ preferences_paneSync:
+ bug_numbers:
+ - 1620358
+ description: >
+ Records the items interacted with in the Sync section of preferences.
+ See https://firefox-source-docs.mozilla.org/browser/BrowserUsageTelemetry.html
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - shong@mozilla.com
+ - dtownsend@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ preferences_paneContainers:
+ bug_numbers:
+ - 1620358
+ description: >
+ Records the items interacted with in the Containers section of
+ preferences.
+ See https://firefox-source-docs.mozilla.org/browser/BrowserUsageTelemetry.html
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - shong@mozilla.com
+ - dtownsend@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ preferences_paneExperimental:
+ bug_numbers:
+ - 1620358
+ - 1651986
+ description: >
+ Records the items interacted with in the Experimental section of
+ preferences.
+ See https://firefox-source-docs.mozilla.org/browser/BrowserUsageTelemetry.html
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - shong@mozilla.com
+ - dtownsend@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ preferences_paneMoreFromMozilla:
+ bug_numbers:
+ - 1738187
+ description: >
+ Records the items interacted with in the More From Mozilla section of
+ preferences.
+ See https://firefox-source-docs.mozilla.org/browser/BrowserUsageTelemetry.html
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - shong@mozilla.com
+ - pdahiya@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ preferences_paneUnknown:
+ bug_numbers:
+ - 1620358
+ - 1651986
+ description: >
+ Records the items interacted with in any other section of preferences.
+ See https://firefox-source-docs.mozilla.org/browser/BrowserUsageTelemetry.html
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - shong@mozilla.com
+ - dtownsend@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ textrecognition_error:
+ bug_numbers:
+ - 1783261
+ description: >
+ Recorded when text recognition in images fails for some unknown reason.
+ notification_emails:
+ - gtatum@mozilla.com
+ - nordzilla@mozilla.com
+ expires: never
+ kind: uint
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+cookie.banners:
+ normal_window_service_mode:
+ bug_numbers:
+ - 1790781
+ - 1804259
+ description: >
+ The pref value of the cookie banner service mode for normal windows.
+ expires: never
+ kind: boolean
+ keyed: true
+ notification_emails:
+ - tihuang@mozilla.com
+ - pbz@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ private_window_service_mode:
+ bug_numbers:
+ - 1790781
+ - 1804259
+ description: >
+ The pref value of the cookie banner service mode for private windows.
+ expires: never
+ kind: boolean
+ keyed: true
+ notification_emails:
+ - tihuang@mozilla.com
+ - pbz@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ service_detect_only:
+ bug_numbers:
+ - 1809700
+ description: >
+ Tracks the value of the cookiebanners.service.detectOnly pref.
+ expires: never
+ kind: boolean
+ notification_emails:
+ - tihuang@mozilla.com
+ - pbz@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ rule_lookup_by_load:
+ bug_numbers:
+ - 1797073
+ - 1804259
+ - 1827765
+ - 1850874
+ - 1861317
+ description: >
+ Counts the number of hit/miss of cookie banner rule lookups for every
+ load. We collect three types of counters, including counters for overall
+ rule lookup, counters for cookie rule lookup and counters for click rule
+ lookup. We also divide the counter by top-level loads and iframe loads.
+ expires: '128'
+ kind: uint
+ keyed: true
+ notification_emails:
+ - tihuang@mozilla.com
+ - pbz@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ rule_lookup_by_domain:
+ bug_numbers:
+ - 1797073
+ - 1804259
+ - 1827765
+ - 1850874
+ - 1861317
+ description: >
+ Counts the number of hit/miss of cookie banner rule lookups for domain.
+ We collect three types of counters, including counters for overall
+ rule lookup, counters for cookie rule lookup and counters for click rule
+ lookup. We also divide the counter by top-level loads and iframe loads.
+ For each domain, we will only collect once.
+ expires: '128'
+ kind: uint
+ keyed: true
+ notification_emails:
+ - tihuang@mozilla.com
+ - pbz@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+cookie.banners.click:
+ result:
+ bug_numbers:
+ - 1797078
+ - 1804259
+ - 1827765
+ - 1850874
+ - 1861317
+ description: >
+ Given a matching cookie banner click rule, how often do we handle or fail
+ to handle cookie banners, labelled by reason. The 'success' and 'fail'
+ counters count the total numbers independently of the reason counters.
+ Counters are incremented after the content window has been destroyed.
+ expires: '128'
+ keyed: true
+ kind: uint
+ notification_emails:
+ - pbz@mozilla.com
+ - tihuang@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'content'
+ release_channel_collection: opt-out
+
+extensions.apis.dnr:
+ startup_cache_entries:
+ bug_numbers:
+ - 1803363
+ - 1850890
+ description: >
+ Counters for startup cache data hits or misses on initializating
+ DNR rules for extensions loaded on application startup.
+ The expected keys are 'hit' and 'miss'.
+ This probe is mirrored from a Glean metric with the same name.
+ keyed: true
+ expires: '126'
+ kind: uint
+ notification_emails:
+ - addons-dev-internal@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ evaluate_rules_count_max:
+ bug_numbers:
+ - 1803363
+ - 1850890
+ description: >
+ Max amount of DNR rules being evaluated.
+ expires: '126'
+ kind: uint
+ notification_emails:
+ - addons-dev-internal@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+extensions.quarantinedDomains:
+
+ listsize:
+ bug_numbers:
+ - 1840615
+ - 1866196
+ description: >
+ Number of domains listed in the quarantined domains list pref for the client during
+ this session.
+ kind: uint
+ expires: "130"
+ notification_emails:
+ - addons-dev-internal@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ listhash:
+ bug_numbers:
+ - 1841683
+ - 1866196
+ description: >
+ SHA1 cryptographic hash of the quarantined domains string pref.
+ kind: string
+ expires: "130"
+ notification_emails:
+ - addons-dev-internal@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ remotehash:
+ bug_numbers:
+ - 1840615
+ - 1866196
+ description: >
+ SHA1 cryptographic hash of the quarantined domains string pref as it was
+ set based on the value got synced from the RemoteSettings collection.
+ kind: string
+ expires: "130"
+ notification_emails:
+ - addons-dev-internal@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+extensions.startupCache:
+ write_byteLength:
+ bug_numbers:
+ - 1767336
+ description: >
+ The amount of bytes writted into the Extensions StartupCache file.
+ expires: "128"
+ kind: uint
+ notification_emails:
+ - addons-dev-internal@mozilla.com
+ - lgreco@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ read_errors:
+ bug_numbers:
+ - 1767336
+ description: >
+ The amount of times an unexpected error has been raised while
+ reading the Extensions StartupCache file
+ expires: "128"
+ kind: uint
+ keyed: true
+ notification_emails:
+ - addons-dev-internal@mozilla.com
+ - lgreco@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ load_time:
+ bug_numbers:
+ - 1767336
+ description: >
+ Time to load and deserialize the extensions startupCache data.
+ This scalar is mirroring the never expiring Glean metric named
+ extensions.startup_cache_load_time, which is defined in
+ toolkit/components/extensions/metrics.yaml.
+ Unlike the related glean metric, this scalar has an expiring date,
+ set as a deadline to consider removing the mirrored scalar in
+ favor to just use the Glean metric).
+ expires: "132"
+ kind: uint
+ notification_emails:
+ - addons-dev-internal@mozilla.com
+ - lgreco@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+security:
+ https_only_mode_enabled:
+ bug_numbers:
+ - 1620244
+ description: >
+ Measures user retention of the HTTPS-Only Mode.
+ 0 = never enabled, 1 = enabled, 2 = disabled (but was enabled)
+ expires: never
+ kind: uint
+ notification_emails:
+ - julianwels@mozilla.com
+ - seceng-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ https_only_mode_enabled_pbm:
+ bug_numbers:
+ - 1647719
+ description: >
+ Measures user retention of the HTTPS-Only Mode in Private Browsing.
+ 0 = https-only never enabled in PBM,
+ 1 = https-only enabled in PBM,
+ 2 = https-only disabled in PBM (but was enabled)
+ expires: never
+ kind: uint
+ notification_emails:
+ - julianwels@mozilla.com
+ - ckerschb@mozilla.com
+ - seceng-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ global_privacy_control_enabled:
+ bug_numbers:
+ - 1734185
+ description: >
+ Measures user retention of the Global Privacy Control.
+ 0 = never enabled, 1 = enabled, 2 = disabled (but was enabled)
+ expires: never
+ kind: uint
+ notification_emails:
+ - seceng-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ client_auth_cert_usage:
+ bug_numbers:
+ - 1749884
+ description: >
+ Measures how many servers have requested a client authentication
+ certificate (key: "requested") and how many times the user has opted to
+ send one in response (key: "sent").
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - dkeeler@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - main
+
+pwmgr:
+ potentially_breached_passwords:
+ bug_numbers:
+ - 1577808
+ description: >
+ The number of potentially breached passwords, as determined by
+ LoginBreaches.getPotentialBreachesByLoginGUID.
+ expires: never
+ kind: uint
+ notification_emails:
+ - seceng-telemetry@mozilla.com
+ - passwords-dev@mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+bloburl:
+ resolve_stopped:
+ bug_numbers:
+ - 1843158
+ description: >
+ Counts how many times we do not resolve a blob URL
+ because of different partition keys
+ expires: "127"
+ kind: uint
+ notification_emails:
+ - amadan@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - content
+
+contentblocking:
+ cryptomining_blocking_enabled:
+ bug_numbers:
+ - 1522919
+ description: >
+ True if cryptominer blocking is enabled globally at startup.
+ expires: never
+ kind: boolean
+ notification_emails:
+ - aedelstein@mozilla.com
+ - seceng-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - main
+ fingerprinting_blocking_enabled:
+ bug_numbers:
+ - 1522919
+ description: >
+ True if fingerprinter blocking is enabled globally at startup.
+ expires: never
+ kind: boolean
+ notification_emails:
+ - aedelstein@mozilla.com
+ - seceng-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - main
+ category:
+ bug_numbers:
+ - 1529425
+ description: >
+ This scalar reports the value of the content blocking category pref (0 = "standard", 1 = "strict", 2 = "custom", 3 = some other value, this is not supported).
+ expires: never
+ kind: uint
+ notification_emails:
+ - aedelstein@mozilla.com
+ - seceng-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - main
+ trackers_blocked_count:
+ bug_numbers:
+ - 1577030
+ - 1610894
+ - 1645088
+ - 1678209
+ description: >
+ A count of the number of tracking events blocked.
+ expires: never
+ kind: uint
+ notification_emails:
+ - ewright@mozilla.com
+ - seceng-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+datasanitization:
+ privacy_sanitize_sanitizeOnShutdown:
+ bug_numbers:
+ - 1589753
+ - 1617241
+ - 1645089
+ - 1678210
+ description: >
+ A boolean reporting the value of the privacy.sanitize.sanitizeOnShutdown pref.
+ expires: never
+ kind: boolean
+ notification_emails:
+ - ewright@mozilla.com
+ - seceng-telemetry@mozilla.com
+ release_channel_collection: opt-in
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ privacy_clearOnShutdown_cookies:
+ bug_numbers:
+ - 1589753
+ - 1617241
+ - 1645089
+ - 1678210
+ description: >
+ A boolean reporting the value of the privacy.clearOnShutdown.cookies pref.
+ expires: never
+ kind: boolean
+ notification_emails:
+ - ewright@mozilla.com
+ - seceng-telemetry@mozilla.com
+ release_channel_collection: opt-in
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ privacy_clearOnShutdown_history:
+ bug_numbers:
+ - 1589753
+ - 1617241
+ - 1645089
+ - 1678210
+ description: >
+ A boolean reporting the value of the privacy.clearOnShutdown.history pref.
+ expires: never
+ kind: boolean
+ notification_emails:
+ - ewright@mozilla.com
+ - seceng-telemetry@mozilla.com
+ release_channel_collection: opt-in
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ privacy_clearOnShutdown_formdata:
+ bug_numbers:
+ - 1589753
+ - 1617241
+ - 1645089
+ - 1678210
+ description: >
+ A boolean reporting the value of the privacy.clearOnShutdown.formdata pref.
+ expires: never
+ kind: boolean
+ notification_emails:
+ - ewright@mozilla.com
+ - seceng-telemetry@mozilla.com
+ release_channel_collection: opt-in
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ privacy_clearOnShutdown_downloads:
+ bug_numbers:
+ - 1589753
+ - 1617241
+ - 1645089
+ - 1678210
+ description: >
+ A boolean reporting the value of the privacy.clearOnShutdown.downloads pref.
+ expires: never
+ kind: boolean
+ notification_emails:
+ - ewright@mozilla.com
+ - seceng-telemetry@mozilla.com
+ release_channel_collection: opt-in
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ privacy_clearOnShutdown_cache:
+ bug_numbers:
+ - 1589753
+ - 1617241
+ - 1645089
+ - 1678210
+ description: >
+ A boolean reporting the value of the privacy.clearOnShutdown.cache pref.
+ expires: never
+ kind: boolean
+ notification_emails:
+ - ewright@mozilla.com
+ - seceng-telemetry@mozilla.com
+ release_channel_collection: opt-in
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ privacy_clearOnShutdown_sessions:
+ bug_numbers:
+ - 1589753
+ - 1617241
+ - 1645089
+ - 1678210
+ description: >
+ A boolean reporting the value of the privacy.clearOnShutdown.sessions pref.
+ expires: never
+ kind: boolean
+ notification_emails:
+ - ewright@mozilla.com
+ - seceng-telemetry@mozilla.com
+ release_channel_collection: opt-in
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ privacy_clearOnShutdown_offlineApps:
+ bug_numbers:
+ - 1589753
+ - 1617241
+ - 1645089
+ - 1678210
+ description: >
+ A boolean reporting the value of the privacy.clearOnShutdown.offlineApps pref.
+ expires: never
+ kind: boolean
+ notification_emails:
+ - ewright@mozilla.com
+ - seceng-telemetry@mozilla.com
+ release_channel_collection: opt-in
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ privacy_clearOnShutdown_siteSettings:
+ bug_numbers:
+ - 1589753
+ - 1617241
+ - 1645089
+ - 1678210
+ description: >
+ A boolean reporting the value of the privacy.clearOnShutdown.siteSettings pref.
+ expires: never
+ kind: boolean
+ notification_emails:
+ - ewright@mozilla.com
+ - seceng-telemetry@mozilla.com
+ release_channel_collection: opt-in
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ privacy_clearOnShutdown_openWindows:
+ bug_numbers:
+ - 1589753
+ - 1617241
+ - 1645089
+ - 1678210
+ description: >
+ A boolean reporting the value of the privacy.clearOnShutdown.openWindows pref.
+ expires: never
+ kind: boolean
+ notification_emails:
+ - ewright@mozilla.com
+ - seceng-telemetry@mozilla.com
+ release_channel_collection: opt-in
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ session_permission_exceptions:
+ bug_numbers:
+ - 1589753
+ - 1617241
+ - 1645089
+ - 1678210
+ - 1744559
+ description: >
+ A count of how many "session" cookie exceptions a user has set.
+ expires: never
+ kind: uint
+ notification_emails:
+ - pbz@mozilla.com
+ - seceng-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+downloads:
+ panel_shown:
+ bug_numbers:
+ - 1627676
+ description: >
+ The count of how many times the downloads panel was shown per session.
+ expires: never
+ kind: uint
+ notification_emails:
+ - rtestard@mozilla.com
+ - emalysz@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ file_opened:
+ bug_numbers:
+ - 1627676
+ description: >
+ The count of how many times files were opened from the download panel.
+ expires: never
+ kind: uint
+ notification_emails:
+ - rtestard@mozilla.com
+ - emalysz@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+findbar:
+ shown:
+ bug_numbers:
+ - 1627688
+ description: >
+ The count of how many times the "Find toolbar" was shown per session.
+ expires: never
+ kind: uint
+ notification_emails:
+ - rtestard@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+ find_prev:
+ bug_numbers:
+ - 1627688
+ description: >
+ The count of how many times the find previous button was used per session.
+ expires: never
+ kind: uint
+ notification_emails:
+ - rtestard@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+ find_next:
+ bug_numbers:
+ - 1627688
+ description: >
+ The count of how many times the find next button was used per session.
+ expires: never
+ kind: uint
+ notification_emails:
+ - rtestard@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+ highlight_all:
+ bug_numbers:
+ - 1627688
+ description: >
+ The count of how many times the "Highlight All" button was used in find toolbar.
+ expires: never
+ kind: uint
+ notification_emails:
+ - rtestard@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+ match_case:
+ bug_numbers:
+ - 1627688
+ description: >
+ The count of how many times the "Match Case" button was used in find toolbar.
+ expires: never
+ kind: uint
+ notification_emails:
+ - rtestard@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+ match_diacritics:
+ bug_numbers:
+ - 1627688
+ description: >
+ The count of how many times the "Match Diacritics" button was used in find toolbar.
+ expires: never
+ kind: uint
+ notification_emails:
+ - rtestard@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+ whole_words:
+ bug_numbers:
+ - 1627688
+ description: >
+ The count of how many times the "Whole Words" button was used in find toolbar.
+ expires: never
+ kind: uint
+ notification_emails:
+ - rtestard@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+sidebar:
+ opened:
+ bug_numbers:
+ - 1648530
+ description: >
+ The number of times the sidebar was opened, per view (e.g.: bookmarks, history,
+ synced tabs).
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - emalysz@mozilla.com
+ - rtestard@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ search:
+ bug_numbers:
+ - 1648524
+ description: >
+ The number of searches from the sidebar, per view (e.g.: bookmarks, history).
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - emalysz@mozilla.com
+ - rtestard@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ link:
+ bug_numbers:
+ - 1815706
+ description: >
+ The number of history items opened from the History sidebar.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - rtestard@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+library:
+ link:
+ bug_numbers:
+ - 1815906
+ description: >
+ The number of history or bookmark items opened from the Library window
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - firefox-view-engineers@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ opened:
+ bug_numbers:
+ - 1815906
+ description: >
+ The number of times the Library window was opened, keyed by 'history' or 'bookmarks'
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - firefox-view-engineers@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ search:
+ bug_numbers:
+ - 1815906
+ description: >
+ The number of history-specific or bookmark-specific searches made from the Library window
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - firefox-view-engineers@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+os.environment:
+ is_admin_without_uac:
+ bug_numbers:
+ - 1567219
+ description: >
+ Indicates that the process is lauched with Admin privileges but without
+ UAC.
+ expires: never
+ kind: boolean
+ notification_emails:
+ - tkikuchi@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ operating_systems:
+ - "windows"
+
+ allowed_app_sources:
+ bug_numbers:
+ - 1659157
+ description: >
+ Indicates what sources the OS is currently configured to allow apps to be
+ installed from. Possible values are "Anywhere", "Recommendations",
+ "PreferStore", "StoreOnly", "NoSuchFeature", or "Error". "Recommendations"
+ allows installs from anywhere, but recommends a comparable app from the
+ store, if available. "NoSuchFeature" will be reported on versions of
+ Windows that do not have an app source setting.
+ expires: never
+ kind: string
+ notification_emails:
+ - rtestard@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'thunderbird'
+ record_in_processes:
+ - main
+ operating_systems:
+ - "windows"
+
+ launch_method:
+ bug_numbers:
+ - 1685213
+ - 1725298
+ - 1754656
+ description: >
+ Records how Firefox was started on Windows.
+ Currently will be one of "Desktop", "DesktopPrivate",
+ "StartMenu" (including pins), "StartMenuPrivate",
+ "Taskbar", "TaskbarPrivate", "OtherShortcut", or "Other"
+ expires: never
+ kind: string
+ notification_emails:
+ - application-update-telemetry-alerts@mozilla.com
+ - rtestard@mozilla.com
+ - shong@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - firefox
+ record_in_processes:
+ - main
+ operating_systems:
+ - windows
+
+ launched_to_handle:
+ bug_numbers:
+ - 1243603
+ - 1781984
+ description: >
+ Records counts for when Firefox was launched afresh (i.e., was not already
+ running) to handle a file type or protocol with `-osint -url ...`. The
+ result is split into keys which represent the file extension: currently,
+ the set of file types Firefox registers to handle, namely ".avif", ".htm",
+ ".html", ".pdf", ".shtml", ".xht", ".xhtml", ".svg", ".webp", and the set
+ of protocol schemes that Firefox registers to handle, namely "about",
+ "http", "https", "mailto". If Firefox was launched to handle a file type
+ or protocol it does not register to handle by default, the count is
+ recorded as ".<other extension>" or "<other protocol>", respectively
+ (neither of which are valid extension or protocol identifiers).
+ keyed: true
+ expires: never
+ kind: uint
+ notification_emails:
+ - application-update-telemetry-alerts@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - firefox
+ record_in_processes:
+ - main
+ operating_systems:
+ - windows
+
+ invoked_to_handle:
+ bug_numbers:
+ - 1243603
+ - 1781984
+ description: >
+ Records counts for when Firefox was invoked (i.e., was already running and
+ was not launched) to handle a file type or protocol with `-osint -url
+ ...`. The result is split into keys which represent the file extension:
+ currently, the set of file types Firefox registers to handle, namely
+ ".avif", ".htm", ".html", ".pdf", ".shtml", ".xht", ".xhtml", ".svg",
+ ".webp", and the set of protocol schemes that Firefox registers to handle,
+ namely "about", "http", "https", "mailto". If Firefox was invoked to
+ handle a file type or protocol it does not register to handle by default,
+ the count is recorded as ".<other extension>" or "<other protocol>",
+ respectively (neither of which are valid extension or protocol
+ identifiers).
+ keyed: true
+ expires: never
+ kind: uint
+ notification_emails:
+ - application-update-telemetry-alerts@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - firefox
+ record_in_processes:
+ - main
+ operating_systems:
+ - windows
+
+ is_default_handler:
+ bug_numbers:
+ - 1743914
+ - 1781984
+ description: >
+ Records whether Firefox was the default handler for particular file types
+ or protocols. The result is split into keys which represent the file
+ extension: currently, a subset of the file types Firefox registers to
+ handle, namely ".pdf". In the future, more file types may be recorded,
+ and/or a subset of the protocol schemes that Firefox registers to may be
+ recorded.
+ keyed: true
+ expires: never
+ kind: boolean
+ notification_emails:
+ - application-update-telemetry-alerts@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - firefox
+ record_in_processes:
+ - main
+ operating_systems:
+ - windows
+
+ is_kept_in_dock:
+ bug_numbers:
+ - 1715348
+ description: Whether this app was kept in macOS Dock on startup
+ expires: never
+ kind: boolean
+ notification_emails:
+ - elee@mozilla.com
+ - shong@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - firefox
+ record_in_processes:
+ - main
+ operating_systems:
+ - mac
+
+ is_taskbar_pinned:
+ bug_numbers:
+ - 1685213
+ - 1725298
+ description: Whether the non-Private Browsing version of this app was pinned to taskbar on startup
+ expires: never
+ kind: boolean
+ notification_emails:
+ - application-update-telemetry-alerts@mozilla.com
+ - shong@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - firefox
+ record_in_processes:
+ - main
+ operating_systems:
+ - windows
+
+ is_taskbar_pinned_private:
+ bug_numbers:
+ - 1751038
+ description: Whether the Private Browsing version of this app was pinned to taskbar on startup
+ expires: never
+ kind: boolean
+ notification_emails:
+ - application-update-telemetry-alerts@mozilla.com
+ - shong@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - firefox
+ record_in_processes:
+ - main
+ operating_systems:
+ - windows
+
+pictureinpicture:
+ opened_method:
+ bug_numbers:
+ - 1560590
+ - 1616045
+ description: >
+ The number of times a Picture-in-Picture window was opened, per trigger
+ mechanism (e.g.: the video toggle, the context menu).
+ expires: "92"
+ kind: uint
+ keyed: true
+ notification_emails:
+ - mconley@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - content
+ saw_toggle:
+ bug_numbers:
+ - 1657155
+ description: >
+ The number of times a Picture-in-Picture toggle was presented to the user when
+ they hovered the video with the mouse pointer.
+ expires: "92"
+ kind: uint
+ notification_emails:
+ - mconley@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - content
+ most_concurrent_players:
+ bug_numbers:
+ - 1677107
+ - 1706843
+ - 1736690
+ description:
+ The most concurrent Picture-in-Picture players the user had open at any one time.
+ expires: "never"
+ kind: uint
+ notification_emails:
+ - mconley@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ toggle_enabled:
+ bug_numbers:
+ - 1639774
+ description: Whether the user has Picture-in-Picture enabled.
+ expires: "never"
+ kind: boolean
+ notification_emails:
+ - mconley@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+preferences:
+ prefs_file_was_invalid:
+ bug_numbers:
+ - 1367813
+ - 1682571
+ description: >-
+ Set to true if a failure occurred reading profile/prefs.js.
+ expires: never
+ kind: boolean
+ notification_emails:
+ - kwright@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ search_query:
+ bug_numbers:
+ - 1359306
+ description: >-
+ Each key is a search query string when user performs a search action within
+ about:preferences, and each value is the number of times that key is recorded.
+ The telemetry data will be recorded if there is a successful search result highlighted.
+ expires: "62"
+ kind: uint
+ keyed: true
+ notification_emails:
+ - chsiang@mozilla.com
+ release_channel_collection: opt-in
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - main
+
+webrtc.video:
+ recv_codec_used:
+ bug_numbers:
+ - 1556766
+ description: >
+ The video codec used for receiving video on a WebRTC call.
+ expires: "never"
+ kind: uint
+ keyed: true
+ notification_emails:
+ - dminor@mozilla.com
+ - nohlmeier@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'content'
+
+ send_codec_used:
+ bug_numbers:
+ - 1556766
+ description: >
+ The video codec used for sending video on a WebRTC call.
+ expires: "never"
+ kind: uint
+ keyed: true
+ notification_emails:
+ - dminor@mozilla.com
+ - nohlmeier@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'content'
+
+webrtc.sdp:
+ parser_diff:
+ bug_numbers:
+ - 1432955
+ - 1529787
+ - 1588571
+ description: >
+ The number of differences between the C based sipcc SDP parser and
+ the new rust based rsdparsa SDP parser keyed by predefined
+ names of attributes and values that do not match between
+ the sipcc parsing result and the rsdparsa parsing result or
+ between the rsdparsa parsing result and the original sdp.
+ This should help to improve the new rsdparsa to replace
+ the sipcc parser.
+ expires: "77"
+ kind: uint
+ keyed: true
+ notification_emails:
+ - nohlmeier@mozilla.com
+ release_channel_collection: opt-in
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'content'
+
+mathml:
+ doc_count:
+ bug_numbers:
+ - 1362187
+ - 1538985
+ - 1590350
+ description: >
+ The number of documents that contained enabled MathML elements.
+ expires: never
+ kind: uint
+ notification_emails:
+ - emilio@mozilla.com
+ - fred.wang@free.fr
+ - dev-tech-layout@lists.mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'content'
+
+mediarecorder:
+ recording_count:
+ bug_numbers:
+ - 1400757
+ - 1429765
+ - 1480589
+ - 1532391
+ description: >
+ The number of times a MediaRecorder has been started. Recorded when a MediaRecorder starts
+ expires: "72"
+ kind: uint
+ notification_emails:
+ - media-alerts@mozilla.com
+ release_channel_collection: opt-in
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - main
+ - content
+
+media:
+ element_in_page_count:
+ bug_numbers:
+ - 1476456
+ - 1499803
+ - 1570634
+ - 1652108
+ description: >
+ The number of times a document hierarchy contained at least one HTMLMediaElement. In addition, the telemetry probe 'MIXED_CONTENT_UNBLOCK_COUNTER' records the total number of loaded top level content documents, which can help us calculate the result of the percentage of documents using media.
+ expires: never
+ kind: uint
+ notification_emails:
+ - media-alerts@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ control_platform_usage:
+ bug_numbers:
+ - 1680856
+ description: >
+ Counts the number of times users enable and use media control on different platforms. 0=EnabledOn, 1=UsedOn.
+ expires: "91"
+ kind: uint
+ keyed: true
+ notification_emails:
+ - media-alerts@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ decode_error_per_mime_type:
+ bug_numbers:
+ - 1688775
+ - 1804291
+ description: >
+ Count the number of times the media with a specific mime type (Eg. audio/vorbis, that would be used as the key) encounters a decode error during playback.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - media-alerts@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'content'
+ - 'gpu'
+ - 'rdd'
+ - 'utility'
+
+ audio_process_per_codec_name:
+ bug_numbers:
+ - 1804249
+ - 1861305
+ description: >
+ Process where audio decoding happens and codec name
+ expires: "132"
+ kind: uint
+ keyed: true
+ notification_emails:
+ - media-alerts@mozilla.com
+ - alissy@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ record_in_processes:
+ - 'main'
+ - 'content'
+ - 'rdd'
+ - 'utility'
+ release_channel_collection: opt-out
+
+ video_hardware_decoding_support:
+ bug_numbers:
+ - 1733722
+ - 1754657
+ - 1811157
+ description: >
+ Record whether hardware decoding is supported for the specific video codec. The key is the type of video codec, eg. video/vp9.
+ expires: never
+ kind: boolean
+ keyed: true
+ notification_emails:
+ - media-alerts@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ - 'content'
+
+ video_hd_hardware_decoding_support:
+ bug_numbers:
+ - 1741286
+ - 1754657
+ - 1811157
+ description: >
+ Record whether hardware decoding is supported for the specific video codec, this only records video in HD (1280*720) or higher resultion. The key is the type of video codec, eg. video/vp9.
+ expires: never
+ kind: boolean
+ keyed: true
+ notification_emails:
+ - media-alerts@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ - 'content'
+
+ device_hardware_decoding_support:
+ bug_numbers:
+ - 1850594
+ description: >
+ Record the hardware decoding availability on devices, the key is the name of video codec, eg. h264, av1.
+ expires: never
+ kind: boolean
+ keyed: true
+ notification_emails:
+ - media-alerts@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'gpu'
+
+# The following section contains content process base counters.
+dom.contentprocess:
+ buildID_mismatch:
+ bug_numbers:
+ - 1366808
+ description: >
+ The number of times the about:restartrequired page appeared due to a
+ buildID mismatch between the parent and the content processes.
+ expires: never
+ kind: uint
+ notification_emails:
+ - spohl@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+ buildID_mismatch_false_positive:
+ bug_numbers:
+ - 1651133
+ - 1730045
+ - 1754658
+ - 1777404
+ - 1817104
+ - 1866197
+ description: >
+ The number of times a process crashed early but we could verify it was not
+ because of buildID mismatch between the parent and the content processes.
+ expires: "135"
+ kind: uint
+ notification_emails:
+ - alissy@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ os_priority_lowered:
+ bug_numbers:
+ - 1538987
+ description: >
+ The number of times a content process has had its OS priority lowered
+ due to only containing background tabs without audible media playing.
+ expires: never
+ kind: uint
+ notification_emails:
+ - mconley@mozilla.com
+ - gsvelto@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+ os_priority_raised:
+ bug_numbers:
+ - 1538987
+ description: >
+ The number of times a content process has had its OS priority raised
+ due to containing at least one foregrounded tab, or a tab with audible
+ media has started playing in it.
+ expires: never
+ kind: uint
+ notification_emails:
+ - mconley@mozilla.com
+ - gsvelto@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+ os_priority_change_considered:
+ bug_numbers:
+ - 1538987
+ description: >
+ The number of times we've had the opportunity to change content process
+ priority due to a tab switch or a tab being opened.
+ expires: never
+ kind: uint
+ notification_emails:
+ - mconley@mozilla.com
+ - gsvelto@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+# The following section contains parent process base counters.
+dom.parentprocess:
+ private_window_used:
+ bug_numbers:
+ - 1491047
+ description: >
+ Whether a private browsing window has been used in the session.
+ expires: never
+ kind: boolean
+ notification_emails:
+ - seceng-telemetry@mozilla.com
+ - ehsan+telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ process_launch_errors:
+ bug_numbers:
+ - 1819311
+ description: >
+ Collect precise set of error code and calling site upon process creation failure path.
+ expires: "never"
+ kind: uint
+ keyed: true
+ notification_emails:
+ - alissy@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ release_channel_collection: opt-out
+
+devtools.toolbar.eyedropper:
+ opened:
+ bug_numbers:
+ - 1247985
+ - 1352115
+ description: Number of times the DevTools Eyedropper has been opened via the inspector toolbar.
+ expires: never
+ kind: uint
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+devtools.copy.unique.css.selector:
+ opened:
+ bug_numbers:
+ - 1323700
+ - 1352115
+ - 1566395
+ description: Number of times the DevTools copy unique CSS selector has been used.
+ expires: never
+ kind: uint
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ - mbalfanz@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+devtools.copy.full.css.selector:
+ opened:
+ bug_numbers:
+ - 1323700
+ - 1352115
+ - 1566395
+ description: Number of times the DevTools copy full CSS selector has been used.
+ expires: never
+ kind: uint
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ - mbalfanz@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+devtools.copy.xpath:
+ opened:
+ bug_numbers:
+ - 987877
+ - 1566395
+ description: Number of times the DevTools copy XPath has been used.
+ expires: never
+ kind: uint
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ - mbalfanz@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+devtools.layout.flexboxhighlighter:
+ opened:
+ bug_numbers:
+ - 1509907
+ description: >
+ Number of times the DevTools flexbox highlighter was activated from the layout view.
+ expires: never
+ kind: uint
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+devtools.markup.flexboxhighlighter:
+ opened:
+ bug_numbers:
+ - 1509907
+ description: >
+ Number of times the DevTools flexbox highlighter was activated from the markup view.
+ expires: never
+ kind: uint
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+devtools.rules.flexboxhighlighter:
+ opened:
+ bug_numbers:
+ - 1509907
+ description: >
+ Number of times the DevTools flexbox highlighter was activated from the rules view.
+ expires: never
+ kind: uint
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+devtools.markup.gridinspector:
+ opened:
+ bug_numbers:
+ - 1509907
+ description: >
+ Number of times the DevTools grid inspector was opened from the markup view.
+ expires: never
+ kind: uint
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+devtools.rules.gridinspector:
+ opened:
+ bug_numbers:
+ - 1373483
+ description: >
+ Number of times the DevTools grid inspector was opened from the rules view.
+ expires: never
+ kind: uint
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+devtools.grid.gridinspector:
+ opened:
+ bug_numbers:
+ - 1373483
+ description: >
+ Number of times the DevTools grid inspector was opened from the grid view.
+ expires: never
+ kind: uint
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+devtools.grid.showGridAreasOverlay:
+ checked:
+ bug_numbers:
+ - 1373483
+ description: >
+ Number of times the DevTools grid inspector's "Display grid areas" was checked.
+ expires: never
+ kind: uint
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+devtools.grid.showGridLineNumbers:
+ checked:
+ bug_numbers:
+ - 1373483
+ description: >
+ Number of times the DevTools grid inspector's "Display grid numbers" was checked.
+ expires: never
+ kind: uint
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+devtools.grid.showInfiniteLines:
+ checked:
+ bug_numbers:
+ - 1373483
+ description: >
+ Number of times the DevTools grid inspector's "Extend grid lines infinitely" was checked.
+ expires: never
+ kind: uint
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+devtools.inspector:
+ three_pane_enabled:
+ bug_numbers:
+ - 1437881
+ - 1566395
+ description: >
+ Number of times the DevTools inspector was opened with the 3 pane inspector enabled,
+ keyed by true/false.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ - gl@mozilla.com
+ - mbalfanz@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ node_selection_count:
+ bug_numbers:
+ - 1550794
+ description: >
+ Number of times a different node is marked as selected in the Inspector regardless
+ of the cause: context menu, manual selection in markup view, etc.
+ expires: "never"
+ kind: uint
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ - mbalfanz@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+devtools.shadowdom:
+ shadow_root_displayed:
+ bug_numbers:
+ - 1470128
+ - 1566393
+ description: >
+ Whether the markup view displayed any #shadow-root element in the UI.
+ expires: "never"
+ kind: boolean
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ - jdescottes@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ shadow_root_expanded:
+ bug_numbers:
+ - 1470128
+ - 1566393
+ description: >
+ Whether the user expanded any #shadow-root element.
+ expires: "never"
+ kind: boolean
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ - jdescottes@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ reveal_link_clicked:
+ bug_numbers:
+ - 1470128
+ - 1566393
+ description: >
+ Whether the user clicked on any "reveal" link. "reveal" links are displayed in
+ shadow dom trees in the markup view.
+ expires: "never"
+ kind: boolean
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ - jdescottes@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+devtools:
+ current_theme:
+ bug_numbers:
+ - 1396811
+ description: >
+ Number of times DevTools was opened, keyed by theme.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+devtools.tooltip:
+ shown:
+ bug_numbers:
+ - 1553471
+ description: >
+ Number of times a tooltip was shown, keyed by tooltip type. Currently supported types are "image", "font-family", "inactive-css", "css-compatibility", "css-query-container" and "variable."
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+devtools.responsive:
+ toolbox_opened_first:
+ bug_numbers:
+ - 1444497
+ description: >
+ Number of Responsive Design Mode opens with a toolbox already open.
+ expires: never
+ kind: uint
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ - jryans@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - main
+ release_channel_collection: opt-out
+ open_trigger:
+ bug_numbers:
+ - 1444497
+ description: >
+ Number of Responsive Design Mode opens keyed by the UI entry point used.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ - jryans@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - main
+ release_channel_collection: opt-out
+
+devtools.tool:
+ registered:
+ bug_numbers:
+ - 1447302
+ - 1503568
+ - 1587985
+ description: >
+ Recorded on enable tool checkbox check/uncheck in Developer Tools options
+ panel. Boolean stating if the tool was enabled or disabled by the user.
+ Keyed by tool id. Current default tools with their id's are defined in
+ https://searchfox.org/mozilla-central/source/devtools/client/definitions.js
+ expires: never
+ kind: boolean
+ keyed: true
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ - accessibility@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+devtools.accessibility:
+ node_inspected_count:
+ bug_numbers:
+ - 1447302
+ - 1503568
+ - 1587985
+ description: >
+ Number of times a DOM node was inspected from within the Accessibility
+ tool.
+ expires: never
+ kind: uint
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ - accessibility@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ opened_count:
+ bug_numbers:
+ - 1447302
+ - 1503568
+ - 1587985
+ description: >
+ Number of times the DevTools Accessibility tool has been opened.
+ expires: never
+ kind: uint
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ - accessibility@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ picker_used_count:
+ bug_numbers:
+ - 1447302
+ - 1503568
+ - 1587985
+ description: >
+ Number of times the picker tool has been used in DevTools Accessibility
+ panel.
+ expires: never
+ kind: uint
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ - accessibility@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ select_accessible_for_node:
+ bug_numbers:
+ - 1447302
+ - 1503568
+ - 1587985
+ description: >
+ Number of times an accessible object was inspected from outside the
+ Accessibility tool (navigation to Accessibility panel). Keyed by the
+ source of user action (inspector context menu, browser context menu, etc).
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ - accessibility@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ service_enabled_count:
+ bug_numbers:
+ - 1447302
+ - 1503568
+ - 1587985
+ description: >
+ Number of times platform accessibility has been enabled in DevTools
+ Accessibility panel.
+ expires: never
+ kind: uint
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ - accessibility@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ accessible_context_menu_opened:
+ bug_numbers:
+ - 1507870
+ - 1587985
+ description: >
+ Number of times a context menu was opened for an accessible object in the
+ accessibility tree.
+ expires: never
+ kind: uint
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ - accessibility@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ accessible_context_menu_item_activated:
+ bug_numbers:
+ - 1507870
+ - 1587985
+ description: >
+ Number of times a context menu item for an accessible object was activated (with
+ mouse or keyboard) from the context menu opened in the accessibility tree. Keyed by
+ the id of the context menu item.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ - accessibility@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ audit_activated:
+ bug_numbers:
+ - 1548241
+ - 1587985
+ description: >
+ Number of times accessibility audit was activated (with mouse or keyboard)
+ from the accessibility panel's toolbar. Keyed by the audit filter type
+ (e.g. "CONTRAST").
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ - accessibility@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ simulation_activated:
+ bug_numbers:
+ - 1567200
+ - 1587985
+ description: >
+ Number of times accessibility simulation was activated (with mouse or keyboard)
+ from the accessibility panel's toolbar. Keyed by the simulation type
+ (e.g. "DEUTERANOPIA").
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ - accessibility@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+devtools.application:
+ opened_count:
+ bug_numbers:
+ - 1451734
+ description: >
+ Number of times the DevTools Application panel has been opened.
+ expires: "66"
+ kind: uint
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ - jdescottes@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+devtools.toolbox:
+ tabs_reordered:
+ bug_numbers:
+ - 1456551
+ - 1566362
+ description: >
+ Number of times the DevTools tab was reordered.
+ Keyed by tab's id.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ - daisuke@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+devtools.changesview:
+ opened_count:
+ bug_numbers:
+ - 1509890
+ description: >
+ Number of times the Changes panel has been opened.
+ expires: never
+ kind: uint
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ - mbalfanz@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+devtools.markup.scrollable.badge:
+ clicked:
+ bug_numbers:
+ - 1660818
+ description: >
+ Number of times the scrollable inspector badge has been clicked.
+ expires: never
+ kind: uint
+ notification_emails:
+ - dev-developer-tools@lists.mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+dom.event:
+ confluence_load_count:
+ bug_numbers:
+ - 1514940
+ description: >
+ Number of times Confluence instances are loaded, and whether they are
+ compatible with conflated keypress event model or not, using "new"/"old".
+ I.e., collecting percentage of too old Confluence instance use count in
+ all Confluence instance use count.
+ expires: "71"
+ keyed: true
+ kind: uint
+ notification_emails:
+ - mnakano.birchill@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'content'
+
+ office_online_load_count:
+ bug_numbers:
+ - 1545410
+ description: >
+ Number of times Office Online Server instances are loaded, and whether
+ they are compatible with conflated keypress event model or not, using
+ "new"/"old". I.e., collecting percentage of too old Confluence instance
+ use count in all Office Online Server instance use count.
+ expires: "71"
+ keyed: true
+ kind: uint
+ notification_emails:
+ - mnakano.birchill@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'content'
+
+navigator.storage:
+ estimate_count:
+ bug_numbers:
+ - 1359708
+ description: >
+ Number of times navigator.storage.estimate has been used.
+ expires: "60"
+ kind: uint
+ notification_emails:
+ - shuang@mozilla.com
+ - ttung@mozilla.com
+ release_channel_collection: opt-in
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+ - 'content'
+
+ persist_count:
+ bug_numbers:
+ - 1359708
+ description: >
+ Number of times navigator.storage.persist has been used.
+ expires: "60"
+ kind: uint
+ notification_emails:
+ - shuang@mozilla.com
+ - ttung@mozilla.com
+ release_channel_collection: opt-in
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+ - 'content'
+
+mozstorage:
+ sqlitejsm_transaction_timeout:
+ bug_numbers:
+ - 1727261
+ description: >
+ Collection of javascript modules that created a Sqlite.sys.mjs transaction
+ taking too long and timing out. This can be used to identify and optimize
+ those modules transactions.
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - mak@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+telemetry:
+ accumulate_unknown_histogram_keys:
+ bug_numbers:
+ - 1343855
+ description: >
+ The count of attempted accumulations to unknown histogram keys for
+ histograms that restrict the set of allowed keys ('keys' property).
+ The names of the offending histograms are used as keys in this probe.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - all
+
+ accumulate_clamped_values:
+ bug_numbers:
+ - 1438335
+ description: >
+ The count of accumulations that had to be clamped down to not overflow,
+ keyed to the histogram name of the overflowing accumulation.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ - chutten@mozilla.com
+ release_channel_collection: opt-in
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ event_counts:
+ bug_numbers:
+ - 1440673
+ description: >
+ The counts of events recorded in the process, category, method, and
+ object of the key, even if event recording for the category was not
+ enabled.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ - chutten@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'all'
+
+ data_upload_optin:
+ bug_numbers:
+ - 1445921
+ description: >
+ User opted into sending Telemetry data again.
+ expires: "never"
+ kind: boolean
+ notification_emails:
+ - jrediger@mozilla.com
+ - telemetry-client-dev@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - 'main'
+
+ keyed_scalars_exceed_limit:
+ bug_numbers:
+ - 1451813
+ description: >
+ The number of times keyed scalars exceeded the number of keys limit, keyed by scalar name.
+ expires: "never"
+ kind: uint
+ keyed: true
+ notification_emails:
+ - chutten@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ keyed_scalars_unknown_keys:
+ bug_numbers:
+ - 1365529
+ description: >
+ The count of attempted accumulations to unknown scalar keys for
+ scalars that restrict the set of allowed keys ('keys' property).
+ The names of the offending scalars are used as keys in this probe.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ - brizental@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - all
+
+
+telemetry.discarded:
+ accumulations:
+ bug_numbers:
+ - 1369041
+ description: >
+ Number of discarded accumulations to histograms in child processes
+ expires: "never"
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'all_childs'
+ keyed_accumulations:
+ bug_numbers:
+ - 1369041
+ description: >
+ Number of discarded accumulations to keyed histograms in child processes
+ expires: "never"
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'all_childs'
+ scalar_actions:
+ bug_numbers:
+ - 1369041
+ description: >
+ Number of discarded actions on scalars in child processes
+ expires: "never"
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'all_childs'
+ keyed_scalar_actions:
+ bug_numbers:
+ - 1369041
+ description: >
+ Number of discarded actions on keyed scalars in child processes
+ expires: "never"
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'all_childs'
+ child_events:
+ bug_numbers:
+ - 1369041
+ description: >
+ Number of discarded events in child processes
+ expires: "never"
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'all_childs'
+
+normandy:
+ recipe_freshness:
+ bug_numbers:
+ - 1530508
+ description: >
+ For each recipe ID seen by the Normandy client, its last_modified.
+ expires: "never"
+ keyed: true
+ kind: uint
+ notification_emails:
+ - product-delivery@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - main
+
+# The following section contains graphics-related scalars.
+gfx.advanced.layers:
+ failure_id:
+ bug_numbers:
+ - 1365879
+ description: >
+ Each key is a failure for initializing Advanced Layers, and each value
+ is the number of times that failure was recorded. The failure codes
+ are unique strings present in MLGDeviceD3D11::Initialize, such that
+ a failure can be traced back to a specific Direct3D API call.
+ keyed: true
+ kind: uint
+ expires: "70"
+ notification_emails:
+ - gfx-telemetry-alerts@mozilla.com
+ - rhunt@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+gfx.omtp:
+ paint_wait_ratio:
+ bug_numbers:
+ - 1386968
+ - 1518669
+ - 1673949
+ - 1674950
+ description: >
+ Ratio (in units of 1/100th of a percent) of how many times OMTP waited
+ for a paint for more than 200us, versus the total number of paints.
+ keyed: false
+ kind: uint
+ expires: "never" # 2021-10-01 this will move back to non-release collection.
+ notification_emails:
+ - gfx-telemetry-alerts@mozilla.com
+ - rhunt@mozilla.com
+ - perf-telemetry-alerts@mozilla.com
+ - barret@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'content'
+
+gfx.canvas.remote:
+ activated:
+ bug_numbers:
+ - 1641722
+ - 1672582
+ - 1706844
+ - 1739290
+ description: >
+ Number of times remote canvas 2D has been activated.
+ kind: uint
+ expires: "103"
+ notification_emails:
+ - gfx-telemetry-alerts@mozilla.com
+ - bowen@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'gpu'
+
+ deactivated_no_device:
+ bug_numbers:
+ - 1641722
+ - 1672582
+ - 1706844
+ - 1739290
+ - 1852144
+ description: >
+ Number of times remote canvas 2D has been deactivated due to device creation failure.
+ kind: uint
+ expires: "130"
+ notification_emails:
+ - gfx-telemetry-alerts@mozilla.com
+ - bowen@mozilla.com
+ - aosmond@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'gpu'
+
+ deactivated_bad_stream:
+ bug_numbers:
+ - 1641722
+ - 1672582
+ - 1706844
+ - 1739290
+ - 1852144
+ description: >
+ Number of times remote canvas 2D has been deactivated due to a stream read failure.
+ kind: uint
+ expires: "130"
+ notification_emails:
+ - gfx-telemetry-alerts@mozilla.com
+ - bowen@mozilla.com
+ - aosmond@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'gpu'
+
+gfx.hdr:
+ windows_display_colorspace_bitfield:
+ bug_numbers:
+ - 1514840
+ - 1690597
+ description: >
+ A bitfield representation of the available DXGI color spaces of the connected displays on Windows.
+ See (https://docs.microsoft.com/en-us/windows/desktop/api/dxgicommon/ne-dxgicommon-dxgi_color_space_type)
+ for definitions of color spaces. Each N'th bit of this scalar indicates whether the DXGI color space with
+ index 'N' is available on at least one connected monitor.
+ keyed: false
+ kind: uint
+ expires: never
+ notification_emails:
+ - gfx-telemetry-alerts@mozilla.com
+ - jmuizelaaar@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+gfx:
+ skipped_composites:
+ bug_numbers:
+ - 1797975
+ description: >
+ Number of skipped composites, happening when rendering is too slow to keep up with content.
+ keyed: false
+ kind: uint
+ expires: "never"
+ notification_emails:
+ - gfx-telemetry-alerts@mozilla.com
+ - perf-telemetry-alerts@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+ - 'gpu'
+ os_compositor:
+ bug_numbers:
+ - 1623492
+ description: >
+ Boolean indicating whether the os compositor is being used by WebRender.
+ Only collected in the first subsession.
+ expires: never
+ kind: boolean
+ notification_emails:
+ - gfx-telemetry-alerts@mozilla.com
+ - jmuizelaar@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ release_channel_collection: opt-out
+ linux_window_protocol:
+ bug_numbers:
+ - 1645732
+ description: >
+ Windowing protocol on Linux. Can be Wayland, WaylandDRM, XWayland, or X11
+ expires: never
+ kind: string
+ notification_emails:
+ - gfx-telemetry-alerts@mozilla.com
+ - aosmond@mozilla.com
+ - mkaply@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ release_channel_collection: opt-out
+ supports_hdr:
+ bug_numbers:
+ - 1798067
+ description: >
+ Does the hardware support accurate display of HDR content
+ expires: never
+ kind: boolean
+ notification_emails:
+ - gfx-telemetry-alerts@mozilla.com
+ - bwerth@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ release_channel_collection: opt-out
+
+apz:
+ scrollwheel_overshoot:
+ bug_numbers:
+ - 1836870
+ description: >
+ Count of overshoot events, where the user reverses scrollwheel direction soon after the last scrollwheel input.
+ expires: "126"
+ kind: uint
+ notification_emails:
+ - botond@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ - 'gpu'
+ release_channel_collection: opt-out
+
+# The following section contains the form autofill related scalars.
+formautofill:
+ availability:
+ bug_numbers:
+ - 1386959
+ description: A boolean sent once per session to represent whether the formautofill is available in the build
+ expires: never
+ kind: boolean
+ notification_emails:
+ - autofill@lists.mozilla.org
+ - passwords-dev@mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+formautofill.addresses:
+ detected_sections_count:
+ bug_numbers:
+ - 1801039
+ description: >
+ Count at detection time number of address form sections.
+ A single form can contain more than one address form section.
+ expires: never
+ kind: uint
+ notification_emails:
+ - autofill@lists.mozilla.org
+ - passwords-dev@mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'content'
+ submitted_sections_count:
+ bug_numbers:
+ - 1656344
+ description: >
+ Count at submission time number of credit card form sections submitted.
+ expires: never
+ kind: uint
+ notification_emails:
+ - autofill@lists.mozilla.org
+ - passwords-dev@mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'content'
+ autofill_profiles_count:
+ bug_numbers:
+ - 1801039
+ description: >
+ Count at store time how many address autofill profiles user has.
+ expires: never
+ kind: uint
+ notification_emails:
+ - autofill@lists.mozilla.org
+ - passwords-dev@mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+formautofill.creditCards:
+ autofill_profiles_count:
+ bug_numbers:
+ - 990203
+ description: >
+ Count at store time how many credit card autofill profiles user has.
+ expires: never
+ kind: uint
+ notification_emails:
+ - autofill@lists.mozilla.org
+ - passwords-dev@mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ detected_sections_count:
+ bug_numbers:
+ - 1656344
+ description: >
+ Count at detection time number of credit card form sections.
+ A single form can contain more than one credit card form section.
+ expires: never
+ kind: uint
+ notification_emails:
+ - autofill@lists.mozilla.org
+ - passwords-dev@mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'content'
+ submitted_sections_count:
+ bug_numbers:
+ - 1656344
+ description: >
+ Count at submission time number of credit card form sections submitted.
+ expires: never
+ kind: uint
+ notification_emails:
+ - autofill@lists.mozilla.org
+ - jmathies@mozilla.com
+ - chsiang@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'content'
+
+# This section is for notification bars testing
+notificationbar:
+ # Used only for testing
+ testone:
+ bug_numbers:
+ - 1676943
+ description: >
+ Used to test notificationbar telemetry
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ # Used only for testing
+ testtwo:
+ bug_numbers:
+ - 1676943
+ description: >
+ Used to test notificationbar telemetry
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+# The following section contains scalars for the Screenshots extension.
+screenshots:
+ download:
+ bug_numbers:
+ - 1412411
+ description: >
+ The count of new screenshots created, then downloaded per session
+ via the Firefox Screenshots feature. Excludes failed attempts due
+ to any error.
+ expires: never
+ kind: uint
+ notification_emails:
+ - screenshots-dev@mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ upload:
+ bug_numbers:
+ - 1412411
+ description: >
+ The count of new screenshots created, then uploaded per session
+ via the Firefox Screenshots feature. Excludes attempts to create
+ and upload the screenshot that may have failed.
+ expires: never
+ kind: uint
+ notification_emails:
+ - screenshots-dev@mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ copy:
+ bug_numbers:
+ - 1412411
+ description: >
+ The count of new screenshots created, then copied directly to the
+ clipboard per session via the Firefox Screenshots feature.
+ expires: never
+ kind: uint
+ notification_emails:
+ - screenshots-dev@mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ visible:
+ bug_numbers:
+ - 1722774
+ description: >
+ The count of new screenshots created using just the viewport,
+ no scrolling.
+ expires: never
+ kind: uint
+ notification_emails:
+ - screenshots-dev@mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ full_page:
+ bug_numbers:
+ - 1722774
+ description: >
+ The count of new screenshots created by capturing the full,
+ scrolled page.
+ expires: never
+ kind: uint
+ notification_emails:
+ - screenshots-dev@mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ custom:
+ bug_numbers:
+ - 1722774
+ description: >
+ The count of new screenshots created using the drag handles
+ to draw the box size and position.
+ expires: never
+ kind: uint
+ notification_emails:
+ - screenshots-dev@mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ element:
+ bug_numbers:
+ - 1722774
+ description: >
+ The count of new screenshots created using the highlighter
+ to select element bounds.
+ expires: never
+ kind: uint
+ notification_emails:
+ - screenshots-dev@mozilla.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+# The following section is for tracking the number of failure for indexedDB.
+idb.failure:
+ unknown_objectstore_empty_database:
+ bug_numbers:
+ - 1669730
+ description: >
+ Tracking the number of attempts to open an unknown objectstore on an
+ empty IndexedDB database.
+ expires: "89"
+ kind: uint
+ notification_emails:
+ - storage-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'content'
+ unknown_objectstore_non_empty_database:
+ bug_numbers:
+ - 1669730
+ description: >
+ Tracking the number of attempts to open an unknown objectstore on a
+ non-empty IndexedDB database.
+ expires: "89"
+ kind: uint
+ notification_emails:
+ - storage-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'content'
+
+# The following section contains scalars for printing.
+printing:
+ paper_size:
+ bug_numbers:
+ - 1660686
+ description: >
+ A counter of the number of times a given paper size (recorded as
+ "width x height" in points) is used for printing.
+ keyed: true
+ expires: "89"
+ kind: uint
+ notification_emails:
+ - jwatt@jwatt.org
+ - jkew@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'content'
+
+ dialog_opened_without_preview:
+ bug_numbers:
+ - 1600623
+ description: >
+ A counter incremented every time the print dialog is opened without
+ opening print preview.
+ expires: never
+ kind: uint
+ notification_emails:
+ - jwatt@jwatt.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'content'
+
+ dialog_without_preview_cancelled:
+ bug_numbers:
+ - 1600623
+ description: >
+ A counter incremented every time a print dialog that was opened without
+ print preview is cancelled.
+ expires: never
+ kind: uint
+ notification_emails:
+ - jwatt@jwatt.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'content'
+
+ dialog_opened_via_preview:
+ bug_numbers:
+ - 1600623
+ description: >
+ A counter incremented every time the print dialog is opened from the old
+ print preview.
+ expires: never
+ kind: uint
+ notification_emails:
+ - jwatt@jwatt.org
+ - emalysz@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'content'
+
+ dialog_opened_via_preview_tm:
+ bug_numbers:
+ - 1657220
+ description: >
+ A counter incremented every time a user opens the system print dialog
+ from the print preview interface.
+ expires: never
+ kind: uint
+ notification_emails:
+ - jwatt@jwatt.org
+ - emalysz@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ dialog_via_preview_cancelled:
+ bug_numbers:
+ - 1600623
+ description: >
+ A counter incremented every time a print dialog opened from the old print
+ preview is cancelled.
+ expires: never
+ kind: uint
+ notification_emails:
+ - jwatt@jwatt.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'content'
+
+ dialog_via_preview_cancelled_tm:
+ bug_numbers:
+ - 1657220
+ description: >
+ A counter incremented every time a user cancels a system print dialog
+ that they opened from the print preview interface.
+ expires: never
+ kind: uint
+ notification_emails:
+ - jwatt@jwatt.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ error:
+ bug_numbers:
+ - 1630105
+ description: >
+ A counter incremented every time the user encounters an error printing.
+ The result is split into keys which represent the values of
+ error codes related to printing. Possible values are listed in 'keys'.
+ "FAILURE" is a catch-all code for an error we don't recognize.
+ keyed: true
+ keys:
+ - 'GFX_PRINTER_NO_PRINTER_AVAILABLE'
+ - 'GFX_PRINTER_NAME_NOT_FOUND'
+ - 'GFX_PRINTER_COULD_NOT_OPEN_FILE'
+ - 'GFX_PRINTER_STARTDOC'
+ - 'GFX_PRINTER_ENDDOC'
+ - 'GFX_PRINTER_STARTPAGE'
+ - 'GFX_PRINTER_DOC_IS_BUSY'
+ - 'ABORT'
+ - 'NOT_AVAILABLE'
+ - 'NOT_IMPLEMENTED'
+ - 'OUT_OF_MEMORY'
+ - 'UNEXPECTED'
+ - 'FAILURE'
+ - 'FALLBACK_PAPER_LIST'
+ - 'LAST_USED_PRINTER'
+ - 'PAPER_MARGINS'
+ - 'PRINT_DESTINATIONS'
+ - 'PRINT_PREVIEW'
+ - 'PRINTER_LIST'
+ - 'PRINTER_PROPERTIES'
+ - 'PRINTER_SETTINGS'
+ - 'UNWRITEABLE_MARGIN'
+ expires: never
+ kind: uint
+ notification_emails:
+ - jaws@mozilla.com
+ - jwatt@jwatt.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ preview_opened:
+ bug_numbers:
+ - 1600623
+ description: >
+ A counter incremented every time the user initiates the old print preview.
+ expires: never
+ kind: uint
+ notification_emails:
+ - jwatt@jwatt.org
+ - emalysz@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'content'
+
+ preview_opened_tm:
+ bug_numbers:
+ - 1657220
+ description: >
+ A counter incremented every time a user opens print preview.
+ expires: never
+ kind: uint
+ notification_emails:
+ - jwatt@jwatt.org
+ - emalysz@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ preview_cancelled:
+ bug_numbers:
+ - 1600623
+ description: >
+ A counter incremented every time an old print preview session exits
+ without printing.
+ expires: never
+ kind: uint
+ notification_emails:
+ - jwatt@jwatt.org
+ - emalysz@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'content'
+
+ preview_cancelled_tm:
+ bug_numbers:
+ - 1657220
+ description: >
+ A counter incremented every time a user exits print preview without
+ printing.
+ expires: never
+ kind: uint
+ notification_emails:
+ - jwatt@jwatt.org
+ - emalysz@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ settings_changed:
+ bug_numbers:
+ - 1653386
+ description: >
+ A count of how many times the user changed a setting in print preview,
+ broken down per setting.
+ keyed: true
+ expires: never
+ kind: uint
+ notification_emails:
+ - emalysz@mozilla.com
+ - rtestard@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - "firefox"
+ record_in_processes:
+ - 'main'
+
+ silent_print:
+ bug_numbers:
+ - 1600623
+ description: >
+ A counter incremented every time a silent print (a print without a
+ print settings dialog being opened) is initiated. This happens when
+ extensions invoke ExtensionAPI.tabs.saveAsPDF, for example, or when the
+ print.always_print_silent pref is set.
+ expires: never
+ kind: uint
+ notification_emails:
+ - jwatt@jwatt.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'content'
+
+ target_type:
+ bug_numbers:
+ - 1600623
+ description: >
+ A counter incremented every time the user prints to a certain target
+ type. The 'pdf_unknown' count is for printers with names that we don't
+ know about/check for, but that have 'pdf' in their name, and is mainly a
+ sanity check that we're not missing significant counts in 'pdf_file'.
+ For the most part, the 'unknown' count will be prints to a physical
+ printer, but we can't know for sure since third party drivers could also
+ be print to file drivers that we don't otherwise catch in the other
+ counts.
+ keyed: true
+ keys:
+ - 'pdf_file'
+ - 'pdf_unknown'
+ - 'xps_file'
+ - 'unknown'
+ expires: never
+ kind: uint
+ notification_emails:
+ - jwatt@jwatt.org
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+# The following section contains probes that record timestamps.
+timestamps:
+ first_paint:
+ bug_numbers:
+ - 1386186
+ description: >
+ Record the timestamp of the first content window paint, in milliseconds since process start.
+ expires: never
+ kind: uint
+ notification_emails:
+ - perf-telemetry-alerts@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - main
+
+ first_paint_two:
+ bug_numbers:
+ - 1556568
+ description: >
+ Record the timestamp of the first main window paint, in milliseconds since process start.
+ Intended to replace first_paint since first_paint is broken.
+ expires: never
+ kind: uint
+ notification_emails:
+ - perf-telemetry-alerts@mozilla.com
+ - gfx-telemetry-alerts@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - firefox
+ - fennec
+ record_in_processes:
+ - main
+
+ about_home_topsites_first_paint:
+ bug_numbers:
+ - 1518521
+ description: >
+ Record the timestamp of when the first about:home's Topsites are painted. Only records if
+ about:home is set as the default homepage, and if sessions are not being restored by default.
+ expires: never
+ kind: uint
+ notification_emails:
+ - perf-telemetry-alerts@mozilla.com
+ - mconley@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - main
+
+browser.timings:
+ last_shutdown:
+ bug_numbers:
+ - 1429510
+ description: >
+ The time, in milliseconds, it took to complete the last shutdown.
+ On successful shutdown, Telemetry saves this to disk into Telemetry.ShutdownTime.txt.
+ On the next startup this is loaded and recorded.
+ expires: never
+ kind: uint
+ notification_emails:
+ - perf-telemetry-alerts@mozilla.com
+ - florian@mozilla.com
+ release_channel_collection: opt-in
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+# The following section contains the service worker scalars.
+sw:
+ cors_res_for_so_req_count:
+ bug_numbers:
+ - 1416629
+ description: >
+ The count of number of synthesize response made by service workers and
+ it's a cors type resposne for a same-origin mode request.
+ expires: "61"
+ kind: uint
+ notification_emails:
+ - sw-telemetry@mozilla.com
+ - ttung@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+ - 'content'
+
+ alternative_body_used_count:
+ bug_numbers:
+ - 1423623
+ - 1433916
+ description: >
+ The count of number of synthesize response using alternative body.
+ expires: "68"
+ kind: uint
+ notification_emails:
+ - sw-telemetry@mozilla.com
+ - echuang@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+ - 'content'
+
+serviceworker:
+ registrations:
+ description: >
+ Count how many registrations occur. File bugs in Core::DOM in case of a Telemetry regression.
+
+ record_in_processes:
+ - 'main'
+ products:
+ - 'firefox'
+ - 'fennec'
+ expires: "102"
+ kind: uint
+ bug_numbers:
+ - 1740335
+ notification_emails:
+ - sw-telemetry@mozilla.com
+ - rjesup@mozilla.com
+ - echuang@mozilla.com
+ keyed: true
+ keys:
+ - All
+ - Fetch
+
+ running_max:
+ description: >
+ The maximum number of simultaneous running ServiceWorkers in a session.
+
+ record_in_processes:
+ - 'main'
+ products:
+ - 'firefox'
+ - 'fennec'
+ expires: "102"
+ kind: uint
+ bug_numbers:
+ - 1740335
+ notification_emails:
+ - sw-telemetry@mozilla.com
+ - rjesup@mozilla.com
+ - echuang@mozilla.com
+ keyed: true
+ keys:
+ - All
+ - Fetch
+
+widget:
+ ime_name_on_windows:
+ bug_numbers:
+ - 1215818
+ description: >
+ Locale ID and name of IME which was selected by users on Windows. This
+ does NOT collect legacy IMM-IME names since we cannot get readable names
+ and we do not support IMM-IME so aggressively because IME vendors
+ should've already released TIP for TSF for supporting Windows 8 or later
+ completely.
+ keyed: true
+ expires: never
+ kind: boolean
+ notification_emails:
+ - mnakano.birchill@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ ime_name_on_windows_inserted_crlf:
+ bug_numbers:
+ - 1865256
+ description:
+ Same data as ime_name_on_windows, but only collecting IME names which
+ inserted CRLF to check whether the feature works.
+ keyed: true
+ expires: never
+ kind: boolean
+ notification_emails:
+ - mnakano.birchill@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ dark_mode:
+ bug_numbers:
+ - 1601846
+ description: >
+ Whether the OS theme is dark.
+ expires: never
+ kind: boolean
+ notification_emails:
+ - layout-telemetry-alerts@mozilla.com
+ - cmccormack@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ ime_name_on_mac:
+ bug_numbers:
+ - 1215818
+ description: >
+ Name of IME which was selected by users on macOS. The value is Input
+ Source ID if non-Japanese IME was open. Otherwise, if Japanese IME was
+ open, the value is Bundle ID. Input Source ID includes input mode, but
+ Bundle ID does not include input mode.
+ keyed: true
+ expires: never
+ kind: boolean
+ notification_emails:
+ - mnakano.birchill@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ ime_name_on_linux:
+ bug_numbers:
+ - 1215818
+ description: >
+ Name of active IM (e.g., xim, fcitx, ibus, etc) which was actually set by
+ env on Linux. Different from Windows and macOS, this value includes
+ non-IME users even though this is recoded when first compositionstart
+ event because dead key is also implemented by IME on Linux.
+ keyed: true
+ expires: never
+ kind: boolean
+ notification_emails:
+ - mnakano.birchill@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ gtk_theme_has_scrollbar_buttons:
+ bug_numbers:
+ - 1669135
+ description: >
+ Whether the active Gtk theme has scrollbar buttons.
+ kind: boolean
+ expires: "87"
+ notification_emails:
+ - layout-telemetry-alerts@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ gtk_theme_scrollbar_uses_images:
+ bug_numbers:
+ - 1670145
+ description: >
+ Whether the active Gtk theme uses background images for its scrollbar
+ parts.
+ kind: boolean
+ expires: "87"
+ notification_emails:
+ - layout-telemetry-alerts@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ gtk_version:
+ bug_numbers:
+ - 1670145
+ description: >
+ The version of Gtk 3 in use.
+ kind: string
+ expires: never
+ notification_emails:
+ - layout-telemetry-alerts@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+# The following section contains memory reporter counters.
+memoryreporter:
+ max_ghost_windows:
+ bug_numbers:
+ - 1454724
+ description: >
+ The maximum number of leaked ghost windows seen.
+ expires: "66"
+ kind: uint
+ notification_emails:
+ - memshrink-telemetry-alerts@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'content'
+
+update:
+ binarytransparencyresult:
+ bug_numbers:
+ - 1501889
+ description: >
+ If the binary transparency information for an update does not verify
+ successfully, this probe will contain an error code from
+ toolkit/mozapps/update/common/updatererrors.h indicating why.
+ expires: "73"
+ kind: uint
+ keyed: true
+ notification_emails:
+ - application-update-telemetry-alerts@mozilla.com
+ - seceng-telemetry@mozilla.com
+ - dkeeler@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - main
+
+ bitshresult:
+ bug_numbers:
+ - 1343669
+ - 1540193
+ - 1520321
+ description: >
+ If a BITS download fails on the Windows side (that is to say, BITS
+ ecounters an error rather than Firefox failing to interact with BITS),
+ it will likely give an hresult error indicating what happened. This probe
+ reports those error codes to allow us to see if BITS is commonly failing
+ on some systems. This probe is keyed on the type of update download,
+ either "PARTIAL" or "COMPLETE".
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - application-update-telemetry-alerts@mozilla.com
+ - bytesized@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - main
+ operating_systems:
+ - windows
+
+ move_result:
+ bug_numbers:
+ - 353804
+ description: >
+ Firefox keeps downloading updates in a different place from updates that
+ are ready to install. Once the download completes, the resulting file has
+ to be moved from the downloading update directory to the ready update
+ directory. This probe counts the results that we get when attempting to
+ perform this file move. Valid values for the keys for this probe are
+ stored in the MOVE_RESULT_* values in UpdateTelemetry.jsm.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - application-update-telemetry-alerts@mozilla.com
+ - bytesized@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ no_window_auto_restarts:
+ bug_numbers:
+ - 1720742
+ description: >
+ On macOS, all browser windows can be closed without exiting Firefox. If
+ there is an update pending in that case, and we can install it silently,
+ we restart automatically in order to do so. This probe counts the number
+ of times the browser restarts to silently install a pending update.
+ expires: never
+ kind: uint
+ keyed: false
+ notification_emails:
+ - application-update-telemetry-alerts@mozilla.com
+ - bytesized@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ suppress_prompts:
+ bug_numbers:
+ - 1749155
+ description: >
+ When an update is available and app.update.auto is disabled, a popup is
+ opened prompting the user to download and install the update. The pref
+ app.update.suppressPrompts causes Firefox Nightly to wait up to 7 days
+ before showing the prompt, instead showing a badge and banner in the
+ meantime. It also prevents Nightly from showing update restart prompts,
+ instead showing a badge and banner immediately. This value is set for
+ the users who set this pref to true.
+ expires: never
+ kind: boolean
+ keyed: false
+ notification_emails:
+ - application-update-telemetry-alerts@mozilla.com
+ - bytesized@mozilla.com
+ release_channel_collection: opt-in
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ version_pin:
+ bug_numbers:
+ - 1762957
+ description: >
+ The AppUpdatePin Enterprise Policy controls the maximum version that the
+ browser can update to. If the policy is set, this probe will report the
+ policy's value. The possible policy values are "X." and "X.Y.", where
+ X is the pinned major version and Y is the pinned minor version.
+ expires: never
+ kind: string
+ keyed: false
+ notification_emails:
+ - application-update-telemetry-alerts@mozilla.com
+ - bytesized@mozilla.com
+ - telemetry-client-dev@thunderbird.net
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'thunderbird'
+ record_in_processes:
+ - main
+
+# The following section contains search counters.
+browser.search:
+ data_transferred:
+ bug_numbers:
+ - 1614777
+ - 1619954
+ - 1656135
+ - 1694852
+ - 1736658
+ description: >
+ Records bytes that have been transferred for search requests, including
+ top-level documents, sub-resources, iframes and search suggestions.
+ The key format could be one of:
+ <search-provider-name>
+ <search-provider-name>-pb
+ sggt-<search-engine-id>
+ sggt-<search-engine-id>-pb
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - tihuang@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ release_channel_collection: opt-out
+
+browser.search.content:
+ urlbar:
+ bug_numbers:
+ - 1089670
+ - 1475571
+ - 1482158
+ - 1545172
+ - 1634564
+ description: >
+ Records counts for in-content searches where the search was most likely started from the urlbar. The key format is <provider>:[tagged|tagged-follow-on|organic]:[code|other|none]
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ urlbar_handoff:
+ bug_numbers:
+ - 1732429
+ description: >
+ The count URI loads triggered in a subsession from the urlbar via handoff,
+ broken down by the originating action.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ urlbar_persisted:
+ bug_numbers:
+ - 1779471
+ description: >
+ Records counts for in-content searches where the search was most likely started from the urlbar after the search term was shown in the urlbar. The key format is <provider>:[tagged|tagged-follow-on|organic]:[code|other|none]
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ urlbar_searchmode:
+ bug_numbers:
+ - 1089670
+ - 1475571
+ - 1482158
+ - 1545172
+ - 1634564
+ description: >
+ Records counts for in-content searches where the search was most likely started from the urlbar in search mode. The key format is <provider>:[tagged|tagged-follow-on|organic]:[code|other|none]
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ searchbar:
+ bug_numbers:
+ - 1089670
+ - 1475571
+ - 1482158
+ - 1545172
+ - 1634564
+ description: >
+ Records counts for in-content searches where the search was most likely started from the search bar. The key format is <provider>:[tagged|tagged-follow-on|organic]:[code|other|none]
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ about_home:
+ bug_numbers:
+ - 1089670
+ - 1475571
+ - 1482158
+ - 1545172
+ - 1634564
+ description: >
+ Records counts for in-content searches where the search was most likely started from the about:home. The key format is <provider>:[tagged|tagged-follow-on|organic]:[code|other|none]
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ about_newtab:
+ bug_numbers:
+ - 1089670
+ - 1475571
+ - 1482158
+ - 1545172
+ - 1634564
+ description: >
+ Records counts for in-content searches where the search was most likely started from the about:newtab. The key format is <provider>:[tagged|tagged-follow-on|organic]:[code|other|none]
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ contextmenu:
+ bug_numbers:
+ - 1089670
+ - 1475571
+ - 1482158
+ - 1545172
+ - 1634564
+ description: >
+ Records counts for in-content searches where the search was most likely started from the context menu. The key format is <provider>:[tagged|tagged-follow-on|organic]:[code|other|none]
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ webextension:
+ bug_numbers:
+ - 1089670
+ - 1475571
+ - 1482158
+ - 1545172
+ - 1634564
+ description: >
+ Records counts for in-content searches where the search was most likely started from a webextension. The key format is <provider>:[tagged|tagged-follow-on|organic]:[code|other|none]
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ system:
+ bug_numbers:
+ - 1089670
+ - 1475571
+ - 1482158
+ - 1545172
+ - 1634564
+ description: >
+ Records counts for in-content searches where the search was most likely started from the system, e.g. command line. The key format is <provider>:[tagged|tagged-follow-on|organic]:[code|other|none]
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ tabhistory:
+ bug_numbers:
+ - 1634564
+ description: >
+ Records counts for in-content searches where the search was loaded from the tab history. The key format is <provider>:[tagged|tagged-follow-on|organic]:[code|other|none]
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ reload:
+ bug_numbers:
+ - 1634564
+ description: >
+ Records counts for in-content searches where the search was loaded by reloading the page. The key format is <provider>:[tagged|tagged-follow-on|organic]:[code|other|none]
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ unknown:
+ bug_numbers:
+ - 1089670
+ - 1475571
+ - 1482158
+ - 1545172
+ - 1634564
+ description: >
+ Records counts for in-content searches where the search was started from a place that could not be determined or not categorized by the other scalars. The key format is <provider>:[tagged|tagged-follow-on|organic]:[code|other|none]
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+browser.search.withads:
+ urlbar:
+ bug_numbers:
+ - 1664847
+ description: >
+ Records counts of SERP pages with adverts displayed where the search was started from the urlbar. The key format is ‘<provider>:<tagged|organic>’
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ urlbar_handoff:
+ bug_numbers:
+ - 1732429
+ description: >
+ The count URI loads triggered in a subsession from the urlbar via handoff,
+ broken down by the originating action.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ urlbar_persisted:
+ bug_numbers:
+ - 1779471
+ description: >
+ Records counts of SERP pages with adverts displayed where the search was started from the urlbar after the search term was shown in the urlbar. The key format is ‘<provider>:<tagged|organic>’
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ urlbar_searchmode:
+ bug_numbers:
+ - 1664847
+ description: >
+ Records counts of SERP pages with adverts displayed where the search was started from the urlbar in search mode. The key format is ‘<provider>:<tagged|organic>’
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ searchbar:
+ bug_numbers:
+ - 1664847
+ description: >
+ Records counts of SERP pages with adverts displayed where the search was started from the search bar. The key format is ‘<provider>:<tagged|organic>’
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ about_home:
+ bug_numbers:
+ - 1664847
+ description: >
+ Records counts of SERP pages with adverts displayed where the search was started from about:home. The key format is ‘<provider>:<tagged|organic>’
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ about_newtab:
+ bug_numbers:
+ - 1664847
+ description: >
+ Records counts of SERP pages with adverts displayed where the search was started from about:newtab. The key format is ‘<provider>:<tagged|organic>’
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ contextmenu:
+ bug_numbers:
+ - 1664847
+ description: >
+ Records counts of SERP pages with adverts displayed where the search was started from the context menu. The key format is ‘<provider>:<tagged|organic>’
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ webextension:
+ bug_numbers:
+ - 1664847
+ description: >
+ Records counts of SERP pages with adverts displayed where the search was started from a webextension. The key format is ‘<provider>:<tagged|organic>’
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ system:
+ bug_numbers:
+ - 1664847
+ description: >
+ Records counts of SERP pages with adverts displayed where the search was started from the system, e.g. command line. The key format is ‘<provider>:<tagged|organic>’
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ tabhistory:
+ bug_numbers:
+ - 1664847
+ description: >
+ Records counts of SERP pages with adverts displayed where the search was loaded from the tab history. The key format is ‘<provider>:<tagged|organic>’
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ reload:
+ bug_numbers:
+ - 1664847
+ description: >
+ Records counts of SERP pages with adverts displayed where the search was loaded by reloading the page. The key format is ‘<provider>:<tagged|organic>’
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ unknown:
+ bug_numbers:
+ - 1664847
+ description: >
+ Records counts of SERP pages with adverts displayed where the search was started from a place not categorized by the other scalars. The key format is ‘<provider>:<tagged|organic>’
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+browser.search.adclicks:
+ urlbar:
+ bug_numbers:
+ - 1664847
+ description: >
+ Records clicks of adverts on SERP pages where the search was started from the urlbar. The key format is ‘<provider>:<tagged|organic>’
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ urlbar_handoff:
+ bug_numbers:
+ - 1732429
+ description: >
+ The count URI loads triggered in a subsession from the urlbar via handoff,
+ broken down by the originating action.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ urlbar_persisted:
+ bug_numbers:
+ - 1779471
+ description: >
+ Records clicks of adverts on SERP pages where the search was started from the urlbar after the search term was shown in the urlbar. The key format is ‘<provider>:<tagged|organic>’
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ urlbar_searchmode:
+ bug_numbers:
+ - 1664847
+ description: >
+ Records clicks of adverts on SERP pages where the search was started from the urlbar in search mode. The key format is ‘<provider>:<tagged|organic>’
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ searchbar:
+ bug_numbers:
+ - 1664847
+ description: >
+ Records clicks of adverts on SERP pages where the search was started from the search bar. The key format is ‘<provider>:<tagged|organic>’
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ about_home:
+ bug_numbers:
+ - 1664847
+ description: >
+ Records clicks of adverts on SERP pages where the search was started from about:home. The key format is ‘<provider>:<tagged|organic>’
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ about_newtab:
+ bug_numbers:
+ - 1664847
+ description: >
+ Records clicks of adverts on SERP pages where the search was started from about:newtab. The key format is ‘<provider>:<tagged|organic>’
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ contextmenu:
+ bug_numbers:
+ - 1664847
+ description: >
+ Records clicks of adverts on SERP pages where the search was started from the context menu. The key format is ‘<provider>:<tagged|organic>’
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ webextension:
+ bug_numbers:
+ - 1664847
+ description: >
+ Records clicks of adverts on SERP pages where the search was started from a webextension. The key format is ‘<provider>:<tagged|organic>’
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ system:
+ bug_numbers:
+ - 1664847
+ description: >
+ Records clicks of adverts on SERP pages where the search was started from the system, e.g. command line. The key format is ‘<provider>:<tagged|organic>’
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ tabhistory:
+ bug_numbers:
+ - 1664847
+ description: >
+ Records clicks of adverts on SERP pages where the search was loaded from the tab history. The key format is ‘<provider>:<tagged|organic>’
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ reload:
+ bug_numbers:
+ - 1664847
+ description: >
+ Records clicks of adverts on SERP pages where the search was loaded by reloading the page. The key format is ‘<provider>:<tagged|organic>’
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ unknown:
+ bug_numbers:
+ - 1664847
+ description: >
+ Records clicks of adverts on SERP pages where the search was started from a place not categorized by the other scalars. The key format is ‘<provider>:<tagged|organic>’
+ See https://firefox-source-docs.mozilla.org/browser/search/telemetry.html#browser-search-content
+ expires: never
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - rev-data@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+browser.searchinit:
+ engine_invalid_webextension:
+ bug_numbers:
+ - 1665938
+ - 1691050
+ - 1725501
+ - 1763528
+ - 1801017
+ - 1832463
+ - 1871199
+ description: >
+ Records the WebExtension ID of a search engine where the WebExtension
+ is not installed (= 1), disabled (= 2), search engine no longer specified (= 4),
+ a different name (= 5), where the submission URL is different between the
+ search engine and the WebExtension (= 6). The value '3' has been replaced
+ by '6' to distinguish newer entries.
+ expires: "129"
+ keyed: true
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - mbanner@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ release_channel_collection: opt-out
+
+ secure_opensearch_engine_count:
+ bug_numbers:
+ - 1789438
+ - 1811158
+ - 1846899
+ description: >
+ Records the number of secure (i.e., using https) OpenSearch search
+ engines a given user has installed
+ expires: "126"
+ keyed: false
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ release_channel_collection: opt-out
+
+ insecure_opensearch_engine_count:
+ bug_numbers:
+ - 1789438
+ - 1811158
+ - 1846899
+ description: >
+ Records the number of insecure (i.e., using http) OpenSearch search
+ engines a given user has installed
+ expires: "126"
+ keyed: false
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ release_channel_collection: opt-out
+
+ secure_opensearch_update_count:
+ bug_numbers:
+ - 1789438
+ - 1811158
+ - 1846899
+ description: >
+ Records the number of OpenSearch search engines with secure updates
+ enabled (i.e., using https) a given user has installed
+ expires: "126"
+ keyed: false
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ release_channel_collection: opt-out
+
+ insecure_opensearch_update_count:
+ bug_numbers:
+ - 1789438
+ - 1811158
+ - 1846899
+ description: >
+ Records the number of OpenSearch search engines with insecure updates
+ enabled (i.e., using http) a given user has installed
+ expires: "126"
+ keyed: false
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ release_channel_collection: opt-out
+
+startup:
+ profile_selection_reason:
+ bug_numbers:
+ - 1522934
+ - 1570652
+ - 1623406
+ - 1749887
+ description: >
+ How the profile was selected during startup. One of the following reasons:
+ unknown:
+ Generally should not happen, set as a default in case no other reason
+ occured.
+ profile-manager:
+ The profile was selected by the profile manager.
+ profile-reset:
+ The profile was selected for reset, normally this would mean a restart.
+ restart:
+ The user restarted the application, the same profile as previous will
+ be used.
+ argument-profile:
+ The profile was selected by the --profile command line argument.
+ argument-p:
+ The profile was selected by the -p command line argument.
+ firstrun-claimed-default:
+ A first run of a dedicated profiles build chose the old default
+ profile to be the default for this install.
+ firstrun-skipped-default:
+ A first run of a dedicated profiles build skipped over the old default
+ profile and created a new profile.
+ restart-claimed-default:
+ A first run of a dedicated profiles build after a restart chose the
+ old default profile to be the default for this install.
+ restart-skipped-default:
+ A first run of a dedicated profiles build after a restart skipped over
+ the old default profile and created a new profile.
+ firstrun-created-default:
+ A first run of the application created a new profile to use.
+ default:
+ The default profile was selected as normal.
+ expires: "never"
+ keyed: false
+ kind: string
+ notification_emails:
+ - dtownsend@mozilla.com
+ - rtestard@mozilla.com
+ - gkaberere@mozilla.com
+ - jhollek@mozilla.com
+ - echo@mozilla.com
+ - shong@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - main
+ record_into_store:
+ - main
+ - new-profile
+
+ profile_database_version:
+ bug_numbers:
+ - 1878339
+ description: >
+ The version of the profiles.ini database loaded during startup. A value
+ of "0" is used to indicate that no file was present during startup.
+ expires: "133"
+ keyed: false
+ kind: string
+ notification_emails:
+ - dtownsend@mozilla.com
+ - kbryant@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'thunderbird'
+ record_in_processes:
+ - main
+ record_into_store:
+ - main
+ - new-profile
+
+ profile_count:
+ bug_numbers:
+ - 1878339
+ description: >
+ This records the number of known profiles after startup completes. This
+ includes any profiles that were created during startup.
+ expires: "133"
+ keyed: false
+ kind: uint
+ notification_emails:
+ - dtownsend@mozilla.com
+ - kbryant@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'thunderbird'
+ record_in_processes:
+ - main
+ record_into_store:
+ - main
+ - new-profile
+
+ is_cold:
+ bug_numbers:
+ - 1542833
+ description: >
+ Whether or not this startup is the first startup since OS reboot (according to our
+ best guess.)
+ expires: never
+ kind: boolean
+ notification_emails:
+ - dothayer@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ seconds_since_last_os_restart:
+ bug_numbers:
+ - 1654063
+ description: >
+ The time in seconds between the first browser window loading, and the
+ time the OS started. This can give us an indication of whether starting
+ the browser may have been the first thing the user did after starting
+ their computer.
+ expires: never
+ kind: uint
+ notification_emails:
+ - dothayer@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ skeleton_ui_disabled_reason:
+ bug_numbers:
+ - 1686301
+ description: >
+ The reason why the skeleton UI was not shown to the user. This should be
+ one of the following values:
+ None:
+ There was no reason for not showing the skeleton UI. This should not
+ be recorded.
+ Ineligible:
+ The user was ineligible for the skeleton UI due to information which
+ should already be trivially available via telemetry (i.e., the user
+ is running an OS version that doesn't support the skeleton UI.)
+ Disabled:
+ The registry specified "Enabled" as 0 for the skeleton UI.
+ EnabledKeyDoesNotExist:
+ The "Enabled" key in the registry does not exist.
+ OOM:
+ We encountered an allocation failure while trying to show the
+ skeleton UI.
+ Cmdline:
+ The skeleton UI was disabled due to the presence of an unsupported
+ command line argument.
+ EnvVars:
+ The skeleton UI was disabled due to an explicitly unsupported
+ environment variable.
+ FailedToOpenRegistryKey:
+ We were unable to open the skeleton UI Windows registry key.
+ RegistryError:
+ We encountered an error trying to read/write a skeleton UI registry
+ value.
+ FailedLoadingDynamicProcs:
+ We were unable to dynamically load one of the functions we require in
+ order to show the skeleton UI.
+ FailedGettingLock:
+ We were unable to acquire the skeleton UI lock - this should mean
+ that an existing instance of the application is already running.
+ FilesystemFailure:
+ Encountered some error getting the path to a file or reading from a
+ particular file while trying to show the skeleton UI.
+ NoStartWithLastProfile:
+ The user has set up their profiles so Firefox always shows the
+ profile manager during startup.
+ FailedRegisteringWindowClass:
+ We failed to register the window class needed to show the skeleton UI
+ window.
+ CorruptData:
+ Clearly invalid data was found when trying to load the necessary
+ information to show the skeleton UI in the right place.
+ BadWindowDimensions:
+ Window dimensions outside of the supported size for the skeleton UI.
+ FailedGettingMonitorInfo:
+ Couldn't get the monitor information when trying to display the
+ skeleton UI.
+ CreateWindowFailed:
+ The call to CreateWindow failed.
+ FailedGettingDC:
+ We failed to get the HDC to draw into.
+ FailedBlitting:
+ We failed blitting the actual skeleton UI to the HDC.
+ FailedFillingBottomRect:
+ We failed filling the solid color of the main browser below the
+ browser chrome.
+ CrashedOnce:
+ At some point the skeleton UI crashed, so we've disabled it for the
+ user.
+ Unknown:
+ An unknown error occurred - this generally should not happen.
+ expires: "98"
+ keyed: false
+ kind: string
+ notification_emails:
+ - dothayer@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ is_restored_by_macos:
+ bug_numbers:
+ - 639707
+ description: >
+ Recorded on every launch of a Firefox install on macOS, with a boolean
+ value indicating whether Firefox was restored by macOS or if it was
+ manually launched by a user.
+ expires: "116"
+ keyed: false
+ kind: boolean
+ notification_emails:
+ - spohl@mozilla.com
+ operating_systems:
+ - mac
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ release_channel_collection: opt-out
+
+script.preloader:
+ mainthread_recompile:
+ bug_numbers:
+ - 1364235
+ - 1590385
+ - 1623098
+ - 1649954
+ - 1689257
+ - 1754639
+ description:
+ How many times we ended up recompiling a script from the script preloader
+ on the main thread.
+ expires: "never"
+ keyed: false
+ kind: uint
+ notification_emails:
+ - dothayer@mozilla.com
+ - plawless@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+ - 'content'
+
+networking:
+ nss_initialization:
+ bug_numbers:
+ - 1628734
+ description:
+ The time in milliseconds to initialize the NSS component in the
+ parent process.
+ expires: "never"
+ keyed: false
+ kind: uint
+ notification_emails:
+ - mconley@mozilla.com
+ - dkeeler@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ loading_certs_task:
+ bug_numbers:
+ - 1628734
+ description:
+ The time in milliseconds to load any external certificates. This occurs
+ off of the main-thread, but can block main-thread operations.
+ expires: "never"
+ keyed: false
+ kind: uint
+ notification_emails:
+ - mconley@mozilla.com
+ - dkeeler@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ data_transferred_v3_kb:
+ bug_numbers:
+ - 1533363
+ - 1546903
+ - 1546796
+ - 1546906
+ - 1550756
+ - 1572666
+ - 1590124
+ - 1621800
+ - 1668604
+ - 1706842
+ description: >
+ How many KB of HTTP data transferred by type, there are some categories
+ separated by keys based on 5 parameters:
+ 1. normal browsing or private browsing
+ 2. system principal or not
+ 3. first party or third party
+ 4. class of service (Leader/Background/Others)
+ 5. tracking classification (Basic/Content/FingerprintingContent)
+
+ Available keys are:
+ "Y0_N1Sys",
+ "Y1_N1",
+ "Y2_N3Oth",
+ "Y3_N3BasicLead",
+ "Y4_N3BasicBg",
+ "Y5_N3BasicOth",
+ "Y6_N3ContentLead",
+ "Y7_N3ContentBg",
+ "Y8_N3ContentOth",
+ "Y9_N3FpLead",
+ "Y10_N3FpBg",
+ "Y11_N3FpOth",
+ "Y12_P1Sys",
+ "Y13_P1",
+ "Y14_P3Oth",
+ "Y15_P3BasicLead",
+ "Y16_P3BasicBg",
+ "Y17_P3BasicOth",
+ "Y18_P3ContentLead",
+ "Y19_P3ContentBg",
+ "Y20_P3ContentOth",
+ "Y21_P3FpLead",
+ "Y22_P3FpBg",
+ "Y23_P3FpOth"
+ expires: "never"
+ keyed: true
+ kind: uint
+ notification_emails:
+ - arthur@mozilla.com
+ - tanvi@mozilla.com
+ - tihuang@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+ release_channel_collection: opt-out
+
+ data_transferred_captive_portal:
+ bug_numbers:
+ - 1543005
+ description: >
+ Hom many KB has been transfer over a captive portal during a subsession.
+ expires: "73"
+ keyed: false
+ kind: uint
+ notification_emails:
+ - ddamjanovic@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+ - 'socket'
+
+ http_transactions_captive_portal:
+ bug_numbers:
+ - 1543005
+ description: >
+ Number of http transactions transfer over a captive portal during a subsession.
+ expires: "73"
+ keyed: false
+ kind: uint
+ notification_emails:
+ - ddamjanovic@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+ - 'socket'
+
+ http_connections_captive_portal:
+ bug_numbers:
+ - 1543005
+ description: >
+ Number of http connections transfer over a captive portal during a subsession.
+ expires: "73"
+ keyed: false
+ kind: uint
+ notification_emails:
+ - ddamjanovic@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+ - 'socket'
+
+ captive_portal_banner_displayed:
+ bug_numbers:
+ - 1868145
+ description: >
+ Number of times the captive portal banner was displayed in this session.
+ expires: never
+ release_channel_collection: opt-out
+ keyed: false
+ kind: uint
+ notification_emails:
+ - necko@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'all'
+
+ captive_portal_banner_display_time:
+ bug_numbers:
+ - 1868145
+ description: >
+ Number of seconds the captive portal banner was displayed for, keyed by close reason:
+ "success" - the portal login was completed
+ "abort" - the portal login was aborted (e.g. loss of network)
+ "dismiss" - the user dismissed the banner
+ expires: never
+ release_channel_collection: opt-out
+ keyed: true
+ keys:
+ - 'success'
+ - 'abort'
+ - 'dismiss'
+ kind: uint
+ notification_emails:
+ - necko@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'all'
+
+ http3_enabled:
+ bug_numbers:
+ - 1652104
+ description: >
+ True if HTTP3 is enabled at the start of a session.
+ expires: never
+ kind: boolean
+ keyed: false
+ notification_emails:
+ - necko@mozilla.com
+ - ddamjanovic@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ - 'content'
+
+ https_rr_prefs_usage:
+ bug_numbers:
+ - 1686421
+ description: >
+ A bitfield representation for the usage of HTTPS RR prefs.
+ bit 0: network.dns.upgrade_with_https_rr is enabled or not.
+ bit 1: network.dns.use_https_rr_as_altsvc is enabled or not.
+ bit 2: network.dns.echconfig.enabled is enabled or not.
+ expires: never
+ kind: uint
+ keyed: false
+ notification_emails:
+ - necko@mozilla.com
+ - kershaw@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ trr_connection_cycle_count:
+ bug_numbers:
+ - 1737198
+ description: >
+ Number of times we cycled the TRR connection during a subsession.
+ Keyed by TRR provider URL.
+ expires: never
+ keyed: true
+ kind: uint
+ release_channel_collection: opt-out
+ notification_emails:
+ - necko@mozilla.com
+ - nhnt11@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ - 'socket'
+
+ data_transferred_per_content_type:
+ bug_numbers:
+ - 1808695
+ description: >
+ Number of bytes we transferred keyed by "contentType"
+ expires: "130"
+ keyed: true
+ kind: uint
+ release_channel_collection: opt-out
+ notification_emails:
+ - necko@mozilla.com
+ - rtestard@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ data_transferred_pb_per_content_type:
+ bug_numbers:
+ - 1808695
+ description: >
+ Number of bytes we transferred in private browsing keyed by "contentType"
+ expires: "130"
+ keyed: true
+ kind: uint
+ release_channel_collection: opt-out
+ notification_emails:
+ - necko@mozilla.com
+ - rtestard@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ record_in_processes:
+ - 'main'
+
+ speculative_connect_outcome:
+ bug_numbers:
+ - 909865
+ description: >
+ Counts the occurrence of each outcome of a speculative connection
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - necko@mozilla.com
+ - acreskey@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ doh_heuristics_attempts:
+ bug_numbers:
+ - 1784258
+ description: >
+ The number of times we ran DoH heuristics.
+ kind: uint
+ expires: never # capturing continuously for monitoring purposes
+ release_channel_collection: opt-out
+ notification_emails:
+ - necko@mozilla.com
+ - vgosu@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ doh_heuristics_pass_count:
+ bug_numbers:
+ - 1784258
+ description: >
+ The number of times we passed DoH heuristics.
+ kind: uint
+ expires: never # capturing continuously for monitoring purposes
+ release_channel_collection: opt-out
+ notification_emails:
+ - necko@mozilla.com
+ - vgosu@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ doh_heuristics_result:
+ bug_numbers:
+ - 1784258
+ description: >
+ The value of this scalar indicates the result of the last heuristic run.
+ 0: "incomplete"
+ 1: "pass"
+ 2: "opt-out"
+ 3: "manually-disabled"
+ 4: "manually-enabled"
+ 5: "enterprise-disabled"
+ 6: "enterprise-present"
+ 7: "enterprise-enabled"
+ 8: "vpn"
+ 9: "proxy"
+ 10: "nrpt"
+ 11: "parental"
+ 12: "modifiedRoots"
+ 13: "thirdPartyRoots"
+ 14: "google"
+ 15: "youtube"
+ 16: "zscaler"
+ 17: "canary"
+ 18: "ignored"
+ kind: uint
+ expires: never # capturing continuously for monitoring purposes
+ release_channel_collection: opt-out
+ notification_emails:
+ - necko@mozilla.com
+ - vgosu@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ doh_heuristic_ever_tripped:
+ bug_numbers:
+ - 1784258
+ description: >
+ True if this heuristic (key) was ever tripped during the session.
+ kind: boolean
+ keyed: true
+ expires: never # capturing continuously for monitoring purposes
+ release_channel_collection: opt-out
+ notification_emails:
+ - necko@mozilla.com
+ - vgosu@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+blocklist:
+ lastModified_rs_addons:
+ bug_numbers:
+ - 1572711
+ - 1607744
+ - 1649960
+ - 1689274
+ - 1730037
+ - 1763529
+ - 1811159
+ - 1861296
+ description: >
+ Keep track of the last time the "addons" remotesetting blocklist has been successfully
+ updated (as a datetime string in UTC format), set to "Missing Date" when the timestamp
+ couldn't be retrieved.
+ expires: "132"
+ kind: string
+ release_channel_collection: opt-out
+ notification_emails:
+ - addons-dev-internal@mozilla.com
+ - rwu@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - main
+
+ lastModified_rs_addons_mlbf:
+ bug_numbers:
+ - 1633466
+ - 1649960
+ - 1689274
+ - 1730037
+ - 1763529
+ - 1811159
+ - 1861296
+ description: >
+ Keep track of the last time the "addons-bloomfilters" remotesetting blocklist has been successfully
+ updated (as a datetime string in UTC format), set to "Missing Date" when the timestamp
+ couldn't be retrieved.
+ expires: "132"
+ kind: string
+ release_channel_collection: opt-out
+ notification_emails:
+ - addons-dev-internal@mozilla.com
+ - rwu@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ mlbf_source:
+ bug_numbers:
+ - 1662857
+ - 1730037
+ - 1763529
+ - 1811159
+ - 1861296
+ description: >
+ The source of the RemoteSettings attachment that holds the bloom filter.
+ Possible values are "dump_match", "cache_match", "remote_match", "dump_fallback", "cache_fallback", "unknown".
+ "dump_match", "cache_match" and "remote_match" are expected known-good values, and means that the loaded
+ bloomfilter matches the blocklist record in the RemoteSettings collection. The prefix denotes the immediate
+ source of the loaded data: "dump" means packaged with the application, "remote" means a freshly downloaded
+ bloomfilter, "cache" means a previously downloaded bloomfilter.
+ "dump_fallback" and "cache_fallback" means that the last known bloomfilter was used, despite it not matching
+ the latest record in the RemoteSettings collection. In this case the outdated bloomfilter is used as a fallback
+ (e.g. because the latest version cannot be downloaded).
+ "unknown" means that the bloomfilter cannot be loaded at all. This can happen if the blocklist is disabled
+ via preferences or enterprise policies.
+ expires: "132"
+ kind: string
+ release_channel_collection: opt-out
+ notification_emails:
+ - addons-dev-internal@mozilla.com
+ - rwu@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ mlbf_generation_time:
+ bug_numbers:
+ - 1633466
+ - 1649960
+ - 1689274
+ - 1730037
+ - 1763529
+ - 1811159
+ - 1861296
+ description: >
+ Keep track of the generation time of the addon blocklist's bloom filter. This marks the
+ point in time until which signed add-ons are recognized by the selected bloom filter from the
+ addons-bloomfilters collection.
+ The value is a datetime string in UTC format, or "Missing Date" when unavailable.
+ expires: "132"
+ kind: string
+ release_channel_collection: opt-out
+ notification_emails:
+ - addons-dev-internal@mozilla.com
+ - rwu@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ mlbf_stash_time_oldest:
+ bug_numbers:
+ - 1633466
+ - 1649960
+ - 1689274
+ - 1730037
+ - 1763529
+ - 1811159
+ - 1861296
+ description: >
+ Keep track of the timestamp of the oldest stash of the addons blocklist.
+ The value is a datetime string in UTC format, or "Missing Date" when unavailable.
+ expires: "132"
+ kind: string
+ release_channel_collection: opt-out
+ notification_emails:
+ - addons-dev-internal@mozilla.com
+ - rwu@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ mlbf_stash_time_newest:
+ bug_numbers:
+ - 1633466
+ - 1649960
+ - 1689274
+ - 1730037
+ - 1763529
+ - 1811159
+ - 1861296
+ description: >
+ Keep track of the timestamp of the most recent stash of the addons blocklist.
+ The value is a datetime string in UTC format, or "Missing Date" when unavailable.
+ expires: "132"
+ kind: string
+ release_channel_collection: opt-out
+ notification_emails:
+ - addons-dev-internal@mozilla.com
+ - rwu@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+# The following section is for the "deletion-request" ping (bug 1585410). All the
+# extra identifiers other than `client_id` could be added here.
+deletion.request:
+ impression_id:
+ bug_numbers:
+ - 1602064
+ description: >
+ An identifier used by user interaction pings in Pocket/newtab and Messaging System.
+ expires: never
+ kind: string
+ notification_emails:
+ - najiang@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ record_into_store:
+ - 'deletion-request'
+
+ sync_device_id:
+ bug_numbers:
+ - 1604844
+ description: >
+ An identifier used by sync ping, to identify the current Firefox profile for a specific Account.
+ expires: never
+ kind: string
+ notification_emails:
+ - rfkelly@mozilla.com
+ - sync-team@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ record_into_store:
+ - 'deletion-request'
+
+ ecosystem_client_id:
+ bug_numbers:
+ - 1635662
+ description: >
+ An identifier used by the ecosystem ping, to delete all ecosystem
+ metrics collected from this profile if the user opts out of telemetry.
+ expires: never
+ kind: string
+ notification_emails:
+ - lina@mozilla.com
+ - sync-team@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ record_into_store:
+ - 'deletion-request'
+
+ context_id:
+ bug_numbers:
+ - 1729474
+ description: >
+ An identifier to identify users for Contextual Services user interaction pings.
+ expires: never
+ kind: string
+ notification_emails:
+ - najiang@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ record_into_store:
+ - 'deletion-request'
+
+policies:
+ count:
+ bug_numbers:
+ - 1432897
+ description: >-
+ A uint with the number of active enterprise policies, collected once at
+ startup.
+ expires: never
+ kind: uint
+ notification_emails:
+ - mkaply@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ is_enterprise:
+ bug_numbers:
+ - 1803804
+ description: >-
+ Attempt to determine if the user is an enterprise user based on various signals.
+ expires: never
+ kind: boolean
+ notification_emails:
+ - mkaply@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+urlbar.trending:
+ block:
+ bug_numbers:
+ - 1848048
+ description: >-
+ User has blocked seeing trending results.
+ expires: never
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+urlbar:
+ abandonment:
+ bug_numbers:
+ - 1752953
+ description: >
+ A uint recording the number of abandoned engagements in the urlbar. An
+ abandonment occurs when the user begins using the urlbar but stops before
+ completing the engagement. This can happen when the user clicks outside
+ the urlbar to focus a different part of the window. It can also happen
+ when the user switches to another window while the urlbar is focused.
+ expires: never
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - adw@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ autofill_deletion:
+ bug_numbers:
+ - 1815342
+ description: >
+ A uint recording the deletion count for autofilled string in the urlbar.
+ This occurs when the user deletes whole autofilled string by BACKSPACE or
+ DELETE key while the autofilled string is selected.
+ expires: never
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ engagement:
+ bug_numbers:
+ - 1752953
+ description: >
+ A uint recording the number of engagements the user completes in the
+ urlbar. An engagement occurs when the user navigates to a page using the
+ urlbar, for example by picking a result in the urlbar panel or typing a
+ search term or URL in the urlbar and pressing the enter key.
+ expires: never
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - adw@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ tips:
+ bug_numbers:
+ - 1608461
+ description: >
+ A keyed uint recording how many times particular tips are shown in the
+ Urlbar and how often their confirm and help buttons are pressed.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ - adw@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+urlbar.impression:
+ autofill_about:
+ bug_numbers:
+ - 1774305
+ - 1780188
+ description: >
+ Counts how many times about-page type was autofilled per engagement.
+ expires: never
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ autofill_adaptive:
+ bug_numbers:
+ - 1774305
+ - 1780188
+ description: >
+ Counts how many times adaptive history type was autofilled per engagement.
+ expires: never
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ autofill_origin:
+ bug_numbers:
+ - 1774305
+ - 1780188
+ description: >
+ Counts how many times origin type was autofilled per engagement.
+ expires: never
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ autofill_other:
+ bug_numbers:
+ - 1774305
+ - 1780188
+ description: >
+ Counts how many times other type was autofilled per engagement.
+ expires: never
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ autofill_url:
+ bug_numbers:
+ - 1774305
+ - 1780188
+ description: >
+ Counts how many times url type was autofilled per engagement.
+ expires: never
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+urlbar.tabtosearch:
+ impressions:
+ bug_numbers:
+ - 1686330
+ description: >
+ A keyed uint recording how many times non-onboarding tab-to-search results
+ are shown in the Urlbar for a particular search engine.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-in
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ impressions_onboarding:
+ bug_numbers:
+ - 1686330
+ description: >
+ A keyed uint recording how many times onboarding tab-to-search results
+ are shown in the Urlbar for a particular search engine.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-in
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+urlbar.picked:
+ autofill_about:
+ bug_numbers:
+ - 1770483
+ description: >
+ Counts how many times an about-page autofill result (e.g.,
+ "about:config") was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ autofill_adaptive:
+ bug_numbers:
+ - 1597791
+ description: >
+ Counts how many times this result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ autofill_origin:
+ bug_numbers:
+ - 1597791
+ description: >
+ Counts how many times an origin (domain) autofill result (e.g.,
+ "mozilla.org") was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ autofill_other:
+ bug_numbers:
+ - 1770483
+ description: >
+ Counts how many times some other type of autofill result that does not
+ have a specific keyed scalar was picked at a given index. This is a
+ fallback that is used when the code is not properly setting a specific
+ autofill type, and it should not normally be used. If it appears in the
+ data, it means we need to investigate and fix the code that is not
+ properly setting a specific autofill type.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ autofill_url:
+ bug_numbers:
+ - 1597791
+ description: >
+ Counts how many times a URL autofill result (e.g.,
+ "mozilla.org/firefox") was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ bookmark:
+ bug_numbers:
+ - 1671508
+ description: >
+ Counts how many times this result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ bookmark_adaptive:
+ bug_numbers:
+ - 1466103
+ description: >
+ Counts how many times this result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ clipboard:
+ bug_numbers:
+ - 1842247
+ description: >
+ Counts how many times this result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ dynamic:
+ bug_numbers:
+ - 1671508
+ description: >
+ Counts how many times this result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ dynamic_wikipedia:
+ bug_numbers:
+ - 1819797
+ description: >
+ Counts how many times this result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ extension:
+ bug_numbers:
+ - 1671508
+ description: >
+ Counts how many times this result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ formhistory:
+ bug_numbers:
+ - 1671508
+ description: >
+ Counts how many times this result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ history:
+ bug_numbers:
+ - 1671508
+ description: >
+ Counts how many times this result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ history_adaptive:
+ bug_numbers:
+ - 1466103
+ description: >
+ Counts how many times this result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ keyword:
+ bug_numbers:
+ - 1671508
+ description: >
+ Counts how many times this result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ navigational:
+ bug_numbers:
+ - 1819797
+ description: >
+ Counts how many times this result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ quickaction:
+ bug_numbers:
+ - 1783154
+ description: >
+ Counts how many times this result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ quicksuggest:
+ bug_numbers:
+ - 1761059
+ description: >
+ Counts how many times this result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ recent_search:
+ bug_numbers:
+ - 1852848
+ description: >
+ Counts how many times this result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ remotetab:
+ bug_numbers:
+ - 1671508
+ description: >
+ Counts how many times this result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ searchengine:
+ bug_numbers:
+ - 1671508
+ description: >
+ Counts how many times this result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ searchsuggestion:
+ bug_numbers:
+ - 1671508
+ description: >
+ Counts how many times this result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ searchsuggestion_rich:
+ bug_numbers:
+ - 1834359
+ description: >
+ Counts how many times this result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ switchtab:
+ bug_numbers:
+ - 1671508
+ description: >
+ Counts how many times this result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ tabtosearch:
+ bug_numbers:
+ - 1671508
+ description: >
+ Counts how many times this result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ tip:
+ bug_numbers:
+ - 1671508
+ description: >
+ Counts how many times this result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ topsite:
+ bug_numbers:
+ - 1671508
+ description: >
+ Counts how many times this result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ trending:
+ bug_numbers:
+ - 1823543
+ description: >
+ Counts how many times this result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ trending_rich:
+ bug_numbers:
+ - 1834359
+ description: >
+ Counts how many times this result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ unknown:
+ bug_numbers:
+ - 1671508
+ description: >
+ Counts how many times an unknown result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ visiturl:
+ bug_numbers:
+ - 1671508
+ description: >
+ Counts how many times this result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ weather:
+ bug_numbers:
+ - 1815699
+ description: >
+ Counts how many times this result type was picked at a given index.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+urlbar.persistedsearchterms:
+ revert_by_popup_count:
+ bug_numbers:
+ - 1815971
+ description:
+ The count of the number of times search terms were removed from the
+ urlbar due to a shown PopupNotification. This event can happen when
+ a user loads a SERP and a PopupNotification is shown, as well as when
+ a user switches away from a tab on a SERP showing a PopupNotification
+ and switches back to it.
+ expires: never
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ view_count:
+ bug_numbers:
+ - 1815971
+ description:
+ The count of the number of times search terms persisted in the Urlbar.
+ This gets recorded after a user loads a SERP that persists search terms,
+ or switches back to an existing tab that should be showing the persisted
+ search terms in the Urlbar, regardless of whether PopupNotification
+ cleared the search terms from the Urlbar.
+ expires: never
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+urlbar.searchmode:
+ bookmarkmenu:
+ bug_numbers:
+ - 1665076
+ description: >
+ A keyed uint recording how many times the user entered a particular search
+ mode after selecing the Search Bookmarks menu item in the Library menu.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ handoff:
+ bug_numbers:
+ - 1654680
+ - 1665076
+ description: >
+ A keyed uint recording how many times the user entered a particular search
+ mode after being handed off from the search bar on the new tab page.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ keywordoffer:
+ bug_numbers:
+ - 1654680
+ description: >
+ A keyed uint recording how many times the user entered a particular search
+ mode after selecting a keyword offer result.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ oneoff:
+ bug_numbers:
+ - 1654680
+ description: >
+ A keyed uint recording how many times the user entered a particular search
+ mode after selecting a one-off.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ shortcut:
+ bug_numbers:
+ - 1654680
+ description: >
+ A keyed uint recording how many times the user entered a particular search
+ mode after pressing a keyboard shortcut.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ tabmenu:
+ bug_numbers:
+ - 1665076
+ description: >
+ A keyed uint recording how many times the user entered a particular search
+ mode after selecting the Search Tabs menu item in the Tab Overflow menu.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ tabtosearch:
+ bug_numbers:
+ - 1647923
+ description: >
+ A keyed uint recording how many times the user entered a particular search
+ mode after selecting a tab-to-search result.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ tabtosearch_onboard:
+ bug_numbers:
+ - 1665934
+ description: >
+ A keyed uint recording how many times the user entered a particular search
+ mode after selecting a tab-to-search onboarding result.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ topsites_newtab:
+ bug_numbers:
+ - 1654680
+ description: >
+ A keyed uint recording how many times the user entered a particular search
+ mode after selecting a search shortcut Top Site on the New Tab Page.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ topsites_urlbar:
+ bug_numbers:
+ - 1654680
+ description: >
+ A keyed uint recording how many times the user entered a particular search
+ mode after selecting a search shortcut Top Site in the Urlbar.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ touchbar:
+ bug_numbers:
+ - 1657414
+ description: >
+ A keyed uint recording how many times the user entered a particular search
+ mode after selecting a search shortcut on the macOS Touch Bar.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ typed:
+ bug_numbers:
+ - 1654680
+ description: >
+ A keyed uint recording how many times the user entered a particular search
+ mode after typing an alias.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ historymenu:
+ bug_numbers:
+ - 1736939
+ description: >
+ A keyed uint recording how many times the user entered a particular search
+ mode after selecing the Search History menu item in a History menu.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ other:
+ bug_numbers:
+ - 1654680
+ description: >
+ A keyed uint recording how many times the user entered a particular search
+ mode from an unknown entry point. If values appear for this probe in
+ Telemetry, we should review the entry points to search mode.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+urlbar.picked.searchmode:
+ bookmarkmenu:
+ bug_numbers:
+ - 1674874
+ description: >
+ A keyed uint recording how many times the user picked a particular kind of
+ result while in search mode after entering search mode from the Search
+ Bookmarks menu item in the Library menu.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ handoff:
+ bug_numbers:
+ - 1674874
+ description: >
+ A keyed uint recording how many times the user picked a particular kind of
+ result while in search mode after entering search mode by being handed off
+ from the search bar on the new tab page.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ keywordoffer:
+ bug_numbers:
+ - 1674874
+ description: >
+ A keyed uint recording how many times the user picked a particular kind of
+ result while in search mode after entering search mode by selecting a
+ keyword offer result.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ oneoff:
+ bug_numbers:
+ - 1674874
+ description: >
+ A keyed uint recording how many times the user picked a particular kind of
+ result while in search mode after entering search mode by selecting a
+ one-off.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ shortcut:
+ bug_numbers:
+ - 1674874
+ description: >
+ A keyed uint recording how many times the user picked a particular kind of
+ result while in search mode after entering search mode by pressing a
+ keyboard shortcut.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ tabmenu:
+ bug_numbers:
+ - 1674874
+ description: >
+ A keyed uint recording how many times the user picked a particular kind of
+ result while in search mode after entering search mode by selecting the
+ Search Tabs menu item in the Tab Overflow menu.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ tabtosearch:
+ bug_numbers:
+ - 1674874
+ description: >
+ A keyed uint recording how many times the user picked a particular kind of
+ result while in search mode after entering search mode by selecting a
+ tab-to-search result.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ tabtosearch_onboard:
+ bug_numbers:
+ - 1674874
+ description: >
+ A keyed uint recording how many times the user picked a particular kind of
+ result while in search mode after entering search mode by selecting a
+ tab-to-search onboarding result.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ topsites_newtab:
+ bug_numbers:
+ - 1674874
+ description: >
+ A keyed uint recording how many times the user picked a particular kind of
+ result while in search mode after entering search mode by selecting a
+ search shortcut Top Site on the New Tab Page.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ topsites_urlbar:
+ bug_numbers:
+ - 1674874
+ description: >
+ A keyed uint recording how many times the user picked a particular kind of
+ result while in search mode after entering search mode by selecting a
+ search shortcut Top Site in the Urlbar.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ touchbar:
+ bug_numbers:
+ - 1674874
+ description: >
+ A keyed uint recording how many times the user picked a particular kind of
+ result while in search mode after entering search mode by selecting a
+ search shortcut on the macOS Touch Bar.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ typed:
+ bug_numbers:
+ - 1674874
+ description: >
+ A keyed uint recording how many times the user picked a particular kind of
+ result while in search mode after entering search mode by typing an alias.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ historymenu:
+ bug_numbers:
+ - 1736939
+ description: >
+ A keyed uint recording how many times the user picked a particular kind of
+ result while in search mode after entering search mode from the Search
+ History menu item in a History menu.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ other:
+ bug_numbers:
+ - 1674874
+ description: >
+ A keyed uint recording how many times the user picked a particular kind of
+ result while in search mode after entering search mode from an unknown
+ entry point. If values appear for this probe in Telemetry, we should
+ review the entry points to search mode.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+urlbar.zeroprefix:
+ abandonment:
+ bug_numbers:
+ - 1806765
+ description: >
+ Counts how many times the zero-prefix urlbar results panel was abandoned.
+ expires: never
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ engagement:
+ bug_numbers:
+ - 1806765
+ description: >
+ Counts how many times a result was picked in the zero-prefix urlbar
+ results panel.
+ expires: never
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ exposure:
+ bug_numbers:
+ - 1806765
+ description: >
+ Counts how many times the zero-prefix urlbar results panel was shown to
+ the user.
+ expires: never
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+quickaction:
+ impression:
+ bug_numbers:
+ - 1806024
+ description: >
+ Counts how many times quickaction results were shown. The key
+ is the in the form "actionkey-N" where N is the number of characters
+ the user typed to be shown the action.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ picked:
+ bug_numbers:
+ - 1783155
+ description: >
+ Counts how many times quickaction results were selected. The key
+ is the in the form "actionkey-N" where N is the number of characters
+ the user typed to be shown the action.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+migration:
+ uninstaller_profile_refresh:
+ bug_numbers:
+ - 1634115
+ - 1672585
+ description: >
+ Set when a profile refresh is initiated from the uninstaller.
+ expires: never
+ kind: boolean
+ notification_emails:
+ - application-update-telemetry-alerts@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ operating_systems:
+ - windows
+ discovered_migrators:
+ bug_numbers:
+ - 1825655
+ description: >
+ A keyed uint recording how many times the migration wizard detected a browser / profile
+ pair for a particular browser. This is incremented every time the discovery occurs,
+ so multiple openings of the wizard will increase the counts each time.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - mconley@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ time_to_produce_migrator_list:
+ bug_numbers:
+ - 1840917
+ description: >
+ The amount of time it took in milliseconds to produce the list of migrators and profiles
+ for the first time the migration wizard opened during the process lifetime.
+ expires: never
+ kind: uint
+ notification_emails:
+ - mconley@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+contextual.services.topsites:
+ impression:
+ bug_numbers:
+ - 1688698
+ description: >
+ A keyed uint recording how many times the user has viewed the sponsored TopSites
+ on the newtab page. The key is a combination of the source and the placement of
+ the TopSites tile (1-based). such as 'urlbar_1', 'newtab_2'.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - najiang@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ click:
+ bug_numbers:
+ - 1688698
+ description: >
+ A keyed uint recording how many times the user has clicked on the sponsored TopSites
+ on the newtab page. The key is a combination of the source and the placement of
+ the TopSites tile (1-based). such as 'urlbar_1', 'newtab_2'.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - najiang@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+
+contextual.services.quicksuggest:
+ impression_dynamic_wikipedia:
+ bug_numbers:
+ - 1800993
+ description: >
+ A keyed uint recording how many times the user has viewed sponsored
+ Firefox Suggest dynamic wikipedia results in the urlbar. The
+ key is the 1-based index of each result.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ impression_nav_notmatched:
+ bug_numbers:
+ - 1819797
+ description: >
+ A keyed uint recording how many times a urlbar engagement occurred while a
+ navigational suggestion was absent. The Nimbus variable
+ recordNavigationalSuggestionTelemetry must be true to record this scalar.
+ Its key is the type of heuristic result that was present at the time of
+ the engagement. Key names are the same as the heuristic result type names
+ recorded in Glean telemetry.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - firefox
+ record_in_processes:
+ - main
+ impression_nav_shown:
+ bug_numbers:
+ - 1819797
+ description: >
+ A keyed uint recording how many times a urlbar engagement occurred while a
+ navigational suggestion was present. The Nimbus variable
+ recordNavigationalSuggestionTelemetry must be true to record this scalar.
+ Its key is the type of heuristic result that was present at the time of
+ the engagement. Key names are the same as the heuristic result type names
+ recorded in Glean telemetry.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - firefox
+ record_in_processes:
+ - main
+ impression_nav_superceded:
+ bug_numbers:
+ - 1819797
+ description: >
+ A keyed uint recording how many times a urlbar engagement occurred when a
+ navigational suggestion was matched but superceded by a heuristic result.
+ The Nimbus variable recordNavigationalSuggestionTelemetry must be true to
+ record this scalar. Its key is the type of heuristic result that was
+ present at the time of the engagement. Key names are the same as the
+ heuristic result type names recorded in Glean telemetry.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - firefox
+ record_in_processes:
+ - main
+ impression_nonsponsored:
+ bug_numbers:
+ - 1800993
+ description: >
+ A keyed uint recording how many times the user has viewed non-sponsored
+ Firefox Suggest (a.k.a. Quick Suggest) results in the urlbar. The key is
+ the 1-based index of each result.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ impression_sponsored:
+ bug_numbers:
+ - 1800993
+ description: >
+ A keyed uint recording how many times the user has viewed sponsored Firefox
+ Suggest (a.k.a. Quick Suggest) results in the urlbar. The key is the 1-based
+ index of each result.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ impression_weather:
+ bug_numbers:
+ - 1804536
+ description: >
+ A keyed uint recording how many times the user has viewed Firefox Suggest
+ (a.k.a. Quick Suggest) weather result in the urlbar. The key is the
+ 1-based index of each result.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ click_dynamic_wikipedia:
+ bug_numbers:
+ - 1800993
+ description: >
+ A keyed uint recording how many times the user has clicked on Firefox
+ Suggest dynamic wikipedia results in the urlbar (not including the
+ help button). The key is the 1-based index of each result.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ click_nav_notmatched:
+ bug_numbers:
+ - 1819797
+ description: >
+ A keyed uint recording how many times a heuristic result was clicked while
+ a navigational suggestion was absent. The Nimbus variable
+ recordNavigationalSuggestionTelemetry must be true to record this
+ scalar. Its key is the type of heuristic result that was clicked. Key
+ names are the same as the heuristic result type names recorded in Glean
+ telemetry.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - firefox
+ record_in_processes:
+ - main
+ click_nav_shown_heuristic:
+ bug_numbers:
+ - 1819797
+ description: >
+ A keyed uint recording how many times a heuristic result was clicked while
+ a navigational suggestion was present. The Nimbus variable
+ recordNavigationalSuggestionTelemetry must be true to record this
+ scalar. Its key is the type of heuristic result that was clicked. Key
+ names are the same as the heuristic result type names recorded in Glean
+ telemetry.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - firefox
+ record_in_processes:
+ - main
+ click_nav_shown_nav:
+ bug_numbers:
+ - 1819797
+ description: >
+ A keyed uint recording how many times a navigational suggestion was
+ clicked. The Nimbus variable recordNavigationalSuggestionTelemetry must be
+ true to record this scalar. Its key is the type of heuristic result that
+ was present at the time of the engagement. Key names are the same as the
+ heuristic result type names recorded in Glean telemetry.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - firefox
+ record_in_processes:
+ - main
+ click_nav_superceded:
+ bug_numbers:
+ - 1819797
+ description: >
+ A keyed uint recording how many times a heuristic result was clicked when
+ a navigational suggestion was matched but superceded by the heuristic.
+ The Nimbus variable recordNavigationalSuggestionTelemetry must be true to
+ record this scalar. Its key is the type of heuristic result that was
+ clicked. Key names are the same as the heuristic result type names
+ recorded in Glean telemetry.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - firefox
+ record_in_processes:
+ - main
+ click_nonsponsored:
+ bug_numbers:
+ - 1800993
+ description: >
+ A keyed uint recording how many times the user has clicked on non-sponsored
+ Firefox Suggests (a.k.a. Quick Suggest) results in the urlbar (not
+ including the help button). The key is the 1-based index of each result.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ click_sponsored:
+ bug_numbers:
+ - 1800993
+ description: >
+ A keyed uint recording how many times the user has clicked on sponsored
+ Firefox Suggests (a.k.a. Quick Suggest) results in the urlbar (not
+ including the help button). The key is the 1-based index of each result.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ click_weather:
+ bug_numbers:
+ - 1804536
+ description: >
+ A keyed uint recording how many times the user has clicked on Firefox
+ Suggests (a.k.a. Quick Suggest) weather results in the urlbar (not
+ including the help and dismiss buttons). The key is the 1-based index of
+ each result.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ help_dynamic_wikipedia:
+ bug_numbers:
+ - 1800993
+ description: >
+ A keyed uint recording how many times the user has clicked on the help
+ button in sponsored Firefox Suggest dynamic wikipedia results
+ in the urlbar (not including the help button). The key is the 1-based
+ index of each result.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ help_nonsponsored:
+ bug_numbers:
+ - 1800993
+ description: >
+ A keyed uint recording how many times the user has clicked on the help
+ button in non-sponsored Firefox Suggests (a.k.a. Quick Suggest) results in
+ the urlbar. The key is the 1-based index of each result.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ help_sponsored:
+ bug_numbers:
+ - 1800993
+ description: >
+ A keyed uint recording how many times the user has clicked on the help
+ button in sponsored Firefox Suggests (a.k.a. Quick Suggest) results in
+ the urlbar. The key is the 1-based index of each result.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ help_weather:
+ bug_numbers:
+ - 1804536
+ description: >
+ A keyed uint recording how many times the user has clicked on the help
+ button in Firefox Suggests (a.k.a. Quick Suggest) weather results in the
+ urlbar. The key is the 1-based index of each result.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ block_dynamic_wikipedia:
+ bug_numbers:
+ - 1800993
+ description: >
+ A keyed uint recording how many times the user has blocked/dismissed
+ sponsored Firefox Suggest dynamic wikipedia results in the
+ urlbar. The key is the 1-based index of each result.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ block_nonsponsored:
+ bug_numbers:
+ - 1761059
+ description: >
+ A keyed uint recording how many times the user has blocked/dismissed
+ non-sponsored Firefox Suggest results in the urlbar, including best
+ matches (a.k.a. top picks). The key is the 1-based index of each result.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ block_sponsored:
+ bug_numbers:
+ - 1761059
+ description: >
+ A keyed uint recording how many times the user has blocked/dismissed
+ sponsored Firefox Suggest results in the urlbar, including best matches
+ (a.k.a. top picks). The key is the 1-based index of each result.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ block_weather:
+ bug_numbers:
+ - 1804536
+ description: >
+ A keyed uint recording how many times the user has blocked/dismissed
+ Firefox Suggest weather results in the urlbar. The key is the 1-based
+ index of each result.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+shopping:
+ nimbus_disabled:
+ bug_numbers:
+ - 1847231
+ description: >
+ Indicates if Nimbus has disabled the use of the shopping component.
+ This scalar is mirroring the Glean metric named
+ shopping.settings.nimbus_disabled_shopping, which is defined in
+ toolkit/components/shopping/metrics.yaml.
+ expires: "134"
+ kind: boolean
+ notification_emails:
+ - betling@mozilla.com
+ - fx-desktop-shopping-eng@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ component_opted_out:
+ bug_numbers:
+ - 1847231
+ description: >
+ Indicates if the user has opted out of using the shopping component.
+ Set during shopping component init and updated when changed in browser.
+ This scalar is mirroring the Glean metric named
+ shopping.settings.component_opted_out, which is defined in
+ toolkit/components/shopping/metrics.yaml.
+ expires: "134"
+ kind: boolean
+ notification_emails:
+ - betling@mozilla.com
+ - fx-desktop-shopping-eng@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ has_onboarded:
+ bug_numbers:
+ - 1847231
+ description: >
+ Indicates if the user has completed the Shopping product Onboarding
+ experience. Set during shopping component init and updated when changed
+ in browser. This scalar is mirroring the Glean metric named
+ shopping.settings.has_onboarded, which is defined in
+ toolkit/components/shopping/metrics.yaml.
+ expires: "134"
+ kind: boolean
+ notification_emails:
+ - betling@mozilla.com
+ - fx-desktop-shopping-eng@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ product_page_visits:
+ bug_numbers:
+ - 1848160
+ description: >
+ Counts number of visits to a supported retailer product page while
+ enrolled in either the control or treatment branches of the shopping
+ experiment.
+ expires: "134"
+ kind: uint
+ notification_emails:
+ - betling@mozilla.com
+ - fx-desktop-shopping-eng@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ disabled_ads:
+ bug_numbers:
+ - 1858540
+ description: >
+ Indicates if the user has manually disabled ads. Set during shopping
+ component init and updated when changed in browser.
+ expires: "134"
+ kind: boolean
+ notification_emails:
+ - betling@mozilla.com
+ - fx-desktop-shopping-eng@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+ auto_open_user_disabled:
+ bug_numbers:
+ - 1879119
+ description: >
+ Indicates if the user has manually disabled the auto open sidebar feature.
+ Set during shopping component init and updated when changed in browser.
+ expires: "134"
+ kind: boolean
+ notification_emails:
+ - betling@mozilla.com
+ - fx-desktop-shopping-eng@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+power:
+ cpu_time_bogus_values:
+ bug_numbers:
+ - 1755733
+ description: >
+ Impossibly large CPU time values that were discarded.
+ expires: never
+ kind: uint
+ notification_emails:
+ - florian@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ - 'content'
+ - 'gpu'
+ - 'socket'
+
+ cpu_time_per_process_type_ms:
+ bug_numbers:
+ - 1747138
+ description: >
+ CPU time used by each process type in ms.
+ expires: never
+ kind: uint
+ notification_emails:
+ - florian@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'all'
+ keyed: true
+ keys: &per_process_type_keys
+ - parent.active
+ - parent.active.playing-audio
+ - parent.active.playing-video
+ - parent.inactive
+ - parent.inactive.playing-audio
+ - parent.inactive.playing-video
+ - prealloc
+ - privilegedabout
+ - rdd
+ - socket
+ - web.background
+ - web.background-perceivable
+ - web.foreground
+ - extension
+ - gpu
+ - gmplugin
+ - utility
+
+ gpu_time_bogus_values:
+ bug_numbers:
+ - 1755733
+ description: >
+ Impossibly large GPU time values that were discarded.
+ expires: never
+ kind: uint
+ notification_emails:
+ - florian@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ - 'gpu'
+
+ gpu_time_per_process_type_ms:
+ bug_numbers:
+ - 1747138
+ description: >
+ GPU time used by each process type in ms.
+ expires: never
+ kind: uint
+ notification_emails:
+ - florian@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ - 'gpu'
+ keyed: true
+ keys: *per_process_type_keys
+
+ wakeups_per_process_type:
+ bug_numbers:
+ - 1759535
+ description: >
+ How many times threads woke up and could have woken up a CPU core. Broken down by process type.
+ expires: never
+ kind: uint
+ notification_emails:
+ - florian@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'all'
+ keyed: true
+ keys: *per_process_type_keys
+
+ total_cpu_time_ms:
+ bug_numbers:
+ - 1736040
+ - 1748627
+ description: >
+ Total CPU time used by all processes in ms.
+ expires: never
+ kind: uint
+ notification_emails:
+ - florian@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'all'
+
+ total_gpu_time_ms:
+ bug_numbers:
+ - 1743176
+ - 1748627
+ description: >
+ Total GPU time used by all processes in ms.
+ expires: never
+ kind: uint
+ notification_emails:
+ - florian@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ - 'gpu'
+
+ total_thread_wakeups:
+ bug_numbers:
+ - 1759535
+ description: >
+ How many times threads woke up and could have woken up a CPU core.
+ expires: never
+ kind: uint
+ notification_emails:
+ - florian@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'all'
+
+# The following section is for probes testing the Telemetry system. They will not be
+# submitted in pings and are only used for testing.
+telemetry.test:
+ unsigned_int_kind:
+ bug_numbers:
+ - 1276190
+ - 1685406
+ description: >
+ This is a test uint type with a really long description, maybe spanning even multiple
+ lines, to just prove a point: everything works just fine.
+ expires: never
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'geckoview_streaming'
+ - 'thunderbird'
+ record_in_processes:
+ - 'main' # test_ChildScalars.js depends on this being main-only.
+
+ string_kind:
+ bug_numbers:
+ - 1276190
+ - 1685406
+ description: A string test type with a one line comment that works just fine!
+ expires: never
+ kind: string
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'geckoview_streaming'
+ - 'thunderbird'
+ record_in_processes:
+ - 'main'
+
+ boolean_kind:
+ bug_numbers:
+ - 1281214
+ - 1685406
+ description: A boolean test type with a one line comment that works just fine!
+ expires: never
+ kind: boolean
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'geckoview_streaming'
+ - 'thunderbird'
+ record_in_processes:
+ - 'main'
+
+ expired:
+ bug_numbers:
+ - 1276190
+ description: This is an expired testing scalar; not meant to be touched.
+ expires: "4"
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - 'main'
+
+ unexpired:
+ bug_numbers:
+ - 1276190
+ description: This is an unexpired testing scalar; not meant to be touched.
+ expires: "375"
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - 'main'
+
+ release_optin:
+ bug_numbers:
+ - 1276190
+ description: A testing scalar; not meant to be touched.
+ expires: never
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ release_channel_collection: opt-in
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - 'main'
+
+ release_optout:
+ bug_numbers:
+ - 1276190
+ description: A testing scalar; not meant to be touched.
+ expires: never
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - 'main'
+
+ keyed_release_optin:
+ bug_numbers:
+ - 1277806
+ description: A testing scalar; not meant to be touched.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ release_channel_collection: opt-in
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - 'main'
+
+ keyed_release_optout:
+ bug_numbers:
+ - 1277806
+ description: A testing scalar; not meant to be touched.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - 'main'
+
+ keyed_expired:
+ bug_numbers:
+ - 1277806
+ description: This is an expired testing scalar; not meant to be touched.
+ expires: "4"
+ kind: uint
+ keyed: true
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - 'main'
+
+ keyed_unsigned_int:
+ bug_numbers:
+ - 1277806
+ - 1685406
+ description: A testing keyed uint scalar; not meant to be touched.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - 'main' # test_ChildScalars.js depends on this being main-only
+
+ mirror_for_labeled_counter:
+ bug_numbers:
+ - 1277806
+ - 1685406
+ - 1758795
+ description: A testing keyed uint scalar; not meant to be touched.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - 'main'
+ - 'content'
+
+ keyed_with_keys:
+ bug_numbers:
+ - 1277806
+ description: A testing keyed scalar with defined keys; not meant to be touched.
+ expires: never
+ kind: uint
+ keyed: true
+ keys:
+ - 'only'
+ - 'meant'
+ - 'for'
+ - 'testing'
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - 'main'
+
+ child_keyed_unsigned_int:
+ bug_numbers:
+ - 1466490
+ description: A testing keyed uint scalar; not meant to be touched.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - 'content'
+
+ keyed_boolean_kind:
+ bug_numbers:
+ - 1277806
+ - 1685406
+ description: A testing keyed boolean scalar; not meant to be touched.
+ expires: never
+ kind: boolean
+ keyed: true
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - 'main'
+ - 'content'
+ release_channel_collection: opt-out
+
+ content_only_uint:
+ bug_numbers:
+ - 1278556
+ description: A testing uint scalar; not meant to be touched.
+ expires: never
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - 'content'
+
+ rdd_only_uint:
+ bug_numbers:
+ - 1804291
+ description: A testing uint scalar; not meant to be touched.
+ expires: never
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ products:
+ - 'firefox'
+ - 'thunderbird'
+ record_in_processes:
+ - 'rdd'
+
+ socket_only_uint:
+ bug_numbers:
+ - 1486033
+ description: A testing uint scalar; not meant to be touched.
+ expires: never
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - 'socket'
+
+ utility_only_uint:
+ bug_numbers:
+ - 1794409
+ description: A testing uint scalar; not meant to be touched.
+ expires: never
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - 'utility'
+
+ all_processes_uint:
+ bug_numbers:
+ - 1278556
+ description: A testing uint scalar; not meant to be touched.
+ expires: never
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - 'all'
+
+ all_child_processes_string:
+ bug_numbers:
+ - 1278556
+ description: A testing string scalar; not meant to be touched.
+ expires: never
+ kind: string
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - 'all_childs'
+
+ default_products:
+ bug_numbers:
+ - 1452552
+ description: A testing uint scalar; not meant to be touched
+ expires: never
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - 'all'
+
+ desktop_only:
+ bug_numbers:
+ - 1452552
+ description: A testing uint scalar; not meant to be touched
+ expires: never
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ record_in_processes:
+ - 'all'
+ products:
+ - firefox
+ - thunderbird
+
+ keyed_desktop_only:
+ bug_numbers:
+ - 1452552
+ description: A keyed testing uint scalar; not meant to be touched
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ record_in_processes:
+ - 'all'
+ products:
+ - firefox
+ - thunderbird
+
+ multiproduct:
+ bug_numbers:
+ - 1452552
+ description: A testing uint scalar; not meant to be touched
+ expires: never
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ record_in_processes:
+ - 'all'
+ products:
+ - firefox
+ - fennec
+ - thunderbird
+
+ mobile_only:
+ bug_numbers:
+ - 1452552
+ description: A testing uint scalar; not meant to be touched
+ expires: never
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ record_in_processes:
+ - 'all'
+ products:
+ - fennec
+
+ keyed_mobile_only:
+ bug_numbers:
+ - 1452552
+ description: A keyed testing uint scalar; not meant to be touched
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ record_in_processes:
+ - 'all'
+ products:
+ - fennec
+
+ main_only:
+ bug_numbers:
+ - 1498164
+ description: >
+ This is a test uint type with only the main store.
+ expires: never
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - 'main'
+ record_into_store:
+ - 'main'
+
+ sync_only:
+ bug_numbers:
+ - 1498164
+ description: >
+ This is a test uint type with only the sync store.
+ expires: never
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - 'main'
+ record_into_store:
+ - 'sync'
+
+ impression_id_only:
+ bug_numbers:
+ - 1602064
+ description: >
+ This is a test uint type with only the deletion-request store.
+ expires: never
+ kind: string
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ - najiang@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - 'main'
+ record_into_store:
+ - 'deletion-request'
+
+ multiple_stores:
+ bug_numbers:
+ - 1498164
+ description: >
+ This is a test uint type with multiple stores.
+ expires: never
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - 'main'
+ record_into_store:
+ - 'main'
+ - 'sync'
+
+ multiple_stores_string:
+ bug_numbers:
+ - 1498164
+ - 1685406
+ description: >
+ This is a test string type with multiple stores.
+ expires: never
+ kind: string
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - 'main'
+ record_into_store:
+ - 'main'
+ - 'sync'
+
+ multiple_stores_bool:
+ bug_numbers:
+ - 1498164
+ description: >
+ This is a test bool type with multiple stores.
+ expires: never
+ kind: boolean
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - 'main'
+ record_into_store:
+ - 'main'
+ - 'sync'
+
+ multiple_stores_keyed:
+ bug_numbers:
+ - 1498164
+ description: >
+ This is a test keyed uint type with multiple stores.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+ products:
+ - 'firefox'
+ - 'fennec'
+ - 'thunderbird'
+ record_in_processes:
+ - 'main'
+ record_into_store:
+ - 'main'
+ - 'sync'
+
+ mirror_for_date:
+ bug_numbers: [1685406]
+ description: Test only. This is a mirror probe for a Glean datetime metric.
+ expires: never
+ kind: string
+ notification_emails: [glean-team@mozilla.com]
+ products: ["firefox", "thunderbird"]
+ record_in_processes: ["all"]
+ release_channel_collection: opt-out
+
+ mirror_for_timespan:
+ bug_numbers: [1685406]
+ description: Test only. This is a mirror probe for a Glean timespan metric.
+ expires: never
+ kind: uint
+ notification_emails: [glean-team@mozilla.com]
+ products: ["firefox", "thunderbird"]
+ record_in_processes: ["all"]
+ release_channel_collection: opt-out
+
+ mirror_for_timespan_nanos:
+ bug_numbers: [1704106]
+ description: Test only. This is a mirror probe for a Glean timespan metric.
+ expires: never
+ kind: uint
+ notification_emails: [glean-team@mozilla.com]
+ products: ["firefox", "thunderbird"]
+ record_in_processes: ["all"]
+ release_channel_collection: opt-out
+
+ mirror_for_labeled_bool:
+ bug_numbers: [1685406]
+ description: Test only. This is a mirror probe for a Glean labeled_boolean metric.
+ expires: never
+ kind: boolean
+ keyed: true
+ notification_emails: [glean-team@mozilla.com]
+ products: ["firefox", "thunderbird"]
+ record_in_processes: ["all"]
+ release_channel_collection: opt-out
+
+ mirror_for_quantity:
+ bug_numbers: [1704846]
+ description: Test only. This is a mirror probe for a Glean quantity metric.
+ expires: never
+ kind: uint
+ notification_emails: [glean-team@mozilla.com]
+ products: ["firefox", "thunderbird"]
+ record_in_processes: ["all"]
+ release_channel_collection: opt-out
+
+ mirror_for_rate:
+ bug_numbers: [1694496]
+ description: >
+ Test only. This is a mirror probe for a Glean rate metric.
+ Keys are 'numerator' and 'denominator'.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails: [glean-team@mozilla.com]
+ products: ["firefox", "thunderbird"]
+ record_in_processes: ["all"]
+ release_channel_collection: opt-out
+
+ mirror_for_counter:
+ bug_numbers: [1758795]
+ description: >
+ Test only. This is a mirror probe for a Glean counter metric.
+ expires: never
+ kind: uint
+ notification_emails: [glean-team@mozilla.com]
+ products: ["firefox", "thunderbird"]
+ record_in_processes: ["all"]
+ release_channel_collection: opt-out
+
+ another_mirror_for_labeled_counter:
+ bug_numbers: [1758795]
+ description: >
+ Test only. This is a mirror probe for a Glean labeled_counter metric.
+ Keys are labels in the mirrored metric.
+ expires: never
+ kind: uint
+ keyed: true
+ notification_emails: [glean-team@mozilla.com]
+ products: ["firefox", "thunderbird"]
+ record_in_processes: ["all"]
+ release_channel_collection: opt-out
+
+ mirror_for_url:
+ bug_numbers: [1766980]
+ description: >
+ Test only. This is a mirror probe for a Glean Url metric.
+ expires: never
+ kind: string
+ notification_emails: [glean-team@mozilla.com]
+ products: ["firefox", "thunderbird"]
+ record_in_processes: ["all"]
+ release_channel_collection: opt-out
+
+cache:
+ integrity_check_count:
+ bug_numbers:
+ - 1687685
+ description: >
+ The number of errors detected by a Cache API pragma_integrity_check() run in response to a constraint error detected by a VACUUM after performing a schema upgrade.
+ expires: "96"
+ kind: uint
+ notification_emails:
+ - storage-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+opaque.response.blocking:
+ parsing_size_kb:
+ bug_numbers:
+ - 1695987
+ description: >
+ How many KB of response bodies are parsed by ORB?
+ expires: "93"
+ kind: uint
+ notification_emails:
+ - echuang@mozilla.com
+ - necko@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+ release_channel_collection: opt-out
+
+ cross_origin_opaque_response_count:
+ bug_numbers:
+ - 1804638
+ description: >
+ The number of loads of cross origin opaque resources.
+ expires: never
+ kind: uint
+ notification_emails:
+ - farre@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+ javascript_validation_count:
+ bug_numbers:
+ - 1804638
+ description: >
+ The number of times we run the JS validator.
+ expires: never
+ kind: uint
+ notification_emails:
+ - farre@mozilla.com
+ products:
+ - 'firefox'
+ record_in_processes:
+ - 'main'
+
+places:
+ sponsored_visit_no_triggering_url:
+ bug_numbers:
+ - 1787961
+ description: >
+ Number of sponsored visits that could not find their triggering URL in
+ history. We expect this to be a small number just due to the navigation
+ layer manipulating URLs. A large or growing value may be a concern.
+
+ expires: never
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ pages_need_frecency_recalculation:
+ bug_numbers:
+ - 1811209
+ description: >
+ Number of pages that need a frecency recalculation.
+ expires: never
+ kind: uint
+ notification_emails:
+ - fx-search-telemetry@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ previousday_visits:
+ bug_numbers:
+ - 1824380
+ description: >
+ Number of history entries for the previous day.
+ expires: never
+ kind: uint
+ notification_emails:
+ - firefox-view-engineers@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+firstStartup:
+ statusCode:
+ bug_numbers:
+ - 1749345
+ description: >
+ Status of the FirstRun service, which runs post-install/early-startup in Firefox.
+ expires: never
+ kind: uint
+ notification_emails:
+ - rhelmer@mozilla.com
+ - mconley@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ elapsed:
+ bug_numbers:
+ - 1749345
+ description: >
+ Number of milliseconds the FirstRun service took to run.
+ expires: never
+ kind: uint
+ notification_emails:
+ - rhelmer@mozilla.com
+ - mconley@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ normandyInitTime:
+ bug_numbers:
+ - 1841138
+ description: >
+ Number of milliseconds until Normandy.init resolved in FirstStartup.
+ expires: never
+ kind: uint
+ notification_emails:
+ - rhelmer@mozilla.com
+ - mconley@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+ deleteTasksTime:
+ bug_numbers:
+ - 1841138
+ description: >
+ Number of milliseconds until TaskScheduler.deleteAllTasks resolved in FirstStartup.
+ expires: never
+ kind: uint
+ notification_emails:
+ - rhelmer@mozilla.com
+ - mconley@mozilla.com
+ release_channel_collection: opt-out
+ products:
+ - 'firefox'
+ record_in_processes:
+ - main
+
+# NOTE: Please don't add new definitions below this point. Consider adding
+# them earlier in the file and leave the telemetry.test category as the last
+# one for readability.
diff --git a/toolkit/components/telemetry/TelemetryStartup.manifest b/toolkit/components/telemetry/TelemetryStartup.manifest
new file mode 100644
index 0000000000..b82b459b5e
--- /dev/null
+++ b/toolkit/components/telemetry/TelemetryStartup.manifest
@@ -0,0 +1 @@
+category profile-after-change TelemetryStartup @mozilla.org/base/telemetry-startup;1 process=main \ No newline at end of file
diff --git a/toolkit/components/telemetry/TelemetryStartup.sys.mjs b/toolkit/components/telemetry/TelemetryStartup.sys.mjs
new file mode 100644
index 0000000000..445cd906c2
--- /dev/null
+++ b/toolkit/components/telemetry/TelemetryStartup.sys.mjs
@@ -0,0 +1,44 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ TelemetryController: "resource://gre/modules/TelemetryController.sys.mjs",
+ TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs",
+});
+
+/**
+ * TelemetryStartup is needed to forward the "profile-after-change" notification
+ * to TelemetryController.jsm.
+ */
+export function TelemetryStartup() {}
+
+TelemetryStartup.prototype.QueryInterface = ChromeUtils.generateQI([
+ "nsIObserver",
+]);
+TelemetryStartup.prototype.observe = function (aSubject, aTopic, aData) {
+ if (aTopic == "profile-after-change") {
+ // In the content process, this is done in ContentProcessSingleton.js.
+ lazy.TelemetryController.observe(null, aTopic, null);
+ }
+ if (aTopic == "profile-after-change") {
+ annotateEnvironment();
+ lazy.TelemetryEnvironment.registerChangeListener(
+ "CrashAnnotator",
+ annotateEnvironment
+ );
+ lazy.TelemetryEnvironment.onInitialized().then(() => annotateEnvironment());
+ }
+};
+
+function annotateEnvironment() {
+ try {
+ let env = JSON.stringify(lazy.TelemetryEnvironment.currentEnvironment);
+ Services.appinfo.annotateCrashReport("TelemetryEnvironment", env);
+ } catch (e) {
+ // crash reporting not built or disabled? Ignore errors
+ }
+}
diff --git a/toolkit/components/telemetry/UserInteractions.yaml b/toolkit/components/telemetry/UserInteractions.yaml
new file mode 100644
index 0000000000..1005172038
--- /dev/null
+++ b/toolkit/components/telemetry/UserInteractions.yaml
@@ -0,0 +1,37 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# This file contains a definition of the UserInteractions that are recorded in
+# Telemetry via BHR.
+#
+# They are submitted as annotations with the "bhr" pings and can be inspected
+# in about:telemetry.
+#
+# These UserInteraction entries are subject to Data Collection Review. Do not
+# add or modify any of these entries without data-review+ from a Data Steward.
+#
+# Please attempt to keep the categories and UserInteraction names in
+# alphabetical order.
+#
+# Definitions
+# -----------
+# A UserInteraction definition looks like this:
+#
+# category: <category name> # mandatory
+# name: <interaction name> # mandatory
+# description: <description of user interaction> # mandatory
+# bug_numbers: [<list of bug numbers>] # mandatory
+#
+# Precise documentation for UserInteractions can be found at
+# https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/collection/user-interactions.html
+
+browser.tabs:
+ opening:
+ description: A new tab is being opened.
+ bug_numbers: [1662160]
+
+testing:
+ interaction:
+ description: A user interaction just for testing.
+ bug_numbers: [1661304]
diff --git a/toolkit/components/telemetry/app/ClientID.sys.mjs b/toolkit/components/telemetry/app/ClientID.sys.mjs
new file mode 100644
index 0000000000..e8c3db69b6
--- /dev/null
+++ b/toolkit/components/telemetry/app/ClientID.sys.mjs
@@ -0,0 +1,372 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { Log } from "resource://gre/modules/Log.sys.mjs";
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+const LOGGER_NAME = "Toolkit.Telemetry";
+const LOGGER_PREFIX = "ClientID::";
+// Must match ID in TelemetryUtils
+const CANARY_CLIENT_ID = "c0ffeec0-ffee-c0ff-eec0-ffeec0ffeec0";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ CommonUtils: "resource://services-common/utils.sys.mjs",
+});
+
+ChromeUtils.defineLazyGetter(lazy, "CryptoHash", () => {
+ return Components.Constructor(
+ "@mozilla.org/security/hash;1",
+ "nsICryptoHash",
+ "initWithString"
+ );
+});
+
+ChromeUtils.defineLazyGetter(lazy, "gDatareportingPath", () => {
+ return PathUtils.join(
+ Services.dirsvc.get("ProfD", Ci.nsIFile).path,
+ "datareporting"
+ );
+});
+
+ChromeUtils.defineLazyGetter(lazy, "gStateFilePath", () => {
+ return PathUtils.join(lazy.gDatareportingPath, "state.json");
+});
+
+const PREF_CACHED_CLIENTID = "toolkit.telemetry.cachedClientID";
+
+/**
+ * Checks if client ID has a valid format.
+ *
+ * @param {String} id A string containing the client ID.
+ * @return {Boolean} True when the client ID has valid format, or False
+ * otherwise.
+ */
+function isValidClientID(id) {
+ const UUID_REGEX =
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
+ return UUID_REGEX.test(id);
+}
+
+export var ClientID = Object.freeze({
+ /**
+ * This returns a promise resolving to the the stable client ID we use for
+ * data reporting (FHR & Telemetry).
+ *
+ * @return {Promise<string>} The stable client ID.
+ */
+ getClientID() {
+ return ClientIDImpl.getClientID();
+ },
+
+ /**
+ * Get the client id synchronously without hitting the disk.
+ * This returns:
+ * - the current on-disk client id if it was already loaded
+ * - the client id that we cached into preferences (if any)
+ * - null otherwise
+ */
+ getCachedClientID() {
+ return ClientIDImpl.getCachedClientID();
+ },
+
+ async getClientIdHash() {
+ return ClientIDImpl.getClientIdHash();
+ },
+
+ /**
+ * Sets the client ID to the canary (known) client ID,
+ * writing it to disk and updating the cached version.
+ *
+ * Use `removeClientID` followed by `getClientID` to clear the
+ * existing ID and generate a new, random one if required.
+ *
+ * @return {Promise<void>}
+ */
+ setCanaryClientID() {
+ return ClientIDImpl.setCanaryClientID();
+ },
+
+ /**
+ * Clears the client ID asynchronously, removing it
+ * from disk. Use `getClientID()` to generate
+ * a fresh ID after calling this method.
+ *
+ * Should only be used if a reset is explicitly requested by the user.
+ *
+ * @return {Promise<void>}
+ */
+ removeClientID() {
+ return ClientIDImpl.removeClientID();
+ },
+
+ /**
+ * Only used for testing. Invalidates the cached client ID so that it is
+ * read again from file, but doesn't remove the existing ID from disk.
+ */
+ _reset() {
+ return ClientIDImpl._reset();
+ },
+});
+
+var ClientIDImpl = {
+ _clientID: null,
+ _clientIDHash: null,
+ _loadClientIdTask: null,
+ _saveClientIdTask: null,
+ _removeClientIdTask: null,
+ _logger: null,
+
+ _loadClientID() {
+ if (this._loadClientIdTask) {
+ return this._loadClientIdTask;
+ }
+
+ this._loadClientIdTask = this._doLoadClientID();
+ let clear = () => (this._loadClientIdTask = null);
+ this._loadClientIdTask.then(clear, clear);
+ return this._loadClientIdTask;
+ },
+
+ /**
+ * Load the client ID from the DataReporting Service state file. If it is
+ * missing, we generate a new one.
+ */
+ async _doLoadClientID() {
+ this._log.trace(`_doLoadClientID`);
+ // If there's a removal in progress, let's wait for it
+ await this._removeClientIdTask;
+
+ // Try to load the client id from the DRS state file.
+ let hasCurrentClientID = false;
+ try {
+ let state = await IOUtils.readJSON(lazy.gStateFilePath);
+ if (state) {
+ hasCurrentClientID = this.updateClientID(state.clientID);
+ if (hasCurrentClientID) {
+ this._log.trace(`_doLoadClientID: Client IDs loaded from state.`);
+ return {
+ clientID: this._clientID,
+ };
+ }
+ }
+ } catch (e) {
+ // fall through to next option
+ }
+
+ // Absent or broken state file? Check prefs as last resort.
+ if (!hasCurrentClientID) {
+ const cachedID = this.getCachedClientID();
+ // Calling `updateClientID` with `null` logs an error, which breaks tests.
+ if (cachedID) {
+ hasCurrentClientID = this.updateClientID(cachedID);
+ }
+ }
+
+ // We're missing the ID from the DRS state file and prefs.
+ // Generate a new one.
+ if (!hasCurrentClientID) {
+ this.updateClientID(lazy.CommonUtils.generateUUID());
+ }
+ this._saveClientIdTask = this._saveClientID();
+
+ // Wait on persisting the id. Otherwise failure to save the ID would result in
+ // the client creating and subsequently sending multiple IDs to the server.
+ // This would appear as multiple clients submitting similar data, which would
+ // result in orphaning.
+ await this._saveClientIdTask;
+
+ this._log.trace("_doLoadClientID: New client ID loaded and persisted.");
+ return {
+ clientID: this._clientID,
+ };
+ },
+
+ /**
+ * Save the client ID to the client ID file.
+ *
+ * @return {Promise} A promise resolved when the client ID is saved to disk.
+ */
+ async _saveClientID() {
+ try {
+ this._log.trace(`_saveClientID`);
+ let obj = {
+ clientID: this._clientID,
+ };
+ await IOUtils.makeDirectory(lazy.gDatareportingPath);
+ await IOUtils.writeJSON(lazy.gStateFilePath, obj, {
+ tmpPath: `${lazy.gStateFilePath}.tmp`,
+ });
+ this._saveClientIdTask = null;
+ } catch (ex) {
+ if (!DOMException.isInstance(ex) || ex.name !== "AbortError") {
+ throw ex;
+ }
+ }
+ },
+
+ /**
+ * This returns a promise resolving to the the stable client ID we use for
+ * data reporting (FHR & Telemetry).
+ *
+ * @return {Promise<string>} The stable client ID.
+ */
+ async getClientID() {
+ if (!this._clientID) {
+ let { clientID } = await this._loadClientID();
+ if (AppConstants.platform != "android") {
+ Glean.legacyTelemetry.clientId.set(clientID);
+ }
+ return clientID;
+ }
+
+ return Promise.resolve(this._clientID);
+ },
+
+ /**
+ * Get the client id synchronously without hitting the disk.
+ * This returns:
+ * - the current on-disk client id if it was already loaded
+ * - the client id that we cached into preferences (if any)
+ * - null otherwise
+ */
+ getCachedClientID() {
+ if (this._clientID) {
+ // Already loaded the client id from disk.
+ return this._clientID;
+ }
+
+ // If the client id cache contains a value of the wrong type,
+ // reset the pref. We need to do this before |getStringPref| since
+ // it will just return |null| in that case and we won't be able
+ // to distinguish between the missing pref and wrong type cases.
+ if (
+ Services.prefs.prefHasUserValue(PREF_CACHED_CLIENTID) &&
+ Services.prefs.getPrefType(PREF_CACHED_CLIENTID) !=
+ Ci.nsIPrefBranch.PREF_STRING
+ ) {
+ this._log.error(
+ "getCachedClientID - invalid client id type in preferences, resetting"
+ );
+ Services.prefs.clearUserPref(PREF_CACHED_CLIENTID);
+ }
+
+ // Not yet loaded, return the cached client id if we have one.
+ let id = Services.prefs.getStringPref(PREF_CACHED_CLIENTID, null);
+ if (id === null) {
+ return null;
+ }
+ if (!isValidClientID(id)) {
+ this._log.error(
+ "getCachedClientID - invalid client id in preferences, resetting",
+ id
+ );
+ Services.prefs.clearUserPref(PREF_CACHED_CLIENTID);
+ return null;
+ }
+ return id;
+ },
+
+ async getClientIdHash() {
+ if (!this._clientIDHash) {
+ let byteArr = new TextEncoder().encode(await this.getClientID());
+ let hash = new lazy.CryptoHash("sha256");
+ hash.update(byteArr, byteArr.length);
+ this._clientIDHash = lazy.CommonUtils.bytesAsHex(hash.finish(false));
+ }
+ return this._clientIDHash;
+ },
+
+ /*
+ * Resets the module. This is for testing only.
+ */
+ async _reset() {
+ await this._loadClientIdTask;
+ await this._saveClientIdTask;
+ this._clientID = null;
+ this._clientIDHash = null;
+ },
+
+ async setCanaryClientID() {
+ this._log.trace("setCanaryClientID");
+ this.updateClientID(CANARY_CLIENT_ID);
+
+ this._saveClientIdTask = this._saveClientID();
+ await this._saveClientIdTask;
+ return this._clientID;
+ },
+
+ async _doRemoveClientID() {
+ this._log.trace("_doRemoveClientID");
+
+ // Reset the cached client ID.
+ this._clientID = null;
+ this._clientIDHash = null;
+
+ // Clear the client id from the preference cache.
+ Services.prefs.clearUserPref(PREF_CACHED_CLIENTID);
+
+ // If there is a save in progress, wait for it to complete.
+ await this._saveClientIdTask;
+
+ // Remove the client-id-containing state file from disk
+ await IOUtils.remove(lazy.gStateFilePath);
+ },
+
+ async removeClientID() {
+ this._log.trace("removeClientID");
+
+ if (AppConstants.platform != "android") {
+ // We can't clear the client_id in Glean, but we can make it the canary.
+ Glean.legacyTelemetry.clientId.set(CANARY_CLIENT_ID);
+ }
+
+ // Wait for the removal.
+ // Asynchronous calls to getClientID will also be blocked on this.
+ this._removeClientIdTask = this._doRemoveClientID();
+ let clear = () => (this._removeClientIdTask = null);
+ this._removeClientIdTask.then(clear, clear);
+
+ await this._removeClientIdTask;
+ },
+
+ /**
+ * Sets the client id to the given value and updates the value cached in
+ * preferences only if the given id is a valid.
+ *
+ * @param {String} id A string containing the client ID.
+ * @return {Boolean} True when the client ID has valid format, or False
+ * otherwise.
+ */
+ updateClientID(id) {
+ if (!isValidClientID(id)) {
+ this._log.error("updateClientID - invalid client ID", id);
+ return false;
+ }
+
+ this._clientID = id;
+ if (AppConstants.platform != "android") {
+ Glean.legacyTelemetry.clientId.set(id);
+ }
+
+ this._clientIDHash = null;
+ Services.prefs.setStringPref(PREF_CACHED_CLIENTID, this._clientID);
+ return true;
+ },
+
+ /**
+ * A helper for getting access to telemetry logger.
+ */
+ get _log() {
+ if (!this._logger) {
+ this._logger = Log.repository.getLoggerWithMessagePrefix(
+ LOGGER_NAME,
+ LOGGER_PREFIX
+ );
+ }
+
+ return this._logger;
+ },
+};
diff --git a/toolkit/components/telemetry/app/TelemetryArchive.sys.mjs b/toolkit/components/telemetry/app/TelemetryArchive.sys.mjs
new file mode 100644
index 0000000000..d2024ce95d
--- /dev/null
+++ b/toolkit/components/telemetry/app/TelemetryArchive.sys.mjs
@@ -0,0 +1,121 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { Log } from "resource://gre/modules/Log.sys.mjs";
+
+import { TelemetryUtils } from "resource://gre/modules/TelemetryUtils.sys.mjs";
+
+const LOGGER_NAME = "Toolkit.Telemetry";
+const LOGGER_PREFIX = "TelemetryArchive::";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ TelemetryStorage: "resource://gre/modules/TelemetryStorage.sys.mjs",
+});
+
+export var TelemetryArchive = {
+ /**
+ * Get a list of the archived pings, sorted by the creation date.
+ * Note that scanning the archived pings on disk is delayed on startup,
+ * use promizeInitialized() to access this after scanning.
+ *
+ * @return {Promise<sequence<Object>>}
+ * A list of the archived ping info in the form:
+ * { id: <string>,
+ * timestampCreated: <number>,
+ * type: <string> }
+ */
+ promiseArchivedPingList() {
+ return TelemetryArchiveImpl.promiseArchivedPingList();
+ },
+
+ /**
+ * Load an archived ping from disk by id, asynchronously.
+ *
+ * @param id {String} The pings UUID.
+ * @return {Promise<PingData>} A promise resolved with the pings data on success.
+ */
+ promiseArchivedPingById(id) {
+ return TelemetryArchiveImpl.promiseArchivedPingById(id);
+ },
+
+ /**
+ * Archive a ping and persist it to disk.
+ *
+ * @param {object} ping The ping data to archive.
+ * @return {promise} Promise that is resolved when the ping is successfully archived.
+ */
+ promiseArchivePing(ping) {
+ return TelemetryArchiveImpl.promiseArchivePing(ping);
+ },
+};
+
+/**
+ * Checks if pings can be archived. Some products (e.g. Thunderbird) might not want
+ * to do that.
+ * @return {Boolean} True if pings should be archived, false otherwise.
+ */
+function shouldArchivePings() {
+ return Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.ArchiveEnabled,
+ false
+ );
+}
+
+var TelemetryArchiveImpl = {
+ _logger: null,
+
+ get _log() {
+ if (!this._logger) {
+ this._logger = Log.repository.getLoggerWithMessagePrefix(
+ LOGGER_NAME,
+ LOGGER_PREFIX
+ );
+ }
+
+ return this._logger;
+ },
+
+ promiseArchivePing(ping) {
+ if (!shouldArchivePings()) {
+ this._log.trace("promiseArchivePing - archiving is disabled");
+ return Promise.resolve();
+ }
+
+ for (let field of ["creationDate", "id", "type"]) {
+ if (!(field in ping)) {
+ this._log.warn("promiseArchivePing - missing field " + field);
+ return Promise.reject(new Error("missing field " + field));
+ }
+ }
+
+ return lazy.TelemetryStorage.saveArchivedPing(ping);
+ },
+
+ _buildArchivedPingList(archivedPingsMap) {
+ let list = Array.from(archivedPingsMap, p => ({
+ id: p[0],
+ timestampCreated: p[1].timestampCreated,
+ type: p[1].type,
+ }));
+
+ list.sort((a, b) => a.timestampCreated - b.timestampCreated);
+
+ return list;
+ },
+
+ promiseArchivedPingList() {
+ this._log.trace("promiseArchivedPingList");
+
+ return lazy.TelemetryStorage.loadArchivedPingList().then(loadedInfo => {
+ return this._buildArchivedPingList(loadedInfo);
+ });
+ },
+
+ promiseArchivedPingById(id) {
+ this._log.trace("promiseArchivedPingById - id: " + id);
+ return lazy.TelemetryStorage.loadArchivedPing(id);
+ },
+};
diff --git a/toolkit/components/telemetry/app/TelemetryController.sys.mjs b/toolkit/components/telemetry/app/TelemetryController.sys.mjs
new file mode 100644
index 0000000000..e994181f39
--- /dev/null
+++ b/toolkit/components/telemetry/app/TelemetryController.sys.mjs
@@ -0,0 +1,41 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This module chooses the correct telemetry controller module to load
+ * based on the process:
+ *
+ * - TelemetryControllerParent is loaded only in the parent process, and
+ * contains code specific to the parent.
+ * - TelemetryControllerContent is loaded only in content processes, and
+ * contains code specific to them.
+ *
+ * Both the parent and the content modules load TelemetryControllerBase,
+ * which contains code which is common to all processes.
+ *
+ * This division is important for content process memory usage and
+ * startup time. The parent-specific code occupies tens of KB of memory
+ * which, multiplied by the number of content processes we have, adds up
+ * fast.
+ */
+
+// We can't use Services.appinfo here because tests stub out the appinfo
+// service, and if we touch Services.appinfo now, the built-in version
+// will be cached in place of the stub.
+const isParentProcess =
+ // eslint-disable-next-line mozilla/use-services
+ Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime).processType ===
+ Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT;
+
+export var TelemetryController;
+if (isParentProcess) {
+ ({ TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryControllerParent.sys.mjs"
+ ));
+} else {
+ ({ TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryControllerContent.sys.mjs"
+ ));
+}
diff --git a/toolkit/components/telemetry/app/TelemetryControllerBase.sys.mjs b/toolkit/components/telemetry/app/TelemetryControllerBase.sys.mjs
new file mode 100644
index 0000000000..cf0a7bd372
--- /dev/null
+++ b/toolkit/components/telemetry/app/TelemetryControllerBase.sys.mjs
@@ -0,0 +1,141 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+import { Log } from "resource://gre/modules/Log.sys.mjs";
+
+const LOGGER_NAME = "Toolkit.Telemetry";
+const LOGGER_PREFIX = "TelemetryController::";
+
+const PREF_BRANCH_LOG = "toolkit.telemetry.log.";
+const PREF_LOG_LEVEL = "toolkit.telemetry.log.level";
+const PREF_LOG_DUMP = "toolkit.telemetry.log.dump";
+
+const PREF_TELEMETRY_ENABLED = "toolkit.telemetry.enabled";
+
+const Preferences = Object.freeze({
+ OverridePreRelease: "toolkit.telemetry.testing.overridePreRelease",
+ Unified: "toolkit.telemetry.unified",
+});
+
+/**
+ * Setup Telemetry logging. This function also gets called when loggin related
+ * preferences change.
+ */
+var gLogger = null;
+var gPrefixLogger = null;
+var gLogAppenderDump = null;
+
+export var TelemetryControllerBase = Object.freeze({
+ // Whether the FHR/Telemetry unification features are enabled.
+ // Changing this pref requires a restart.
+ IS_UNIFIED_TELEMETRY: Services.prefs.getBoolPref(Preferences.Unified, false),
+
+ Preferences,
+
+ /**
+ * Returns the state of the Telemetry enabled preference, making sure
+ * it correctly evaluates to a boolean type.
+ */
+ get isTelemetryEnabled() {
+ return Services.prefs.getBoolPref(PREF_TELEMETRY_ENABLED, false) === true;
+ },
+
+ get log() {
+ if (!gPrefixLogger) {
+ gPrefixLogger = Log.repository.getLoggerWithMessagePrefix(
+ LOGGER_NAME,
+ LOGGER_PREFIX
+ );
+ }
+ return gPrefixLogger;
+ },
+
+ configureLogging() {
+ if (!gLogger) {
+ gLogger = Log.repository.getLogger(LOGGER_NAME);
+
+ // Log messages need to go to the browser console.
+ let consoleAppender = new Log.ConsoleAppender(new Log.BasicFormatter());
+ gLogger.addAppender(consoleAppender);
+
+ Services.prefs.addObserver(PREF_BRANCH_LOG, this.configureLogging);
+ }
+
+ // Make sure the logger keeps up with the logging level preference.
+ gLogger.level =
+ Log.Level[Services.prefs.getStringPref(PREF_LOG_LEVEL, "Warn")];
+
+ // If enabled in the preferences, add a dump appender.
+ let logDumping = Services.prefs.getBoolPref(PREF_LOG_DUMP, false);
+ if (logDumping != !!gLogAppenderDump) {
+ if (logDumping) {
+ gLogAppenderDump = new Log.DumpAppender(new Log.BasicFormatter());
+ gLogger.addAppender(gLogAppenderDump);
+ } else {
+ gLogger.removeAppender(gLogAppenderDump);
+ gLogAppenderDump = null;
+ }
+ }
+ },
+
+ /**
+ * Set the Telemetry core recording flag for Unified Telemetry.
+ */
+ setTelemetryRecordingFlags() {
+ // Enable extended Telemetry on pre-release channels and disable it
+ // on Release/ESR.
+ let prereleaseChannels = [
+ "nightly",
+ "nightly-autoland",
+ "nightly-try",
+ "aurora",
+ "beta",
+ ];
+ if (!AppConstants.MOZILLA_OFFICIAL) {
+ // Turn extended telemetry for local developer builds.
+ prereleaseChannels.push("default");
+ }
+ const isPrereleaseChannel = prereleaseChannels.includes(
+ AppConstants.MOZ_UPDATE_CHANNEL
+ );
+ const isReleaseCandidateOnBeta =
+ AppConstants.MOZ_UPDATE_CHANNEL === "release" &&
+ Services.prefs.getCharPref("app.update.channel", null) === "beta";
+ Services.telemetry.canRecordBase = true;
+ Services.telemetry.canRecordExtended =
+ isPrereleaseChannel ||
+ isReleaseCandidateOnBeta ||
+ Services.prefs.getBoolPref(this.Preferences.OverridePreRelease, false);
+ },
+
+ /**
+ * Perform telemetry initialization for either chrome or content process.
+ * @return {Boolean} True if Telemetry is allowed to record at least base (FHR) data,
+ * false otherwise.
+ */
+ enableTelemetryRecording: function enableTelemetryRecording() {
+ // Configure base Telemetry recording.
+ // Unified Telemetry makes it opt-out. If extended Telemetry is enabled, base recording
+ // is always on as well.
+ if (this.IS_UNIFIED_TELEMETRY) {
+ this.setTelemetryRecordingFlags();
+ } else {
+ // We're not on unified Telemetry, stick to the old behaviour for
+ // supporting Fennec.
+ Services.telemetry.canRecordBase = Services.telemetry.canRecordExtended =
+ this.isTelemetryEnabled;
+ }
+
+ this.log.config(
+ "enableTelemetryRecording - canRecordBase:" +
+ Services.telemetry.canRecordBase +
+ ", canRecordExtended: " +
+ Services.telemetry.canRecordExtended
+ );
+
+ return Services.telemetry.canRecordBase;
+ },
+});
diff --git a/toolkit/components/telemetry/app/TelemetryControllerContent.sys.mjs b/toolkit/components/telemetry/app/TelemetryControllerContent.sys.mjs
new file mode 100644
index 0000000000..e5c11d5d28
--- /dev/null
+++ b/toolkit/components/telemetry/app/TelemetryControllerContent.sys.mjs
@@ -0,0 +1,84 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { TelemetryControllerBase } from "resource://gre/modules/TelemetryControllerBase.sys.mjs";
+
+export var TelemetryController = Object.freeze({
+ /**
+ * Used only for testing purposes.
+ */
+ testInitLogging() {
+ TelemetryControllerBase.configureLogging();
+ },
+
+ /**
+ * Used only for testing purposes.
+ */
+ testSetupContent() {
+ return Impl.setupContentTelemetry(true);
+ },
+
+ /**
+ * Send a notification.
+ */
+ observe(aSubject, aTopic, aData) {
+ return Impl.observe(aSubject, aTopic, aData);
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+});
+
+var Impl = {
+ // This is true when running in the test infrastructure.
+ _testMode: false,
+
+ get _log() {
+ return TelemetryControllerBase.log;
+ },
+
+ /**
+ * This triggers basic telemetry initialization for content processes.
+ * @param {Boolean} [testing=false] True if we are in test mode, false otherwise.
+ */
+ setupContentTelemetry(testing = false) {
+ this._testMode = testing;
+
+ // The thumbnail service also runs in a content process, even with e10s off.
+ // We need to check if e10s is on so we don't submit child payloads for it.
+ // We still need xpcshell child tests to work, so we skip this if test mode is enabled.
+ if (testing || Services.appinfo.browserTabsRemoteAutostart) {
+ // We call |enableTelemetryRecording| here to make sure that Telemetry.canRecord* flags
+ // are in sync between chrome and content processes.
+ if (!TelemetryControllerBase.enableTelemetryRecording()) {
+ this._log.trace(
+ "setupContentTelemetry - Content process recording disabled."
+ );
+ return;
+ }
+ }
+ Services.telemetry.earlyInit();
+
+ let options = testing ? { timeout: 0 } : {};
+ ChromeUtils.idleDispatch(() => Services.telemetry.delayedInit(), options);
+ },
+
+ /**
+ * This observer drives telemetry.
+ */
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == "content-process-ready-for-script") {
+ TelemetryControllerBase.configureLogging();
+
+ this._log.trace(`observe - ${aTopic} notified.`);
+
+ this.setupContentTelemetry();
+ }
+ },
+};
+
+// Used by service registration, which requires a callable function.
+export function getTelemetryController() {
+ return TelemetryController;
+}
diff --git a/toolkit/components/telemetry/app/TelemetryControllerParent.sys.mjs b/toolkit/components/telemetry/app/TelemetryControllerParent.sys.mjs
new file mode 100644
index 0000000000..b9f8d5f85a
--- /dev/null
+++ b/toolkit/components/telemetry/app/TelemetryControllerParent.sys.mjs
@@ -0,0 +1,1413 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+import { AsyncShutdown } from "resource://gre/modules/AsyncShutdown.sys.mjs";
+import { DeferredTask } from "resource://gre/modules/DeferredTask.sys.mjs";
+
+import { TelemetryUtils } from "resource://gre/modules/TelemetryUtils.sys.mjs";
+import { TelemetryControllerBase } from "resource://gre/modules/TelemetryControllerBase.sys.mjs";
+
+const Utils = TelemetryUtils;
+
+const PING_FORMAT_VERSION = 4;
+
+// Delay before intializing telemetry (ms)
+const TELEMETRY_DELAY =
+ Services.prefs.getIntPref("toolkit.telemetry.initDelay", 60) * 1000;
+// Delay before initializing telemetry if we're testing (ms)
+const TELEMETRY_TEST_DELAY = 1;
+
+// How long to wait (ms) before sending the new profile ping on the first
+// run of a new profile.
+const NEWPROFILE_PING_DEFAULT_DELAY = 30 * 60 * 1000;
+
+// Ping types.
+const PING_TYPE_MAIN = "main";
+const PING_TYPE_DELETION_REQUEST = "deletion-request";
+const PING_TYPE_UNINSTALL = "uninstall";
+
+// Session ping reasons.
+const REASON_GATHER_PAYLOAD = "gather-payload";
+const REASON_GATHER_SUBSESSION_PAYLOAD = "gather-subsession-payload";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ ClientID: "resource://gre/modules/ClientID.sys.mjs",
+ CoveragePing: "resource://gre/modules/CoveragePing.sys.mjs",
+ TelemetryArchive: "resource://gre/modules/TelemetryArchive.sys.mjs",
+ TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs",
+ TelemetryEventPing: "resource://gre/modules/EventPing.sys.mjs",
+ TelemetryHealthPing: "resource://gre/modules/HealthPing.sys.mjs",
+ TelemetryModules: "resource://gre/modules/ModulesPing.sys.mjs",
+ TelemetryReportingPolicy:
+ "resource://gre/modules/TelemetryReportingPolicy.sys.mjs",
+ TelemetrySend: "resource://gre/modules/TelemetrySend.sys.mjs",
+ TelemetrySession: "resource://gre/modules/TelemetrySession.sys.mjs",
+ TelemetryStorage: "resource://gre/modules/TelemetryStorage.sys.mjs",
+ TelemetryUntrustedModulesPing:
+ "resource://gre/modules/UntrustedModulesPing.sys.mjs",
+ UninstallPing: "resource://gre/modules/UninstallPing.sys.mjs",
+ UpdatePing: "resource://gre/modules/UpdatePing.sys.mjs",
+ jwcrypto: "resource://services-crypto/jwcrypto.sys.mjs",
+});
+
+/**
+ * This is a policy object used to override behavior for testing.
+ */
+export var Policy = {
+ now: () => new Date(),
+ generatePingId: () => Utils.generateUUID(),
+ getCachedClientID: () => lazy.ClientID.getCachedClientID(),
+};
+
+export var TelemetryController = Object.freeze({
+ /**
+ * Used only for testing purposes.
+ */
+ testInitLogging() {
+ TelemetryControllerBase.configureLogging();
+ },
+
+ /**
+ * Used only for testing purposes.
+ */
+ testReset() {
+ return Impl.reset();
+ },
+
+ /**
+ * Used only for testing purposes.
+ */
+ testSetup() {
+ return Impl.setupTelemetry(true);
+ },
+
+ /**
+ * Used only for testing purposes.
+ */
+ testShutdown() {
+ return Impl.shutdown();
+ },
+
+ /**
+ * Used only for testing purposes.
+ */
+ testPromiseJsProbeRegistration() {
+ return Promise.resolve(Impl._probeRegistrationPromise);
+ },
+
+ /**
+ * Register 'dynamic builtin' probes from the JSON definition files.
+ * This is needed to support adding new probes in developer builds
+ * without rebuilding the whole codebase.
+ *
+ * This is not meant to be used outside of local developer builds.
+ */
+ testRegisterJsProbes() {
+ return Impl.registerJsProbes();
+ },
+
+ /**
+ * Used only for testing purposes.
+ */
+ testPromiseDeletionRequestPingSubmitted() {
+ return Promise.resolve(Impl._deletionRequestPingSubmittedPromise);
+ },
+
+ /**
+ * Send a notification.
+ */
+ observe(aSubject, aTopic, aData) {
+ return Impl.observe(aSubject, aTopic, aData);
+ },
+
+ /**
+ * Submit ping payloads to Telemetry. This will assemble a complete ping, adding
+ * environment data, client id and some general info.
+ * Depending on configuration, the ping will be sent to the server (immediately or later)
+ * and archived locally.
+ *
+ * To identify the different pings and to be able to query them pings have a type.
+ * A type is a string identifier that should be unique to the type ping that is being submitted,
+ * it should only contain alphanumeric characters and '-' for separation, i.e. satisfy:
+ * /^[a-z0-9][a-z0-9-]+[a-z0-9]$/i
+ *
+ * @param {String} aType The type of the ping.
+ * @param {Object} aPayload The actual data payload for the ping.
+ * @param {Object} [aOptions] Options object.
+ * @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
+ * id, false otherwise.
+ * @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the
+ * environment data.
+ * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data.
+ * @param {Boolean} [aOptions.usePingSender=false] if true, send the ping using the PingSender.
+ * @param {String} [aOptions.overrideClientId=undefined] if set, override the
+ * client id to the provided value. Implies aOptions.addClientId=true.
+ * @returns {Promise} Test-only - a promise that resolves with the ping id once the ping is stored or sent.
+ */
+ submitExternalPing(aType, aPayload, aOptions = {}) {
+ aOptions.addClientId = aOptions.addClientId || false;
+ aOptions.addEnvironment = aOptions.addEnvironment || false;
+ aOptions.usePingSender = aOptions.usePingSender || false;
+
+ return Impl.submitExternalPing(aType, aPayload, aOptions);
+ },
+
+ /**
+ * Get the current session ping data as it would be sent out or stored.
+ *
+ * @param {bool} aSubsession Whether to get subsession data. Optional, defaults to false.
+ * @return {object} The current ping data if Telemetry is enabled, null otherwise.
+ */
+ getCurrentPingData(aSubsession = false) {
+ return Impl.getCurrentPingData(aSubsession);
+ },
+
+ /**
+ * Save a ping to disk.
+ *
+ * @param {String} aType The type of the ping.
+ * @param {Object} aPayload The actual data payload for the ping.
+ * @param {Object} [aOptions] Options object.
+ * @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
+ * id, false otherwise.
+ * @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the
+ * environment data.
+ * @param {Boolean} [aOptions.overwrite=false] true overwrites a ping with the same name,
+ * if found.
+ * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data.
+ * @param {String} [aOptions.overrideClientId=undefined] if set, override the
+ * client id to the provided value. Implies aOptions.addClientId=true.
+ *
+ * @returns {Promise} A promise that resolves with the ping id when the ping is saved to
+ * disk.
+ */
+ addPendingPing(aType, aPayload, aOptions = {}) {
+ let options = aOptions;
+ options.addClientId = aOptions.addClientId || false;
+ options.addEnvironment = aOptions.addEnvironment || false;
+ options.overwrite = aOptions.overwrite || false;
+
+ return Impl.addPendingPing(aType, aPayload, options);
+ },
+
+ /**
+ * Check if we have an aborted-session ping from a previous session.
+ * If so, submit and then remove it.
+ *
+ * @return {Promise} Promise that is resolved when the ping is saved.
+ */
+ checkAbortedSessionPing() {
+ return Impl.checkAbortedSessionPing();
+ },
+
+ /**
+ * Save an aborted-session ping to disk without adding it to the pending pings.
+ *
+ * @param {Object} aPayload The ping payload data.
+ * @return {Promise} Promise that is resolved when the ping is saved.
+ */
+ saveAbortedSessionPing(aPayload) {
+ return Impl.saveAbortedSessionPing(aPayload);
+ },
+
+ /**
+ * Remove the aborted-session ping if any exists.
+ *
+ * @return {Promise} Promise that is resolved when the ping was removed.
+ */
+ removeAbortedSessionPing() {
+ return Impl.removeAbortedSessionPing();
+ },
+
+ /**
+ * Create an uninstall ping and write it to disk, replacing any already present.
+ * This is stored independently from other pings, and only read by
+ * the Windows uninstaller.
+ *
+ * WINDOWS ONLY, does nothing and resolves immediately on other platforms.
+ *
+ * @return {Promise} Resolved when the ping has been saved.
+ */
+ saveUninstallPing() {
+ return Impl.saveUninstallPing();
+ },
+
+ /**
+ * Allows the sync ping to tell the controller that it is initializing, so
+ * should be included in the orderly shutdown process.
+ *
+ * @param {Function} aFnShutdown The function to call as telemetry shuts down.
+
+ */
+ registerSyncPingShutdown(afnShutdown) {
+ Impl.registerSyncPingShutdown(afnShutdown);
+ },
+
+ /**
+ * Allows waiting for TelemetryControllers delayed initialization to complete.
+ * The returned promise is guaranteed to resolve before TelemetryController is shutting down.
+ * @return {Promise} Resolved when delayed TelemetryController initialization completed.
+ */
+ promiseInitialized() {
+ return Impl.promiseInitialized();
+ },
+});
+
+var Impl = {
+ _initialized: false,
+ _initStarted: false, // Whether we started setting up TelemetryController.
+ _shuttingDown: false, // Whether the browser is shutting down.
+ _shutDown: false, // Whether the browser has shut down.
+ _logger: null,
+ _prevValues: {},
+ // The previous build ID, if this is the first run with a new build.
+ // Undefined if this is not the first run, or the previous build ID is unknown.
+ _previousBuildID: undefined,
+ _clientID: null,
+ // A task performing delayed initialization
+ _delayedInitTask: null,
+ // The deferred promise resolved when the initialization task completes.
+ _delayedInitTaskDeferred: null,
+
+ // This is a public barrier Telemetry clients can use to add blockers to the shutdown
+ // of TelemetryController.
+ // After this barrier, clients can not submit Telemetry pings anymore.
+ _shutdownBarrier: new AsyncShutdown.Barrier(
+ "TelemetryController: Waiting for clients."
+ ),
+ // This state is included in the async shutdown annotation for crash pings and reports.
+ _shutdownState: "Shutdown not started.",
+ // This is a private barrier blocked by pending async ping activity (sending & saving).
+ _connectionsBarrier: new AsyncShutdown.Barrier(
+ "TelemetryController: Waiting for pending ping activity"
+ ),
+ // This is true when running in the test infrastructure.
+ _testMode: false,
+ // The task performing the delayed sending of the "new-profile" ping.
+ _delayedNewPingTask: null,
+ // The promise used to wait for the JS probe registration (dynamic builtin).
+ _probeRegistrationPromise: null,
+ // The promise of any outstanding task sending the "deletion-request" ping.
+ _deletionRequestPingSubmittedPromise: null,
+ // A function to shutdown the sync/fxa ping, or null if that ping has not
+ // self-initialized.
+ _fnSyncPingShutdown: null,
+
+ get _log() {
+ return TelemetryControllerBase.log;
+ },
+
+ /**
+ * Get the data for the "application" section of the ping.
+ */
+ _getApplicationSection() {
+ // Querying architecture and update channel can throw. Make sure to recover and null
+ // those fields.
+ let arch = null;
+ try {
+ arch = Services.sysinfo.get("arch");
+ } catch (e) {
+ this._log.trace(
+ "_getApplicationSection - Unable to get system architecture.",
+ e
+ );
+ }
+
+ let updateChannel = null;
+ try {
+ updateChannel = Utils.getUpdateChannel();
+ } catch (e) {
+ this._log.trace(
+ "_getApplicationSection - Unable to get update channel.",
+ e
+ );
+ }
+
+ return {
+ architecture: arch,
+ buildId: Services.appinfo.appBuildID,
+ name: Services.appinfo.name,
+ version: Services.appinfo.version,
+ displayVersion: AppConstants.MOZ_APP_VERSION_DISPLAY,
+ vendor: Services.appinfo.vendor,
+ platformVersion: Services.appinfo.platformVersion,
+ xpcomAbi: Services.appinfo.XPCOMABI,
+ channel: updateChannel,
+ };
+ },
+
+ /**
+ * Assemble a complete ping following the common ping format specification.
+ *
+ * @param {String} aType The type of the ping.
+ * @param {Object} aPayload The actual data payload for the ping.
+ * @param {Object} aOptions Options object.
+ * @param {Boolean} aOptions.addClientId true if the ping should contain the client
+ * id, false otherwise.
+ * @param {Boolean} aOptions.addEnvironment true if the ping should contain the
+ * environment data.
+ * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data.
+ * @param {String} [aOptions.overrideClientId=undefined] if set, override the
+ * client id to the provided value. Implies aOptions.addClientId=true.
+ * @param {Boolean} [aOptions.useEncryption=false] if true, encrypt data client-side before sending.
+ * @param {Object} [aOptions.publicKey=null] the public key to use if encryption is enabled (JSON Web Key).
+ * @param {String} [aOptions.encryptionKeyId=null] the public key ID to use if encryption is enabled.
+ * @param {String} [aOptions.studyName=null] the study name to use.
+ * @param {String} [aOptions.schemaName=null] the schema name to use if encryption is enabled.
+ * @param {String} [aOptions.schemaNamespace=null] the schema namespace to use if encryption is enabled.
+ * @param {String} [aOptions.schemaVersion=null] the schema version to use if encryption is enabled.
+ * @param {Boolean} [aOptions.addPioneerId=false] true if the ping should contain the Pioneer id, false otherwise.
+ * @param {Boolean} [aOptions.overridePioneerId=undefined] if set, override the
+ * pioneer id to the provided value. Only works if aOptions.addPioneerId=true.
+ * @returns {Object} An object that contains the assembled ping data.
+ */
+ assemblePing: function assemblePing(aType, aPayload, aOptions = {}) {
+ this._log.trace(
+ "assemblePing - Type " + aType + ", aOptions " + JSON.stringify(aOptions)
+ );
+
+ // Clone the payload data so we don't race against unexpected changes in subobjects that are
+ // still referenced by other code.
+ // We can't trust all callers to do this properly on their own.
+ let payload = Cu.cloneInto(aPayload, {});
+
+ // Fill the common ping fields.
+ let pingData = {
+ type: aType,
+ id: Policy.generatePingId(),
+ creationDate: Policy.now().toISOString(),
+ version: PING_FORMAT_VERSION,
+ application: this._getApplicationSection(),
+ payload,
+ };
+
+ if (aOptions.addClientId || aOptions.overrideClientId) {
+ pingData.clientId = aOptions.overrideClientId || this._clientID;
+ }
+
+ if (aOptions.addEnvironment) {
+ pingData.environment =
+ aOptions.overrideEnvironment ||
+ lazy.TelemetryEnvironment.currentEnvironment;
+ }
+
+ return pingData;
+ },
+
+ /**
+ * Track any pending ping send and save tasks through the promise passed here.
+ * This is needed to block shutdown on any outstanding ping activity.
+ */
+ _trackPendingPingTask(aPromise) {
+ this._connectionsBarrier.client.addBlocker(
+ "Waiting for ping task",
+ aPromise
+ );
+ },
+
+ /**
+ * Internal function to assemble a complete ping, adding environment data, client id
+ * and some general info. This waits on the client id to be loaded/generated if it's
+ * not yet available. Note that this function is synchronous unless we need to load
+ * the client id.
+ * Depending on configuration, the ping will be sent to the server (immediately or later)
+ * and archived locally.
+ *
+ * @param {String} aType The type of the ping.
+ * @param {Object} aPayload The actual data payload for the ping.
+ * @param {Object} [aOptions] Options object.
+ * @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
+ * id, false otherwise.
+ * @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the
+ * environment data.
+ * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data.
+ * @param {Boolean} [aOptions.usePingSender=false] if true, send the ping using the PingSender.
+ * @param {Boolean} [aOptions.useEncryption=false] if true, encrypt data client-side before sending.
+ * @param {Object} [aOptions.publicKey=null] the public key to use if encryption is enabled (JSON Web Key).
+ * @param {String} [aOptions.encryptionKeyId=null] the public key ID to use if encryption is enabled.
+ * @param {String} [aOptions.studyName=null] the study name to use.
+ * @param {String} [aOptions.schemaName=null] the schema name to use if encryption is enabled.
+ * @param {String} [aOptions.schemaNamespace=null] the schema namespace to use if encryption is enabled.
+ * @param {String} [aOptions.schemaVersion=null] the schema version to use if encryption is enabled.
+ * @param {Boolean} [aOptions.addPioneerId=false] true if the ping should contain the Pioneer id, false otherwise.
+ * @param {Boolean} [aOptions.overridePioneerId=undefined] if set, override the
+ * pioneer id to the provided value. Only works if aOptions.addPioneerId=true.
+ * @param {String} [aOptions.overrideClientId=undefined] if set, override the
+ * client id to the provided value. Implies aOptions.addClientId=true.
+ * @returns {Promise} Test-only - a promise that is resolved with the ping id once the ping is stored or sent.
+ */
+ async _submitPingLogic(aType, aPayload, aOptions) {
+ // Make sure to have a clientId if we need one. This cover the case of submitting
+ // a ping early during startup, before Telemetry is initialized, if no client id was
+ // cached.
+ if (!this._clientID && aOptions.addClientId && !aOptions.overrideClientId) {
+ this._log.trace("_submitPingLogic - Waiting on client id");
+ Services.telemetry
+ .getHistogramById("TELEMETRY_PING_SUBMISSION_WAITING_CLIENTID")
+ .add();
+ // We can safely call |getClientID| here and during initialization: we would still
+ // spawn and return one single loading task.
+ this._clientID = await lazy.ClientID.getClientID();
+ }
+
+ let pingData = this.assemblePing(aType, aPayload, aOptions);
+ this._log.trace("submitExternalPing - ping assembled, id: " + pingData.id);
+
+ if (aOptions.useEncryption === true) {
+ try {
+ if (!aOptions.publicKey) {
+ throw new Error("Public key is required when using encryption.");
+ }
+
+ if (
+ !(
+ aOptions.schemaName &&
+ aOptions.schemaNamespace &&
+ aOptions.schemaVersion
+ )
+ ) {
+ throw new Error(
+ "Schema name, namespace, and version are required when using encryption."
+ );
+ }
+
+ const payload = {};
+ payload.encryptedData = await lazy.jwcrypto.generateJWE(
+ aOptions.publicKey,
+ new TextEncoder().encode(JSON.stringify(aPayload))
+ );
+
+ payload.schemaVersion = aOptions.schemaVersion;
+ payload.schemaName = aOptions.schemaName;
+ payload.schemaNamespace = aOptions.schemaNamespace;
+
+ payload.encryptionKeyId = aOptions.encryptionKeyId;
+
+ if (aOptions.addPioneerId === true) {
+ if (aOptions.overridePioneerId) {
+ // The caller provided a substitute id, let's use that
+ // instead of querying the pref.
+ payload.pioneerId = aOptions.overridePioneerId;
+ } else {
+ // This will throw if there is no pioneer ID set.
+ payload.pioneerId = Services.prefs.getStringPref(
+ "toolkit.telemetry.pioneerId"
+ );
+ }
+ payload.studyName = aOptions.studyName;
+ }
+
+ pingData.payload = payload;
+ } catch (e) {
+ this._log.error("_submitPingLogic - Unable to encrypt ping", e);
+ // Do not attempt to continue
+ throw e;
+ }
+ }
+
+ // Always persist the pings if we are allowed to. We should not yield on any of the
+ // following operations to keep this function synchronous for the majority of the calls.
+ let archivePromise = lazy.TelemetryArchive.promiseArchivePing(
+ pingData
+ ).catch(e =>
+ this._log.error(
+ "submitExternalPing - Failed to archive ping " + pingData.id,
+ e
+ )
+ );
+ let p = [archivePromise];
+
+ p.push(
+ lazy.TelemetrySend.submitPing(pingData, {
+ usePingSender: aOptions.usePingSender,
+ })
+ );
+
+ return Promise.all(p).then(() => pingData.id);
+ },
+
+ /**
+ * Submit ping payloads to Telemetry.
+ *
+ * @param {String} aType The type of the ping.
+ * @param {Object} aPayload The actual data payload for the ping.
+ * @param {Object} [aOptions] Options object.
+ * @param {Boolean} [aOptions.addClientId=false] true if the ping should contain the client
+ * id, false otherwise.
+ * @param {Boolean} [aOptions.addEnvironment=false] true if the ping should contain the
+ * environment data.
+ * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data.
+ * @param {Boolean} [aOptions.usePingSender=false] if true, send the ping using the PingSender.
+ * @param {Boolean} [aOptions.useEncryption=false] if true, encrypt data client-side before sending.
+ * @param {Object} [aOptions.publicKey=null] the public key to use if encryption is enabled (JSON Web Key).
+ * @param {String} [aOptions.encryptionKeyId=null] the public key ID to use if encryption is enabled.
+ * @param {String} [aOptions.studyName=null] the study name to use.
+ * @param {String} [aOptions.schemaName=null] the schema name to use if encryption is enabled.
+ * @param {String} [aOptions.schemaNamespace=null] the schema namespace to use if encryption is enabled.
+ * @param {String} [aOptions.schemaVersion=null] the schema version to use if encryption is enabled.
+ * @param {Boolean} [aOptions.addPioneerId=false] true if the ping should contain the Pioneer id, false otherwise.
+ * @param {Boolean} [aOptions.overridePioneerId=undefined] if set, override the
+ * pioneer id to the provided value. Only works if aOptions.addPioneerId=true.
+ * @param {String} [aOptions.overrideClientId=undefined] if set, override the
+ * client id to the provided value. Implies aOptions.addClientId=true.
+ * @returns {Promise} Test-only - a promise that is resolved with the ping id once the ping is stored or sent.
+ */
+ submitExternalPing: function send(aType, aPayload, aOptions) {
+ this._log.trace(
+ "submitExternalPing - type: " +
+ aType +
+ ", aOptions: " +
+ JSON.stringify(aOptions)
+ );
+
+ // Reject pings sent after shutdown.
+ if (this._shutDown) {
+ const errorMessage =
+ "submitExternalPing - Submission is not allowed after shutdown, discarding ping of type: " +
+ aType;
+ this._log.error(errorMessage);
+ return Promise.reject(new Error(errorMessage));
+ }
+
+ // Enforce the type string to only contain sane characters.
+ const typeUuid = /^[a-z0-9][a-z0-9-]+[a-z0-9]$/i;
+ if (!typeUuid.test(aType)) {
+ this._log.error("submitExternalPing - invalid ping type: " + aType);
+ let histogram = Services.telemetry.getKeyedHistogramById(
+ "TELEMETRY_INVALID_PING_TYPE_SUBMITTED"
+ );
+ histogram.add(aType, 1);
+ return Promise.reject(new Error("Invalid type string submitted."));
+ }
+ // Enforce that the payload is an object.
+ if (
+ aPayload === null ||
+ typeof aPayload !== "object" ||
+ Array.isArray(aPayload)
+ ) {
+ this._log.error(
+ "submitExternalPing - invalid payload type: " + typeof aPayload
+ );
+ let histogram = Services.telemetry.getHistogramById(
+ "TELEMETRY_INVALID_PAYLOAD_SUBMITTED"
+ );
+ histogram.add(1);
+ return Promise.reject(new Error("Invalid payload type submitted."));
+ }
+
+ let promise = this._submitPingLogic(aType, aPayload, aOptions);
+ this._trackPendingPingTask(promise);
+ return promise;
+ },
+
+ /**
+ * Save a ping to disk.
+ *
+ * @param {String} aType The type of the ping.
+ * @param {Object} aPayload The actual data payload for the ping.
+ * @param {Object} aOptions Options object.
+ * @param {Boolean} aOptions.addClientId true if the ping should contain the client id,
+ * false otherwise.
+ * @param {Boolean} aOptions.addEnvironment true if the ping should contain the
+ * environment data.
+ * @param {Boolean} aOptions.overwrite true overwrites a ping with the same name, if found.
+ * @param {Object} [aOptions.overrideEnvironment=null] set to override the environment data.
+ * @param {String} [aOptions.overrideClientId=undefined] if set, override the
+ * client id to the provided value. Implies aOptions.addClientId=true.
+ *
+ * @returns {Promise} A promise that resolves with the ping id when the ping is saved to
+ * disk.
+ */
+ addPendingPing: function addPendingPing(aType, aPayload, aOptions) {
+ this._log.trace(
+ "addPendingPing - Type " +
+ aType +
+ ", aOptions " +
+ JSON.stringify(aOptions)
+ );
+
+ let pingData = this.assemblePing(aType, aPayload, aOptions);
+
+ let savePromise = lazy.TelemetryStorage.savePendingPing(pingData);
+ let archivePromise = lazy.TelemetryArchive.promiseArchivePing(
+ pingData
+ ).catch(e => {
+ this._log.error(
+ "addPendingPing - Failed to archive ping " + pingData.id,
+ e
+ );
+ });
+
+ // Wait for both the archiving and ping persistence to complete.
+ let promises = [savePromise, archivePromise];
+ return Promise.all(promises).then(() => pingData.id);
+ },
+
+ /**
+ * Check whether we have an aborted-session ping. If so add it to the pending pings and archive it.
+ *
+ * @return {Promise} Promise that is resolved when the ping is submitted and archived.
+ */
+ async checkAbortedSessionPing() {
+ let ping = await lazy.TelemetryStorage.loadAbortedSessionPing();
+ this._log.trace(
+ "checkAbortedSessionPing - found aborted-session ping: " + !!ping
+ );
+ if (!ping) {
+ return;
+ }
+
+ try {
+ // Previous aborted-session might have been with a canary client ID.
+ // Don't send it.
+ if (ping.clientId != Utils.knownClientID) {
+ await lazy.TelemetryStorage.savePendingPing(ping);
+ await lazy.TelemetryArchive.promiseArchivePing(ping);
+ }
+ } catch (e) {
+ this._log.error(
+ "checkAbortedSessionPing - Unable to add the pending ping",
+ e
+ );
+ } finally {
+ await lazy.TelemetryStorage.removeAbortedSessionPing();
+ }
+ },
+
+ /**
+ * Save an aborted-session ping to disk without adding it to the pending pings.
+ *
+ * @param {Object} aPayload The ping payload data.
+ * @return {Promise} Promise that is resolved when the ping is saved.
+ */
+ saveAbortedSessionPing(aPayload) {
+ this._log.trace("saveAbortedSessionPing");
+ const options = { addClientId: true, addEnvironment: true };
+ const pingData = this.assemblePing(PING_TYPE_MAIN, aPayload, options);
+ return lazy.TelemetryStorage.saveAbortedSessionPing(pingData);
+ },
+
+ removeAbortedSessionPing() {
+ return lazy.TelemetryStorage.removeAbortedSessionPing();
+ },
+
+ async saveUninstallPing() {
+ if (AppConstants.platform != "win") {
+ return undefined;
+ }
+
+ this._log.trace("saveUninstallPing");
+
+ let payload = {};
+ try {
+ payload.otherInstalls = lazy.UninstallPing.getOtherInstallsCount();
+ this._log.info(
+ "saveUninstallPing - otherInstalls",
+ payload.otherInstalls
+ );
+ } catch (e) {
+ this._log.warn("saveUninstallPing - getOtherInstallCount failed", e);
+ }
+ const options = { addClientId: true, addEnvironment: true };
+ const pingData = this.assemblePing(PING_TYPE_UNINSTALL, payload, options);
+
+ return lazy.TelemetryStorage.saveUninstallPing(pingData);
+ },
+
+ /**
+ * This triggers basic telemetry initialization and schedules a full initialized for later
+ * for performance reasons.
+ *
+ * This delayed initialization means TelemetryController init can be in the following states:
+ * 1) setupTelemetry was never called
+ * or it was called and
+ * 2) _delayedInitTask was scheduled, but didn't run yet.
+ * 3) _delayedInitTask is currently running.
+ * 4) _delayedInitTask finished running and is nulled out.
+ *
+ * @return {Promise} Resolved when TelemetryController and TelemetrySession are fully
+ * initialized. This is only used in tests.
+ */
+ setupTelemetry: function setupTelemetry(testing) {
+ this._initStarted = true;
+ this._shuttingDown = false;
+ this._shutDown = false;
+ this._testMode = testing;
+
+ this._log.trace("setupTelemetry");
+
+ if (this._delayedInitTask) {
+ this._log.error("setupTelemetry - init task already running");
+ return this._delayedInitTaskDeferred.promise;
+ }
+
+ if (this._initialized && !this._testMode) {
+ this._log.error("setupTelemetry - already initialized");
+ return Promise.resolve();
+ }
+
+ // Enable adding scalars in artifact builds and build faster modes.
+ // The function is async: we intentionally don't wait for it to complete
+ // as we don't want to delay startup.
+ this._probeRegistrationPromise = this.registerJsProbes();
+
+ // This will trigger displaying the datachoices infobar.
+ lazy.TelemetryReportingPolicy.setup();
+
+ if (!TelemetryControllerBase.enableTelemetryRecording()) {
+ this._log.config(
+ "setupChromeProcess - Telemetry recording is disabled, skipping Chrome process setup."
+ );
+ return Promise.resolve();
+ }
+
+ this._attachObservers();
+
+ // Perform a lightweight, early initialization for the component, just registering
+ // a few observers and initializing the session.
+ lazy.TelemetrySession.earlyInit(this._testMode);
+ Services.telemetry.earlyInit();
+
+ // Annotate crash reports so that we get pings for startup crashes
+ lazy.TelemetrySend.earlyInit();
+
+ // For very short session durations, we may never load the client
+ // id from disk.
+ // We try to cache it in prefs to avoid this, even though this may
+ // lead to some stale client ids.
+ this._clientID = lazy.ClientID.getCachedClientID();
+
+ // Init the update ping telemetry as early as possible. This won't have
+ // an impact on startup.
+ lazy.UpdatePing.earlyInit();
+
+ // Delay full telemetry initialization to give the browser time to
+ // run various late initializers. Otherwise our gathered memory
+ // footprint and other numbers would be too optimistic.
+ this._delayedInitTaskDeferred = Promise.withResolvers();
+ this._delayedInitTask = new DeferredTask(
+ async () => {
+ try {
+ // TODO: This should probably happen after all the delayed init here.
+ this._initialized = true;
+ await lazy.TelemetryEnvironment.delayedInit();
+
+ // Load the ClientID.
+ this._clientID = await lazy.ClientID.getClientID();
+
+ // Fix-up a canary client ID if detected.
+ const uploadEnabled = Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ false
+ );
+ if (uploadEnabled && this._clientID == Utils.knownClientID) {
+ this._log.trace(
+ "Upload enabled, but got canary client ID. Resetting."
+ );
+ await lazy.ClientID.removeClientID();
+ this._clientID = await lazy.ClientID.getClientID();
+ } else if (!uploadEnabled && this._clientID != Utils.knownClientID) {
+ this._log.trace(
+ "Upload disabled, but got a valid client ID. Setting canary client ID."
+ );
+ await lazy.ClientID.setCanaryClientID();
+ this._clientID = await lazy.ClientID.getClientID();
+ }
+
+ await lazy.TelemetrySend.setup(this._testMode);
+
+ // Perform TelemetrySession delayed init.
+ await lazy.TelemetrySession.delayedInit();
+ await Services.telemetry.delayedInit();
+
+ if (
+ Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.NewProfilePingEnabled,
+ false
+ ) &&
+ !lazy.TelemetrySession.newProfilePingSent
+ ) {
+ // Kick off the scheduling of the new-profile ping.
+ this.scheduleNewProfilePing();
+ }
+
+ // Purge the pings archive by removing outdated pings. We don't wait for
+ // this task to complete, but TelemetryStorage blocks on it during
+ // shutdown.
+ lazy.TelemetryStorage.runCleanPingArchiveTask();
+
+ // Now that FHR/healthreporter is gone, make sure to remove FHR's DB from
+ // the profile directory. This is a temporary measure that we should drop
+ // in the future.
+ lazy.TelemetryStorage.removeFHRDatabase();
+
+ // The init sequence is forced to run on shutdown for short sessions and
+ // we don't want to start TelemetryModules as the timer registration will fail.
+ if (!this._shuttingDown) {
+ // Report the modules loaded in the Firefox process.
+ lazy.TelemetryModules.start();
+
+ // Send coverage ping.
+ await lazy.CoveragePing.startup();
+
+ // Start the untrusted modules ping, which reports events where
+ // untrusted modules were loaded into the Firefox process.
+ if (AppConstants.platform == "win") {
+ lazy.TelemetryUntrustedModulesPing.start();
+ }
+ }
+
+ lazy.TelemetryEventPing.startup();
+
+ if (uploadEnabled) {
+ await this.saveUninstallPing().catch(e =>
+ this._log.warn("_delayedInitTask - saveUninstallPing failed", e)
+ );
+ } else {
+ await lazy.TelemetryStorage.removeUninstallPings().catch(e =>
+ this._log.warn("_delayedInitTask - saveUninstallPing", e)
+ );
+ }
+
+ this._delayedInitTaskDeferred.resolve();
+ } catch (e) {
+ this._delayedInitTaskDeferred.reject(e);
+ } finally {
+ this._delayedInitTask = null;
+ }
+ },
+ this._testMode ? TELEMETRY_TEST_DELAY : TELEMETRY_DELAY,
+ this._testMode ? 0 : undefined
+ );
+
+ IOUtils.sendTelemetry.addBlocker(
+ "TelemetryController: shutting down",
+ () => this.shutdown(),
+ () => this._getState()
+ );
+
+ this._delayedInitTask.arm();
+ return this._delayedInitTaskDeferred.promise;
+ },
+
+ // Do proper shutdown waiting and cleanup.
+ async _cleanupOnShutdown() {
+ if (!this._initialized) {
+ return;
+ }
+
+ this._detachObservers();
+
+ // Now do an orderly shutdown.
+ try {
+ if (this._delayedNewPingTask) {
+ await this._delayedNewPingTask.finalize();
+ }
+
+ lazy.UpdatePing.shutdown();
+
+ lazy.TelemetryEventPing.shutdown();
+
+ // Shutdown the sync ping if it is initialized - this is likely, but not
+ // guaranteed, to submit a "shutdown" sync ping.
+ if (this._fnSyncPingShutdown) {
+ this._fnSyncPingShutdown();
+ }
+
+ // Stop the datachoices infobar display.
+ lazy.TelemetryReportingPolicy.shutdown();
+ lazy.TelemetryEnvironment.shutdown();
+
+ // Stop any ping sending.
+ await lazy.TelemetrySend.shutdown();
+
+ // Send latest data.
+ await lazy.TelemetryHealthPing.shutdown();
+
+ await lazy.TelemetrySession.shutdown();
+ await Services.telemetry.shutdown();
+
+ // First wait for clients processing shutdown.
+ await this._shutdownBarrier.wait();
+
+ // ... and wait for any outstanding async ping activity.
+ await this._connectionsBarrier.wait();
+
+ if (AppConstants.platform !== "android") {
+ // No PingSender on Android.
+ lazy.TelemetrySend.flushPingSenderBatch();
+ }
+
+ // Perform final shutdown operations.
+ await lazy.TelemetryStorage.shutdown();
+ } finally {
+ // Reset state.
+ this._initialized = false;
+ this._initStarted = false;
+ this._shutDown = true;
+ }
+ },
+
+ shutdown() {
+ this._log.trace("shutdown");
+
+ this._shuttingDown = true;
+
+ // We can be in one the following states here:
+ // 1) setupTelemetry was never called
+ // or it was called and
+ // 2) _delayedInitTask was scheduled, but didn't run yet.
+ // 3) _delayedInitTask is running now.
+ // 4) _delayedInitTask finished running already.
+
+ // This handles 1).
+ if (!this._initStarted) {
+ this._shutDown = true;
+ return Promise.resolve();
+ }
+
+ // This handles 4).
+ if (!this._delayedInitTask) {
+ // We already ran the delayed initialization.
+ return this._cleanupOnShutdown();
+ }
+
+ // This handles 2) and 3).
+ return this._delayedInitTask
+ .finalize()
+ .then(() => this._cleanupOnShutdown());
+ },
+
+ /**
+ * This observer drives telemetry.
+ */
+ observe(aSubject, aTopic, aData) {
+ // The logger might still be not available at this point.
+ if (aTopic == "profile-after-change") {
+ // If we don't have a logger, we need to make sure |Log.repository.getLogger()| is
+ // called before |getLoggerWithMessagePrefix|. Otherwise logging won't work.
+ TelemetryControllerBase.configureLogging();
+ }
+
+ this._log.trace(`observe - ${aTopic} notified.`);
+
+ switch (aTopic) {
+ case "profile-after-change":
+ // profile-after-change is only registered for chrome processes.
+ return this.setupTelemetry();
+ case "nsPref:changed":
+ if (aData == TelemetryUtils.Preferences.FhrUploadEnabled) {
+ return this._onUploadPrefChange();
+ }
+ }
+ return undefined;
+ },
+
+ /**
+ * Register the sync ping's shutdown handler.
+ */
+ registerSyncPingShutdown(fnShutdown) {
+ if (this._fnSyncPingShutdown) {
+ throw new Error("The sync ping shutdown handler is already registered.");
+ }
+ this._fnSyncPingShutdown = fnShutdown;
+ },
+
+ /**
+ * Get an object describing the current state of this module for AsyncShutdown diagnostics.
+ */
+ _getState() {
+ return {
+ initialized: this._initialized,
+ initStarted: this._initStarted,
+ haveDelayedInitTask: !!this._delayedInitTask,
+ shutdownBarrier: this._shutdownBarrier.state,
+ connectionsBarrier: this._connectionsBarrier.state,
+ sendModule: lazy.TelemetrySend.getShutdownState(),
+ haveDelayedNewProfileTask: !!this._delayedNewPingTask,
+ };
+ },
+
+ /**
+ * Called whenever the FHR Upload preference changes (e.g. when user disables FHR from
+ * the preferences panel), this triggers sending the "deletion-request" ping.
+ */
+ _onUploadPrefChange() {
+ const uploadEnabled = Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ false
+ );
+ if (uploadEnabled) {
+ this._log.trace(
+ "_onUploadPrefChange - upload was enabled again. Resetting client ID"
+ );
+
+ // Delete cached client ID immediately, so other usage is forced to refetch it.
+ this._clientID = null;
+
+ // Generate a new client ID and make sure this module uses the new version
+ let p = (async () => {
+ await lazy.ClientID.removeClientID();
+ let id = await lazy.ClientID.getClientID();
+ this._clientID = id;
+ Services.telemetry.scalarSet("telemetry.data_upload_optin", true);
+
+ await this.saveUninstallPing().catch(e =>
+ this._log.warn("_onUploadPrefChange - saveUninstallPing failed", e)
+ );
+ })();
+
+ this._shutdownBarrier.client.addBlocker(
+ "TelemetryController: resetting client ID after data upload was enabled",
+ p
+ );
+
+ return;
+ }
+
+ let p = (async () => {
+ try {
+ // 1. Cancel the current pings.
+ // 2. Clear unpersisted pings
+ await lazy.TelemetrySend.clearCurrentPings();
+
+ // 3. Remove all pending pings
+ await lazy.TelemetryStorage.removeAppDataPings();
+ await lazy.TelemetryStorage.runRemovePendingPingsTask();
+ await lazy.TelemetryStorage.removeUninstallPings();
+ } catch (e) {
+ this._log.error(
+ "_onUploadPrefChange - error clearing pending pings",
+ e
+ );
+ } finally {
+ // 4. Reset session and subsession counter
+ lazy.TelemetrySession.resetSubsessionCounter();
+
+ // 5. Collect any additional identifiers we want to send in the
+ // deletion request.
+ const scalars = Services.telemetry.getSnapshotForScalars(
+ "deletion-request",
+ /* clear */ true
+ );
+
+ // 6. Set ClientID to a known value
+ let oldClientId = await lazy.ClientID.getClientID();
+ await lazy.ClientID.setCanaryClientID();
+ this._clientID = await lazy.ClientID.getClientID();
+
+ // 7. Send the deletion-request ping.
+ this._log.trace("_onUploadPrefChange - Sending deletion-request ping.");
+ this.submitExternalPing(
+ PING_TYPE_DELETION_REQUEST,
+ { scalars },
+ { overrideClientId: oldClientId }
+ );
+ this._deletionRequestPingSubmittedPromise = null;
+ }
+ })();
+
+ this._deletionRequestPingSubmittedPromise = p;
+ this._shutdownBarrier.client.addBlocker(
+ "TelemetryController: removing pending pings after data upload was disabled",
+ p
+ );
+
+ Services.obs.notifyObservers(
+ null,
+ TelemetryUtils.TELEMETRY_UPLOAD_DISABLED_TOPIC
+ );
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsISupportsWeakReference"]),
+
+ _attachObservers() {
+ if (TelemetryControllerBase.IS_UNIFIED_TELEMETRY) {
+ // Watch the FHR upload setting to trigger "deletion-request" pings.
+ Services.prefs.addObserver(
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ this,
+ true
+ );
+ }
+ },
+
+ /**
+ * Remove the preference observer to avoid leaks.
+ */
+ _detachObservers() {
+ if (TelemetryControllerBase.IS_UNIFIED_TELEMETRY) {
+ Services.prefs.removeObserver(
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ this
+ );
+ }
+ },
+
+ /**
+ * Allows waiting for TelemetryControllers delayed initialization to complete.
+ * This will complete before TelemetryController is shutting down.
+ * @return {Promise} Resolved when delayed TelemetryController initialization completed.
+ */
+ promiseInitialized() {
+ return this._delayedInitTaskDeferred.promise;
+ },
+
+ getCurrentPingData(aSubsession) {
+ this._log.trace("getCurrentPingData - subsession: " + aSubsession);
+
+ // Telemetry is disabled, don't gather any data.
+ if (!Services.telemetry.canRecordBase) {
+ return null;
+ }
+
+ const reason = aSubsession
+ ? REASON_GATHER_SUBSESSION_PAYLOAD
+ : REASON_GATHER_PAYLOAD;
+ const type = PING_TYPE_MAIN;
+ const payload = lazy.TelemetrySession.getPayload(reason);
+ const options = { addClientId: true, addEnvironment: true };
+ const ping = this.assemblePing(type, payload, options);
+
+ return ping;
+ },
+
+ async reset() {
+ this._clientID = null;
+ this._fnSyncPingShutdown = null;
+ this._detachObservers();
+
+ let sessionReset = lazy.TelemetrySession.testReset();
+
+ this._connectionsBarrier = new AsyncShutdown.Barrier(
+ "TelemetryController: Waiting for pending ping activity"
+ );
+ this._shutdownBarrier = new AsyncShutdown.Barrier(
+ "TelemetryController: Waiting for clients."
+ );
+
+ // We need to kick of the controller setup first for tests that check the
+ // cached client id.
+ let controllerSetup = this.setupTelemetry(true);
+
+ await sessionReset;
+ await lazy.TelemetrySend.reset();
+ await lazy.TelemetryStorage.reset();
+ await lazy.TelemetryEnvironment.testReset();
+
+ await controllerSetup;
+ },
+
+ /**
+ * Schedule sending the "new-profile" ping.
+ */
+ scheduleNewProfilePing() {
+ this._log.trace("scheduleNewProfilePing");
+
+ const sendDelay = Services.prefs.getIntPref(
+ TelemetryUtils.Preferences.NewProfilePingDelay,
+ NEWPROFILE_PING_DEFAULT_DELAY
+ );
+
+ this._delayedNewPingTask = new DeferredTask(async () => {
+ try {
+ await this.sendNewProfilePing();
+ } finally {
+ this._delayedNewPingTask = null;
+ }
+ }, sendDelay);
+
+ this._delayedNewPingTask.arm();
+ },
+
+ /**
+ * Generate and send the new-profile ping
+ */
+ async sendNewProfilePing() {
+ this._log.trace(
+ "sendNewProfilePing - shutting down: " + this._shuttingDown
+ );
+
+ const scalars = Services.telemetry.getSnapshotForScalars(
+ "new-profile",
+ /* clear */ true
+ );
+
+ // Generate the payload.
+ const payload = {
+ reason: this._shuttingDown ? "shutdown" : "startup",
+ processes: {
+ parent: {
+ scalars: scalars.parent,
+ },
+ },
+ };
+
+ // Generate and send the "new-profile" ping. This uses the
+ // pingsender if we're shutting down.
+ let options = {
+ addClientId: true,
+ addEnvironment: true,
+ usePingSender: this._shuttingDown,
+ };
+ // TODO: we need to be smarter about when to send the ping (and save the
+ // state to file). |requestIdleCallback| is currently only accessible
+ // through DOM. See bug 1361996.
+ await TelemetryController.submitExternalPing(
+ "new-profile",
+ payload,
+ options
+ ).then(
+ () => lazy.TelemetrySession.markNewProfilePingSent(),
+ e =>
+ this._log.error(
+ "sendNewProfilePing - failed to submit new-profile ping",
+ e
+ )
+ );
+ },
+
+ /**
+ * Register 'dynamic builtin' probes from the JSON definition files.
+ * This is needed to support adding new probes in developer builds
+ * without rebuilding the whole codebase.
+ *
+ * This is not meant to be used outside of local developer builds.
+ */
+ async registerJsProbes() {
+ // We don't support this outside of developer builds.
+ if (AppConstants.MOZILLA_OFFICIAL && !this._testMode) {
+ return;
+ }
+
+ this._log.trace("registerJsProbes - registering builtin JS probes");
+
+ await this.registerScalarProbes();
+ await this.registerEventProbes();
+ },
+
+ _loadProbeDefinitions(filename) {
+ let probeFile = Services.dirsvc.get("GreD", Ci.nsIFile);
+ probeFile.append(filename);
+ if (!probeFile.exists()) {
+ this._log.trace(
+ `loadProbeDefinitions - no builtin JS probe file ${filename}`
+ );
+ return null;
+ }
+
+ return IOUtils.readUTF8(probeFile.path);
+ },
+
+ async registerScalarProbes() {
+ this._log.trace(
+ "registerScalarProbes - registering scalar builtin JS probes"
+ );
+
+ // Load the scalar probes JSON file.
+ const scalarProbeFilename = "ScalarArtifactDefinitions.json";
+ let scalarJSProbes = {};
+ try {
+ let fileContent = await this._loadProbeDefinitions(scalarProbeFilename);
+ scalarJSProbes = JSON.parse(fileContent, (property, value) => {
+ // Fixup the "kind" property: it's a string, and we need the constant
+ // coming from nsITelemetry.
+ if (property !== "kind" || typeof value != "string") {
+ return value;
+ }
+
+ let newValue;
+ switch (value) {
+ case "nsITelemetry::SCALAR_TYPE_COUNT":
+ newValue = Services.telemetry.SCALAR_TYPE_COUNT;
+ break;
+ case "nsITelemetry::SCALAR_TYPE_BOOLEAN":
+ newValue = Services.telemetry.SCALAR_TYPE_BOOLEAN;
+ break;
+ case "nsITelemetry::SCALAR_TYPE_STRING":
+ newValue = Services.telemetry.SCALAR_TYPE_STRING;
+ break;
+ }
+ return newValue;
+ });
+ } catch (ex) {
+ this._log.error(
+ `registerScalarProbes - there was an error loading ${scalarProbeFilename}`,
+ ex
+ );
+ }
+
+ // Register the builtin probes.
+ for (let category in scalarJSProbes) {
+ // Expire the expired scalars
+ for (let name in scalarJSProbes[category]) {
+ let def = scalarJSProbes[category][name];
+ if (
+ !def ||
+ !def.expires ||
+ def.expires == "never" ||
+ def.expires == "default"
+ ) {
+ continue;
+ }
+ if (
+ Services.vc.compare(AppConstants.MOZ_APP_VERSION, def.expires) >= 0
+ ) {
+ def.expired = true;
+ }
+ }
+ Services.telemetry.registerBuiltinScalars(
+ category,
+ scalarJSProbes[category]
+ );
+ }
+ },
+
+ async registerEventProbes() {
+ this._log.trace(
+ "registerEventProbes - registering builtin JS Event probes"
+ );
+
+ // Load the event probes JSON file.
+ const eventProbeFilename = "EventArtifactDefinitions.json";
+ let eventJSProbes = {};
+ try {
+ let fileContent = await this._loadProbeDefinitions(eventProbeFilename);
+ eventJSProbes = JSON.parse(fileContent);
+ } catch (ex) {
+ this._log.error(
+ `registerEventProbes - there was an error loading ${eventProbeFilename}`,
+ ex
+ );
+ }
+
+ // Register the builtin probes.
+ for (let category in eventJSProbes) {
+ for (let name in eventJSProbes[category]) {
+ let def = eventJSProbes[category][name];
+ if (
+ !def ||
+ !def.expires ||
+ def.expires == "never" ||
+ def.expires == "default"
+ ) {
+ continue;
+ }
+ if (
+ Services.vc.compare(AppConstants.MOZ_APP_VERSION, def.expires) >= 0
+ ) {
+ def.expired = true;
+ }
+ }
+ Services.telemetry.registerBuiltinEvents(
+ category,
+ eventJSProbes[category]
+ );
+ }
+ },
+};
diff --git a/toolkit/components/telemetry/app/TelemetryEnvironment.sys.mjs b/toolkit/components/telemetry/app/TelemetryEnvironment.sys.mjs
new file mode 100644
index 0000000000..302a4040b3
--- /dev/null
+++ b/toolkit/components/telemetry/app/TelemetryEnvironment.sys.mjs
@@ -0,0 +1,2134 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { Log } from "resource://gre/modules/Log.sys.mjs";
+
+import { TelemetryUtils } from "resource://gre/modules/TelemetryUtils.sys.mjs";
+
+import { ObjectUtils } from "resource://gre/modules/ObjectUtils.sys.mjs";
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+import { UpdateUtils } from "resource://gre/modules/UpdateUtils.sys.mjs";
+
+const Utils = TelemetryUtils;
+
+import {
+ AddonManager,
+ AddonManagerPrivate,
+} from "resource://gre/modules/AddonManager.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ AttributionCode: "resource:///modules/AttributionCode.sys.mjs",
+ ProfileAge: "resource://gre/modules/ProfileAge.sys.mjs",
+ WindowsRegistry: "resource://gre/modules/WindowsRegistry.sys.mjs",
+ WindowsVersionInfo:
+ "resource://gre/modules/components-utils/WindowsVersionInfo.sys.mjs",
+});
+
+ChromeUtils.defineLazyGetter(lazy, "fxAccounts", () => {
+ return ChromeUtils.importESModule(
+ "resource://gre/modules/FxAccounts.sys.mjs"
+ ).getFxAccountsSingleton();
+});
+
+// The maximum length of a string (e.g. description) in the addons section.
+const MAX_ADDON_STRING_LENGTH = 100;
+// The maximum length of a string value in the settings.attribution object.
+const MAX_ATTRIBUTION_STRING_LENGTH = 100;
+// The maximum lengths for the experiment id and branch in the experiments section.
+const MAX_EXPERIMENT_ID_LENGTH = 100;
+const MAX_EXPERIMENT_BRANCH_LENGTH = 100;
+const MAX_EXPERIMENT_TYPE_LENGTH = 20;
+const MAX_EXPERIMENT_ENROLLMENT_ID_LENGTH = 40;
+
+/**
+ * This is a policy object used to override behavior for testing.
+ */
+// eslint-disable-next-line no-unused-vars
+export var Policy = {
+ now: () => new Date(),
+ _intlLoaded: false,
+ _browserDelayedStartup() {
+ if (Policy._intlLoaded) {
+ return Promise.resolve();
+ }
+ return new Promise(resolve => {
+ let startupTopic = "browser-delayed-startup-finished";
+ Services.obs.addObserver(function observer(subject, topic) {
+ if (topic == startupTopic) {
+ Services.obs.removeObserver(observer, startupTopic);
+ resolve();
+ }
+ }, startupTopic);
+ });
+ },
+};
+
+// This is used to buffer calls to setExperimentActive and friends, so that we
+// don't prematurely initialize our environment if it is called early during
+// startup.
+var gActiveExperimentStartupBuffer = new Map();
+
+var gGlobalEnvironment;
+function getGlobal() {
+ if (!gGlobalEnvironment) {
+ gGlobalEnvironment = new EnvironmentCache();
+ }
+ return gGlobalEnvironment;
+}
+
+export var TelemetryEnvironment = {
+ get currentEnvironment() {
+ return getGlobal().currentEnvironment;
+ },
+
+ onInitialized() {
+ return getGlobal().onInitialized();
+ },
+
+ delayedInit() {
+ return getGlobal().delayedInit();
+ },
+
+ registerChangeListener(name, listener) {
+ return getGlobal().registerChangeListener(name, listener);
+ },
+
+ unregisterChangeListener(name) {
+ return getGlobal().unregisterChangeListener(name);
+ },
+
+ /**
+ * Add an experiment annotation to the environment.
+ * If an annotation with the same id already exists, it will be overwritten.
+ * This triggers a new subsession, subject to throttling.
+ *
+ * @param {String} id The id of the active experiment.
+ * @param {String} branch The experiment branch.
+ * @param {Object} [options] Optional object with options.
+ * @param {String} [options.type=false] The specific experiment type.
+ * @param {String} [options.enrollmentId=undefined] The id of the enrollment.
+ */
+ setExperimentActive(id, branch, options = {}) {
+ if (gGlobalEnvironment) {
+ gGlobalEnvironment.setExperimentActive(id, branch, options);
+ } else {
+ gActiveExperimentStartupBuffer.set(id, { branch, options });
+ }
+ },
+
+ /**
+ * Remove an experiment annotation from the environment.
+ * If the annotation exists, a new subsession will triggered.
+ *
+ * @param {String} id The id of the active experiment.
+ */
+ setExperimentInactive(id) {
+ if (gGlobalEnvironment) {
+ gGlobalEnvironment.setExperimentInactive(id);
+ } else {
+ gActiveExperimentStartupBuffer.delete(id);
+ }
+ },
+
+ /**
+ * Returns an object containing the data for the active experiments.
+ *
+ * The returned object is of the format:
+ *
+ * {
+ * "<experiment id>": { branch: "<branch>" },
+ * // …
+ * }
+ */
+ getActiveExperiments() {
+ if (gGlobalEnvironment) {
+ return gGlobalEnvironment.getActiveExperiments();
+ }
+
+ const result = {};
+ for (const [id, { branch }] of gActiveExperimentStartupBuffer.entries()) {
+ result[id] = branch;
+ }
+ return result;
+ },
+
+ shutdown() {
+ return getGlobal().shutdown();
+ },
+
+ // Policy to use when saving preferences. Exported for using them in tests.
+ // Reports "<user-set>" if there is a value set on the user branch
+ RECORD_PREF_STATE: 1,
+
+ // Reports the value set on the user branch, if one is set
+ RECORD_PREF_VALUE: 2,
+
+ // Reports the active value (set on either the user or default branch)
+ // for this pref, if one is set
+ RECORD_DEFAULTPREF_VALUE: 3,
+
+ // Reports "<set>" if a value for this pref is defined on either the user
+ // or default branch
+ RECORD_DEFAULTPREF_STATE: 4,
+
+ // Testing method
+ async testWatchPreferences(prefMap) {
+ return getGlobal()._watchPreferences(prefMap);
+ },
+
+ /**
+ * Intended for use in tests only.
+ *
+ * In multiple tests we need a way to shut and re-start telemetry together
+ * with TelemetryEnvironment. This is problematic due to the fact that
+ * TelemetryEnvironment is a singleton. We, therefore, need this helper
+ * method to be able to re-set TelemetryEnvironment.
+ */
+ testReset() {
+ return getGlobal().reset();
+ },
+
+ /**
+ * Intended for use in tests only.
+ */
+ testCleanRestart() {
+ getGlobal().shutdown();
+ gGlobalEnvironment = null;
+ gActiveExperimentStartupBuffer = new Map();
+ return getGlobal();
+ },
+};
+
+const RECORD_PREF_STATE = TelemetryEnvironment.RECORD_PREF_STATE;
+const RECORD_PREF_VALUE = TelemetryEnvironment.RECORD_PREF_VALUE;
+const RECORD_DEFAULTPREF_VALUE = TelemetryEnvironment.RECORD_DEFAULTPREF_VALUE;
+const RECORD_DEFAULTPREF_STATE = TelemetryEnvironment.RECORD_DEFAULTPREF_STATE;
+const DEFAULT_ENVIRONMENT_PREFS = new Map([
+ ["app.feedback.baseURL", { what: RECORD_PREF_VALUE }],
+ ["app.support.baseURL", { what: RECORD_PREF_VALUE }],
+ ["accessibility.browsewithcaret", { what: RECORD_PREF_VALUE }],
+ ["accessibility.force_disabled", { what: RECORD_PREF_VALUE }],
+ ["app.normandy.test-prefs.bool", { what: RECORD_PREF_VALUE }],
+ ["app.normandy.test-prefs.integer", { what: RECORD_PREF_VALUE }],
+ ["app.normandy.test-prefs.string", { what: RECORD_PREF_VALUE }],
+ ["app.shield.optoutstudies.enabled", { what: RECORD_PREF_VALUE }],
+ ["app.update.interval", { what: RECORD_PREF_VALUE }],
+ ["app.update.service.enabled", { what: RECORD_PREF_VALUE }],
+ ["app.update.silent", { what: RECORD_PREF_VALUE }],
+ ["browser.cache.disk.enable", { what: RECORD_PREF_VALUE }],
+ ["browser.cache.disk.capacity", { what: RECORD_PREF_VALUE }],
+ ["browser.cache.memory.enable", { what: RECORD_PREF_VALUE }],
+ ["browser.formfill.enable", { what: RECORD_PREF_VALUE }],
+ ["browser.migrate.interactions.bookmarks", { what: RECORD_PREF_VALUE }],
+ ["browser.migrate.interactions.csvpasswords", { what: RECORD_PREF_VALUE }],
+ ["browser.migrate.interactions.history", { what: RECORD_PREF_VALUE }],
+ ["browser.migrate.interactions.passwords", { what: RECORD_PREF_VALUE }],
+ ["browser.newtabpage.enabled", { what: RECORD_PREF_VALUE }],
+ ["browser.privatebrowsing.autostart", { what: RECORD_PREF_VALUE }],
+ ["browser.shell.checkDefaultBrowser", { what: RECORD_PREF_VALUE }],
+ ["browser.search.region", { what: RECORD_PREF_VALUE }],
+ ["browser.search.suggest.enabled", { what: RECORD_PREF_VALUE }],
+ ["browser.search.widget.inNavBar", { what: RECORD_DEFAULTPREF_VALUE }],
+ ["browser.startup.homepage", { what: RECORD_PREF_STATE }],
+ ["browser.startup.page", { what: RECORD_PREF_VALUE }],
+ ["browser.urlbar.autoFill", { what: RECORD_DEFAULTPREF_VALUE }],
+ [
+ "browser.urlbar.autoFill.adaptiveHistory.enabled",
+ { what: RECORD_DEFAULTPREF_VALUE },
+ ],
+ [
+ "browser.urlbar.dnsResolveSingleWordsAfterSearch",
+ { what: RECORD_DEFAULTPREF_VALUE },
+ ],
+ [
+ "browser.urlbar.quicksuggest.onboardingDialogChoice",
+ { what: RECORD_DEFAULTPREF_VALUE },
+ ],
+ [
+ "browser.urlbar.quicksuggest.dataCollection.enabled",
+ { what: RECORD_DEFAULTPREF_VALUE },
+ ],
+ ["browser.urlbar.showSearchSuggestionsFirst", { what: RECORD_PREF_VALUE }],
+ ["browser.urlbar.showSearchTerms.enabled", { what: RECORD_PREF_VALUE }],
+ [
+ "browser.urlbar.suggest.quicksuggest.nonsponsored",
+ { what: RECORD_DEFAULTPREF_VALUE },
+ ],
+ [
+ "browser.urlbar.suggest.quicksuggest.sponsored",
+ { what: RECORD_DEFAULTPREF_VALUE },
+ ],
+ ["browser.urlbar.suggest.searches", { what: RECORD_PREF_VALUE }],
+ ["devtools.chrome.enabled", { what: RECORD_PREF_VALUE }],
+ ["devtools.debugger.enabled", { what: RECORD_PREF_VALUE }],
+ ["devtools.debugger.remote-enabled", { what: RECORD_PREF_VALUE }],
+ ["doh-rollout.doorhanger-decision", { what: RECORD_PREF_VALUE }],
+ ["dom.ipc.processCount", { what: RECORD_PREF_VALUE }],
+ ["dom.max_script_run_time", { what: RECORD_PREF_VALUE }],
+ ["dom.popup_allowed_events", { what: RECORD_PREF_VALUE }],
+ ["editor.truncate_user_pastes", { what: RECORD_PREF_VALUE }],
+ ["extensions.InstallTrigger.enabled", { what: RECORD_PREF_VALUE }],
+ ["extensions.InstallTriggerImpl.enabled", { what: RECORD_PREF_VALUE }],
+ ["extensions.autoDisableScopes", { what: RECORD_PREF_VALUE }],
+ ["extensions.blocklist.enabled", { what: RECORD_PREF_VALUE }],
+ ["extensions.enabledScopes", { what: RECORD_PREF_VALUE }],
+ ["extensions.eventPages.enabled", { what: RECORD_PREF_VALUE }],
+ ["extensions.formautofill.addresses.enabled", { what: RECORD_PREF_VALUE }],
+ [
+ "extensions.formautofill.addresses.capture.enabled",
+ { what: RECORD_PREF_VALUE },
+ ],
+ ["extensions.formautofill.creditCards.enabled", { what: RECORD_PREF_VALUE }],
+ ["extensions.manifestV3.enabled", { what: RECORD_PREF_VALUE }],
+ ["extensions.quarantinedDomains.enabled", { what: RECORD_PREF_VALUE }],
+ ["extensions.strictCompatibility", { what: RECORD_PREF_VALUE }],
+ ["extensions.update.enabled", { what: RECORD_PREF_VALUE }],
+ ["extensions.update.url", { what: RECORD_PREF_VALUE }],
+ ["extensions.update.background.url", { what: RECORD_PREF_VALUE }],
+ ["extensions.screenshots.disabled", { what: RECORD_PREF_VALUE }],
+ ["general.config.filename", { what: RECORD_DEFAULTPREF_STATE }],
+ ["general.smoothScroll", { what: RECORD_PREF_VALUE }],
+ ["gfx.direct2d.disabled", { what: RECORD_PREF_VALUE }],
+ ["gfx.direct2d.force-enabled", { what: RECORD_PREF_VALUE }],
+ ["gfx.webrender.all", { what: RECORD_PREF_VALUE }],
+ ["layers.acceleration.disabled", { what: RECORD_PREF_VALUE }],
+ ["layers.acceleration.force-enabled", { what: RECORD_PREF_VALUE }],
+ ["layers.async-pan-zoom.enabled", { what: RECORD_PREF_VALUE }],
+ ["layers.async-video-oop.enabled", { what: RECORD_PREF_VALUE }],
+ ["layers.d3d11.disable-warp", { what: RECORD_PREF_VALUE }],
+ ["layers.d3d11.force-warp", { what: RECORD_PREF_VALUE }],
+ [
+ "layers.offmainthreadcomposition.force-disabled",
+ { what: RECORD_PREF_VALUE },
+ ],
+ ["layers.prefer-d3d9", { what: RECORD_PREF_VALUE }],
+ ["layers.prefer-opengl", { what: RECORD_PREF_VALUE }],
+ ["layout.css.devPixelsPerPx", { what: RECORD_PREF_VALUE }],
+ ["media.gmp-gmpopenh264.enabled", { what: RECORD_PREF_VALUE }],
+ ["media.gmp-gmpopenh264.lastInstallFailed", { what: RECORD_PREF_VALUE }],
+ ["media.gmp-gmpopenh264.lastInstallFailReason", { what: RECORD_PREF_VALUE }],
+ ["media.gmp-gmpopenh264.lastInstallStart", { what: RECORD_PREF_VALUE }],
+ ["media.gmp-gmpopenh264.lastDownload", { what: RECORD_PREF_VALUE }],
+ ["media.gmp-gmpopenh264.lastDownloadFailed", { what: RECORD_PREF_VALUE }],
+ ["media.gmp-gmpopenh264.lastDownloadFailReason", { what: RECORD_PREF_VALUE }],
+ ["media.gmp-gmpopenh264.lastUpdate", { what: RECORD_PREF_VALUE }],
+ ["media.gmp-gmpopenh264.visible", { what: RECORD_PREF_VALUE }],
+ ["media.gmp-widevinecdm.enabled", { what: RECORD_PREF_VALUE }],
+ ["media.gmp-widevinecdm.lastInstallFailed", { what: RECORD_PREF_VALUE }],
+ ["media.gmp-widevinecdm.lastInstallFailReason", { what: RECORD_PREF_VALUE }],
+ ["media.gmp-widevinecdm.lastInstallStart", { what: RECORD_PREF_VALUE }],
+ ["media.gmp-widevinecdm.lastDownload", { what: RECORD_PREF_VALUE }],
+ ["media.gmp-widevinecdm.lastDownloadFailed", { what: RECORD_PREF_VALUE }],
+ ["media.gmp-widevinecdm.lastDownloadFailReason", { what: RECORD_PREF_VALUE }],
+ ["media.gmp-widevinecdm.lastUpdate", { what: RECORD_PREF_VALUE }],
+ ["media.gmp-widevinecdm.visible", { what: RECORD_PREF_VALUE }],
+ ["media.gmp-manager.lastCheck", { what: RECORD_PREF_VALUE }],
+ ["media.gmp-manager.lastEmptyCheck", { what: RECORD_PREF_VALUE }],
+ ["network.http.windows-sso.enabled", { what: RECORD_PREF_VALUE }],
+ ["network.proxy.autoconfig_url", { what: RECORD_PREF_STATE }],
+ ["network.proxy.http", { what: RECORD_PREF_STATE }],
+ ["network.proxy.ssl", { what: RECORD_PREF_STATE }],
+ ["network.trr.mode", { what: RECORD_PREF_VALUE }],
+ ["network.trr.strict_native_fallback", { what: RECORD_DEFAULTPREF_VALUE }],
+ ["pdfjs.disabled", { what: RECORD_PREF_VALUE }],
+ ["places.history.enabled", { what: RECORD_PREF_VALUE }],
+ ["privacy.firstparty.isolate", { what: RECORD_PREF_VALUE }],
+ ["privacy.resistFingerprinting", { what: RECORD_PREF_VALUE }],
+ ["privacy.fingerprintingProtection", { what: RECORD_PREF_VALUE }],
+ ["privacy.fingerprintingProtection.pbmode", { what: RECORD_PREF_VALUE }],
+ ["privacy.trackingprotection.enabled", { what: RECORD_PREF_VALUE }],
+ ["privacy.donottrackheader.enabled", { what: RECORD_PREF_VALUE }],
+ ["security.enterprise_roots.auto-enabled", { what: RECORD_PREF_VALUE }],
+ ["security.enterprise_roots.enabled", { what: RECORD_PREF_VALUE }],
+ ["security.pki.mitm_detected", { what: RECORD_PREF_VALUE }],
+ ["security.mixed_content.block_active_content", { what: RECORD_PREF_VALUE }],
+ ["security.mixed_content.block_display_content", { what: RECORD_PREF_VALUE }],
+ ["security.tls.version.enable-deprecated", { what: RECORD_PREF_VALUE }],
+ ["signon.management.page.breach-alerts.enabled", { what: RECORD_PREF_VALUE }],
+ ["signon.autofillForms", { what: RECORD_PREF_VALUE }],
+ ["signon.generation.enabled", { what: RECORD_PREF_VALUE }],
+ ["signon.rememberSignons", { what: RECORD_PREF_VALUE }],
+ ["signon.firefoxRelay.feature", { what: RECORD_PREF_VALUE }],
+ ["toolkit.telemetry.pioneerId", { what: RECORD_PREF_STATE }],
+ [
+ "widget.content.gtk-high-contrast.enabled",
+ { what: RECORD_DEFAULTPREF_VALUE },
+ ],
+ ["xpinstall.signatures.required", { what: RECORD_PREF_VALUE }],
+ ["nimbus.debug", { what: RECORD_PREF_VALUE }],
+]);
+
+if (AppConstants.platform == "linux" || AppConstants.platform == "macosx") {
+ DEFAULT_ENVIRONMENT_PREFS.set(
+ "intl.ime.use_composition_events_for_insert_text",
+ { what: RECORD_PREF_VALUE }
+ );
+}
+
+const LOGGER_NAME = "Toolkit.Telemetry";
+
+const PREF_BLOCKLIST_ENABLED = "extensions.blocklist.enabled";
+const PREF_DISTRIBUTION_ID = "distribution.id";
+const PREF_DISTRIBUTION_VERSION = "distribution.version";
+const PREF_DISTRIBUTOR = "app.distributor";
+const PREF_DISTRIBUTOR_CHANNEL = "app.distributor.channel";
+const PREF_APP_PARTNER_BRANCH = "app.partner.";
+const PREF_PARTNER_ID = "mozilla.partner.id";
+
+const COMPOSITOR_CREATED_TOPIC = "compositor:created";
+const COMPOSITOR_PROCESS_ABORTED_TOPIC = "compositor:process-aborted";
+const DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC =
+ "distribution-customization-complete";
+const GFX_FEATURES_READY_TOPIC = "gfx-features-ready";
+const SEARCH_ENGINE_MODIFIED_TOPIC = "browser-search-engine-modified";
+const SEARCH_SERVICE_TOPIC = "browser-search-service";
+const SESSIONSTORE_WINDOWS_RESTORED_TOPIC = "sessionstore-windows-restored";
+const PREF_CHANGED_TOPIC = "nsPref:changed";
+const GMP_PROVIDER_REGISTERED_TOPIC = "gmp-provider-registered";
+const AUTO_UPDATE_PREF_CHANGE_TOPIC =
+ UpdateUtils.PER_INSTALLATION_PREFS["app.update.auto"].observerTopic;
+const BACKGROUND_UPDATE_PREF_CHANGE_TOPIC =
+ UpdateUtils.PER_INSTALLATION_PREFS["app.update.background.enabled"]
+ .observerTopic;
+const SERVICES_INFO_CHANGE_TOPIC = "sync-ui-state:update";
+
+/**
+ * Enforces the parameter to a boolean value.
+ * @param aValue The input value.
+ * @return {Boolean|Object} If aValue is a boolean or a number, returns its truthfulness
+ * value. Otherwise, return null.
+ */
+function enforceBoolean(aValue) {
+ if (typeof aValue !== "number" && typeof aValue !== "boolean") {
+ return null;
+ }
+ return Boolean(aValue);
+}
+
+/**
+ * Get the current browser locale.
+ * @return a string with the locale or null on failure.
+ */
+function getBrowserLocale() {
+ try {
+ return Services.locale.appLocaleAsBCP47;
+ } catch (e) {
+ return null;
+ }
+}
+
+/**
+ * Get the current OS locale.
+ * @return a string with the OS locale or null on failure.
+ */
+function getSystemLocale() {
+ try {
+ return Cc["@mozilla.org/intl/ospreferences;1"].getService(
+ Ci.mozIOSPreferences
+ ).systemLocale;
+ } catch (e) {
+ return null;
+ }
+}
+
+/**
+ * Get the current OS locales.
+ * @return an array of strings with the OS locales or null on failure.
+ */
+function getSystemLocales() {
+ try {
+ return Cc["@mozilla.org/intl/ospreferences;1"].getService(
+ Ci.mozIOSPreferences
+ ).systemLocales;
+ } catch (e) {
+ return null;
+ }
+}
+
+/**
+ * Get the current OS regional preference locales.
+ * @return an array of strings with the OS regional preference locales or null on failure.
+ */
+function getRegionalPrefsLocales() {
+ try {
+ return Cc["@mozilla.org/intl/ospreferences;1"].getService(
+ Ci.mozIOSPreferences
+ ).regionalPrefsLocales;
+ } catch (e) {
+ return null;
+ }
+}
+
+function getIntlSettings() {
+ return {
+ requestedLocales: Services.locale.requestedLocales,
+ availableLocales: Services.locale.availableLocales,
+ appLocales: Services.locale.appLocalesAsBCP47,
+ systemLocales: getSystemLocales(),
+ regionalPrefsLocales: getRegionalPrefsLocales(),
+ acceptLanguages: Services.prefs
+ .getComplexValue("intl.accept_languages", Ci.nsIPrefLocalizedString)
+ .data.split(",")
+ .map(str => str.trim()),
+ };
+}
+
+/**
+ * Safely get a sysinfo property and return its value. If the property is not
+ * available, return aDefault.
+ *
+ * @param aPropertyName the property name to get.
+ * @param aDefault the value to return if aPropertyName is not available.
+ * @return The property value, if available, or aDefault.
+ */
+function getSysinfoProperty(aPropertyName, aDefault) {
+ try {
+ // |getProperty| may throw if |aPropertyName| does not exist.
+ return Services.sysinfo.getProperty(aPropertyName);
+ } catch (e) {}
+
+ return aDefault;
+}
+
+/**
+ * Safely get a gfxInfo field and return its value. If the field is not available, return
+ * aDefault.
+ *
+ * @param aPropertyName the property name to get.
+ * @param aDefault the value to return if aPropertyName is not available.
+ * @return The property value, if available, or aDefault.
+ */
+function getGfxField(aPropertyName, aDefault) {
+ let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+
+ try {
+ // Accessing the field may throw if |aPropertyName| does not exist.
+ let gfxProp = gfxInfo[aPropertyName];
+ if (gfxProp !== undefined && gfxProp !== "") {
+ return gfxProp;
+ }
+ } catch (e) {}
+
+ return aDefault;
+}
+
+/**
+ * Returns a substring of the input string.
+ *
+ * @param {String} aString The input string.
+ * @param {Integer} aMaxLength The maximum length of the returned substring. If this is
+ * greater than the length of the input string, we return the whole input string.
+ * @return {String} The substring or null if the input string is null.
+ */
+function limitStringToLength(aString, aMaxLength) {
+ if (typeof aString !== "string") {
+ return null;
+ }
+ return aString.substring(0, aMaxLength);
+}
+
+/**
+ * Force a value to be a string.
+ * Only if the value is null, null is returned instead.
+ */
+function forceToStringOrNull(aValue) {
+ if (aValue === null) {
+ return null;
+ }
+
+ return String(aValue);
+}
+
+/**
+ * Get the information about a graphic adapter.
+ *
+ * @param aSuffix A suffix to add to the properties names.
+ * @return An object containing the adapter properties.
+ */
+function getGfxAdapter(aSuffix = "") {
+ // Note that gfxInfo, and so getGfxField, might return "Unknown" for the RAM on failures,
+ // not null.
+ let memoryMB = parseInt(getGfxField("adapterRAM" + aSuffix, null), 10);
+ if (Number.isNaN(memoryMB)) {
+ memoryMB = null;
+ }
+
+ return {
+ description: getGfxField("adapterDescription" + aSuffix, null),
+ vendorID: getGfxField("adapterVendorID" + aSuffix, null),
+ deviceID: getGfxField("adapterDeviceID" + aSuffix, null),
+ subsysID: getGfxField("adapterSubsysID" + aSuffix, null),
+ RAM: memoryMB,
+ driver: getGfxField("adapterDriver" + aSuffix, null),
+ driverVendor: getGfxField("adapterDriverVendor" + aSuffix, null),
+ driverVersion: getGfxField("adapterDriverVersion" + aSuffix, null),
+ driverDate: getGfxField("adapterDriverDate" + aSuffix, null),
+ };
+}
+
+/**
+ * Encapsulates the asynchronous magic interfacing with the addon manager. The builder
+ * is owned by a parent environment object and is an addon listener.
+ */
+function EnvironmentAddonBuilder(environment) {
+ this._environment = environment;
+
+ // The pending task blocks addon manager shutdown. It can either be the initial load
+ // or a change load.
+ this._pendingTask = null;
+
+ // Have we added an observer to listen for blocklist changes that still needs to be
+ // removed:
+ this._gmpProviderObserverAdded = false;
+
+ // Set to true once initial load is complete and we're watching for changes.
+ this._loaded = false;
+
+ // The state reported by the shutdown blocker if we hang shutdown.
+ this._shutdownState = "Initial";
+}
+EnvironmentAddonBuilder.prototype = {
+ /**
+ * Get the initial set of addons.
+ * @returns Promise<void> when the initial load is complete.
+ */
+ async init() {
+ AddonManager.beforeShutdown.addBlocker(
+ "EnvironmentAddonBuilder",
+ () => this._shutdownBlocker(),
+ { fetchState: () => this._shutdownState }
+ );
+
+ this._pendingTask = (async () => {
+ try {
+ this._shutdownState = "Awaiting _updateAddons";
+ // Gather initial addons details
+ await this._updateAddons();
+
+ if (!this._environment._addonsAreFull) {
+ // The addon database has not been loaded, wait for it to
+ // initialize and gather full data as soon as it does.
+ this._shutdownState = "Awaiting AddonManagerPrivate.databaseReady";
+ await AddonManagerPrivate.databaseReady;
+
+ // Now gather complete addons details.
+ this._shutdownState = "Awaiting second _updateAddons";
+ await this._updateAddons();
+ }
+ } catch (err) {
+ this._environment._log.error("init - Exception in _updateAddons", err);
+ } finally {
+ this._pendingTask = null;
+ this._shutdownState = "_pendingTask init complete. No longer blocking.";
+ }
+ })();
+
+ return this._pendingTask;
+ },
+
+ /**
+ * Register an addon listener and watch for changes.
+ */
+ watchForChanges() {
+ this._loaded = true;
+ AddonManager.addAddonListener(this);
+ },
+
+ // AddonListener
+ onEnabled(addon) {
+ this._onAddonChange(addon);
+ },
+ onDisabled(addon) {
+ this._onAddonChange(addon);
+ },
+ onInstalled(addon) {
+ this._onAddonChange(addon);
+ },
+ onUninstalling(addon) {
+ this._onAddonChange(addon);
+ },
+ onUninstalled(addon) {
+ this._onAddonChange(addon);
+ },
+ onPropertyChanged(addon, propertiesChanged) {
+ // Avoid to update the telemetry environment for onPropertyChanged
+ // calls that we are not actually interested in (and quarantineIgnoredByApp
+ // is not expected to change at runtime, unless the entire active addons
+ // entry is also replaced, e.g. on the extension being uninstalled and
+ // installed again).
+ if (!propertiesChanged.includes("quarantineIgnoredByUser")) {
+ return;
+ }
+ this._onAddonChange(addon);
+ },
+
+ _onAddonChange(addon) {
+ if (addon && addon.isBuiltin && !addon.isSystem) {
+ return;
+ }
+ this._environment._log.trace("_onAddonChange");
+ this._checkForChanges("addons-changed");
+ },
+
+ // nsIObserver
+ observe(aSubject, aTopic, aData) {
+ this._environment._log.trace("observe - Topic " + aTopic);
+ if (aTopic == GMP_PROVIDER_REGISTERED_TOPIC) {
+ Services.obs.removeObserver(this, GMP_PROVIDER_REGISTERED_TOPIC);
+ this._gmpProviderObserverAdded = false;
+ let gmpPluginsPromise = this._getActiveGMPlugins();
+ gmpPluginsPromise.then(
+ gmpPlugins => {
+ let { addons } = this._environment._currentEnvironment;
+ addons.activeGMPlugins = gmpPlugins;
+ },
+ err => {
+ this._environment._log.error(
+ "blocklist observe: Error collecting plugins",
+ err
+ );
+ }
+ );
+ }
+ },
+
+ _checkForChanges(changeReason) {
+ if (this._pendingTask) {
+ this._environment._log.trace(
+ "_checkForChanges - task already pending, dropping change with reason " +
+ changeReason
+ );
+ return;
+ }
+
+ this._shutdownState = "_checkForChanges awaiting _updateAddons";
+ this._pendingTask = this._updateAddons().then(
+ result => {
+ this._pendingTask = null;
+ this._shutdownState = "No longer blocking, _updateAddons resolved";
+ if (result.changed) {
+ this._environment._onEnvironmentChange(
+ changeReason,
+ result.oldEnvironment
+ );
+ }
+ },
+ err => {
+ this._pendingTask = null;
+ this._shutdownState = "No longer blocking, _updateAddons rejected";
+ this._environment._log.error(
+ "_checkForChanges: Error collecting addons",
+ err
+ );
+ }
+ );
+ },
+
+ _shutdownBlocker() {
+ if (this._loaded) {
+ AddonManager.removeAddonListener(this);
+ if (this._gmpProviderObserverAdded) {
+ Services.obs.removeObserver(this, GMP_PROVIDER_REGISTERED_TOPIC);
+ }
+ }
+
+ // At startup, _pendingTask is set to a Promise that does not resolve
+ // until the addons database has been read so complete details about
+ // addons are available. Returning it here will cause it to block
+ // profileBeforeChange, guranteeing that full information will be
+ // available by the time profileBeforeChangeTelemetry is fired.
+ return this._pendingTask;
+ },
+
+ /**
+ * Collect the addon data for the environment.
+ *
+ * This should only be called from _pendingTask; otherwise we risk
+ * running this during addon manager shutdown.
+ *
+ * @returns Promise<Object> This returns a Promise resolved with a status object with the following members:
+ * changed - Whether the environment changed.
+ * oldEnvironment - Only set if a change occured, contains the environment data before the change.
+ */
+ async _updateAddons() {
+ this._environment._log.trace("_updateAddons");
+
+ let addons = {
+ activeAddons: await this._getActiveAddons(),
+ theme: await this._getActiveTheme(),
+ activeGMPlugins: await this._getActiveGMPlugins(),
+ };
+
+ let result = {
+ changed:
+ !this._environment._currentEnvironment.addons ||
+ !ObjectUtils.deepEqual(
+ addons.activeAddons,
+ this._environment._currentEnvironment.addons.activeAddons
+ ),
+ };
+
+ if (result.changed) {
+ this._environment._log.trace("_updateAddons: addons differ");
+ result.oldEnvironment = Cu.cloneInto(
+ this._environment._currentEnvironment,
+ {}
+ );
+ }
+ this._environment._currentEnvironment.addons = addons;
+
+ return result;
+ },
+
+ /**
+ * Get the addon data in object form.
+ * @return Promise<object> containing the addon data.
+ */
+ async _getActiveAddons() {
+ // Request addons, asynchronously.
+ // "theme" is excluded because it is already handled by _getActiveTheme.
+ let { addons: allAddons, fullData } = await AddonManager.getActiveAddons(
+ AddonManagerPrivate.getAddonTypesByProvider("XPIProvider").filter(
+ addonType => addonType != "theme"
+ )
+ );
+
+ this._environment._addonsAreFull = fullData;
+ let activeAddons = {};
+ for (let addon of allAddons) {
+ // Don't collect any information about the new built-in search webextensions
+ if (addon.isBuiltin && !addon.isSystem) {
+ continue;
+ }
+ // Weird addon data in the wild can lead to exceptions while collecting
+ // the data.
+ try {
+ // Make sure to have valid dates.
+ let updateDate = new Date(Math.max(0, addon.updateDate));
+
+ activeAddons[addon.id] = {
+ version: limitStringToLength(addon.version, MAX_ADDON_STRING_LENGTH),
+ scope: addon.scope,
+ type: addon.type,
+ updateDay: Utils.millisecondsToDays(updateDate.getTime()),
+ isSystem: addon.isSystem,
+ isWebExtension: addon.isWebExtension,
+ multiprocessCompatible: true,
+ };
+
+ // getActiveAddons() gives limited data during startup and full
+ // data after the addons database is loaded.
+ if (fullData) {
+ let installDate = new Date(Math.max(0, addon.installDate));
+ Object.assign(activeAddons[addon.id], {
+ blocklisted:
+ addon.blocklistState !== Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
+ description: limitStringToLength(
+ addon.description,
+ MAX_ADDON_STRING_LENGTH
+ ),
+ name: limitStringToLength(addon.name, MAX_ADDON_STRING_LENGTH),
+ userDisabled: enforceBoolean(addon.userDisabled),
+ appDisabled: addon.appDisabled,
+ foreignInstall: enforceBoolean(addon.foreignInstall),
+ hasBinaryComponents: false,
+ installDay: Utils.millisecondsToDays(installDate.getTime()),
+ signedState: addon.signedState,
+ quarantineIgnoredByApp: enforceBoolean(
+ addon.quarantineIgnoredByApp
+ ),
+ quarantineIgnoredByUser: enforceBoolean(
+ addon.quarantineIgnoredByUser
+ ),
+ });
+ }
+ } catch (ex) {
+ this._environment._log.error(
+ "_getActiveAddons - An addon was discarded due to an error",
+ ex
+ );
+ continue;
+ }
+ }
+
+ return activeAddons;
+ },
+
+ /**
+ * Get the currently active theme data in object form.
+ * @return Promise<object> containing the active theme data.
+ */
+ async _getActiveTheme() {
+ // Request themes, asynchronously.
+ let { addons: themes } = await AddonManager.getActiveAddons(["theme"]);
+
+ let activeTheme = {};
+ // We only store information about the active theme.
+ let theme = themes.find(theme => theme.isActive);
+ if (theme) {
+ // Make sure to have valid dates.
+ let installDate = new Date(Math.max(0, theme.installDate));
+ let updateDate = new Date(Math.max(0, theme.updateDate));
+
+ activeTheme = {
+ id: theme.id,
+ blocklisted:
+ theme.blocklistState !== Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
+ description: limitStringToLength(
+ theme.description,
+ MAX_ADDON_STRING_LENGTH
+ ),
+ name: limitStringToLength(theme.name, MAX_ADDON_STRING_LENGTH),
+ userDisabled: enforceBoolean(theme.userDisabled),
+ appDisabled: theme.appDisabled,
+ version: limitStringToLength(theme.version, MAX_ADDON_STRING_LENGTH),
+ scope: theme.scope,
+ foreignInstall: enforceBoolean(theme.foreignInstall),
+ hasBinaryComponents: false,
+ installDay: Utils.millisecondsToDays(installDate.getTime()),
+ updateDay: Utils.millisecondsToDays(updateDate.getTime()),
+ };
+ }
+
+ return activeTheme;
+ },
+
+ /**
+ * Get the GMPlugins data in object form.
+ *
+ * @return Object containing the GMPlugins data.
+ *
+ * This should only be called from _pendingTask; otherwise we risk
+ * running this during addon manager shutdown.
+ */
+ async _getActiveGMPlugins() {
+ // If we haven't yet loaded the blocklist, pass back dummy data for now,
+ // and add an observer to update this data as soon as we get it.
+ if (!AddonManager.hasProvider("GMPProvider")) {
+ if (!this._gmpProviderObserverAdded) {
+ Services.obs.addObserver(this, GMP_PROVIDER_REGISTERED_TOPIC);
+ this._gmpProviderObserverAdded = true;
+ }
+ return {
+ "dummy-gmp": {
+ version: "0.1",
+ userDisabled: false,
+ applyBackgroundUpdates: 1,
+ },
+ };
+ }
+ // Request plugins, asynchronously.
+ let allPlugins = await AddonManager.getAddonsByTypes(["plugin"]);
+
+ let activeGMPlugins = {};
+ for (let plugin of allPlugins) {
+ // Only get info for active GMplugins.
+ if (!plugin.isGMPlugin || !plugin.isActive) {
+ continue;
+ }
+
+ try {
+ activeGMPlugins[plugin.id] = {
+ version: plugin.version,
+ userDisabled: enforceBoolean(plugin.userDisabled),
+ applyBackgroundUpdates: plugin.applyBackgroundUpdates,
+ };
+ } catch (ex) {
+ this._environment._log.error(
+ "_getActiveGMPlugins - A GMPlugin was discarded due to an error",
+ ex
+ );
+ continue;
+ }
+ }
+
+ return activeGMPlugins;
+ },
+};
+
+function EnvironmentCache() {
+ this._log = Log.repository.getLoggerWithMessagePrefix(
+ LOGGER_NAME,
+ "TelemetryEnvironment::"
+ );
+ this._log.trace("constructor");
+
+ this._shutdown = false;
+ // Don't allow querying the search service too early to prevent
+ // impacting the startup performance.
+ this._canQuerySearch = false;
+ // To guard against slowing down startup, defer gathering heavy environment
+ // entries until the session is restored.
+ this._sessionWasRestored = false;
+
+ // A map of listeners that will be called on environment changes.
+ this._changeListeners = new Map();
+
+ // A map of watched preferences which trigger an Environment change when
+ // modified. Every entry contains a recording policy (RECORD_PREF_*).
+ this._watchedPrefs = DEFAULT_ENVIRONMENT_PREFS;
+
+ this._currentEnvironment = {
+ build: this._getBuild(),
+ partner: this._getPartner(),
+ system: this._getSystem(),
+ };
+
+ this._addObservers();
+
+ // Build the remaining asynchronous parts of the environment. Don't register change listeners
+ // until the initial environment has been built.
+
+ let p = [this._updateSettings()];
+ this._addonBuilder = new EnvironmentAddonBuilder(this);
+ p.push(this._addonBuilder.init());
+
+ this._currentEnvironment.profile = {};
+ p.push(this._updateProfile());
+ if (AppConstants.MOZ_BUILD_APP == "browser") {
+ p.push(this._loadAttributionAsync());
+ }
+ p.push(this._loadAsyncUpdateSettings());
+ p.push(this._loadIntlData());
+
+ for (const [
+ id,
+ { branch, options },
+ ] of gActiveExperimentStartupBuffer.entries()) {
+ this.setExperimentActive(id, branch, options);
+ }
+ gActiveExperimentStartupBuffer = null;
+
+ let setup = () => {
+ this._initTask = null;
+ this._startWatchingPrefs();
+ this._addonBuilder.watchForChanges();
+ this._updateGraphicsFeatures();
+ return this.currentEnvironment;
+ };
+
+ this._initTask = Promise.all(p).then(
+ () => setup(),
+ err => {
+ // log errors but eat them for consumers
+ this._log.error("EnvironmentCache - error while initializing", err);
+ return setup();
+ }
+ );
+
+ // Addons may contain partial or full data depending on whether the Addons DB
+ // has had a chance to load. Do we have full data yet?
+ this._addonsAreFull = false;
+}
+EnvironmentCache.prototype = {
+ /**
+ * The current environment data. The returned data is cloned to avoid
+ * unexpected sharing or mutation.
+ * @returns object
+ */
+ get currentEnvironment() {
+ return Cu.cloneInto(this._currentEnvironment, {});
+ },
+
+ /**
+ * Wait for the current enviroment to be fully initialized.
+ * @returns Promise<object>
+ */
+ onInitialized() {
+ if (this._initTask) {
+ return this._initTask;
+ }
+ return Promise.resolve(this.currentEnvironment);
+ },
+
+ /**
+ * This gets called when the delayed init completes.
+ */
+ async delayedInit() {
+ this._processData = await Services.sysinfo.processInfo;
+ let processData = await Services.sysinfo.processInfo;
+ // Remove isWow64 and isWowARM64 from processData
+ // to strip it down to just CPU info
+ delete processData.isWow64;
+ delete processData.isWowARM64;
+
+ let oldEnv = null;
+ if (!this._initTask) {
+ oldEnv = this.currentEnvironment;
+ }
+
+ this._cpuData = this._getCPUData();
+ // Augment the return value from the promises with cached values
+ this._cpuData = { ...processData, ...this._cpuData };
+
+ this._currentEnvironment.system.cpu = this._getCPUData();
+
+ if (AppConstants.platform == "win") {
+ this._hddData = await Services.sysinfo.diskInfo;
+ let osData = await Services.sysinfo.osInfo;
+
+ if (!this._initTask) {
+ // We've finished creating the initial env, so notify for the update
+ // This is all a bit awkward because `currentEnvironment` clones
+ // the object, which we need to pass to the notification, but we
+ // should only notify once we've updated the current environment...
+ // Ideally, _onEnvironmentChange should somehow deal with all this
+ // instead of all the consumers.
+ oldEnv = this.currentEnvironment;
+ }
+
+ this._osData = this._getOSData();
+
+ // Augment the return values from the promises with cached values
+ this._osData = Object.assign(osData, this._osData);
+
+ this._currentEnvironment.system.os = this._getOSData();
+ this._currentEnvironment.system.hdd = this._getHDDData();
+
+ // Windows only values stored in processData
+ this._currentEnvironment.system.isWow64 = this._getProcessData().isWow64;
+ this._currentEnvironment.system.isWowARM64 =
+ this._getProcessData().isWowARM64;
+ }
+
+ if (!this._initTask) {
+ this._onEnvironmentChange("system-info", oldEnv);
+ }
+ },
+
+ /**
+ * Register a listener for environment changes.
+ * @param name The name of the listener. If a new listener is registered
+ * with the same name, the old listener will be replaced.
+ * @param listener function(reason, oldEnvironment) - Will receive a reason for
+ the change and the environment data before the change.
+ */
+ registerChangeListener(name, listener) {
+ this._log.trace("registerChangeListener for " + name);
+ if (this._shutdown) {
+ this._log.warn("registerChangeListener - already shutdown");
+ return;
+ }
+ this._changeListeners.set(name, listener);
+ },
+
+ /**
+ * Unregister from listening to environment changes.
+ * It's fine to call this on an unitialized TelemetryEnvironment.
+ * @param name The name of the listener to remove.
+ */
+ unregisterChangeListener(name) {
+ this._log.trace("unregisterChangeListener for " + name);
+ if (this._shutdown) {
+ this._log.warn("registerChangeListener - already shutdown");
+ return;
+ }
+ this._changeListeners.delete(name);
+ },
+
+ setExperimentActive(id, branch, options) {
+ this._log.trace(`setExperimentActive - id: ${id}, branch: ${branch}`);
+ // Make sure both the id and the branch have sane lengths.
+ const saneId = limitStringToLength(id, MAX_EXPERIMENT_ID_LENGTH);
+ const saneBranch = limitStringToLength(
+ branch,
+ MAX_EXPERIMENT_BRANCH_LENGTH
+ );
+ if (!saneId || !saneBranch) {
+ this._log.error(
+ "setExperimentActive - the provided arguments are not strings."
+ );
+ return;
+ }
+
+ // Warn the user about any content truncation.
+ if (saneId.length != id.length || saneBranch.length != branch.length) {
+ this._log.warn(
+ "setExperimentActive - the experiment id or branch were truncated."
+ );
+ }
+
+ // Truncate the experiment type if present.
+ if (options.hasOwnProperty("type")) {
+ let type = limitStringToLength(options.type, MAX_EXPERIMENT_TYPE_LENGTH);
+ if (type.length != options.type.length) {
+ options.type = type;
+ this._log.warn(
+ "setExperimentActive - the experiment type was truncated."
+ );
+ }
+ }
+
+ // Truncate the enrollment id if present.
+ if (options.hasOwnProperty("enrollmentId")) {
+ let enrollmentId = limitStringToLength(
+ options.enrollmentId,
+ MAX_EXPERIMENT_ENROLLMENT_ID_LENGTH
+ );
+ if (enrollmentId.length != options.enrollmentId.length) {
+ options.enrollmentId = enrollmentId;
+ this._log.warn(
+ "setExperimentActive - the enrollment id was truncated."
+ );
+ }
+ }
+
+ let oldEnvironment = Cu.cloneInto(this._currentEnvironment, {});
+ // Add the experiment annotation.
+ let experiments = this._currentEnvironment.experiments || {};
+ experiments[saneId] = { branch: saneBranch };
+ if (options.hasOwnProperty("type")) {
+ experiments[saneId].type = options.type;
+ }
+ if (options.hasOwnProperty("enrollmentId")) {
+ experiments[saneId].enrollmentId = options.enrollmentId;
+ }
+ this._currentEnvironment.experiments = experiments;
+ // Notify of the change.
+ this._onEnvironmentChange("experiment-annotation-changed", oldEnvironment);
+ },
+
+ setExperimentInactive(id) {
+ this._log.trace("setExperimentInactive");
+ let experiments = this._currentEnvironment.experiments || {};
+ if (id in experiments) {
+ // Only attempt to notify if a previous annotation was found and removed.
+ let oldEnvironment = Cu.cloneInto(this._currentEnvironment, {});
+ // Remove the experiment annotation.
+ delete this._currentEnvironment.experiments[id];
+ // Notify of the change.
+ this._onEnvironmentChange(
+ "experiment-annotation-changed",
+ oldEnvironment
+ );
+ }
+ },
+
+ getActiveExperiments() {
+ return Cu.cloneInto(this._currentEnvironment.experiments || {}, {});
+ },
+
+ shutdown() {
+ this._log.trace("shutdown");
+ this._shutdown = true;
+ },
+
+ /**
+ * Only used in tests, set the preferences to watch.
+ * @param aPreferences A map of preferences names and their recording policy.
+ */
+ _watchPreferences(aPreferences) {
+ this._stopWatchingPrefs();
+ this._watchedPrefs = aPreferences;
+ this._updateSettings();
+ this._startWatchingPrefs();
+ },
+
+ /**
+ * Get an object containing the values for the watched preferences. Depending on the
+ * policy, the value for a preference or whether it was changed by user is reported.
+ *
+ * @return An object containing the preferences values.
+ */
+ _getPrefData() {
+ let prefData = {};
+ for (let [pref, policy] of this._watchedPrefs.entries()) {
+ let prefValue = this._getPrefValue(pref, policy.what);
+
+ if (prefValue === undefined) {
+ continue;
+ }
+
+ prefData[pref] = prefValue;
+ }
+ return prefData;
+ },
+
+ /**
+ * Get the value of a preference given the preference name and the policy.
+ * @param pref Name of the preference.
+ * @param what Policy of the preference.
+ *
+ * @returns The value we need to store for this preference. It can be undefined
+ * or null if the preference is invalid or has a value set by the user.
+ */
+ _getPrefValue(pref, what) {
+ // Check the policy for the preference and decide if we need to store its value
+ // or whether it changed from the default value.
+ let prefType = Services.prefs.getPrefType(pref);
+
+ if (
+ what == TelemetryEnvironment.RECORD_DEFAULTPREF_VALUE ||
+ what == TelemetryEnvironment.RECORD_DEFAULTPREF_STATE
+ ) {
+ // For default prefs, make sure they exist
+ if (prefType == Ci.nsIPrefBranch.PREF_INVALID) {
+ return undefined;
+ }
+ } else if (!Services.prefs.prefHasUserValue(pref)) {
+ // For user prefs, make sure they are set
+ return undefined;
+ }
+
+ if (what == TelemetryEnvironment.RECORD_DEFAULTPREF_STATE) {
+ return "<set>";
+ } else if (what == TelemetryEnvironment.RECORD_PREF_STATE) {
+ return "<user-set>";
+ } else if (prefType == Ci.nsIPrefBranch.PREF_STRING) {
+ return Services.prefs.getStringPref(pref);
+ } else if (prefType == Ci.nsIPrefBranch.PREF_BOOL) {
+ return Services.prefs.getBoolPref(pref);
+ } else if (prefType == Ci.nsIPrefBranch.PREF_INT) {
+ return Services.prefs.getIntPref(pref);
+ } else if (prefType == Ci.nsIPrefBranch.PREF_INVALID) {
+ return null;
+ }
+ throw new Error(
+ `Unexpected preference type ("${prefType}") for "${pref}".`
+ );
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsISupportsWeakReference"]),
+
+ /**
+ * Start watching the preferences.
+ */
+ _startWatchingPrefs() {
+ this._log.trace("_startWatchingPrefs - " + this._watchedPrefs);
+
+ Services.prefs.addObserver("", this, true);
+ },
+
+ _onPrefChanged(aData) {
+ this._log.trace("_onPrefChanged");
+ let oldEnvironment = Cu.cloneInto(this._currentEnvironment, {});
+ this._currentEnvironment.settings.userPrefs[aData] = this._getPrefValue(
+ aData,
+ this._watchedPrefs.get(aData).what
+ );
+ this._onEnvironmentChange("pref-changed", oldEnvironment);
+ },
+
+ /**
+ * Do not receive any more change notifications for the preferences.
+ */
+ _stopWatchingPrefs() {
+ this._log.trace("_stopWatchingPrefs");
+
+ Services.prefs.removeObserver("", this);
+ },
+
+ _addObservers() {
+ // Watch the search engine change and service topics.
+ Services.obs.addObserver(this, SESSIONSTORE_WINDOWS_RESTORED_TOPIC);
+ Services.obs.addObserver(this, COMPOSITOR_CREATED_TOPIC);
+ Services.obs.addObserver(this, COMPOSITOR_PROCESS_ABORTED_TOPIC);
+ Services.obs.addObserver(this, DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC);
+ Services.obs.addObserver(this, GFX_FEATURES_READY_TOPIC);
+ Services.obs.addObserver(this, SEARCH_ENGINE_MODIFIED_TOPIC);
+ Services.obs.addObserver(this, SEARCH_SERVICE_TOPIC);
+ Services.obs.addObserver(this, AUTO_UPDATE_PREF_CHANGE_TOPIC);
+ Services.obs.addObserver(this, BACKGROUND_UPDATE_PREF_CHANGE_TOPIC);
+ Services.obs.addObserver(this, SERVICES_INFO_CHANGE_TOPIC);
+ },
+
+ _removeObservers() {
+ Services.obs.removeObserver(this, SESSIONSTORE_WINDOWS_RESTORED_TOPIC);
+ Services.obs.removeObserver(this, COMPOSITOR_CREATED_TOPIC);
+ Services.obs.removeObserver(this, COMPOSITOR_PROCESS_ABORTED_TOPIC);
+ try {
+ Services.obs.removeObserver(
+ this,
+ DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC
+ );
+ } catch (ex) {}
+ Services.obs.removeObserver(this, GFX_FEATURES_READY_TOPIC);
+ Services.obs.removeObserver(this, SEARCH_ENGINE_MODIFIED_TOPIC);
+ Services.obs.removeObserver(this, SEARCH_SERVICE_TOPIC);
+ Services.obs.removeObserver(this, AUTO_UPDATE_PREF_CHANGE_TOPIC);
+ Services.obs.removeObserver(this, BACKGROUND_UPDATE_PREF_CHANGE_TOPIC);
+ Services.obs.removeObserver(this, SERVICES_INFO_CHANGE_TOPIC);
+ },
+
+ observe(aSubject, aTopic, aData) {
+ this._log.trace("observe - aTopic: " + aTopic + ", aData: " + aData);
+ switch (aTopic) {
+ case SEARCH_ENGINE_MODIFIED_TOPIC:
+ if (
+ aData != "engine-default" &&
+ aData != "engine-default-private" &&
+ aData != "engine-changed"
+ ) {
+ return;
+ }
+ if (
+ aData == "engine-changed" &&
+ aSubject.QueryInterface(Ci.nsISearchEngine) &&
+ Services.search.defaultEngine != aSubject
+ ) {
+ return;
+ }
+ // Record the new default search choice and send the change notification.
+ this._onSearchEngineChange();
+ break;
+ case SEARCH_SERVICE_TOPIC:
+ if (aData != "init-complete") {
+ return;
+ }
+ // Now that the search engine init is complete, record the default search choice.
+ this._canQuerySearch = true;
+ this._updateSearchEngine();
+ break;
+ case GFX_FEATURES_READY_TOPIC:
+ case COMPOSITOR_CREATED_TOPIC:
+ // Full graphics information is not available until we have created at
+ // least one off-main-thread-composited window. Thus we wait for the
+ // first compositor to be created and then query nsIGfxInfo again.
+ this._updateGraphicsFeatures();
+ break;
+ case COMPOSITOR_PROCESS_ABORTED_TOPIC:
+ // Our compositor process has been killed for whatever reason, so refresh
+ // our reported graphics features and trigger an environment change.
+ this._onCompositorProcessAborted();
+ break;
+ case DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC:
+ // Distribution customizations are applied after final-ui-startup. query
+ // partner prefs again when they are ready.
+ this._updatePartner();
+ Services.obs.removeObserver(this, aTopic);
+ break;
+ case SESSIONSTORE_WINDOWS_RESTORED_TOPIC:
+ this._sessionWasRestored = true;
+ // Make sure to initialize the search service once we've done restoring
+ // the windows, so that we don't risk loosing search data.
+ Services.search.init();
+ // The default browser check could take some time, so just call it after
+ // the session was restored.
+ this._updateDefaultBrowser();
+ break;
+ case PREF_CHANGED_TOPIC:
+ let options = this._watchedPrefs.get(aData);
+ if (options && !options.requiresRestart) {
+ this._onPrefChanged(aData);
+ }
+ break;
+ case AUTO_UPDATE_PREF_CHANGE_TOPIC:
+ this._currentEnvironment.settings.update.autoDownload = aData == "true";
+ break;
+ case BACKGROUND_UPDATE_PREF_CHANGE_TOPIC:
+ this._currentEnvironment.settings.update.background = aData == "true";
+ break;
+ case SERVICES_INFO_CHANGE_TOPIC:
+ this._updateServicesInfo();
+ break;
+ }
+ },
+
+ /**
+ * Update the default search engine value.
+ */
+ _updateSearchEngine() {
+ if (!this._canQuerySearch) {
+ this._log.trace("_updateSearchEngine - ignoring early call");
+ return;
+ }
+
+ this._log.trace(
+ "_updateSearchEngine - isInitialized: " + Services.search.isInitialized
+ );
+ if (!Services.search.isInitialized) {
+ return;
+ }
+
+ // Make sure we have a settings section.
+ this._currentEnvironment.settings = this._currentEnvironment.settings || {};
+
+ // Update the search engine entry in the current environment.
+ const defaultEngineInfo = Services.search.getDefaultEngineInfo();
+ this._currentEnvironment.settings.defaultSearchEngine =
+ defaultEngineInfo.defaultSearchEngine;
+ this._currentEnvironment.settings.defaultSearchEngineData = {
+ ...defaultEngineInfo.defaultSearchEngineData,
+ };
+ if ("defaultPrivateSearchEngine" in defaultEngineInfo) {
+ this._currentEnvironment.settings.defaultPrivateSearchEngine =
+ defaultEngineInfo.defaultPrivateSearchEngine;
+ }
+ if ("defaultPrivateSearchEngineData" in defaultEngineInfo) {
+ this._currentEnvironment.settings.defaultPrivateSearchEngineData = {
+ ...defaultEngineInfo.defaultPrivateSearchEngineData,
+ };
+ }
+ },
+
+ /**
+ * Update the default search engine value and trigger the environment change.
+ */
+ _onSearchEngineChange() {
+ this._log.trace("_onSearchEngineChange");
+
+ // Finally trigger the environment change notification.
+ let oldEnvironment = Cu.cloneInto(this._currentEnvironment, {});
+ this._updateSearchEngine();
+ this._onEnvironmentChange("search-engine-changed", oldEnvironment);
+ },
+
+ /**
+ * Refresh the Telemetry environment and trigger an environment change due to
+ * a change in compositor process (normally this will mean we've fallen back
+ * from out-of-process to in-process compositing).
+ */
+ _onCompositorProcessAborted() {
+ this._log.trace("_onCompositorProcessAborted");
+
+ // Trigger the environment change notification.
+ let oldEnvironment = Cu.cloneInto(this._currentEnvironment, {});
+ this._updateGraphicsFeatures();
+ this._onEnvironmentChange("gfx-features-changed", oldEnvironment);
+ },
+
+ /**
+ * Update the graphics features object.
+ */
+ _updateGraphicsFeatures() {
+ let gfxData = this._currentEnvironment.system.gfx;
+ try {
+ let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+ gfxData.features = gfxInfo.getFeatures();
+ } catch (e) {
+ this._log.error("nsIGfxInfo.getFeatures() caught error", e);
+ }
+ },
+
+ /**
+ * Update the partner prefs.
+ */
+ _updatePartner() {
+ this._currentEnvironment.partner = this._getPartner();
+ },
+
+ /**
+ * Get the build data in object form.
+ * @return Object containing the build data.
+ */
+ _getBuild() {
+ let buildData = {
+ applicationId: Services.appinfo.ID || null,
+ applicationName: Services.appinfo.name || null,
+ architecture: Services.sysinfo.get("arch"),
+ buildId: Services.appinfo.appBuildID || null,
+ version: Services.appinfo.version || null,
+ vendor: Services.appinfo.vendor || null,
+ displayVersion: AppConstants.MOZ_APP_VERSION_DISPLAY || null,
+ platformVersion: Services.appinfo.platformVersion || null,
+ xpcomAbi: Services.appinfo.XPCOMABI,
+ updaterAvailable: AppConstants.MOZ_UPDATER,
+ };
+
+ return buildData;
+ },
+
+ /**
+ * Determine if we're the default browser.
+ * @returns null on error, true if we are the default browser, or false otherwise.
+ */
+ _isDefaultBrowser() {
+ let isDefault = (service, ...args) => {
+ try {
+ return !!service.isDefaultBrowser(...args);
+ } catch (ex) {
+ this._log.error(
+ "_isDefaultBrowser - Could not determine if default browser",
+ ex
+ );
+ return null;
+ }
+ };
+
+ if (!("@mozilla.org/browser/shell-service;1" in Cc)) {
+ this._log.info(
+ "_isDefaultBrowser - Could not obtain browser shell service"
+ );
+ return null;
+ }
+
+ try {
+ let { ShellService } = ChromeUtils.importESModule(
+ "resource:///modules/ShellService.sys.mjs"
+ );
+ // This uses the same set of flags used by the pref pane.
+ return isDefault(ShellService, false, true);
+ } catch (ex) {
+ this._log.error("_isDefaultBrowser - Could not obtain shell service JSM");
+ }
+
+ try {
+ let shellService = Cc["@mozilla.org/browser/shell-service;1"].getService(
+ Ci.nsIShellService
+ );
+ // This uses the same set of flags used by the pref pane.
+ return isDefault(shellService, true);
+ } catch (ex) {
+ this._log.error("_isDefaultBrowser - Could not obtain shell service", ex);
+ return null;
+ }
+ },
+
+ _updateDefaultBrowser() {
+ if (AppConstants.platform === "android") {
+ return;
+ }
+ // Make sure to have a settings section.
+ this._currentEnvironment.settings = this._currentEnvironment.settings || {};
+ this._currentEnvironment.settings.isDefaultBrowser = this
+ ._sessionWasRestored
+ ? this._isDefaultBrowser()
+ : null;
+ },
+
+ /**
+ * Update the cached settings data.
+ */
+ _updateSettings() {
+ let updateChannel = null;
+ try {
+ updateChannel = Utils.getUpdateChannel();
+ } catch (e) {}
+
+ this._currentEnvironment.settings = {
+ blocklistEnabled: Services.prefs.getBoolPref(
+ PREF_BLOCKLIST_ENABLED,
+ true
+ ),
+ e10sEnabled: Services.appinfo.browserTabsRemoteAutostart,
+ e10sMultiProcesses: Services.appinfo.maxWebProcessCount,
+ fissionEnabled: Services.appinfo.fissionAutostart,
+ telemetryEnabled: Utils.isTelemetryEnabled,
+ locale: getBrowserLocale(),
+ // We need to wait for browser-delayed-startup-finished to ensure that the locales
+ // have settled, once that's happened we can get the intl data directly.
+ intl: Policy._intlLoaded ? getIntlSettings() : {},
+ update: {
+ channel: updateChannel,
+ enabled: !Services.policies || Services.policies.isAllowed("appUpdate"),
+ },
+ userPrefs: this._getPrefData(),
+ sandbox: this._getSandboxData(),
+ };
+
+ // Services.appinfo.launcherProcessState is not available in all build
+ // configurations, in which case an exception may be thrown.
+ try {
+ this._currentEnvironment.settings.launcherProcessState =
+ Services.appinfo.launcherProcessState;
+ } catch (e) {}
+
+ this._currentEnvironment.settings.addonCompatibilityCheckEnabled =
+ AddonManager.checkCompatibility;
+
+ if (AppConstants.MOZ_BUILD_APP == "browser") {
+ this._updateAttribution();
+ }
+ this._updateDefaultBrowser();
+ this._updateSearchEngine();
+ this._loadAsyncUpdateSettingsFromCache();
+ },
+
+ _getSandboxData() {
+ let effectiveContentProcessLevel = null;
+ let contentWin32kLockdownState = null;
+ try {
+ let sandboxSettings = Cc[
+ "@mozilla.org/sandbox/sandbox-settings;1"
+ ].getService(Ci.mozISandboxSettings);
+ effectiveContentProcessLevel =
+ sandboxSettings.effectiveContentSandboxLevel;
+
+ // The possible values for this are defined in the ContentWin32kLockdownState
+ // enum in security/sandbox/common/SandboxSettings.h
+ contentWin32kLockdownState = sandboxSettings.contentWin32kLockdownState;
+ } catch (e) {}
+ return {
+ effectiveContentProcessLevel,
+ contentWin32kLockdownState,
+ };
+ },
+
+ /**
+ * Update the cached profile data.
+ * @returns Promise<> resolved when the I/O is complete.
+ */
+ async _updateProfile() {
+ let profileAccessor = await lazy.ProfileAge();
+
+ let creationDate = await profileAccessor.created;
+ let resetDate = await profileAccessor.reset;
+ let firstUseDate = await profileAccessor.firstUse;
+
+ this._currentEnvironment.profile.creationDate =
+ Utils.millisecondsToDays(creationDate);
+ if (resetDate) {
+ this._currentEnvironment.profile.resetDate =
+ Utils.millisecondsToDays(resetDate);
+ }
+ if (firstUseDate) {
+ this._currentEnvironment.profile.firstUseDate =
+ Utils.millisecondsToDays(firstUseDate);
+ }
+ },
+
+ /**
+ * Load the attribution data object and updates the environment.
+ * @returns Promise<> resolved when the I/O is complete.
+ */
+ async _loadAttributionAsync() {
+ try {
+ await lazy.AttributionCode.getAttrDataAsync();
+ } catch (e) {
+ // The AttributionCode.sys.mjs module might not be always available
+ // (e.g. tests). Gracefully handle this.
+ return;
+ }
+ this._updateAttribution();
+ },
+
+ /**
+ * Update the environment with the cached attribution data.
+ */
+ _updateAttribution() {
+ let data = null;
+ try {
+ data = lazy.AttributionCode.getCachedAttributionData();
+ } catch (e) {
+ // The AttributionCode.sys.mjs module might not be always available
+ // (e.g. tests). Gracefully handle this.
+ }
+
+ if (!data || !Object.keys(data).length) {
+ return;
+ }
+
+ let attributionData = {};
+ for (let key in data) {
+ attributionData[key] =
+ // At least one of these may be boolean, and limitStringToLength
+ // returns null for non-string inputs.
+ typeof data[key] === "string"
+ ? limitStringToLength(data[key], MAX_ATTRIBUTION_STRING_LENGTH)
+ : data[key];
+ }
+ this._currentEnvironment.settings.attribution = attributionData;
+ },
+
+ /**
+ * Load the per-installation update settings, cache them, and add them to the
+ * environment.
+ */
+ async _loadAsyncUpdateSettings() {
+ if (AppConstants.MOZ_UPDATER) {
+ this._updateAutoDownloadCache =
+ await UpdateUtils.getAppUpdateAutoEnabled();
+ this._updateBackgroundCache = await UpdateUtils.readUpdateConfigSetting(
+ "app.update.background.enabled"
+ );
+ } else {
+ this._updateAutoDownloadCache = false;
+ this._updateBackgroundCache = false;
+ }
+ this._loadAsyncUpdateSettingsFromCache();
+ },
+
+ /**
+ * Update the environment with the cached values for per-installation update
+ * settings.
+ */
+ _loadAsyncUpdateSettingsFromCache() {
+ if (this._updateAutoDownloadCache !== undefined) {
+ this._currentEnvironment.settings.update.autoDownload =
+ this._updateAutoDownloadCache;
+ }
+ if (this._updateBackgroundCache !== undefined) {
+ this._currentEnvironment.settings.update.background =
+ this._updateBackgroundCache;
+ }
+ },
+
+ /**
+ * Get i18n data about the system.
+ * @return A promise of completion.
+ */
+ async _loadIntlData() {
+ // Wait for the startup topic.
+ await Policy._browserDelayedStartup();
+ this._currentEnvironment.settings.intl = getIntlSettings();
+ Policy._intlLoaded = true;
+ },
+ // This exists as a separate function for testing.
+ async _getFxaSignedInUser() {
+ return lazy.fxAccounts.getSignedInUser();
+ },
+
+ async _updateServicesInfo() {
+ let syncEnabled = false;
+ let accountEnabled = false;
+ let weaveService =
+ Cc["@mozilla.org/weave/service;1"].getService().wrappedJSObject;
+ syncEnabled = weaveService && weaveService.enabled;
+ if (syncEnabled) {
+ // All sync users are account users, definitely.
+ accountEnabled = true;
+ } else {
+ // Not all account users are sync users. See if they're signed into FxA.
+ try {
+ let user = await this._getFxaSignedInUser();
+ if (user) {
+ accountEnabled = true;
+ }
+ } catch (e) {
+ // We don't know. This might be a transient issue which will clear
+ // itself up later, but the information in telemetry is quite possibly stale
+ // (this is called from a change listener), so clear it out to avoid
+ // reporting data which might be wrong until we can figure it out.
+ delete this._currentEnvironment.services;
+ this._log.error("_updateServicesInfo() caught error", e);
+ return;
+ }
+ }
+ this._currentEnvironment.services = {
+ accountEnabled,
+ syncEnabled,
+ };
+ },
+
+ /**
+ * Get the partner data in object form.
+ * @return Object containing the partner data.
+ */
+ _getPartner() {
+ let defaults = Services.prefs.getDefaultBranch(null);
+ let partnerData = {
+ distributionId: defaults.getStringPref(PREF_DISTRIBUTION_ID, null),
+ distributionVersion: defaults.getCharPref(
+ PREF_DISTRIBUTION_VERSION,
+ null
+ ),
+ partnerId: defaults.getCharPref(PREF_PARTNER_ID, null),
+ distributor: defaults.getCharPref(PREF_DISTRIBUTOR, null),
+ distributorChannel: defaults.getCharPref(PREF_DISTRIBUTOR_CHANNEL, null),
+ };
+
+ // Get the PREF_APP_PARTNER_BRANCH branch and append its children to partner data.
+ let partnerBranch = Services.prefs.getDefaultBranch(
+ PREF_APP_PARTNER_BRANCH
+ );
+ partnerData.partnerNames = partnerBranch.getChildList("");
+
+ return partnerData;
+ },
+
+ _cpuData: null,
+ /**
+ * Get the CPU information.
+ * @return Object containing the CPU information data.
+ */
+ _getCPUData() {
+ if (this._cpuData) {
+ return this._cpuData;
+ }
+
+ this._cpuData = {};
+
+ const CPU_EXTENSIONS = [
+ "hasMMX",
+ "hasSSE",
+ "hasSSE2",
+ "hasSSE3",
+ "hasSSSE3",
+ "hasSSE4A",
+ "hasSSE4_1",
+ "hasSSE4_2",
+ "hasAVX",
+ "hasAVX2",
+ "hasAES",
+ "hasEDSP",
+ "hasARMv6",
+ "hasARMv7",
+ "hasNEON",
+ "hasUserCET",
+ ];
+
+ // Enumerate the available CPU extensions.
+ let availableExts = [];
+ for (let ext of CPU_EXTENSIONS) {
+ if (getSysinfoProperty(ext, false)) {
+ availableExts.push(ext);
+ }
+ }
+
+ this._cpuData.extensions = availableExts;
+
+ return this._cpuData;
+ },
+
+ _processData: null,
+ /**
+ * Get the process information.
+ * @return Object containing the process information data.
+ */
+ _getProcessData() {
+ if (this._processData) {
+ return this._processData;
+ }
+ return {};
+ },
+
+ /**
+ * Get the device information, if we are on a portable device.
+ * @return Object containing the device information data, or null if
+ * not a portable device.
+ */
+ _getDeviceData() {
+ if (AppConstants.platform !== "android") {
+ return null;
+ }
+
+ return {
+ model: getSysinfoProperty("device", null),
+ manufacturer: getSysinfoProperty("manufacturer", null),
+ hardware: getSysinfoProperty("hardware", null),
+ isTablet: getSysinfoProperty("tablet", null),
+ };
+ },
+
+ _osData: null,
+ /**
+ * Get the OS information.
+ * @return Object containing the OS data.
+ */
+ _getOSData() {
+ if (this._osData) {
+ return this._osData;
+ }
+ this._osData = {
+ name: forceToStringOrNull(getSysinfoProperty("name", null)),
+ version: forceToStringOrNull(getSysinfoProperty("version", null)),
+ locale: forceToStringOrNull(getSystemLocale()),
+ };
+
+ if (AppConstants.platform == "android") {
+ this._osData.kernelVersion = forceToStringOrNull(
+ getSysinfoProperty("kernel_version", null)
+ );
+ } else if (AppConstants.platform == "linux") {
+ this._osData.distro = forceToStringOrNull(
+ getSysinfoProperty("distro", null)
+ );
+ this._osData.distroVersion = forceToStringOrNull(
+ getSysinfoProperty("distroVersion", null)
+ );
+ } else if (AppConstants.platform === "win") {
+ // The path to the "UBR" key, queried to get additional version details on Windows.
+ const WINDOWS_UBR_KEY_PATH =
+ "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion";
+
+ let versionInfo = lazy.WindowsVersionInfo.get({ throwOnError: false });
+ this._osData.servicePackMajor = versionInfo.servicePackMajor;
+ this._osData.servicePackMinor = versionInfo.servicePackMinor;
+ this._osData.windowsBuildNumber = versionInfo.buildNumber;
+ // We only need the UBR if we're at or above Windows 10.
+ if (
+ typeof this._osData.version === "string" &&
+ Services.vc.compare(this._osData.version, "10") >= 0
+ ) {
+ // Query the UBR key and only add it to the environment if it's available.
+ // |readRegKey| doesn't throw, but rather returns 'undefined' on error.
+ let ubr = lazy.WindowsRegistry.readRegKey(
+ Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
+ WINDOWS_UBR_KEY_PATH,
+ "UBR",
+ Ci.nsIWindowsRegKey.WOW64_64
+ );
+ this._osData.windowsUBR = ubr !== undefined ? ubr : null;
+ }
+ }
+
+ return this._osData;
+ },
+
+ _hddData: null,
+ /**
+ * Get the HDD information.
+ * @return Object containing the HDD data.
+ */
+ _getHDDData() {
+ if (this._hddData) {
+ return this._hddData;
+ }
+ let nullData = { model: null, revision: null, type: null };
+ return { profile: nullData, binary: nullData, system: nullData };
+ },
+
+ /**
+ * Get registered security product information.
+ * @return Object containing the security product data
+ */
+ _getSecurityAppData() {
+ const maxStringLength = 256;
+
+ const keys = [
+ ["registeredAntiVirus", "antivirus"],
+ ["registeredAntiSpyware", "antispyware"],
+ ["registeredFirewall", "firewall"],
+ ];
+
+ let result = {};
+
+ for (let [inKey, outKey] of keys) {
+ let prop = getSysinfoProperty(inKey, null);
+ if (prop) {
+ prop = limitStringToLength(prop, maxStringLength).split(";");
+ }
+
+ result[outKey] = prop;
+ }
+
+ return result;
+ },
+
+ /**
+ * Get the GFX information.
+ * @return Object containing the GFX data.
+ */
+ _getGFXData() {
+ let gfxData = {
+ D2DEnabled: getGfxField("D2DEnabled", null),
+ DWriteEnabled: getGfxField("DWriteEnabled", null),
+ ContentBackend: getGfxField("ContentBackend", null),
+ Headless: getGfxField("isHeadless", null),
+ EmbeddedInFirefoxReality: getGfxField("EmbeddedInFirefoxReality", null),
+ TargetFrameRate: getGfxField("TargetFrameRate", null),
+ // The following line is disabled due to main thread jank and will be enabled
+ // again as part of bug 1154500.
+ // DWriteVersion: getGfxField("DWriteVersion", null),
+ adapters: [],
+ monitors: [],
+ features: {},
+ };
+
+ if (AppConstants.platform !== "android") {
+ let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+ try {
+ gfxData.monitors = gfxInfo.getMonitors();
+ } catch (e) {
+ this._log.error("nsIGfxInfo.getMonitors() caught error", e);
+ }
+ }
+
+ try {
+ let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+ gfxData.features = gfxInfo.getFeatures();
+ } catch (e) {
+ this._log.error("nsIGfxInfo.getFeatures() caught error", e);
+ }
+
+ // GfxInfo does not yet expose a way to iterate through all the adapters.
+ gfxData.adapters.push(getGfxAdapter(""));
+ gfxData.adapters[0].GPUActive = true;
+
+ // If we have a second adapter add it to the gfxData.adapters section.
+ let hasGPU2 = getGfxField("adapterDeviceID2", null) !== null;
+ if (!hasGPU2) {
+ this._log.trace("_getGFXData - Only one display adapter detected.");
+ return gfxData;
+ }
+
+ this._log.trace("_getGFXData - Two display adapters detected.");
+
+ gfxData.adapters.push(getGfxAdapter("2"));
+ gfxData.adapters[1].GPUActive = getGfxField("isGPU2Active", null);
+
+ return gfxData;
+ },
+
+ /**
+ * Get the system data in object form.
+ * @return Object containing the system data.
+ */
+ _getSystem() {
+ let memoryMB = getSysinfoProperty("memsize", null);
+ if (memoryMB) {
+ // Send RAM size in megabytes. Rounding because sysinfo doesn't
+ // always provide RAM in multiples of 1024.
+ memoryMB = Math.round(memoryMB / 1024 / 1024);
+ }
+
+ let virtualMB = getSysinfoProperty("virtualmemsize", null);
+ if (virtualMB) {
+ // Send the total virtual memory size in megabytes. Rounding because
+ // sysinfo doesn't always provide RAM in multiples of 1024.
+ virtualMB = Math.round(virtualMB / 1024 / 1024);
+ }
+
+ let data = {
+ memoryMB,
+ virtualMaxMB: virtualMB,
+ cpu: this._getCPUData(),
+ os: this._getOSData(),
+ hdd: this._getHDDData(),
+ gfx: this._getGFXData(),
+ appleModelId: getSysinfoProperty("appleModelId", null),
+ hasWinPackageId: getSysinfoProperty("hasWinPackageId", null),
+ };
+
+ if (AppConstants.platform === "win") {
+ // This is only sent for Mozilla produced MSIX packages
+ let winPackageFamilyName = getSysinfoProperty("winPackageFamilyName", "");
+ if (
+ winPackageFamilyName.startsWith("Mozilla.") ||
+ winPackageFamilyName.startsWith("MozillaCorporation.")
+ ) {
+ data = { winPackageFamilyName, ...data };
+ }
+ data = { ...this._getProcessData(), ...data };
+ data.sec = this._getSecurityAppData();
+ } else if (AppConstants.platform == "android") {
+ data.device = this._getDeviceData();
+ }
+
+ return data;
+ },
+
+ _onEnvironmentChange(what, oldEnvironment) {
+ this._log.trace("_onEnvironmentChange for " + what);
+
+ // We are already skipping change events in _checkChanges if there is a pending change task running.
+ if (this._shutdown) {
+ this._log.trace("_onEnvironmentChange - Already shut down.");
+ return;
+ }
+
+ if (ObjectUtils.deepEqual(this._currentEnvironment, oldEnvironment)) {
+ this._log.trace("_onEnvironmentChange - Environment didn't change");
+ return;
+ }
+
+ for (let [name, listener] of this._changeListeners) {
+ try {
+ this._log.debug("_onEnvironmentChange - calling " + name);
+ listener(what, oldEnvironment);
+ } catch (e) {
+ this._log.error(
+ "_onEnvironmentChange - listener " + name + " caught error",
+ e
+ );
+ }
+ }
+ },
+
+ reset() {
+ this._shutdown = false;
+ },
+};
diff --git a/toolkit/components/telemetry/app/TelemetryReportingPolicy.sys.mjs b/toolkit/components/telemetry/app/TelemetryReportingPolicy.sys.mjs
new file mode 100644
index 0000000000..6fe155a2fa
--- /dev/null
+++ b/toolkit/components/telemetry/app/TelemetryReportingPolicy.sys.mjs
@@ -0,0 +1,587 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { Log } from "resource://gre/modules/Log.sys.mjs";
+import { clearTimeout, setTimeout } from "resource://gre/modules/Timer.sys.mjs";
+
+import { Observers } from "resource://services-common/observers.sys.mjs";
+import { TelemetryUtils } from "resource://gre/modules/TelemetryUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ TelemetrySend: "resource://gre/modules/TelemetrySend.sys.mjs",
+});
+
+const LOGGER_NAME = "Toolkit.Telemetry";
+const LOGGER_PREFIX = "TelemetryReportingPolicy::";
+
+// Oldest year to allow in date preferences. The FHR infobar was implemented in
+// 2012 and no dates older than that should be encountered.
+const OLDEST_ALLOWED_ACCEPTANCE_YEAR = 2012;
+
+const PREF_BRANCH = "datareporting.policy.";
+
+// The following preferences are deprecated and will be purged during the preferences
+// migration process.
+const DEPRECATED_FHR_PREFS = [
+ PREF_BRANCH + "dataSubmissionPolicyAccepted",
+ PREF_BRANCH + "dataSubmissionPolicyBypassAcceptance",
+ PREF_BRANCH + "dataSubmissionPolicyResponseType",
+ PREF_BRANCH + "dataSubmissionPolicyResponseTime",
+];
+
+// How much time until we display the data choices notification bar, on the first run.
+const NOTIFICATION_DELAY_FIRST_RUN_MSEC = 60 * 1000; // 60s
+// Same as above, for the next runs.
+const NOTIFICATION_DELAY_NEXT_RUNS_MSEC = 10 * 1000; // 10s
+
+/**
+ * This is a policy object used to override behavior within this module.
+ * Tests override properties on this object to allow for control of behavior
+ * that would otherwise be very hard to cover.
+ */
+export var Policy = {
+ now: () => new Date(),
+ setShowInfobarTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
+ clearShowInfobarTimeout: id => clearTimeout(id),
+ fakeSessionRestoreNotification: () => {
+ TelemetryReportingPolicyImpl.observe(
+ null,
+ "sessionstore-windows-restored",
+ null
+ );
+ },
+};
+
+/**
+ * Represents a request to display data policy.
+ *
+ * Receivers of these instances are expected to call one or more of the on*
+ * functions when events occur.
+ *
+ * When one of these requests is received, the first thing a callee should do
+ * is present notification to the user of the data policy. When the notice
+ * is displayed to the user, the callee should call `onUserNotifyComplete`.
+ *
+ * If for whatever reason the callee could not display a notice,
+ * it should call `onUserNotifyFailed`.
+ *
+ * @param {Object} aLog The log object used to log the error in case of failures.
+ */
+function NotifyPolicyRequest(aLog) {
+ this._log = aLog;
+}
+
+NotifyPolicyRequest.prototype = Object.freeze({
+ /**
+ * Called when the user is notified of the policy.
+ */
+ onUserNotifyComplete() {
+ return TelemetryReportingPolicyImpl._userNotified();
+ },
+
+ /**
+ * Called when there was an error notifying the user about the policy.
+ *
+ * @param error
+ * (Error) Explains what went wrong.
+ */
+ onUserNotifyFailed(error) {
+ this._log.error("onUserNotifyFailed - " + error);
+ },
+});
+
+export var TelemetryReportingPolicy = {
+ // The current policy version number. If the version number stored in the prefs
+ // is smaller than this, data upload will be disabled until the user is re-notified
+ // about the policy changes.
+ DEFAULT_DATAREPORTING_POLICY_VERSION: 1,
+
+ /**
+ * Setup the policy.
+ */
+ setup() {
+ return TelemetryReportingPolicyImpl.setup();
+ },
+
+ /**
+ * Shutdown and clear the policy.
+ */
+ shutdown() {
+ return TelemetryReportingPolicyImpl.shutdown();
+ },
+
+ /**
+ * Check if we are allowed to upload data. In order to submit data both these conditions
+ * should be true:
+ * - The data submission preference should be true.
+ * - The datachoices infobar should have been displayed.
+ *
+ * @return {Boolean} True if we are allowed to upload data, false otherwise.
+ */
+ canUpload() {
+ return TelemetryReportingPolicyImpl.canUpload();
+ },
+
+ /**
+ * Check if this is the first time the browser ran.
+ */
+ isFirstRun() {
+ return TelemetryReportingPolicyImpl.isFirstRun();
+ },
+
+ /**
+ * Test only method, restarts the policy.
+ */
+ reset() {
+ return TelemetryReportingPolicyImpl.reset();
+ },
+
+ /**
+ * Test only method, used to check if user is notified of the policy in tests.
+ */
+ testIsUserNotified() {
+ return TelemetryReportingPolicyImpl.isUserNotifiedOfCurrentPolicy;
+ },
+
+ /**
+ * Test only method, used to simulate the infobar being shown in xpcshell tests.
+ */
+ testInfobarShown() {
+ return TelemetryReportingPolicyImpl._userNotified();
+ },
+
+ /**
+ * Test only method, used to trigger an update of the "first run" state.
+ */
+ testUpdateFirstRun() {
+ TelemetryReportingPolicyImpl._isFirstRun = undefined;
+ TelemetryReportingPolicyImpl.isFirstRun();
+ },
+};
+
+var TelemetryReportingPolicyImpl = {
+ _logger: null,
+ // Keep track of the notification status if user wasn't notified already.
+ _notificationInProgress: false,
+ // The timer used to show the datachoices notification at startup.
+ _startupNotificationTimerId: null,
+ // Keep track of the first session state, as the related preference
+ // is flipped right after the browser starts.
+ _isFirstRun: undefined,
+
+ get _log() {
+ if (!this._logger) {
+ this._logger = Log.repository.getLoggerWithMessagePrefix(
+ LOGGER_NAME,
+ LOGGER_PREFIX
+ );
+ }
+
+ return this._logger;
+ },
+
+ /**
+ * Get the date the policy was notified.
+ * @return {Object} A date object or null on errors.
+ */
+ get dataSubmissionPolicyNotifiedDate() {
+ let prefString = Services.prefs.getStringPref(
+ TelemetryUtils.Preferences.AcceptedPolicyDate,
+ "0"
+ );
+ let valueInteger = parseInt(prefString, 10);
+
+ // Bail out if we didn't store any value yet.
+ if (valueInteger == 0) {
+ this._log.info(
+ "get dataSubmissionPolicyNotifiedDate - No date stored yet."
+ );
+ return null;
+ }
+
+ // If an invalid value is saved in the prefs, bail out too.
+ if (Number.isNaN(valueInteger)) {
+ this._log.error(
+ "get dataSubmissionPolicyNotifiedDate - Invalid date stored."
+ );
+ return null;
+ }
+
+ // Make sure the notification date is newer then the oldest allowed date.
+ let date = new Date(valueInteger);
+ if (date.getFullYear() < OLDEST_ALLOWED_ACCEPTANCE_YEAR) {
+ this._log.error(
+ "get dataSubmissionPolicyNotifiedDate - The stored date is too old."
+ );
+ return null;
+ }
+
+ return date;
+ },
+
+ /**
+ * Set the date the policy was notified.
+ * @param {Object} aDate A valid date object.
+ */
+ set dataSubmissionPolicyNotifiedDate(aDate) {
+ this._log.trace("set dataSubmissionPolicyNotifiedDate - aDate: " + aDate);
+
+ if (!aDate || aDate.getFullYear() < OLDEST_ALLOWED_ACCEPTANCE_YEAR) {
+ this._log.error(
+ "set dataSubmissionPolicyNotifiedDate - Invalid notification date."
+ );
+ return;
+ }
+
+ Services.prefs.setStringPref(
+ TelemetryUtils.Preferences.AcceptedPolicyDate,
+ aDate.getTime().toString()
+ );
+ },
+
+ /**
+ * Whether submission of data is allowed.
+ *
+ * This is the master switch for remote server communication. If it is
+ * false, we never request upload or deletion.
+ */
+ get dataSubmissionEnabled() {
+ // Default is true because we are opt-out.
+ return Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.DataSubmissionEnabled,
+ true
+ );
+ },
+
+ get currentPolicyVersion() {
+ return Services.prefs.getIntPref(
+ TelemetryUtils.Preferences.CurrentPolicyVersion,
+ TelemetryReportingPolicy.DEFAULT_DATAREPORTING_POLICY_VERSION
+ );
+ },
+
+ /**
+ * The minimum policy version which for dataSubmissionPolicyAccepted to
+ * to be valid.
+ */
+ get minimumPolicyVersion() {
+ const minPolicyVersion = Services.prefs.getIntPref(
+ TelemetryUtils.Preferences.MinimumPolicyVersion,
+ 1
+ );
+
+ // First check if the current channel has a specific minimum policy version. If not,
+ // use the general minimum policy version.
+ let channel = "";
+ try {
+ channel = TelemetryUtils.getUpdateChannel();
+ } catch (e) {
+ this._log.error(
+ "minimumPolicyVersion - Unable to retrieve the current channel."
+ );
+ return minPolicyVersion;
+ }
+ const channelPref =
+ TelemetryUtils.Preferences.MinimumPolicyVersion + ".channel-" + channel;
+ return Services.prefs.getIntPref(channelPref, minPolicyVersion);
+ },
+
+ get dataSubmissionPolicyAcceptedVersion() {
+ return Services.prefs.getIntPref(
+ TelemetryUtils.Preferences.AcceptedPolicyVersion,
+ 0
+ );
+ },
+
+ set dataSubmissionPolicyAcceptedVersion(value) {
+ Services.prefs.setIntPref(
+ TelemetryUtils.Preferences.AcceptedPolicyVersion,
+ value
+ );
+ },
+
+ /**
+ * Checks to see if the user has been notified about data submission
+ * @return {Bool} True if user has been notified and the notification is still valid,
+ * false otherwise.
+ */
+ get isUserNotifiedOfCurrentPolicy() {
+ // If we don't have a sane notification date, the user was not notified yet.
+ if (
+ !this.dataSubmissionPolicyNotifiedDate ||
+ this.dataSubmissionPolicyNotifiedDate.getTime() <= 0
+ ) {
+ return false;
+ }
+
+ // The accepted policy version should not be less than the minimum policy version.
+ if (this.dataSubmissionPolicyAcceptedVersion < this.minimumPolicyVersion) {
+ return false;
+ }
+
+ // Otherwise the user was already notified.
+ return true;
+ },
+
+ /**
+ * Test only method, restarts the policy.
+ */
+ reset() {
+ this.shutdown();
+ this._isFirstRun = undefined;
+ return this.setup();
+ },
+
+ /**
+ * Setup the policy.
+ */
+ setup() {
+ this._log.trace("setup");
+
+ // Migrate the data choices infobar, if needed.
+ this._migratePreferences();
+
+ // Add the event observers.
+ Services.obs.addObserver(this, "sessionstore-windows-restored");
+ },
+
+ /**
+ * Clean up the reporting policy.
+ */
+ shutdown() {
+ this._log.trace("shutdown");
+
+ this._detachObservers();
+
+ Policy.clearShowInfobarTimeout(this._startupNotificationTimerId);
+ },
+
+ /**
+ * Detach the observers that were attached during setup.
+ */
+ _detachObservers() {
+ Services.obs.removeObserver(this, "sessionstore-windows-restored");
+ },
+
+ /**
+ * Check if we are allowed to upload data. In order to submit data both these conditions
+ * should be true:
+ * - The data submission preference should be true.
+ * - The datachoices infobar should have been displayed.
+ *
+ * @return {Boolean} True if we are allowed to upload data, false otherwise.
+ */
+ canUpload() {
+ // If data submission is disabled, there's no point in showing the infobar. Just
+ // forbid to upload.
+ if (!this.dataSubmissionEnabled) {
+ return false;
+ }
+
+ // Submission is enabled. We enable upload if user is notified or we need to bypass
+ // the policy.
+ const bypassNotification = Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.BypassNotification,
+ false
+ );
+ return this.isUserNotifiedOfCurrentPolicy || bypassNotification;
+ },
+
+ isFirstRun() {
+ if (this._isFirstRun === undefined) {
+ this._isFirstRun = Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.FirstRun,
+ true
+ );
+ }
+ return this._isFirstRun;
+ },
+
+ /**
+ * Migrate the data policy preferences, if needed.
+ */
+ _migratePreferences() {
+ // Current prefs are mostly the same than the old ones, except for some deprecated ones.
+ for (let pref of DEPRECATED_FHR_PREFS) {
+ Services.prefs.clearUserPref(pref);
+ }
+ },
+
+ /**
+ * Determine whether the user should be notified.
+ */
+ _shouldNotify() {
+ if (!this.dataSubmissionEnabled) {
+ this._log.trace(
+ "_shouldNotify - Data submission disabled by the policy."
+ );
+ return false;
+ }
+
+ const bypassNotification = Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.BypassNotification,
+ false
+ );
+ if (this.isUserNotifiedOfCurrentPolicy || bypassNotification) {
+ this._log.trace(
+ "_shouldNotify - User already notified or bypassing the policy."
+ );
+ return false;
+ }
+
+ if (this._notificationInProgress) {
+ this._log.trace(
+ "_shouldNotify - User not notified, notification already in progress."
+ );
+ return false;
+ }
+
+ return true;
+ },
+
+ /**
+ * Show the data choices infobar if needed.
+ */
+ _showInfobar() {
+ if (!this._shouldNotify()) {
+ return;
+ }
+
+ this._log.trace("_showInfobar - User not notified, notifying now.");
+ this._notificationInProgress = true;
+ let request = new NotifyPolicyRequest(this._log);
+ Observers.notify("datareporting:notify-data-policy:request", request);
+ },
+
+ /**
+ * Called when the user is notified with the infobar or otherwise.
+ */
+ _userNotified() {
+ this._log.trace("_userNotified");
+ this._recordNotificationData();
+ lazy.TelemetrySend.notifyCanUpload();
+ },
+
+ /**
+ * Record date and the version of the accepted policy.
+ */
+ _recordNotificationData() {
+ this._log.trace("_recordNotificationData");
+ this.dataSubmissionPolicyNotifiedDate = Policy.now();
+ this.dataSubmissionPolicyAcceptedVersion = this.currentPolicyVersion;
+ // The user was notified and the notification data saved: the notification
+ // is no longer in progress.
+ this._notificationInProgress = false;
+ },
+
+ /**
+ * Try to open the privacy policy in a background tab instead of showing the infobar.
+ */
+ _openFirstRunPage() {
+ if (!this._shouldNotify()) {
+ return false;
+ }
+
+ let firstRunPolicyURL = Services.prefs.getStringPref(
+ TelemetryUtils.Preferences.FirstRunURL,
+ ""
+ );
+ if (!firstRunPolicyURL) {
+ return false;
+ }
+ firstRunPolicyURL = Services.urlFormatter.formatURL(firstRunPolicyURL);
+
+ const { BrowserWindowTracker } = ChromeUtils.importESModule(
+ "resource:///modules/BrowserWindowTracker.sys.mjs"
+ );
+ let win = BrowserWindowTracker.getTopWindow();
+
+ if (!win) {
+ this._log.info(
+ "Couldn't find browser window to open first-run page. Falling back to infobar."
+ );
+ return false;
+ }
+
+ // We'll consider the user notified once the privacy policy has been loaded
+ // in a background tab even if that tab hasn't been selected.
+ let tab;
+ let progressListener = {};
+ progressListener.onStateChange = (
+ aBrowser,
+ aWebProgress,
+ aRequest,
+ aStateFlags,
+ aStatus
+ ) => {
+ if (
+ aWebProgress.isTopLevel &&
+ tab &&
+ tab.linkedBrowser == aBrowser &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
+ aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK
+ ) {
+ let uri = aBrowser.documentURI;
+ if (
+ uri &&
+ !/^about:(blank|neterror|certerror|blocked)/.test(uri.spec)
+ ) {
+ this._userNotified();
+ } else {
+ this._log.info(
+ "Failed to load first-run page. Falling back to infobar."
+ );
+ this._showInfobar();
+ }
+ removeListeners();
+ }
+ };
+
+ let removeListeners = () => {
+ win.removeEventListener("unload", removeListeners);
+ win.gBrowser.removeTabsProgressListener(progressListener);
+ };
+
+ win.addEventListener("unload", removeListeners);
+ win.gBrowser.addTabsProgressListener(progressListener);
+
+ tab = win.gBrowser.addTab(firstRunPolicyURL, {
+ inBackground: true,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ });
+ return true;
+ },
+
+ observe(aSubject, aTopic, aData) {
+ if (aTopic != "sessionstore-windows-restored") {
+ return;
+ }
+
+ if (this.isFirstRun()) {
+ // We're performing the first run, flip firstRun preference for subsequent runs.
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FirstRun, false);
+
+ try {
+ if (this._openFirstRunPage()) {
+ return;
+ }
+ } catch (e) {
+ this._log.error("Failed to open privacy policy tab: " + e);
+ }
+ }
+
+ // Show the info bar.
+ const delay = this.isFirstRun()
+ ? NOTIFICATION_DELAY_FIRST_RUN_MSEC
+ : NOTIFICATION_DELAY_NEXT_RUNS_MSEC;
+
+ this._startupNotificationTimerId = Policy.setShowInfobarTimeout(
+ // Calling |canUpload| eventually shows the infobar, if needed.
+ () => this._showInfobar(),
+ delay
+ );
+ },
+};
diff --git a/toolkit/components/telemetry/app/TelemetryScheduler.sys.mjs b/toolkit/components/telemetry/app/TelemetryScheduler.sys.mjs
new file mode 100644
index 0000000000..2fc94dc8fc
--- /dev/null
+++ b/toolkit/components/telemetry/app/TelemetryScheduler.sys.mjs
@@ -0,0 +1,422 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { Log } from "resource://gre/modules/Log.sys.mjs";
+
+import { TelemetrySession } from "resource://gre/modules/TelemetrySession.sys.mjs";
+import { TelemetryUtils } from "resource://gre/modules/TelemetryUtils.sys.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+import { clearTimeout, setTimeout } from "resource://gre/modules/Timer.sys.mjs";
+
+const lazy = {};
+
+XPCOMUtils.defineLazyServiceGetters(lazy, {
+ idleService: ["@mozilla.org/widget/useridleservice;1", "nsIUserIdleService"],
+});
+
+const MIN_SUBSESSION_LENGTH_MS =
+ Services.prefs.getIntPref("toolkit.telemetry.minSubsessionLength", 5 * 60) *
+ 1000;
+
+const LOGGER_NAME = "Toolkit.Telemetry";
+
+// Seconds of idle time before pinging.
+// On idle-daily a gather-telemetry notification is fired, during it probes can
+// start asynchronous tasks to gather data.
+const IDLE_TIMEOUT_SECONDS = Services.prefs.getIntPref(
+ "toolkit.telemetry.idleTimeout",
+ 5 * 60
+);
+
+// Execute a scheduler tick every 5 minutes.
+const SCHEDULER_TICK_INTERVAL_MS =
+ Services.prefs.getIntPref(
+ "toolkit.telemetry.scheduler.tickInterval",
+ 5 * 60
+ ) * 1000;
+// When user is idle, execute a scheduler tick every 60 minutes.
+const SCHEDULER_TICK_IDLE_INTERVAL_MS =
+ Services.prefs.getIntPref(
+ "toolkit.telemetry.scheduler.idleTickInterval",
+ 60 * 60
+ ) * 1000;
+
+// The maximum time (ms) until the tick should moved from the idle
+// queue to the regular queue if it hasn't been executed yet.
+const SCHEDULER_TICK_MAX_IDLE_DELAY_MS = 60 * 1000;
+
+// The frequency at which we persist session data to the disk to prevent data loss
+// in case of aborted sessions (currently 5 minutes).
+const ABORTED_SESSION_UPDATE_INTERVAL_MS = 5 * 60 * 1000;
+
+// The tolerance we have when checking if it's midnight (15 minutes).
+const SCHEDULER_MIDNIGHT_TOLERANCE_MS = 15 * 60 * 1000;
+
+/**
+ * This is a policy object used to override behavior for testing.
+ */
+export var Policy = {
+ now: () => new Date(),
+ setSchedulerTickTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
+ clearSchedulerTickTimeout: id => clearTimeout(id),
+ prioEncode: (batchID, prioParams) => PrioEncoder.encode(batchID, prioParams),
+};
+
+/**
+ * TelemetryScheduler contains a single timer driving all regularly-scheduled
+ * Telemetry related jobs. Having a single place with this logic simplifies
+ * reasoning about scheduling actions in a single place, making it easier to
+ * coordinate jobs and coalesce them.
+ */
+export var TelemetryScheduler = {
+ // Tracks the main ping
+ _lastDailyPingTime: 0,
+ // Tracks the aborted session ping
+ _lastSessionCheckpointTime: 0,
+ // Tracks all other pings at regular intervals
+ _lastPeriodicPingTime: 0,
+
+ _log: null,
+
+ // The timer which drives the scheduler.
+ _schedulerTimer: null,
+ // The interval used by the scheduler timer.
+ _schedulerInterval: 0,
+ _shuttingDown: true,
+ _isUserIdle: false,
+
+ /**
+ * Initialises the scheduler and schedules the first daily/aborted session pings.
+ */
+ init() {
+ this._log = Log.repository.getLoggerWithMessagePrefix(
+ LOGGER_NAME,
+ "TelemetryScheduler::"
+ );
+ this._log.trace("init");
+ this._shuttingDown = false;
+ this._isUserIdle = false;
+
+ // Initialize the last daily ping and aborted session last due times to the current time.
+ // Otherwise, we might end up sending daily pings even if the subsession is not long enough.
+ let now = Policy.now();
+ this._lastDailyPingTime = now.getTime();
+ this._lastPeriodicPingTime = now.getTime();
+ this._lastSessionCheckpointTime = now.getTime();
+ this._rescheduleTimeout();
+
+ lazy.idleService.addIdleObserver(this, IDLE_TIMEOUT_SECONDS);
+ Services.obs.addObserver(this, "wake_notification");
+ },
+
+ /**
+ * Stops the scheduler.
+ */
+ shutdown() {
+ if (this._shuttingDown) {
+ if (this._log) {
+ this._log.error("shutdown - Already shut down");
+ } else {
+ console.error("TelemetryScheduler.shutdown - Already shut down");
+ }
+ return;
+ }
+
+ this._log.trace("shutdown");
+ if (this._schedulerTimer) {
+ Policy.clearSchedulerTickTimeout(this._schedulerTimer);
+ this._schedulerTimer = null;
+ }
+
+ lazy.idleService.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS);
+ Services.obs.removeObserver(this, "wake_notification");
+
+ this._shuttingDown = true;
+ },
+
+ // Reset some specific innards without shutting down and re-init'ing.
+ // Test-only method.
+ testReset() {
+ this._idleDispatch?.cancel();
+ this._idleDispatch = undefined;
+ },
+
+ _clearTimeout() {
+ if (this._schedulerTimer) {
+ Policy.clearSchedulerTickTimeout(this._schedulerTimer);
+ }
+ },
+
+ /**
+ * Reschedules the tick timer.
+ */
+ _rescheduleTimeout() {
+ this._log.trace("_rescheduleTimeout - isUserIdle: " + this._isUserIdle);
+ if (this._shuttingDown) {
+ this._log.warn("_rescheduleTimeout - already shutdown");
+ return;
+ }
+
+ this._clearTimeout();
+
+ const now = Policy.now();
+ let timeout = SCHEDULER_TICK_INTERVAL_MS;
+
+ // When the user is idle we want to fire the timer less often.
+ if (this._isUserIdle) {
+ timeout = SCHEDULER_TICK_IDLE_INTERVAL_MS;
+ // We need to make sure though that we don't miss sending pings around
+ // midnight when we use the longer idle intervals.
+ const nextMidnight = TelemetryUtils.getNextMidnight(now);
+ timeout = Math.min(timeout, nextMidnight.getTime() - now.getTime());
+ }
+
+ this._log.trace(
+ "_rescheduleTimeout - scheduling next tick for " +
+ new Date(now.getTime() + timeout)
+ );
+ this._schedulerTimer = Policy.setSchedulerTickTimeout(
+ () => this._onSchedulerTick(),
+ timeout
+ );
+ },
+
+ _sentPingToday(pingTime, nowDate) {
+ // This is today's date and also the previous midnight (0:00).
+ const todayDate = TelemetryUtils.truncateToDays(nowDate);
+ // We consider a ping sent for today if it occured after or at 00:00 today.
+ return pingTime >= todayDate.getTime();
+ },
+
+ /**
+ * Checks if we can send a daily ping or not.
+ * @param {Object} nowDate A date object.
+ * @return {Boolean} True if we can send the daily ping, false otherwise.
+ */
+ _isDailyPingDue(nowDate) {
+ // The daily ping is not due if we already sent one today.
+ if (this._sentPingToday(this._lastDailyPingTime, nowDate)) {
+ this._log.trace("_isDailyPingDue - already sent one today");
+ return false;
+ }
+
+ // Avoid overly short sessions.
+ const timeSinceLastDaily = nowDate.getTime() - this._lastDailyPingTime;
+ if (timeSinceLastDaily < MIN_SUBSESSION_LENGTH_MS) {
+ this._log.trace(
+ "_isDailyPingDue - delaying daily to keep minimum session length"
+ );
+ return false;
+ }
+
+ this._log.trace("_isDailyPingDue - is due");
+ return true;
+ },
+
+ /**
+ * Checks if we can send a regular ping or not.
+ * @param {Object} nowDate A date object.
+ * @return {Boolean} True if we can send the regular pings, false otherwise.
+ */
+ _isPeriodicPingDue(nowDate) {
+ // The periodic ping is not due if we already sent one today.
+ if (this._sentPingToday(this._lastPeriodicPingTime, nowDate)) {
+ this._log.trace("_isPeriodicPingDue - already sent one today");
+ return false;
+ }
+
+ this._log.trace("_isPeriodicPingDue - is due");
+ return true;
+ },
+
+ /**
+ * An helper function to save an aborted-session ping.
+ * @param {Number} now The current time, in milliseconds.
+ * @param {Object} [competingPayload=null] If we are coalescing the daily and the
+ * aborted-session pings, this is the payload for the former. Note
+ * that the reason field of this payload will be changed.
+ * @return {Promise} A promise resolved when the ping is saved.
+ */
+ _saveAbortedPing(now, competingPayload = null) {
+ this._lastSessionCheckpointTime = now;
+ return TelemetrySession.saveAbortedSessionPing(competingPayload).catch(e =>
+ this._log.error("_saveAbortedPing - Failed", e)
+ );
+ },
+
+ /**
+ * The notifications handler.
+ */
+ observe(aSubject, aTopic, aData) {
+ this._log.trace("observe - aTopic: " + aTopic);
+ switch (aTopic) {
+ case "idle":
+ // If the user is idle, increase the tick interval.
+ this._isUserIdle = true;
+ return this._onSchedulerTick();
+ case "active":
+ // User is back to work, restore the original tick interval.
+ this._isUserIdle = false;
+ return this._onSchedulerTick(true);
+ case "wake_notification":
+ // The machine woke up from sleep, trigger a tick to avoid sessions
+ // spanning more than a day.
+ // This is needed because sleep time does not count towards timeouts
+ // on Mac & Linux - see bug 1262386, bug 1204823 et al.
+ return this._onSchedulerTick(true);
+ }
+ return undefined;
+ },
+
+ /**
+ * Creates an object with a method `dispatch` that will call `dispatchFn` unless
+ * the method `cancel` is called beforehand.
+ *
+ * This is used to wrap main thread idle dispatch since it does not provide a
+ * cancel mechanism.
+ */
+ _makeIdleDispatch(dispatchFn) {
+ this._log.trace("_makeIdleDispatch");
+ let fn = dispatchFn;
+ let l = msg => this._log.trace(msg); // need to bind `this`
+ return {
+ cancel() {
+ fn = undefined;
+ },
+ dispatch(resolve, reject) {
+ l("_makeIdleDispatch.dispatch - !!fn: " + !!fn);
+ if (!fn) {
+ return Promise.resolve().then(resolve, reject);
+ }
+ return fn(resolve, reject);
+ },
+ };
+ },
+
+ /**
+ * Performs a scheduler tick. This function manages Telemetry recurring operations.
+ * @param {Boolean} [dispatchOnIdle=false] If true, the tick is dispatched in the
+ * next idle cycle of the main thread.
+ * @return {Promise} A promise, only used when testing, resolved when the scheduled
+ * operation completes.
+ */
+ _onSchedulerTick(dispatchOnIdle = false) {
+ this._log.trace("_onSchedulerTick - dispatchOnIdle: " + dispatchOnIdle);
+ // This call might not be triggered from a timeout. In that case we don't want to
+ // leave any previously scheduled timeouts pending.
+ this._clearTimeout();
+
+ if (this._idleDispatch) {
+ this._idleDispatch.cancel();
+ }
+
+ if (this._shuttingDown) {
+ this._log.warn("_onSchedulerTick - already shutdown.");
+ return Promise.reject(new Error("Already shutdown."));
+ }
+
+ let promise = Promise.resolve();
+ try {
+ if (dispatchOnIdle) {
+ this._idleDispatch = this._makeIdleDispatch((resolve, reject) => {
+ this._log.trace(
+ "_onSchedulerTick - ildeDispatchToMainThread dispatch"
+ );
+ return this._schedulerTickLogic().then(resolve, reject);
+ });
+ promise = new Promise((resolve, reject) =>
+ Services.tm.idleDispatchToMainThread(() => {
+ return this._idleDispatch
+ ? this._idleDispatch.dispatch(resolve, reject)
+ : Promise.resolve().then(resolve, reject);
+ }, SCHEDULER_TICK_MAX_IDLE_DELAY_MS)
+ );
+ } else {
+ promise = this._schedulerTickLogic();
+ }
+ } catch (e) {
+ this._log.error("_onSchedulerTick - There was an exception", e);
+ } finally {
+ this._rescheduleTimeout();
+ }
+
+ // This promise is returned to make testing easier.
+ return promise;
+ },
+
+ /**
+ * Implements the scheduler logic.
+ * @return {Promise} Resolved when the scheduled task completes. Only used in tests.
+ */
+ _schedulerTickLogic() {
+ this._log.trace("_schedulerTickLogic");
+
+ let nowDate = Policy.now();
+ let now = nowDate.getTime();
+
+ // Check if the daily ping is due.
+ const shouldSendDaily = this._isDailyPingDue(nowDate);
+ // Check if other regular pings are due.
+ const shouldSendPeriodic = this._isPeriodicPingDue(nowDate);
+
+ if (shouldSendPeriodic) {
+ this._log.trace("_schedulerTickLogic - Periodic ping due.");
+ this._lastPeriodicPingTime = now;
+ // Send other pings.
+ // ...currently no other pings exist
+ }
+
+ if (shouldSendDaily) {
+ this._log.trace("_schedulerTickLogic - Daily ping due.");
+ this._lastDailyPingTime = now;
+ return TelemetrySession.sendDailyPing();
+ }
+
+ // Check if the aborted-session ping is due. If a daily ping was saved above, it was
+ // already duplicated as an aborted-session ping.
+ const isAbortedPingDue =
+ now - this._lastSessionCheckpointTime >=
+ ABORTED_SESSION_UPDATE_INTERVAL_MS;
+ if (isAbortedPingDue) {
+ this._log.trace("_schedulerTickLogic - Aborted session ping due.");
+ return this._saveAbortedPing(now);
+ }
+
+ // No ping is due.
+ this._log.trace("_schedulerTickLogic - No ping due.");
+ return Promise.resolve();
+ },
+
+ /**
+ * Re-schedule the daily ping if some other equivalent ping was sent.
+ *
+ * This is only called from TelemetrySession when a main ping with reason 'environment-change'
+ * is sent.
+ *
+ * @param {Object} [payload] The payload of the ping that was sent,
+ * to be stored as an aborted-session ping.
+ */
+ rescheduleDailyPing(payload) {
+ if (this._shuttingDown) {
+ this._log.error("rescheduleDailyPing - already shutdown");
+ return;
+ }
+
+ this._log.trace("rescheduleDailyPing");
+ let now = Policy.now();
+
+ // We just generated an environment-changed ping, save it as an aborted session and
+ // update the schedules.
+ this._saveAbortedPing(now.getTime(), payload);
+
+ // If we're close to midnight, skip today's daily ping and reschedule it for tomorrow.
+ let nearestMidnight = TelemetryUtils.getNearestMidnight(
+ now,
+ SCHEDULER_MIDNIGHT_TOLERANCE_MS
+ );
+ if (nearestMidnight) {
+ this._lastDailyPingTime = now.getTime();
+ }
+ },
+};
diff --git a/toolkit/components/telemetry/app/TelemetrySend.sys.mjs b/toolkit/components/telemetry/app/TelemetrySend.sys.mjs
new file mode 100644
index 0000000000..25abfd6da1
--- /dev/null
+++ b/toolkit/components/telemetry/app/TelemetrySend.sys.mjs
@@ -0,0 +1,1701 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * This module is responsible for uploading pings to the server and persisting
+ * pings that can't be send now.
+ * Those pending pings are persisted on disk and sent at the next opportunity,
+ * newest first.
+ */
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+import { ClientID } from "resource://gre/modules/ClientID.sys.mjs";
+import { Log } from "resource://gre/modules/Log.sys.mjs";
+import { ServiceRequest } from "resource://gre/modules/ServiceRequest.sys.mjs";
+
+import { TelemetryUtils } from "resource://gre/modules/TelemetryUtils.sys.mjs";
+import { clearTimeout, setTimeout } from "resource://gre/modules/Timer.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+ TelemetryHealthPing: "resource://gre/modules/HealthPing.sys.mjs",
+ TelemetryReportingPolicy:
+ "resource://gre/modules/TelemetryReportingPolicy.sys.mjs",
+ TelemetryStorage: "resource://gre/modules/TelemetryStorage.sys.mjs",
+});
+
+const Utils = TelemetryUtils;
+
+const LOGGER_NAME = "Toolkit.Telemetry";
+const LOGGER_PREFIX = "TelemetrySend::";
+
+const TOPIC_IDLE_DAILY = "idle-daily";
+// The following topics are notified when Firefox is closing
+// because the OS is shutting down.
+const TOPIC_QUIT_APPLICATION_GRANTED = "quit-application-granted";
+const TOPIC_QUIT_APPLICATION_FORCED = "quit-application-forced";
+const PREF_CHANGED_TOPIC = "nsPref:changed";
+const TOPIC_PROFILE_CHANGE_NET_TEARDOWN = "profile-change-net-teardown";
+
+// Whether the FHR/Telemetry unification features are enabled.
+// Changing this pref requires a restart.
+const IS_UNIFIED_TELEMETRY = Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.Unified,
+ false
+);
+
+const MS_IN_A_MINUTE = 60 * 1000;
+
+const PING_TYPE_DELETION_REQUEST = "deletion-request";
+
+// We try to spread "midnight" pings out over this interval.
+const MIDNIGHT_FUZZING_INTERVAL_MS = 60 * MS_IN_A_MINUTE;
+// We delay sending "midnight" pings on this client by this interval.
+const MIDNIGHT_FUZZING_DELAY_MS = Math.random() * MIDNIGHT_FUZZING_INTERVAL_MS;
+
+// Timeout after which we consider a ping submission failed.
+export const PING_SUBMIT_TIMEOUT_MS = 1.5 * MS_IN_A_MINUTE;
+
+// To keep resource usage in check, we limit ping sending to a maximum number
+// of pings per minute.
+const MAX_PING_SENDS_PER_MINUTE = 10;
+
+// If we have more pending pings then we can send right now, we schedule the next
+// send for after SEND_TICK_DELAY.
+const SEND_TICK_DELAY = 1 * MS_IN_A_MINUTE;
+// If we had any ping send failures since the last ping, we use a backoff timeout
+// for the next ping sends. We increase the delay exponentially up to a limit of
+// SEND_MAXIMUM_BACKOFF_DELAY_MS.
+// This exponential backoff will be reset by external ping submissions & idle-daily.
+const SEND_MAXIMUM_BACKOFF_DELAY_MS = 120 * MS_IN_A_MINUTE;
+
+// Strings to map from XHR.errorCode to TELEMETRY_SEND_FAILURE_TYPE.
+// Echoes XMLHttpRequestMainThread's ErrorType enum.
+// Make sure that any additions done to XHR_ERROR_TYPE enum are also mirrored in
+// TELEMETRY_SEND_FAILURE_TYPE and TELEMETRY_SEND_FAILURE_TYPE_PER_PING's labels.
+const XHR_ERROR_TYPE = [
+ "eOK",
+ "eRequest",
+ "eUnreachable",
+ "eChannelOpen",
+ "eRedirect",
+ "eTerminated",
+];
+
+/**
+ * This is a policy object used to override behavior within this module.
+ * Tests override properties on this object to allow for control of behavior
+ * that would otherwise be very hard to cover.
+ */
+export var Policy = {
+ now: () => new Date(),
+ midnightPingFuzzingDelay: () => MIDNIGHT_FUZZING_DELAY_MS,
+ pingSubmissionTimeout: () => PING_SUBMIT_TIMEOUT_MS,
+ setSchedulerTickTimeout: (callback, delayMs) => setTimeout(callback, delayMs),
+ clearSchedulerTickTimeout: id => clearTimeout(id),
+ gzipCompressString: data => gzipCompressString(data),
+};
+
+/**
+ * Determine if the ping has the new v4 ping format or the legacy v2 one or earlier.
+ */
+function isV4PingFormat(aPing) {
+ return (
+ "id" in aPing &&
+ "application" in aPing &&
+ "version" in aPing &&
+ aPing.version >= 2
+ );
+}
+
+/**
+ * Check if the provided ping is a deletion-request ping.
+ * @param {Object} aPing The ping to check.
+ * @return {Boolean} True if the ping is a deletion-request ping, false otherwise.
+ */
+function isDeletionRequestPing(aPing) {
+ return isV4PingFormat(aPing) && aPing.type == PING_TYPE_DELETION_REQUEST;
+}
+
+/**
+ * Save the provided ping as a pending ping.
+ * @param {Object} aPing The ping to save.
+ * @return {Promise} A promise resolved when the ping is saved.
+ */
+function savePing(aPing) {
+ return lazy.TelemetryStorage.savePendingPing(aPing);
+}
+
+function arrayToString(array) {
+ let buffer = "";
+ // String.fromCharCode can only deal with 500,000 characters at
+ // a time, so chunk the result into parts of that size.
+ const chunkSize = 500000;
+ for (let offset = 0; offset < array.length; offset += chunkSize) {
+ buffer += String.fromCharCode.apply(
+ String,
+ array.slice(offset, offset + chunkSize)
+ );
+ }
+ return buffer;
+}
+
+/**
+ * @return {String} This returns a string with the gzip compressed data.
+ */
+export function gzipCompressString(string) {
+ let observer = {
+ buffer: null,
+ onStreamComplete(loader, context, status, length, result) {
+ this.buffer = arrayToString(result);
+ },
+ };
+
+ let scs = Cc["@mozilla.org/streamConverters;1"].getService(
+ Ci.nsIStreamConverterService
+ );
+ let listener = Cc["@mozilla.org/network/stream-loader;1"].createInstance(
+ Ci.nsIStreamLoader
+ );
+ listener.init(observer);
+ let converter = scs.asyncConvertData("uncompressed", "gzip", listener, null);
+ let stringStream = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(
+ Ci.nsIStringInputStream
+ );
+ stringStream.data = string;
+ converter.onStartRequest(null, null);
+ converter.onDataAvailable(null, stringStream, 0, string.length);
+ converter.onStopRequest(null, null, null);
+ return observer.buffer;
+}
+
+const STANDALONE_PING_TIMEOUT = 30 * 1000; // 30 seconds
+
+export function sendStandalonePing(endpoint, payload, extraHeaders = {}) {
+ return new Promise((resolve, reject) => {
+ let request = new ServiceRequest({ mozAnon: true });
+ request.mozBackgroundRequest = true;
+ request.timeout = STANDALONE_PING_TIMEOUT;
+
+ request.open("POST", endpoint, true);
+ request.overrideMimeType("text/plain");
+ request.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
+ request.setRequestHeader("Content-Encoding", "gzip");
+ request.setRequestHeader("Date", new Date().toUTCString());
+ for (let header in extraHeaders) {
+ request.setRequestHeader(header, extraHeaders[header]);
+ }
+
+ request.onload = event => {
+ if (request.status !== 200) {
+ reject(event);
+ } else {
+ resolve(event);
+ }
+ };
+ request.onerror = reject;
+ request.onabort = reject;
+ request.ontimeout = reject;
+
+ let payloadStream = Cc[
+ "@mozilla.org/io/string-input-stream;1"
+ ].createInstance(Ci.nsIStringInputStream);
+
+ const utf8Payload = new TextEncoder().encode(payload);
+
+ payloadStream.data = gzipCompressString(arrayToString(utf8Payload));
+ request.sendInputStream(payloadStream);
+ });
+}
+
+export var TelemetrySend = {
+ get pendingPingCount() {
+ return TelemetrySendImpl.pendingPingCount;
+ },
+
+ /**
+ * Partial setup that runs immediately at startup. This currently triggers
+ * the crash report annotations.
+ */
+ earlyInit() {
+ TelemetrySendImpl.earlyInit();
+ },
+
+ /**
+ * Initializes this module.
+ *
+ * @param {Boolean} testing Whether this is run in a test. This changes some behavior
+ * to enable proper testing.
+ * @return {Promise} Resolved when setup is finished.
+ */
+ setup(testing = false) {
+ return TelemetrySendImpl.setup(testing);
+ },
+
+ /**
+ * Shutdown this module - this will cancel any pending ping tasks and wait for
+ * outstanding async activity like network and disk I/O.
+ *
+ * @return {Promise} Promise that is resolved when shutdown is finished.
+ */
+ shutdown() {
+ return TelemetrySendImpl.shutdown();
+ },
+
+ /**
+ * Flushes all pings to pingsender that were both
+ * 1. submitted after profile-change-net-teardown, and
+ * 2. wanting to be sent using pingsender.
+ */
+ flushPingSenderBatch() {
+ TelemetrySendImpl.flushPingSenderBatch();
+ },
+
+ /**
+ * Submit a ping for sending. This will:
+ * - send the ping right away if possible or
+ * - save the ping to disk and send it at the next opportunity
+ *
+ * @param {Object} ping The ping data to send, must be serializable to JSON.
+ * @param {Object} [aOptions] Options object.
+ * @param {Boolean} [options.usePingSender=false] if true, send the ping using the PingSender.
+ * @return {Promise} Test-only - a promise that is resolved when the ping is sent or saved.
+ */
+ submitPing(ping, options = {}) {
+ options.usePingSender = options.usePingSender || false;
+ return TelemetrySendImpl.submitPing(ping, options);
+ },
+
+ /**
+ * Check if sending is disabled. If Telemetry is not allowed to upload,
+ * pings are not sent to the server.
+ * If trying to send a deletion-request ping, don't block it.
+ *
+ * @param {Object} [ping=null] A ping to be checked.
+ * @return {Boolean} True if pings can be send to the servers, false otherwise.
+ */
+ sendingEnabled(ping = null) {
+ return TelemetrySendImpl.sendingEnabled(ping);
+ },
+
+ /**
+ * Notify that we can start submitting data to the servers.
+ */
+ notifyCanUpload() {
+ return TelemetrySendImpl.notifyCanUpload();
+ },
+
+ /**
+ * Only used in tests. Used to reset the module data to emulate a restart.
+ */
+ reset() {
+ return TelemetrySendImpl.reset();
+ },
+
+ /**
+ * Only used in tests.
+ */
+ setServer(server) {
+ return TelemetrySendImpl.setServer(server);
+ },
+
+ /**
+ * Clear out unpersisted, yet to be sent, pings and cancel outgoing ping requests.
+ */
+ clearCurrentPings() {
+ return TelemetrySendImpl.clearCurrentPings();
+ },
+
+ /**
+ * Only used in tests to wait on outgoing pending pings.
+ */
+ testWaitOnOutgoingPings() {
+ return TelemetrySendImpl.promisePendingPingActivity();
+ },
+
+ /**
+ * Only used in tests to set whether it is too late in shutdown to send pings.
+ */
+ testTooLateToSend(tooLate) {
+ TelemetrySendImpl._tooLateToSend = tooLate;
+ },
+
+ /**
+ * Test-only - this allows overriding behavior to enable ping sending in debug builds.
+ */
+ setTestModeEnabled(testing) {
+ TelemetrySendImpl.setTestModeEnabled(testing);
+ },
+
+ /**
+ * This returns state info for this module for AsyncShutdown timeout diagnostics.
+ */
+ getShutdownState() {
+ return TelemetrySendImpl.getShutdownState();
+ },
+
+ /**
+ * Send a ping using the ping sender.
+ * This method will not wait for the ping to be sent, instead it will return
+ * as soon as the pingsender program has been launched.
+ *
+ * This method is currently exposed here only for testing purposes as it's
+ * only used internally.
+ *
+ * @param {Array}<Object> pings An array of objects holding url / path pairs
+ * for each ping to be sent. The URL represent the telemetry server the
+ * ping will be sent to and the path points to the ping data. The ping
+ * data files will be deleted if the pings have been submitted
+ * successfully.
+ * @param {callback} observer A function called with parameters
+ * (subject, topic, data) and a topic of "process-finished" or
+ * "process-failed" after pingsender completion.
+ *
+ * @throws NS_ERROR_FAILURE if we couldn't find or run the pingsender
+ * executable.
+ * @throws NS_ERROR_NOT_IMPLEMENTED on Android as the pingsender is not
+ * available.
+ */
+ testRunPingSender(pings, observer) {
+ return TelemetrySendImpl.runPingSender(pings, observer);
+ },
+};
+
+var CancellableTimeout = {
+ _deferred: null,
+ _timer: null,
+
+ /**
+ * This waits until either the given timeout passed or the timeout was cancelled.
+ *
+ * @param {Number} timeoutMs The timeout in ms.
+ * @return {Promise<bool>} Promise that is resolved with false if the timeout was cancelled,
+ * false otherwise.
+ */
+ promiseWaitOnTimeout(timeoutMs) {
+ if (!this._deferred) {
+ this._deferred = Promise.withResolvers();
+ this._timer = Policy.setSchedulerTickTimeout(
+ () => this._onTimeout(),
+ timeoutMs
+ );
+ }
+
+ return this._deferred.promise;
+ },
+
+ _onTimeout() {
+ if (this._deferred) {
+ this._deferred.resolve(false);
+ this._timer = null;
+ this._deferred = null;
+ }
+ },
+
+ cancelTimeout() {
+ if (this._deferred) {
+ Policy.clearSchedulerTickTimeout(this._timer);
+ this._deferred.resolve(true);
+ this._timer = null;
+ this._deferred = null;
+ }
+ },
+};
+
+/**
+ * SendScheduler implements the timer & scheduling behavior for ping sends.
+ */
+export var SendScheduler = {
+ // Whether any ping sends failed since the last tick. If yes, we start with our exponential
+ // backoff timeout.
+ _sendsFailed: false,
+ // The current retry delay after ping send failures. We use this for the exponential backoff,
+ // increasing this value everytime we had send failures since the last tick.
+ _backoffDelay: SEND_TICK_DELAY,
+ _shutdown: false,
+ _sendTask: null,
+ // A string that tracks the last seen send task state, null if it never ran.
+ _sendTaskState: null,
+
+ _logger: null,
+
+ get _log() {
+ if (!this._logger) {
+ this._logger = Log.repository.getLoggerWithMessagePrefix(
+ LOGGER_NAME,
+ LOGGER_PREFIX + "Scheduler::"
+ );
+ }
+
+ return this._logger;
+ },
+
+ shutdown() {
+ this._log.trace("shutdown");
+ this._shutdown = true;
+ CancellableTimeout.cancelTimeout();
+ return Promise.resolve(this._sendTask);
+ },
+
+ start() {
+ this._log.trace("start");
+ this._sendsFailed = false;
+ this._backoffDelay = SEND_TICK_DELAY;
+ this._shutdown = false;
+ },
+
+ /**
+ * Only used for testing, resets the state to emulate a restart.
+ */
+ reset() {
+ this._log.trace("reset");
+ return this.shutdown().then(() => this.start());
+ },
+
+ /**
+ * Notify the scheduler of a failure in sending out pings that warrants retrying.
+ * This will trigger the exponential backoff timer behavior on the next tick.
+ */
+ notifySendsFailed() {
+ this._log.trace("notifySendsFailed");
+ if (this._sendsFailed) {
+ return;
+ }
+
+ this._sendsFailed = true;
+ this._log.trace("notifySendsFailed - had send failures");
+ },
+
+ /**
+ * Returns whether ping submissions are currently throttled.
+ */
+ isThrottled() {
+ const now = Policy.now();
+ const nextPingSendTime = this._getNextPingSendTime(now);
+ return nextPingSendTime > now.getTime();
+ },
+
+ waitOnSendTask() {
+ return Promise.resolve(this._sendTask);
+ },
+
+ triggerSendingPings(immediately) {
+ this._log.trace(
+ "triggerSendingPings - active send task: " +
+ !!this._sendTask +
+ ", immediately: " +
+ immediately
+ );
+
+ if (!this._sendTask) {
+ this._sendTask = this._doSendTask();
+ let clear = () => (this._sendTask = null);
+ this._sendTask.then(clear, clear);
+ } else if (immediately) {
+ CancellableTimeout.cancelTimeout();
+ }
+
+ return this._sendTask;
+ },
+
+ async _doSendTask() {
+ this._sendTaskState = "send task started";
+ this._backoffDelay = SEND_TICK_DELAY;
+ this._sendsFailed = false;
+
+ const resetBackoffTimer = () => {
+ this._backoffDelay = SEND_TICK_DELAY;
+ };
+
+ for (;;) {
+ this._log.trace("_doSendTask iteration");
+ this._sendTaskState = "start iteration";
+
+ if (this._shutdown) {
+ this._log.trace("_doSendTask - shutting down, bailing out");
+ this._sendTaskState = "bail out - shutdown check";
+ return;
+ }
+
+ // Get a list of pending pings, sorted by last modified, descending.
+ // Filter out all the pings we can't send now. This addresses scenarios like "deletion-request" pings
+ // which can be sent even when upload is disabled.
+ let pending = lazy.TelemetryStorage.getPendingPingList();
+ let current = TelemetrySendImpl.getUnpersistedPings();
+ this._log.trace(
+ "_doSendTask - pending: " +
+ pending.length +
+ ", current: " +
+ current.length
+ );
+ // Note that the two lists contain different kind of data. |pending| only holds ping
+ // info, while |current| holds actual ping data.
+ if (!TelemetrySendImpl.sendingEnabled()) {
+ // If sending is disabled, only handle deletion-request pings
+ pending = [];
+ current = current.filter(p => isDeletionRequestPing(p));
+ }
+ this._log.trace(
+ "_doSendTask - can send - pending: " +
+ pending.length +
+ ", current: " +
+ current.length
+ );
+
+ // Bail out if there is nothing to send.
+ if (!pending.length && !current.length) {
+ this._log.trace("_doSendTask - no pending pings, bailing out");
+ this._sendTaskState = "bail out - no pings to send";
+ return;
+ }
+
+ // If we are currently throttled (e.g. fuzzing to avoid midnight spikes), wait for the next send window.
+ const now = Policy.now();
+ if (this.isThrottled()) {
+ const nextPingSendTime = this._getNextPingSendTime(now);
+ this._log.trace(
+ "_doSendTask - throttled, delaying ping send to " +
+ new Date(nextPingSendTime)
+ );
+ this._sendTaskState = "wait for throttling to pass";
+
+ const delay = nextPingSendTime - now.getTime();
+ const cancelled = await CancellableTimeout.promiseWaitOnTimeout(delay);
+ if (cancelled) {
+ this._log.trace(
+ "_doSendTask - throttling wait was cancelled, resetting backoff timer"
+ );
+ resetBackoffTimer();
+ }
+
+ continue;
+ }
+
+ let sending = pending.slice(0, MAX_PING_SENDS_PER_MINUTE);
+ pending = pending.slice(MAX_PING_SENDS_PER_MINUTE);
+ this._log.trace(
+ "_doSendTask - triggering sending of " +
+ sending.length +
+ " pings now" +
+ ", " +
+ pending.length +
+ " pings waiting"
+ );
+
+ this._sendsFailed = false;
+ const sendStartTime = Policy.now();
+ this._sendTaskState = "wait on ping sends";
+ await TelemetrySendImpl.sendPings(
+ current,
+ sending.map(p => p.id)
+ );
+ if (this._shutdown || TelemetrySend.pendingPingCount == 0) {
+ this._log.trace(
+ "_doSendTask - bailing out after sending, shutdown: " +
+ this._shutdown +
+ ", pendingPingCount: " +
+ TelemetrySend.pendingPingCount
+ );
+ this._sendTaskState = "bail out - shutdown & pending check after send";
+ return;
+ }
+
+ // Calculate the delay before sending the next batch of pings.
+ // We start with a delay that makes us send max. 1 batch per minute.
+ // If we had send failures in the last batch, we will override this with
+ // a backoff delay.
+ const timeSinceLastSend = Policy.now() - sendStartTime;
+ let nextSendDelay = Math.max(0, SEND_TICK_DELAY - timeSinceLastSend);
+
+ if (!this._sendsFailed) {
+ this._log.trace(
+ "_doSendTask - had no send failures, resetting backoff timer"
+ );
+ resetBackoffTimer();
+ } else {
+ const newDelay = Math.min(
+ SEND_MAXIMUM_BACKOFF_DELAY_MS,
+ this._backoffDelay * 2
+ );
+ this._log.trace(
+ "_doSendTask - had send failures, backing off -" +
+ " old timeout: " +
+ this._backoffDelay +
+ ", new timeout: " +
+ newDelay
+ );
+ this._backoffDelay = newDelay;
+ nextSendDelay = this._backoffDelay;
+ }
+
+ this._log.trace(
+ "_doSendTask - waiting for next send opportunity, timeout is " +
+ nextSendDelay
+ );
+ this._sendTaskState = "wait on next send opportunity";
+ const cancelled = await CancellableTimeout.promiseWaitOnTimeout(
+ nextSendDelay
+ );
+ if (cancelled) {
+ this._log.trace(
+ "_doSendTask - batch send wait was cancelled, resetting backoff timer"
+ );
+ resetBackoffTimer();
+ }
+ }
+ },
+
+ /**
+ * This helper calculates the next time that we can send pings at.
+ * Currently this mostly redistributes ping sends from midnight until one hour after
+ * to avoid submission spikes around local midnight for daily pings.
+ *
+ * @param now Date The current time.
+ * @return Number The next time (ms from UNIX epoch) when we can send pings.
+ */
+ _getNextPingSendTime(now) {
+ // 1. First we check if the pref is set to skip any delay and send immediately.
+ // 2. Next we check if the time is between 0am and 1am. If it's not, we send
+ // immediately.
+ // 3. If we confirmed the time is indeed between 0am and 1am in step 1, we disallow
+ // sending before (midnight + fuzzing delay), which is a random time between 0am-1am
+ // (decided at startup).
+
+ let disableFuzzingDelay = Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.DisableFuzzingDelay,
+ false
+ );
+ if (disableFuzzingDelay) {
+ return now.getTime();
+ }
+
+ const midnight = Utils.truncateToDays(now);
+ // Don't delay pings if we are not within the fuzzing interval.
+ if (now.getTime() - midnight.getTime() > MIDNIGHT_FUZZING_INTERVAL_MS) {
+ return now.getTime();
+ }
+
+ // Delay ping send if we are within the midnight fuzzing range.
+ // We spread those ping sends out between |midnight| and |midnight + midnightPingFuzzingDelay|.
+ return midnight.getTime() + Policy.midnightPingFuzzingDelay();
+ },
+
+ getShutdownState() {
+ return {
+ shutdown: this._shutdown,
+ hasSendTask: !!this._sendTask,
+ sendsFailed: this._sendsFailed,
+ sendTaskState: this._sendTaskState,
+ backoffDelay: this._backoffDelay,
+ };
+ },
+};
+
+export var TelemetrySendImpl = {
+ _sendingEnabled: false,
+ // Tracks the shutdown state.
+ _shutdown: false,
+ _logger: null,
+ // This tracks all pending ping requests to the server.
+ _pendingPingRequests: new Map(),
+ // This tracks all the pending async ping activity.
+ _pendingPingActivity: new Set(),
+ // This is true when running in the test infrastructure.
+ _testMode: false,
+ // This holds pings that we currently try and haven't persisted yet.
+ _currentPings: new Map(),
+ // Used to skip spawning the pingsender if OS is shutting down.
+ _isOSShutdown: false,
+ // Has the network shut down, making it too late to send pings?
+ _tooLateToSend: false,
+ // Array of {url, path} awaiting flushPingSenderBatch().
+ _pingSenderBatch: [],
+
+ OBSERVER_TOPICS: [
+ TOPIC_IDLE_DAILY,
+ TOPIC_QUIT_APPLICATION_GRANTED,
+ TOPIC_QUIT_APPLICATION_FORCED,
+ TOPIC_PROFILE_CHANGE_NET_TEARDOWN,
+ ],
+
+ OBSERVED_PREFERENCES: [
+ TelemetryUtils.Preferences.TelemetryEnabled,
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ ],
+
+ // Whether sending pings has been overridden.
+ get _overrideOfficialCheck() {
+ return Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.OverrideOfficialCheck,
+ false
+ );
+ },
+
+ get _log() {
+ if (!this._logger) {
+ this._logger = Log.repository.getLoggerWithMessagePrefix(
+ LOGGER_NAME,
+ LOGGER_PREFIX
+ );
+ }
+
+ return this._logger;
+ },
+
+ get pendingPingRequests() {
+ return this._pendingPingRequests;
+ },
+
+ get pendingPingCount() {
+ return (
+ lazy.TelemetryStorage.getPendingPingList().length +
+ this._currentPings.size
+ );
+ },
+
+ setTestModeEnabled(testing) {
+ this._testMode = testing;
+ },
+
+ earlyInit() {
+ this._annotateCrashReport();
+
+ // Install the observer to detect OS shutdown early enough, so
+ // that we catch this before the delayed setup happens.
+ Services.obs.addObserver(this, TOPIC_QUIT_APPLICATION_FORCED);
+ Services.obs.addObserver(this, TOPIC_QUIT_APPLICATION_GRANTED);
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsISupportsWeakReference"]),
+
+ async setup(testing) {
+ this._log.trace("setup");
+
+ this._testMode = testing;
+
+ Services.obs.addObserver(this, TOPIC_IDLE_DAILY);
+ Services.obs.addObserver(this, TOPIC_PROFILE_CHANGE_NET_TEARDOWN);
+
+ this._server = Services.prefs.getStringPref(
+ TelemetryUtils.Preferences.Server,
+ undefined
+ );
+ this._sendingEnabled = true;
+
+ // Annotate crash reports so that crash pings are sent correctly and listen
+ // to pref changes to adjust the annotations accordingly.
+ for (let pref of this.OBSERVED_PREFERENCES) {
+ Services.prefs.addObserver(pref, this, true);
+ }
+ this._annotateCrashReport();
+
+ // Check the pending pings on disk now.
+ try {
+ await this._checkPendingPings();
+ } catch (ex) {
+ this._log.error("setup - _checkPendingPings rejected", ex);
+ }
+
+ // Enforce the pending pings storage quota. It could take a while so don't
+ // block on it.
+ lazy.TelemetryStorage.runEnforcePendingPingsQuotaTask();
+
+ // Start sending pings, but don't block on this.
+ SendScheduler.triggerSendingPings(true);
+ },
+
+ /**
+ * Triggers the crash report annotations depending on the current
+ * configuration. This communicates to the crash reporter if it can send a
+ * crash ping or not. This method can be called safely before setup() has
+ * been called.
+ */
+ _annotateCrashReport() {
+ try {
+ const cr = Cc["@mozilla.org/toolkit/crash-reporter;1"];
+ if (cr) {
+ // This needs to use nsICrashReporter because test_TelemetrySend.js
+ // replaces the crash reporter service, which we can't access here
+ // as Services caches it.
+ // eslint-disable-next-line mozilla/use-services
+ const crs = cr.getService(Ci.nsICrashReporter);
+
+ let clientId = ClientID.getCachedClientID();
+ let server =
+ this._server ||
+ Services.prefs.getStringPref(
+ TelemetryUtils.Preferences.Server,
+ undefined
+ );
+
+ if (
+ !this.sendingEnabled() ||
+ !lazy.TelemetryReportingPolicy.canUpload()
+ ) {
+ // If we cannot send pings then clear the crash annotations
+ crs.removeCrashReportAnnotation("TelemetryClientId");
+ crs.removeCrashReportAnnotation("TelemetryServerURL");
+ } else {
+ crs.annotateCrashReport("TelemetryClientId", clientId);
+ crs.annotateCrashReport("TelemetryServerURL", server);
+ }
+ }
+ } catch (e) {
+ // Ignore errors when crash reporting is disabled
+ }
+ },
+
+ /**
+ * Discard old pings from the pending pings and detect overdue ones.
+ * @return {Boolean} True if we have overdue pings, false otherwise.
+ */
+ async _checkPendingPings() {
+ // Scan the pending pings - that gives us a list sorted by last modified, descending.
+ let infos = await lazy.TelemetryStorage.loadPendingPingList();
+ this._log.info("_checkPendingPings - pending ping count: " + infos.length);
+ if (!infos.length) {
+ this._log.trace("_checkPendingPings - no pending pings");
+ return;
+ }
+
+ const now = Policy.now();
+
+ // Submit the age of the pending pings.
+ for (let pingInfo of infos) {
+ const ageInDays = Utils.millisecondsToDays(
+ Math.abs(now.getTime() - pingInfo.lastModificationDate)
+ );
+ Services.telemetry
+ .getHistogramById("TELEMETRY_PENDING_PINGS_AGE")
+ .add(ageInDays);
+ }
+ },
+
+ async shutdown() {
+ this._shutdown = true;
+
+ for (let pref of this.OBSERVED_PREFERENCES) {
+ // FIXME: When running tests this causes errors to be printed out if
+ // TelemetrySend.shutdown() is called twice in a row without calling
+ // TelemetrySend.setup() in-between.
+ Services.prefs.removeObserver(pref, this);
+ }
+
+ for (let topic of this.OBSERVER_TOPICS) {
+ try {
+ Services.obs.removeObserver(this, topic);
+ } catch (ex) {
+ this._log.error(
+ "shutdown - failed to remove observer for " + topic,
+ ex
+ );
+ }
+ }
+
+ // We can't send anymore now.
+ this._sendingEnabled = false;
+
+ // Cancel any outgoing requests.
+ await this._cancelOutgoingRequests();
+
+ // Stop any active send tasks.
+ await SendScheduler.shutdown();
+
+ // Wait for any outstanding async ping activity.
+ await this.promisePendingPingActivity();
+
+ // Save any outstanding pending pings to disk.
+ await this._persistCurrentPings();
+ },
+
+ flushPingSenderBatch() {
+ if (this._pingSenderBatch.length === 0) {
+ return;
+ }
+ this._log.trace(
+ `flushPingSenderBatch - Sending ${this._pingSenderBatch.length} pings.`
+ );
+ this.runPingSender(this._pingSenderBatch);
+ },
+
+ reset() {
+ this._log.trace("reset");
+
+ this._shutdown = false;
+ this._currentPings = new Map();
+ this._tooLateToSend = false;
+ this._isOSShutdown = false;
+ this._sendingEnabled = true;
+
+ const histograms = [
+ "TELEMETRY_SUCCESS",
+ "TELEMETRY_SEND_SUCCESS",
+ "TELEMETRY_SEND_FAILURE",
+ "TELEMETRY_SEND_FAILURE_TYPE",
+ ];
+
+ histograms.forEach(h => Services.telemetry.getHistogramById(h).clear());
+
+ const keyedHistograms = ["TELEMETRY_SEND_FAILURE_TYPE_PER_PING"];
+
+ keyedHistograms.forEach(h =>
+ Services.telemetry.getKeyedHistogramById(h).clear()
+ );
+
+ return SendScheduler.reset();
+ },
+
+ /**
+ * Notify that we can start submitting data to the servers.
+ */
+ notifyCanUpload() {
+ if (!this._sendingEnabled) {
+ this._log.trace(
+ "notifyCanUpload - notifying before sending is enabled. Ignoring."
+ );
+ return Promise.resolve();
+ }
+ // Let the scheduler trigger sending pings if possible, also inform the
+ // crash reporter that it can send crash pings if appropriate.
+ SendScheduler.triggerSendingPings(true);
+ this._annotateCrashReport();
+
+ return this.promisePendingPingActivity();
+ },
+
+ observe(subject, topic, data) {
+ let setOSShutdown = () => {
+ this._log.trace("setOSShutdown - in OS shutdown");
+ this._isOSShutdown = true;
+ };
+
+ switch (topic) {
+ case TOPIC_IDLE_DAILY:
+ SendScheduler.triggerSendingPings(true);
+ break;
+ case TOPIC_QUIT_APPLICATION_FORCED:
+ setOSShutdown();
+ break;
+ case TOPIC_QUIT_APPLICATION_GRANTED:
+ if (data == "syncShutdown") {
+ setOSShutdown();
+ }
+ break;
+ case PREF_CHANGED_TOPIC:
+ if (this.OBSERVED_PREFERENCES.includes(data)) {
+ this._annotateCrashReport();
+ }
+ break;
+ case TOPIC_PROFILE_CHANGE_NET_TEARDOWN:
+ this._tooLateToSend = true;
+ break;
+ }
+ },
+
+ /**
+ * Spawn the PingSender process that sends a ping. This function does
+ * not return an error or throw, it only logs an error.
+ *
+ * Even if the function doesn't fail, it doesn't mean that the ping was
+ * successfully sent, as we have no control over the spawned process. If it,
+ * succeeds, the ping is eventually removed from the disk to prevent duplicated
+ * submissions.
+ *
+ * @param {String} pingId The id of the ping to send.
+ * @param {String} submissionURL The complete Telemetry-compliant URL for the ping.
+ */
+ _sendWithPingSender(pingId, submissionURL) {
+ this._log.trace(
+ "_sendWithPingSender - sending " + pingId + " to " + submissionURL
+ );
+ try {
+ const pingPath = PathUtils.join(
+ lazy.TelemetryStorage.pingDirectoryPath,
+ pingId
+ );
+ if (this._tooLateToSend) {
+ // We're in shutdown. Batch pings destined for pingsender.
+ this._log.trace("_sendWithPingSender - too late to send. Batching.");
+ this._pingSenderBatch.push({ url: submissionURL, path: pingPath });
+ return;
+ }
+ this.runPingSender([{ url: submissionURL, path: pingPath }]);
+ } catch (e) {
+ this._log.error("_sendWithPingSender - failed to submit ping", e);
+ }
+ },
+
+ submitPing(ping, options) {
+ this._log.trace(
+ "submitPing - ping id: " +
+ ping.id +
+ ", options: " +
+ JSON.stringify(options)
+ );
+
+ if (!this.sendingEnabled(ping)) {
+ this._log.trace("submitPing - Telemetry is not allowed to send pings.");
+ return Promise.resolve();
+ }
+
+ // Send the ping using the PingSender, if requested and the user was
+ // notified of our policy. We don't support the pingsender on Android,
+ // so ignore this option on that platform (see bug 1335917).
+ // Moreover, if the OS is shutting down, we don't want to spawn the
+ // pingsender as it could unnecessarily slow down OS shutdown.
+ // Additionally, it could be be killed before it can complete its tasks,
+ // for example after successfully sending the ping but before removing
+ // the copy from the disk, resulting in receiving duplicate pings when
+ // Firefox restarts.
+ if (
+ options.usePingSender &&
+ !this._isOSShutdown &&
+ lazy.TelemetryReportingPolicy.canUpload() &&
+ AppConstants.platform != "android"
+ ) {
+ const url = this._buildSubmissionURL(ping);
+ // Serialize the ping to the disk and then spawn the PingSender.
+ return savePing(ping).then(() => this._sendWithPingSender(ping.id, url));
+ }
+
+ if (!this.canSendNow) {
+ // Sending is disabled or throttled, add this to the persisted pending pings.
+ this._log.trace(
+ "submitPing - can't send ping now, persisting to disk - " +
+ "canSendNow: " +
+ this.canSendNow
+ );
+ return savePing(ping);
+ }
+
+ // Let the scheduler trigger sending pings if possible.
+ // As a safety mechanism, this resets any currently active throttling.
+ this._log.trace("submitPing - can send pings, trying to send now");
+ this._currentPings.set(ping.id, ping);
+ SendScheduler.triggerSendingPings(true);
+ return Promise.resolve();
+ },
+
+ /**
+ * Only used in tests.
+ */
+ setServer(server) {
+ this._log.trace("setServer", server);
+ this._server = server;
+ },
+
+ /**
+ * Clear out unpersisted, yet to be sent, pings and cancel outgoing ping requests.
+ */
+ async clearCurrentPings() {
+ if (this._shutdown) {
+ this._log.trace("clearCurrentPings - in shutdown, bailing out");
+ return;
+ }
+
+ // Temporarily disable the scheduler. It must not try to reschedule ping sending
+ // while we're deleting them.
+ await SendScheduler.shutdown();
+
+ // Now that the ping activity has settled, abort outstanding ping requests.
+ this._cancelOutgoingRequests();
+
+ // Also, purge current pings.
+ this._currentPings.clear();
+
+ // We might have been interrupted and shutdown could have been started.
+ // We need to bail out in that case to avoid triggering send activity etc.
+ // at unexpected times.
+ if (this._shutdown) {
+ this._log.trace(
+ "clearCurrentPings - in shutdown, not spinning SendScheduler up again"
+ );
+ return;
+ }
+
+ // Enable the scheduler again and spin the send task.
+ SendScheduler.start();
+ SendScheduler.triggerSendingPings(true);
+ },
+
+ _cancelOutgoingRequests() {
+ // Abort any pending ping XHRs.
+ for (let [id, request] of this._pendingPingRequests) {
+ this._log.trace(
+ "_cancelOutgoingRequests - aborting ping request for id " + id
+ );
+ try {
+ request.abort();
+ } catch (e) {
+ this._log.error(
+ "_cancelOutgoingRequests - failed to abort request for id " + id,
+ e
+ );
+ }
+ }
+ this._pendingPingRequests.clear();
+ },
+
+ sendPings(currentPings, persistedPingIds) {
+ let pingSends = [];
+
+ // Prioritize health pings to enable low-latency monitoring.
+ currentPings = [
+ ...currentPings.filter(ping => ping.type === "health"),
+ ...currentPings.filter(ping => ping.type !== "health"),
+ ];
+
+ for (let current of currentPings) {
+ let ping = current;
+ let p = (async () => {
+ try {
+ await this._doPing(ping, ping.id, false);
+ } catch (ex) {
+ this._log.info(
+ "sendPings - ping " + ping.id + " not sent, saving to disk",
+ ex
+ );
+ await savePing(ping);
+ } finally {
+ this._currentPings.delete(ping.id);
+ }
+ })();
+
+ this._trackPendingPingTask(p);
+ pingSends.push(p);
+ }
+
+ if (persistedPingIds.length) {
+ pingSends.push(
+ this._sendPersistedPings(persistedPingIds).catch(ex => {
+ this._log.info("sendPings - persisted pings not sent", ex);
+ })
+ );
+ }
+
+ return Promise.all(pingSends);
+ },
+
+ /**
+ * Send the persisted pings to the server.
+ *
+ * @param {Array<string>} List of ping ids that should be sent.
+ *
+ * @return Promise A promise that is resolved when all pings finished sending or failed.
+ */
+ async _sendPersistedPings(pingIds) {
+ this._log.trace("sendPersistedPings");
+
+ if (this.pendingPingCount < 1) {
+ this._log.trace("_sendPersistedPings - no pings to send");
+ return;
+ }
+
+ if (pingIds.length < 1) {
+ this._log.trace("sendPersistedPings - no pings to send");
+ return;
+ }
+
+ // We can send now.
+ // If there are any send failures, _doPing() sets up handlers that e.g. trigger backoff timer behavior.
+ this._log.trace(
+ "sendPersistedPings - sending " + pingIds.length + " pings"
+ );
+ let pingSendPromises = [];
+ for (let pingId of pingIds) {
+ const id = pingId;
+ pingSendPromises.push(
+ lazy.TelemetryStorage.loadPendingPing(id)
+ .then(data => this._doPing(data, id, true))
+ .catch(e =>
+ this._log.error("sendPersistedPings - failed to send ping " + id, e)
+ )
+ );
+ }
+
+ let promise = Promise.all(pingSendPromises);
+ this._trackPendingPingTask(promise);
+ await promise;
+ },
+
+ _onPingRequestFinished(success, startTime, id, isPersisted) {
+ this._log.trace(
+ "_onPingRequestFinished - success: " +
+ success +
+ ", persisted: " +
+ isPersisted
+ );
+
+ let sendId = success ? "TELEMETRY_SEND_SUCCESS" : "TELEMETRY_SEND_FAILURE";
+ let hsend = Services.telemetry.getHistogramById(sendId);
+ let hsuccess = Services.telemetry.getHistogramById("TELEMETRY_SUCCESS");
+
+ hsend.add(Utils.monotonicNow() - startTime);
+ hsuccess.add(success);
+
+ if (!success) {
+ // Let the scheduler know about send failures for triggering backoff timeouts.
+ SendScheduler.notifySendsFailed();
+ }
+
+ if (success && isPersisted) {
+ return lazy.TelemetryStorage.removePendingPing(id);
+ }
+ return Promise.resolve();
+ },
+
+ _buildSubmissionURL(ping) {
+ const version = isV4PingFormat(ping)
+ ? AppConstants.TELEMETRY_PING_FORMAT_VERSION
+ : 1;
+ return this._server + this._getSubmissionPath(ping) + "?v=" + version;
+ },
+
+ _getSubmissionPath(ping) {
+ // The new ping format contains an "application" section, the old one doesn't.
+ let pathComponents;
+ if (isV4PingFormat(ping)) {
+ // We insert the Ping id in the URL to simplify server handling of duplicated
+ // pings.
+ let app = ping.application;
+ pathComponents = [
+ ping.id,
+ ping.type,
+ app.name,
+ app.version,
+ app.channel,
+ app.buildId,
+ ];
+ } else {
+ // This is a ping in the old format.
+ if (!("slug" in ping)) {
+ // That's odd, we don't have a slug. Generate one so that TelemetryStorage.sys.mjs works.
+ ping.slug = Utils.generateUUID();
+ }
+
+ // Do we have enough info to build a submission URL?
+ let payload = "payload" in ping ? ping.payload : null;
+ if (payload && "info" in payload) {
+ let info = ping.payload.info;
+ pathComponents = [
+ ping.slug,
+ info.reason,
+ info.appName,
+ info.appVersion,
+ info.appUpdateChannel,
+ info.appBuildID,
+ ];
+ } else {
+ // Only use the UUID as the slug.
+ pathComponents = [ping.slug];
+ }
+ }
+
+ let slug = pathComponents.join("/");
+ return "/submit/telemetry/" + slug;
+ },
+
+ _doPingRequest(ping, id, url, options, errorHandler, onloadHandler) {
+ // Don't send cookies with these requests.
+ let request = new ServiceRequest({ mozAnon: true });
+ request.mozBackgroundRequest = true;
+ request.timeout = Policy.pingSubmissionTimeout();
+
+ request.open("POST", url, options);
+ request.overrideMimeType("text/plain");
+ request.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
+ request.setRequestHeader("Date", Policy.now().toUTCString());
+ request.setRequestHeader("Content-Encoding", "gzip");
+ request.onerror = errorHandler;
+ request.ontimeout = errorHandler;
+ request.onabort = errorHandler;
+ request.onload = onloadHandler;
+ this._pendingPingRequests.set(id, request);
+
+ let startTime = Utils.monotonicNow();
+
+ // If that's a legacy ping format, just send its payload.
+ let networkPayload = isV4PingFormat(ping) ? ping : ping.payload;
+
+ const utf8Payload = new TextEncoder().encode(
+ JSON.stringify(networkPayload)
+ );
+
+ Services.telemetry
+ .getHistogramById("TELEMETRY_STRINGIFY")
+ .add(Utils.monotonicNow() - startTime);
+
+ let payloadStream = Cc[
+ "@mozilla.org/io/string-input-stream;1"
+ ].createInstance(Ci.nsIStringInputStream);
+ startTime = Utils.monotonicNow();
+ payloadStream.data = Policy.gzipCompressString(arrayToString(utf8Payload));
+
+ // Check the size and drop pings which are too big.
+ const compressedPingSizeBytes = payloadStream.data.length;
+ if (compressedPingSizeBytes > lazy.TelemetryStorage.MAXIMUM_PING_SIZE) {
+ this._log.error(
+ "_doPing - submitted ping exceeds the size limit, size: " +
+ compressedPingSizeBytes
+ );
+ Services.telemetry
+ .getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_SEND")
+ .add();
+ Services.telemetry
+ .getHistogramById("TELEMETRY_DISCARDED_SEND_PINGS_SIZE_MB")
+ .add(Math.floor(compressedPingSizeBytes / 1024 / 1024));
+ // We don't need to call |request.abort()| as it was not sent yet.
+ this._pendingPingRequests.delete(id);
+
+ lazy.TelemetryHealthPing.recordDiscardedPing(ping.type);
+ return { promise: lazy.TelemetryStorage.removePendingPing(id) };
+ }
+
+ Services.telemetry
+ .getHistogramById("TELEMETRY_COMPRESS")
+ .add(Utils.monotonicNow() - startTime);
+ request.sendInputStream(payloadStream);
+
+ return { payloadStream };
+ },
+
+ _doPing(ping, id, isPersisted) {
+ if (!this.sendingEnabled(ping)) {
+ // We can't send the pings to the server, so don't try to.
+ this._log.trace("_doPing - Can't send ping " + ping.id);
+ return Promise.resolve();
+ }
+
+ if (this._tooLateToSend) {
+ // Too late to send now. Reject so we pend the ping to send it next time.
+ this._log.trace("_doPing - Too late to send ping " + ping.id);
+ Services.telemetry
+ .getHistogramById("TELEMETRY_SEND_FAILURE_TYPE")
+ .add("eTooLate");
+ Services.telemetry
+ .getKeyedHistogramById("TELEMETRY_SEND_FAILURE_TYPE_PER_PING")
+ .add(ping.type, "eTooLate");
+ return Promise.reject();
+ }
+
+ this._log.trace(
+ "_doPing - server: " +
+ this._server +
+ ", persisted: " +
+ isPersisted +
+ ", id: " +
+ id
+ );
+
+ const url = this._buildSubmissionURL(ping);
+
+ const monotonicStartTime = Utils.monotonicNow();
+ let deferred = Promise.withResolvers();
+
+ let onRequestFinished = (success, event) => {
+ let onCompletion = () => {
+ if (success) {
+ deferred.resolve();
+ } else {
+ deferred.reject(event);
+ }
+ };
+
+ this._pendingPingRequests.delete(id);
+ this._onPingRequestFinished(
+ success,
+ monotonicStartTime,
+ id,
+ isPersisted
+ ).then(
+ () => onCompletion(),
+ error => {
+ this._log.error(
+ "_doPing - request success: " + success + ", error: " + error
+ );
+ onCompletion();
+ }
+ );
+ };
+
+ let retryRequest = request => {
+ if (
+ this._shutdown ||
+ ServiceRequest.isOffline ||
+ Services.startup.shuttingDown ||
+ !request.bypassProxyEnabled ||
+ this._tooLateToSend ||
+ request.bypassProxy ||
+ !request.isProxied
+ ) {
+ return false;
+ }
+ ServiceRequest.logProxySource(request.channel, "telemetry.send");
+ // If the request failed, and it's using a proxy, automatically
+ // attempt without proxy.
+ let { payloadStream } = this._doPingRequest(
+ ping,
+ id,
+ url,
+ { bypassProxy: true },
+ errorHandler,
+ onloadHandler
+ );
+ this.payloadStream = payloadStream;
+ return true;
+ };
+
+ let errorHandler = event => {
+ let request = event.target;
+ if (retryRequest(request)) {
+ return;
+ }
+
+ let failure = event.type;
+ if (failure === "error") {
+ failure = XHR_ERROR_TYPE[request.errorCode];
+ }
+
+ lazy.TelemetryHealthPing.recordSendFailure(failure);
+
+ Services.telemetry
+ .getHistogramById("TELEMETRY_SEND_FAILURE_TYPE")
+ .add(failure);
+ Services.telemetry
+ .getKeyedHistogramById("TELEMETRY_SEND_FAILURE_TYPE_PER_PING")
+ .add(ping.type, failure);
+
+ this._log.error(
+ "_doPing - error making request to " + url + ": " + failure
+ );
+ onRequestFinished(false, event);
+ };
+
+ let onloadHandler = event => {
+ let request = event.target;
+ let status = request.status;
+ let statusClass = status - (status % 100);
+ let success = false;
+
+ if (statusClass === 200) {
+ // We can treat all 2XX as success.
+ this._log.info("_doPing - successfully loaded, status: " + status);
+ success = true;
+ } else if (statusClass === 400) {
+ // 4XX means that something with the request was broken.
+ this._log.error(
+ "_doPing - error submitting to " +
+ url +
+ ", status: " +
+ status +
+ " - ping request broken?"
+ );
+ Services.telemetry
+ .getHistogramById("TELEMETRY_PING_EVICTED_FOR_SERVER_ERRORS")
+ .add();
+ // TODO: we should handle this better, but for now we should avoid resubmitting
+ // broken requests by pretending success.
+ success = true;
+ } else if (statusClass === 500) {
+ // 5XX means there was a server-side error and we should try again later.
+ this._log.error(
+ "_doPing - error submitting to " +
+ url +
+ ", status: " +
+ status +
+ " - server error, should retry later"
+ );
+ } else {
+ // We received an unexpected status code.
+ this._log.error(
+ "_doPing - error submitting to " +
+ url +
+ ", status: " +
+ status +
+ ", type: " +
+ event.type
+ );
+ }
+ if (!success && retryRequest(request)) {
+ return;
+ }
+
+ onRequestFinished(success, event);
+ };
+
+ let { payloadStream, promise } = this._doPingRequest(
+ ping,
+ id,
+ url,
+ {},
+ errorHandler,
+ onloadHandler
+ );
+ if (promise) {
+ return promise;
+ }
+ this.payloadStream = payloadStream;
+
+ return deferred.promise;
+ },
+
+ /**
+ * Check if sending is temporarily disabled.
+ * @return {Boolean} True if we can send pings to the server right now, false if
+ * sending is temporarily disabled.
+ */
+ get canSendNow() {
+ // If the reporting policy was not accepted yet, don't send pings.
+ if (!lazy.TelemetryReportingPolicy.canUpload()) {
+ return false;
+ }
+
+ return this._sendingEnabled;
+ },
+
+ /**
+ * Check if sending is disabled. If Telemetry is not allowed to upload,
+ * pings are not sent to the server.
+ * If trying to send a "deletion-request" ping, don't block it.
+ * If unified telemetry is off, don't send pings if Telemetry is disabled.
+ *
+ * @param {Object} [ping=null] A ping to be checked.
+ * @return {Boolean} True if pings can be send to the servers, false otherwise.
+ */
+ sendingEnabled(ping = null) {
+ // We only send pings from official builds, but allow overriding this for tests.
+ if (
+ !Services.telemetry.isOfficialTelemetry &&
+ !this._testMode &&
+ !this._overrideOfficialCheck
+ ) {
+ return false;
+ }
+
+ // With unified Telemetry, the FHR upload setting controls whether we can send pings.
+ // The Telemetry pref enables sending extended data sets instead.
+ if (IS_UNIFIED_TELEMETRY) {
+ // "deletion-request" pings are sent once even if the upload is disabled.
+ if (ping && isDeletionRequestPing(ping)) {
+ return true;
+ }
+ return Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ false
+ );
+ }
+
+ // Without unified Telemetry, the Telemetry enabled pref controls ping sending.
+ return Utils.isTelemetryEnabled;
+ },
+
+ /**
+ * Track any pending ping send and save tasks through the promise passed here.
+ * This is needed to block shutdown on any outstanding ping activity.
+ */
+ _trackPendingPingTask(promise) {
+ let clear = () => this._pendingPingActivity.delete(promise);
+ promise.then(clear, clear);
+ this._pendingPingActivity.add(promise);
+ },
+
+ /**
+ * Return a promise that allows to wait on pending pings.
+ * @return {Object<Promise>} A promise resolved when all the pending pings promises
+ * are resolved.
+ */
+ promisePendingPingActivity() {
+ this._log.trace("promisePendingPingActivity - Waiting for ping task");
+ let p = Array.from(this._pendingPingActivity, p =>
+ p.catch(ex => {
+ this._log.error(
+ "promisePendingPingActivity - ping activity had an error",
+ ex
+ );
+ })
+ );
+ p.push(SendScheduler.waitOnSendTask());
+ return Promise.all(p);
+ },
+
+ async _persistCurrentPings() {
+ for (let [id, ping] of this._currentPings) {
+ try {
+ await savePing(ping);
+ this._log.trace("_persistCurrentPings - saved ping " + id);
+ } catch (ex) {
+ this._log.error("_persistCurrentPings - failed to save ping " + id, ex);
+ } finally {
+ this._currentPings.delete(id);
+ }
+ }
+ },
+
+ /**
+ * Returns the current pending, not yet persisted, pings, newest first.
+ */
+ getUnpersistedPings() {
+ let current = [...this._currentPings.values()];
+ current.reverse();
+ return current;
+ },
+
+ getShutdownState() {
+ return {
+ sendingEnabled: this._sendingEnabled,
+ pendingPingRequestCount: this._pendingPingRequests.size,
+ pendingPingActivityCount: this._pendingPingActivity.size,
+ unpersistedPingCount: this._currentPings.size,
+ persistedPingCount: lazy.TelemetryStorage.getPendingPingList().length,
+ schedulerState: SendScheduler.getShutdownState(),
+ };
+ },
+
+ runPingSender(pings, observer) {
+ if (AppConstants.platform === "android") {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ }
+
+ let suppressPingsender = Services.prefs.getBoolPref(
+ "toolkit.telemetry.testing.suppressPingsender",
+ false
+ );
+ if (suppressPingsender) {
+ this._log.trace("Silently skipping pingsender call in automation");
+ return;
+ }
+
+ // By default, invoke `pingsender[.exe] URL path ...`.
+ let exeName =
+ AppConstants.platform === "win" ? "pingsender.exe" : "pingsender";
+ let params = [];
+
+ if (lazy.NimbusFeatures.pingsender.getVariable("backgroundTaskEnabled")) {
+ // If using pingsender background task, invoke `firefox[.exe] --backgroundtask pingsender URL path ...`.
+ exeName =
+ AppConstants.MOZ_APP_NAME +
+ (AppConstants.platform === "win" ? ".exe" : "");
+ params = ["--backgroundtask", "pingsender"];
+ }
+
+ this._log.info(
+ `Invoking '${exeName}${params.length ? " " + params.join(" ") : ""} ...'`
+ );
+
+ let exe = Services.dirsvc.get("GreBinD", Ci.nsIFile);
+ exe.append(exeName);
+
+ params.push(...pings.flatMap(ping => [ping.url, ping.path]));
+ let process = Cc["@mozilla.org/process/util;1"].createInstance(
+ Ci.nsIProcess
+ );
+ process.init(exe);
+ process.startHidden = true;
+ process.noShell = true;
+ process.runAsync(params, params.length, observer);
+ },
+};
diff --git a/toolkit/components/telemetry/app/TelemetryStorage.sys.mjs b/toolkit/components/telemetry/app/TelemetryStorage.sys.mjs
new file mode 100644
index 0000000000..062a050a9f
--- /dev/null
+++ b/toolkit/components/telemetry/app/TelemetryStorage.sys.mjs
@@ -0,0 +1,2208 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+import { Log } from "resource://gre/modules/Log.sys.mjs";
+import { TelemetryUtils } from "resource://gre/modules/TelemetryUtils.sys.mjs";
+
+const LOGGER_NAME = "Toolkit.Telemetry";
+const LOGGER_PREFIX = "TelemetryStorage::";
+
+const Telemetry = Services.telemetry;
+const Utils = TelemetryUtils;
+
+// Compute the path of the pings archive on the first use.
+const DATAREPORTING_DIR = "datareporting";
+const PINGS_ARCHIVE_DIR = "archived";
+const ABORTED_SESSION_FILE_NAME = "aborted-session-ping";
+const SESSION_STATE_FILE_NAME = "session-state.json";
+
+const lazy = {};
+
+ChromeUtils.defineLazyGetter(lazy, "gDataReportingDir", function () {
+ return PathUtils.join(PathUtils.profileDir, DATAREPORTING_DIR);
+});
+ChromeUtils.defineLazyGetter(lazy, "gPingsArchivePath", function () {
+ return PathUtils.join(lazy.gDataReportingDir, PINGS_ARCHIVE_DIR);
+});
+ChromeUtils.defineLazyGetter(lazy, "gAbortedSessionFilePath", function () {
+ return PathUtils.join(lazy.gDataReportingDir, ABORTED_SESSION_FILE_NAME);
+});
+ChromeUtils.defineESModuleGetters(lazy, {
+ TelemetryHealthPing: "resource://gre/modules/HealthPing.sys.mjs",
+});
+// Maxmimum time, in milliseconds, archive pings should be retained.
+const MAX_ARCHIVED_PINGS_RETENTION_MS = 60 * 24 * 60 * 60 * 1000; // 60 days
+
+// Maximum space the archive can take on disk (in Bytes).
+const ARCHIVE_QUOTA_BYTES = 120 * 1024 * 1024; // 120 MB
+// Maximum space the outgoing pings can take on disk, for Desktop (in Bytes).
+const PENDING_PINGS_QUOTA_BYTES_DESKTOP = 15 * 1024 * 1024; // 15 MB
+// Maximum space the outgoing pings can take on disk, for Mobile (in Bytes).
+const PENDING_PINGS_QUOTA_BYTES_MOBILE = 1024 * 1024; // 1 MB
+
+// The maximum size a pending/archived ping can take on disk.
+const PING_FILE_MAXIMUM_SIZE_BYTES = 1024 * 1024; // 1 MB
+
+// This special value is submitted when the archive is outside of the quota.
+const ARCHIVE_SIZE_PROBE_SPECIAL_VALUE = 300;
+
+// This special value is submitted when the pending pings is outside of the quota, as
+// we don't know the size of the pings above the quota.
+const PENDING_PINGS_SIZE_PROBE_SPECIAL_VALUE = 17;
+
+const UUID_REGEX =
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
+
+/**
+ * This is thrown by |TelemetryStorage.loadPingFile| when reading the ping
+ * from the disk fails.
+ */
+function PingReadError(
+ message = "Error reading the ping file",
+ becauseNoSuchFile = false
+) {
+ Error.call(this, message);
+ let error = new Error();
+ this.name = "PingReadError";
+ this.message = message;
+ this.stack = error.stack;
+ this.becauseNoSuchFile = becauseNoSuchFile;
+}
+PingReadError.prototype = Object.create(Error.prototype);
+PingReadError.prototype.constructor = PingReadError;
+
+/**
+ * This is thrown by |TelemetryStorage.loadPingFile| when parsing the ping JSON
+ * content fails.
+ */
+function PingParseError(message = "Error parsing ping content") {
+ Error.call(this, message);
+ let error = new Error();
+ this.name = "PingParseError";
+ this.message = message;
+ this.stack = error.stack;
+}
+PingParseError.prototype = Object.create(Error.prototype);
+PingParseError.prototype.constructor = PingParseError;
+
+/**
+ * This is a policy object used to override behavior for testing.
+ */
+export var Policy = {
+ now: () => new Date(),
+ getArchiveQuota: () => ARCHIVE_QUOTA_BYTES,
+ getPendingPingsQuota: () =>
+ AppConstants.platform == "android"
+ ? PENDING_PINGS_QUOTA_BYTES_MOBILE
+ : PENDING_PINGS_QUOTA_BYTES_DESKTOP,
+ /**
+ * @param {string} id The ID of the ping that will be written into the file. Can be "*" to
+ * make a pattern to find all pings for this installation.
+ * @return
+ * {
+ * directory: <nsIFile>, // Directory to save pings
+ * file: <string>, // File name for this ping (or pattern for all pings)
+ * }
+ */
+ getUninstallPingPath: id => {
+ // UpdRootD is e.g. C:\ProgramData\Mozilla\updates\<PATH HASH>
+ const updateDirectory = Services.dirsvc.get("UpdRootD", Ci.nsIFile);
+ const installPathHash = updateDirectory.leafName;
+
+ return {
+ // e.g. C:\ProgramData\Mozilla
+ directory: updateDirectory.parent.parent.clone(),
+ file: `uninstall_ping_${installPathHash}_${id}.json`,
+ };
+ },
+};
+
+/**
+ * Wait for all promises in iterable to resolve or reject. This function
+ * always resolves its promise with undefined, and never rejects.
+ */
+function waitForAll(it) {
+ let dummy = () => {};
+ let promises = Array.from(it, p => p.catch(dummy));
+ return Promise.all(promises);
+}
+
+/**
+ * Permanently intern the given string. This is mainly used for the ping.type
+ * strings that can be excessively duplicated in the _archivedPings map. Do not
+ * pass large or temporary strings to this function.
+ */
+function internString(str) {
+ return Symbol.keyFor(Symbol.for(str));
+}
+
+export var TelemetryStorage = {
+ get pingDirectoryPath() {
+ return PathUtils.join(PathUtils.profileDir, "saved-telemetry-pings");
+ },
+
+ /**
+ * The maximum size a ping can have, in bytes.
+ */
+ get MAXIMUM_PING_SIZE() {
+ return PING_FILE_MAXIMUM_SIZE_BYTES;
+ },
+ /**
+ * Shutdown & block on any outstanding async activity in this module.
+ *
+ * @return {Promise} Promise that is resolved when shutdown is complete.
+ */
+ shutdown() {
+ return TelemetryStorageImpl.shutdown();
+ },
+
+ /**
+ * Save an archived ping to disk.
+ *
+ * @param {object} ping The ping data to archive.
+ * @return {promise} Promise that is resolved when the ping is successfully archived.
+ */
+ saveArchivedPing(ping) {
+ return TelemetryStorageImpl.saveArchivedPing(ping);
+ },
+
+ /**
+ * Load an archived ping from disk.
+ *
+ * @param {string} id The pings id.
+ * @return {promise<object>} Promise that is resolved with the ping data.
+ */
+ loadArchivedPing(id) {
+ return TelemetryStorageImpl.loadArchivedPing(id);
+ },
+
+ /**
+ * Get a list of info on the archived pings.
+ * This will scan the archive directory and grab basic data about the existing
+ * pings out of their filename.
+ *
+ * @return {promise<sequence<object>>}
+ */
+ loadArchivedPingList() {
+ return TelemetryStorageImpl.loadArchivedPingList();
+ },
+
+ /**
+ * Clean the pings archive by removing old pings.
+ * This will scan the archive directory.
+ *
+ * @return {Promise} Resolved when the cleanup task completes.
+ */
+ runCleanPingArchiveTask() {
+ return TelemetryStorageImpl.runCleanPingArchiveTask();
+ },
+
+ /**
+ * Run the task to enforce the pending pings quota.
+ *
+ * @return {Promise} Resolved when the cleanup task completes.
+ */
+ runEnforcePendingPingsQuotaTask() {
+ return TelemetryStorageImpl.runEnforcePendingPingsQuotaTask();
+ },
+
+ /**
+ * Run the task to remove all the pending pings
+ *
+ * @return {Promise} Resolved when the pings are removed.
+ */
+ runRemovePendingPingsTask() {
+ return TelemetryStorageImpl.runRemovePendingPingsTask();
+ },
+
+ /**
+ * Remove all pings that are stored in the userApplicationDataDir
+ * under the "Pending Pings" sub-directory.
+ */
+ removeAppDataPings() {
+ return TelemetryStorageImpl.removeAppDataPings();
+ },
+
+ /**
+ * Reset the storage state in tests.
+ */
+ reset() {
+ return TelemetryStorageImpl.reset();
+ },
+
+ /**
+ * Test method that allows waiting on the archive clean task to finish.
+ */
+ testCleanupTaskPromise() {
+ return TelemetryStorageImpl._cleanArchiveTask || Promise.resolve();
+ },
+
+ /**
+ * Test method that allows waiting on the pending pings quota task to finish.
+ */
+ testPendingQuotaTaskPromise() {
+ return (
+ TelemetryStorageImpl._enforcePendingPingsQuotaTask || Promise.resolve()
+ );
+ },
+
+ /**
+ * Save a pending - outgoing - ping to disk and track it.
+ *
+ * @param {Object} ping The ping data.
+ * @return {Promise} Resolved when the ping was saved.
+ */
+ savePendingPing(ping) {
+ return TelemetryStorageImpl.savePendingPing(ping);
+ },
+
+ /**
+ * Saves session data to disk.
+ * @param {Object} sessionData The session data.
+ * @return {Promise} Resolved when the data was saved.
+ */
+ saveSessionData(sessionData) {
+ return TelemetryStorageImpl.saveSessionData(sessionData);
+ },
+
+ /**
+ * Loads session data from a session data file.
+ * @return {Promise<object>} Resolved with the session data in object form.
+ */
+ loadSessionData() {
+ return TelemetryStorageImpl.loadSessionData();
+ },
+
+ /**
+ * Load a pending ping from disk by id.
+ *
+ * @param {String} id The pings id.
+ * @return {Promise} Resolved with the loaded ping data.
+ */
+ loadPendingPing(id) {
+ return TelemetryStorageImpl.loadPendingPing(id);
+ },
+
+ /**
+ * Remove a pending ping from disk by id.
+ *
+ * @param {String} id The pings id.
+ * @return {Promise} Resolved when the ping was removed.
+ */
+ removePendingPing(id) {
+ return TelemetryStorageImpl.removePendingPing(id);
+ },
+
+ /**
+ * Returns a list of the currently pending pings in the format:
+ * {
+ * id: <string>, // The pings UUID.
+ * lastModified: <number>, // Timestamp of the pings last modification.
+ * }
+ * This populates the list by scanning the disk.
+ *
+ * @return {Promise<sequence>} Resolved with the ping list.
+ */
+ loadPendingPingList() {
+ return TelemetryStorageImpl.loadPendingPingList();
+ },
+
+ /**
+ * Returns a list of the currently pending pings in the format:
+ * {
+ * id: <string>, // The pings UUID.
+ * lastModified: <number>, // Timestamp of the pings last modification.
+ * }
+ * This does not scan pending pings on disk.
+ *
+ * @return {sequence} The current pending ping list.
+ */
+ getPendingPingList() {
+ return TelemetryStorageImpl.getPendingPingList();
+ },
+
+ /**
+ * Save an aborted-session ping to disk. This goes to a special location so
+ * it is not picked up as a pending ping.
+ *
+ * @param {object} ping The ping data to save.
+ * @return {promise} Promise that is resolved when the ping is successfully saved.
+ */
+ saveAbortedSessionPing(ping) {
+ return TelemetryStorageImpl.saveAbortedSessionPing(ping);
+ },
+
+ /**
+ * Load the aborted-session ping from disk if present.
+ *
+ * @return {promise<object>} Promise that is resolved with the ping data if found.
+ * Otherwise returns null.
+ */
+ loadAbortedSessionPing() {
+ return TelemetryStorageImpl.loadAbortedSessionPing();
+ },
+
+ /**
+ * Remove the aborted-session ping if present.
+ *
+ * @return {promise} Promise that is resolved once the ping is removed.
+ */
+ removeAbortedSessionPing() {
+ return TelemetryStorageImpl.removeAbortedSessionPing();
+ },
+
+ /**
+ * Save an uninstall ping to disk, removing any old ones from this
+ * installation first.
+ * This is stored independently from other pings, and only read by
+ * the Windows uninstaller.
+ *
+ * WINDOWS ONLY, does nothing and resolves immediately on other platforms.
+ *
+ * @return {promise} Promise that is resolved when the ping has been saved.
+ */
+ saveUninstallPing(ping) {
+ return TelemetryStorageImpl.saveUninstallPing(ping);
+ },
+
+ /**
+ * Remove all uninstall pings from this installation.
+ *
+ * WINDOWS ONLY, does nothing and resolves immediately on other platforms.
+ *
+ * @return {promise} Promise that is resolved when the pings have been removed.
+ */
+ removeUninstallPings() {
+ return TelemetryStorageImpl.removeUninstallPings();
+ },
+
+ /**
+ * Save a single ping to a file.
+ *
+ * @param {object} ping The content of the ping to save.
+ * @param {string} file The destination file.
+ * @param {bool} overwrite If |true|, the file will be overwritten if it exists,
+ * if |false| the file will not be overwritten and no error will be reported if
+ * the file exists.
+ * @returns {promise}
+ */
+ savePingToFile(ping, file, overwrite) {
+ return TelemetryStorageImpl.savePingToFile(ping, file, overwrite);
+ },
+
+ /**
+ * Save a ping to its file.
+ *
+ * @param {object} ping The content of the ping to save.
+ * @param {bool} overwrite If |true|, the file will be overwritten
+ * if it exists.
+ * @returns {promise}
+ */
+ savePing(ping, overwrite) {
+ return TelemetryStorageImpl.savePing(ping, overwrite);
+ },
+
+ /**
+ * Remove the file for a ping
+ *
+ * @param {object} ping The ping.
+ * @returns {promise}
+ */
+ cleanupPingFile(ping) {
+ return TelemetryStorageImpl.cleanupPingFile(ping);
+ },
+
+ /**
+ * Loads a ping file.
+ * @param {String} aFilePath The path of the ping file.
+ * @return {Promise<Object>} A promise resolved with the ping content or rejected if the
+ * ping contains invalid data.
+ */
+ async loadPingFile(aFilePath) {
+ return TelemetryStorageImpl.loadPingFile(aFilePath);
+ },
+
+ /**
+ * Remove FHR database files. This is temporary and will be dropped in
+ * the future.
+ * @return {Promise} Resolved when the database files are deleted.
+ */
+ removeFHRDatabase() {
+ return TelemetryStorageImpl.removeFHRDatabase();
+ },
+
+ /**
+ * Only used in tests, builds an archived ping path from the ping metadata.
+ * @param {String} aPingId The ping id.
+ * @param {Object} aDate The ping creation date.
+ * @param {String} aType The ping type.
+ * @return {String} The full path to the archived ping.
+ */
+ _testGetArchivedPingPath(aPingId, aDate, aType) {
+ return getArchivedPingPath(aPingId, aDate, aType);
+ },
+
+ /**
+ * Only used in tests, this helper extracts ping metadata from a given filename.
+ *
+ * @param fileName {String} The filename.
+ * @return {Object} Null if the filename didn't match the expected form.
+ * Otherwise an object with the extracted data in the form:
+ * { timestamp: <number>,
+ * id: <string>,
+ * type: <string> }
+ */
+ _testGetArchivedPingDataFromFileName(aFileName) {
+ return TelemetryStorageImpl._getArchivedPingDataFromFileName(aFileName);
+ },
+
+ /**
+ * Only used in tests, this helper allows cleaning up the pending ping storage.
+ */
+ testClearPendingPings() {
+ return TelemetryStorageImpl.runRemovePendingPingsTask();
+ },
+};
+
+/**
+ * This object allows the serialisation of asynchronous tasks. This is particularly
+ * useful to serialise write access to the disk in order to prevent race conditions
+ * to corrupt the data being written.
+ * We are using this to synchronize saving to the file that TelemetrySession persists
+ * its state in.
+ */
+function SaveSerializer() {
+ this._queuedOperations = [];
+ this._queuedInProgress = false;
+ this._log = Log.repository.getLoggerWithMessagePrefix(
+ LOGGER_NAME,
+ LOGGER_PREFIX
+ );
+}
+
+SaveSerializer.prototype = {
+ /**
+ * Enqueues an operation to a list to serialise their execution in order to prevent race
+ * conditions. Useful to serialise access to disk.
+ *
+ * @param {Function} aFunction The task function to enqueue. It must return a promise.
+ * @return {Promise} A promise resolved when the enqueued task completes.
+ */
+ enqueueTask(aFunction) {
+ let promise = new Promise((resolve, reject) =>
+ this._queuedOperations.push([aFunction, resolve, reject])
+ );
+
+ if (this._queuedOperations.length == 1) {
+ this._popAndPerformQueuedOperation();
+ }
+ return promise;
+ },
+
+ /**
+ * Make sure to flush all the pending operations.
+ * @return {Promise} A promise resolved when all the pending operations have completed.
+ */
+ flushTasks() {
+ let dummyTask = () => new Promise(resolve => resolve());
+ return this.enqueueTask(dummyTask);
+ },
+
+ /**
+ * Pop a task from the queue, executes it and continue to the next one.
+ * This function recursively pops all the tasks.
+ */
+ _popAndPerformQueuedOperation() {
+ if (!this._queuedOperations.length || this._queuedInProgress) {
+ return;
+ }
+
+ this._log.trace(
+ "_popAndPerformQueuedOperation - Performing queued operation."
+ );
+ let [func, resolve, reject] = this._queuedOperations.shift();
+ let promise;
+
+ try {
+ this._queuedInProgress = true;
+ promise = func();
+ } catch (ex) {
+ this._log.warn(
+ "_popAndPerformQueuedOperation - Queued operation threw during execution. ",
+ ex
+ );
+ this._queuedInProgress = false;
+ reject(ex);
+ this._popAndPerformQueuedOperation();
+ return;
+ }
+
+ if (!promise || typeof promise.then != "function") {
+ let msg = "Queued operation did not return a promise: " + func;
+ this._log.warn("_popAndPerformQueuedOperation - " + msg);
+
+ this._queuedInProgress = false;
+ reject(new Error(msg));
+ this._popAndPerformQueuedOperation();
+ return;
+ }
+
+ promise.then(
+ result => {
+ this._queuedInProgress = false;
+ resolve(result);
+ this._popAndPerformQueuedOperation();
+ },
+ error => {
+ this._log.warn(
+ "_popAndPerformQueuedOperation - Failure when performing queued operation.",
+ error
+ );
+ this._queuedInProgress = false;
+ reject(error);
+ this._popAndPerformQueuedOperation();
+ }
+ );
+ },
+};
+
+var TelemetryStorageImpl = {
+ _logger: null,
+ // Used to serialize aborted session ping writes to disk.
+ _abortedSessionSerializer: new SaveSerializer(),
+ // Used to serialize session state writes to disk.
+ _stateSaveSerializer: new SaveSerializer(),
+
+ // Tracks the archived pings in a Map of (id -> {timestampCreated, type}).
+ // We use this to cache info on archived pings to avoid scanning the disk more than once.
+ _archivedPings: new Map(),
+ // A set of promises for pings currently being archived
+ _activelyArchiving: new Set(),
+ // Track the archive loading task to prevent multiple tasks from being executed.
+ _scanArchiveTask: null,
+ // Track the archive cleanup task.
+ _cleanArchiveTask: null,
+ // Whether we already scanned the archived pings on disk.
+ _scannedArchiveDirectory: false,
+
+ // Track the pending ping removal task.
+ _removePendingPingsTask: null,
+
+ // This tracks all the pending async ping save activity.
+ _activePendingPingSaves: new Set(),
+
+ // Tracks the pending pings in a Map of (id -> {timestampCreated, type}).
+ // We use this to cache info on pending pings to avoid scanning the disk more than once.
+ _pendingPings: new Map(),
+
+ // Track the pending pings enforce quota task.
+ _enforcePendingPingsQuotaTask: null,
+
+ // Track the shutdown process to bail out of the clean up task quickly.
+ _shutdown: false,
+
+ get _log() {
+ if (!this._logger) {
+ this._logger = Log.repository.getLoggerWithMessagePrefix(
+ LOGGER_NAME,
+ LOGGER_PREFIX
+ );
+ }
+
+ return this._logger;
+ },
+
+ /**
+ * Shutdown & block on any outstanding async activity in this module.
+ *
+ * @return {Promise} Promise that is resolved when shutdown is complete.
+ */
+ async shutdown() {
+ this._shutdown = true;
+
+ // If the following tasks are still running, block on them. They will bail out as soon
+ // as possible.
+ await this._abortedSessionSerializer.flushTasks().catch(ex => {
+ this._log.error("shutdown - failed to flush aborted-session writes", ex);
+ });
+
+ if (this._cleanArchiveTask) {
+ await this._cleanArchiveTask.catch(ex => {
+ this._log.error("shutdown - the archive cleaning task failed", ex);
+ });
+ }
+
+ if (this._enforcePendingPingsQuotaTask) {
+ await this._enforcePendingPingsQuotaTask.catch(ex => {
+ this._log.error("shutdown - the pending pings quota task failed", ex);
+ });
+ }
+
+ if (this._removePendingPingsTask) {
+ await this._removePendingPingsTask.catch(ex => {
+ this._log.error("shutdown - the pending pings removal task failed", ex);
+ });
+ }
+
+ // Wait on pending pings still being saved. While IOUtils should have shutdown
+ // blockers in place, we a) have seen weird errors being reported that might
+ // indicate a bad shutdown path and b) might have completion handlers hanging
+ // off the save operations that don't expect to be late in shutdown.
+ await this.promisePendingPingSaves();
+ },
+
+ /**
+ * Save an archived ping to disk.
+ *
+ * @param {object} ping The ping data to archive.
+ * @return {promise} Promise that is resolved when the ping is successfully archived.
+ */
+ saveArchivedPing(ping) {
+ let promise = this._saveArchivedPingTask(ping);
+ this._activelyArchiving.add(promise);
+ promise.then(
+ r => {
+ this._activelyArchiving.delete(promise);
+ },
+ e => {
+ this._activelyArchiving.delete(promise);
+ }
+ );
+ return promise;
+ },
+
+ async _saveArchivedPingTask(ping) {
+ const creationDate = new Date(ping.creationDate);
+ if (this._archivedPings.has(ping.id)) {
+ const data = this._archivedPings.get(ping.id);
+ if (data.timestampCreated > creationDate.getTime()) {
+ this._log.error(
+ "saveArchivedPing - trying to overwrite newer ping with the same id"
+ );
+ return Promise.reject(
+ new Error("trying to overwrite newer ping with the same id")
+ );
+ }
+ this._log.warn(
+ "saveArchivedPing - overwriting older ping with the same id"
+ );
+ }
+
+ // Get the archived ping path and append the lz4 suffix to it (so we have 'jsonlz4').
+ const filePath =
+ getArchivedPingPath(ping.id, creationDate, ping.type) + "lz4";
+ await IOUtils.makeDirectory(PathUtils.parent(filePath));
+ await this.savePingToFile(
+ ping,
+ filePath,
+ /* overwrite*/ true,
+ /* compressed*/ true
+ );
+
+ this._archivedPings.set(ping.id, {
+ timestampCreated: creationDate.getTime(),
+ type: internString(ping.type),
+ });
+
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SESSION_PING_COUNT").add();
+ return undefined;
+ },
+
+ /**
+ * Load an archived ping from disk.
+ *
+ * @param {string} id The pings id.
+ * @return {promise<object>} Promise that is resolved with the ping data.
+ */
+ async loadArchivedPing(id) {
+ const data = this._archivedPings.get(id);
+ if (!data) {
+ this._log.trace("loadArchivedPing - no ping with id: " + id);
+ return Promise.reject(
+ new Error("TelemetryStorage.loadArchivedPing - no ping with id " + id)
+ );
+ }
+
+ const path = getArchivedPingPath(
+ id,
+ new Date(data.timestampCreated),
+ data.type
+ );
+ const pathCompressed = path + "lz4";
+
+ // Purge pings which are too big.
+ let checkSize = async function (path) {
+ const fileSize = await IOUtils.stat(path).then(info => info.size);
+ if (fileSize > PING_FILE_MAXIMUM_SIZE_BYTES) {
+ Telemetry.getHistogramById(
+ "TELEMETRY_DISCARDED_ARCHIVED_PINGS_SIZE_MB"
+ ).add(Math.floor(fileSize / 1024 / 1024));
+ Telemetry.getHistogramById(
+ "TELEMETRY_PING_SIZE_EXCEEDED_ARCHIVED"
+ ).add();
+ await IOUtils.remove(path, { ignoreAbsent: true });
+ throw new Error(
+ `loadArchivedPing - exceeded the maximum ping size: ${fileSize}`
+ );
+ }
+ };
+
+ let ping;
+ try {
+ // Try to load a compressed version of the archived ping first.
+ this._log.trace(
+ "loadArchivedPing - loading ping from: " + pathCompressed
+ );
+ await checkSize(pathCompressed);
+ ping = await this.loadPingFile(pathCompressed, /* compressed*/ true);
+ } catch (ex) {
+ if (!ex.becauseNoSuchFile) {
+ throw ex;
+ }
+ // If that fails, look for the uncompressed version.
+ this._log.trace(
+ "loadArchivedPing - compressed ping not found, loading: " + path
+ );
+ await checkSize(path);
+ ping = await this.loadPingFile(path, /* compressed*/ false);
+ }
+
+ return ping;
+ },
+
+ /**
+ * Saves session data to disk.
+ */
+ saveSessionData(sessionData) {
+ return this._stateSaveSerializer.enqueueTask(() =>
+ this._saveSessionData(sessionData)
+ );
+ },
+
+ async _saveSessionData(sessionData) {
+ await IOUtils.makeDirectory(lazy.gDataReportingDir, {
+ createAncestors: false,
+ });
+
+ let filePath = PathUtils.join(
+ lazy.gDataReportingDir,
+ SESSION_STATE_FILE_NAME
+ );
+ try {
+ await IOUtils.writeJSON(filePath, sessionData);
+ } catch (e) {
+ this._log.error(
+ `_saveSessionData - Failed to write session data to ${filePath}`,
+ e
+ );
+ Telemetry.getHistogramById("TELEMETRY_SESSIONDATA_FAILED_SAVE").add(1);
+ }
+ },
+
+ /**
+ * Loads session data from the session data file.
+ * @return {Promise<Object>} A promise resolved with an object on success,
+ * with null otherwise.
+ */
+ loadSessionData() {
+ return this._stateSaveSerializer.enqueueTask(() => this._loadSessionData());
+ },
+
+ async _loadSessionData() {
+ const dataFile = PathUtils.join(
+ PathUtils.profileDir,
+ DATAREPORTING_DIR,
+ SESSION_STATE_FILE_NAME
+ );
+ let content;
+ try {
+ content = await IOUtils.readUTF8(dataFile);
+ } catch (ex) {
+ this._log.info("_loadSessionData - can not load session data file", ex);
+ Telemetry.getHistogramById("TELEMETRY_SESSIONDATA_FAILED_LOAD").add(1);
+ return null;
+ }
+
+ let data;
+ try {
+ data = JSON.parse(content);
+ } catch (ex) {
+ this._log.error("_loadSessionData - failed to parse session data", ex);
+ Telemetry.getHistogramById("TELEMETRY_SESSIONDATA_FAILED_PARSE").add(1);
+ return null;
+ }
+
+ return data;
+ },
+
+ /**
+ * Remove an archived ping from disk.
+ *
+ * @param {string} id The pings id.
+ * @param {number} timestampCreated The pings creation timestamp.
+ * @param {string} type The pings type.
+ * @return {promise<object>} Promise that is resolved when the pings is removed.
+ */
+ async _removeArchivedPing(id, timestampCreated, type) {
+ this._log.trace(
+ "_removeArchivedPing - id: " +
+ id +
+ ", timestampCreated: " +
+ timestampCreated +
+ ", type: " +
+ type
+ );
+ const path = getArchivedPingPath(id, new Date(timestampCreated), type);
+ const pathCompressed = path + "lz4";
+
+ this._log.trace("_removeArchivedPing - removing ping from: " + path);
+ await IOUtils.remove(path);
+ await IOUtils.remove(pathCompressed);
+ // Remove the ping from the cache.
+ this._archivedPings.delete(id);
+ },
+
+ /**
+ * Clean the pings archive by removing old pings.
+ *
+ * @return {Promise} Resolved when the cleanup task completes.
+ */
+ runCleanPingArchiveTask() {
+ // If there's an archive cleaning task already running, return it.
+ if (this._cleanArchiveTask) {
+ return this._cleanArchiveTask;
+ }
+
+ // Make sure to clear |_cleanArchiveTask| once done.
+ let clear = () => (this._cleanArchiveTask = null);
+ // Since there's no archive cleaning task running, start it.
+ this._cleanArchiveTask = this._cleanArchive().then(clear, clear);
+ return this._cleanArchiveTask;
+ },
+
+ /**
+ * Removes pings which are too old from the pings archive.
+ * @return {Promise} Resolved when the ping age check is complete.
+ */
+ async _purgeOldPings() {
+ this._log.trace("_purgeOldPings");
+
+ const nowDate = Policy.now();
+ const startTimeStamp = nowDate.getTime();
+
+ // Keep track of the newest removed month to update the cache, if needed.
+ let newestRemovedMonthTimestamp = null;
+ let evictedDirsCount = 0;
+ let maxDirAgeInMonths = 0;
+
+ // Walk through the monthly subdirs of the form <YYYY-MM>/
+ for (const path of await IOUtils.getChildren(lazy.gPingsArchivePath)) {
+ const info = await IOUtils.stat(path);
+ if (info.type !== "directory") {
+ continue;
+ }
+
+ const name = PathUtils.filename(path);
+
+ if (this._shutdown) {
+ this._log.trace(
+ "_purgeOldPings - Terminating the clean up task due to shutdown"
+ );
+ return;
+ }
+
+ if (!isValidArchiveDir(name)) {
+ this._log.warn(
+ `_purgeOldPings - skipping invalidly named subdirectory ${path}`
+ );
+ continue;
+ }
+
+ const archiveDate = getDateFromArchiveDir(name);
+ if (!archiveDate) {
+ this._log.warn(
+ `_purgeOldPings - skipping invalid subdirectory date ${path}`
+ );
+ continue;
+ }
+
+ // If this archive directory is older than allowed, remove it.
+ if (
+ startTimeStamp - archiveDate.getTime() >
+ MAX_ARCHIVED_PINGS_RETENTION_MS
+ ) {
+ try {
+ await IOUtils.remove(path, { recursive: true });
+ evictedDirsCount++;
+
+ // Update the newest removed month.
+ newestRemovedMonthTimestamp = Math.max(
+ archiveDate,
+ newestRemovedMonthTimestamp
+ );
+ } catch (ex) {
+ this._log.error(`_purgeOldPings - Unable to remove ${path}`, ex);
+ }
+ } else {
+ // We're not removing this directory, so record the age for the oldest directory.
+ const dirAgeInMonths = Utils.getElapsedTimeInMonths(
+ archiveDate,
+ nowDate
+ );
+ maxDirAgeInMonths = Math.max(dirAgeInMonths, maxDirAgeInMonths);
+ }
+ }
+
+ // Trigger scanning of the archived pings.
+ await this.loadArchivedPingList();
+
+ // Refresh the cache: we could still skip this, but it's cheap enough to keep it
+ // to avoid introducing task dependencies.
+ if (newestRemovedMonthTimestamp) {
+ // Scan the archive cache for pings older than the newest directory pruned above.
+ for (let [id, info] of this._archivedPings) {
+ const timestampCreated = new Date(info.timestampCreated);
+ if (timestampCreated.getTime() > newestRemovedMonthTimestamp) {
+ continue;
+ }
+ // Remove outdated pings from the cache.
+ this._archivedPings.delete(id);
+ }
+ }
+
+ const endTimeStamp = Policy.now().getTime();
+
+ // Save the time it takes to evict old directories and the eviction count.
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OLD_DIRS").add(
+ evictedDirsCount
+ );
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTING_DIRS_MS").add(
+ Math.ceil(endTimeStamp - startTimeStamp)
+ );
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_OLDEST_DIRECTORY_AGE").add(
+ maxDirAgeInMonths
+ );
+ },
+
+ /**
+ * Enforce a disk quota for the pings archive.
+ * @return {Promise} Resolved when the quota check is complete.
+ */
+ async _enforceArchiveQuota() {
+ this._log.trace("_enforceArchiveQuota");
+ let startTimeStamp = Policy.now().getTime();
+
+ // Build an ordered list, from newer to older, of archived pings.
+ let pingList = Array.from(this._archivedPings, p => ({
+ id: p[0],
+ timestampCreated: p[1].timestampCreated,
+ type: p[1].type,
+ }));
+
+ pingList.sort((a, b) => b.timestampCreated - a.timestampCreated);
+
+ // If our archive is too big, we should reduce it to reach 90% of the quota.
+ const SAFE_QUOTA = Policy.getArchiveQuota() * 0.9;
+ // The index of the last ping to keep. Pings older than this one will be deleted if
+ // the archive exceeds the quota.
+ let lastPingIndexToKeep = null;
+ let archiveSizeInBytes = 0;
+
+ // Find the disk size of the archive.
+ for (let i = 0; i < pingList.length; i++) {
+ if (this._shutdown) {
+ this._log.trace(
+ "_enforceArchiveQuota - Terminating the clean up task due to shutdown"
+ );
+ return;
+ }
+
+ let ping = pingList[i];
+
+ // Get the size for this ping.
+ const fileSize = await getArchivedPingSize(
+ ping.id,
+ new Date(ping.timestampCreated),
+ ping.type
+ );
+ if (!fileSize) {
+ this._log.warn(
+ "_enforceArchiveQuota - Unable to find the size of ping " + ping.id
+ );
+ continue;
+ }
+
+ // Enforce a maximum file size limit on archived pings.
+ if (fileSize > PING_FILE_MAXIMUM_SIZE_BYTES) {
+ this._log.error(
+ "_enforceArchiveQuota - removing file exceeding size limit, size: " +
+ fileSize
+ );
+ // We just remove the ping from the disk, we don't bother removing it from pingList
+ // since it won't contribute to the quota.
+ await this._removeArchivedPing(
+ ping.id,
+ ping.timestampCreated,
+ ping.type
+ ).catch(e =>
+ this._log.error(
+ "_enforceArchiveQuota - failed to remove archived ping" + ping.id
+ )
+ );
+ Telemetry.getHistogramById(
+ "TELEMETRY_DISCARDED_ARCHIVED_PINGS_SIZE_MB"
+ ).add(Math.floor(fileSize / 1024 / 1024));
+ Telemetry.getHistogramById(
+ "TELEMETRY_PING_SIZE_EXCEEDED_ARCHIVED"
+ ).add();
+ continue;
+ }
+
+ archiveSizeInBytes += fileSize;
+
+ if (archiveSizeInBytes < SAFE_QUOTA) {
+ // We save the index of the last ping which is ok to keep in order to speed up ping
+ // pruning.
+ lastPingIndexToKeep = i;
+ } else if (archiveSizeInBytes > Policy.getArchiveQuota()) {
+ // Ouch, our ping archive is too big. Bail out and start pruning!
+ break;
+ }
+ }
+
+ // Save the time it takes to check if the archive is over-quota.
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_CHECKING_OVER_QUOTA_MS").add(
+ Math.round(Policy.now().getTime() - startTimeStamp)
+ );
+
+ let submitProbes = (sizeInMB, evictedPings, elapsedMs) => {
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SIZE_MB").add(sizeInMB);
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OVER_QUOTA").add(
+ evictedPings
+ );
+ Telemetry.getHistogramById(
+ "TELEMETRY_ARCHIVE_EVICTING_OVER_QUOTA_MS"
+ ).add(elapsedMs);
+ };
+
+ // Check if we're using too much space. If not, submit the archive size and bail out.
+ if (archiveSizeInBytes < Policy.getArchiveQuota()) {
+ submitProbes(Math.round(archiveSizeInBytes / 1024 / 1024), 0, 0);
+ return;
+ }
+
+ this._log.info(
+ "_enforceArchiveQuota - archive size: " +
+ archiveSizeInBytes +
+ "bytes" +
+ ", safety quota: " +
+ SAFE_QUOTA +
+ "bytes"
+ );
+
+ startTimeStamp = Policy.now().getTime();
+ let pingsToPurge = pingList.slice(lastPingIndexToKeep + 1);
+
+ // Remove all the pings older than the last one which we are safe to keep.
+ for (let ping of pingsToPurge) {
+ if (this._shutdown) {
+ this._log.trace(
+ "_enforceArchiveQuota - Terminating the clean up task due to shutdown"
+ );
+ return;
+ }
+
+ // This list is guaranteed to be in order, so remove the pings at its
+ // beginning (oldest).
+ await this._removeArchivedPing(ping.id, ping.timestampCreated, ping.type);
+ }
+
+ const endTimeStamp = Policy.now().getTime();
+ submitProbes(
+ ARCHIVE_SIZE_PROBE_SPECIAL_VALUE,
+ pingsToPurge.length,
+ Math.ceil(endTimeStamp - startTimeStamp)
+ );
+ },
+
+ async _cleanArchive() {
+ this._log.trace("cleanArchiveTask");
+
+ if (!(await IOUtils.exists(lazy.gPingsArchivePath))) {
+ return;
+ }
+
+ // Remove pings older than allowed.
+ try {
+ await this._purgeOldPings();
+ } catch (ex) {
+ this._log.error(
+ "_cleanArchive - There was an error removing old directories",
+ ex
+ );
+ }
+
+ // Make sure we respect the archive disk quota.
+ await this._enforceArchiveQuota();
+ },
+
+ /**
+ * Run the task to enforce the pending pings quota.
+ *
+ * @return {Promise} Resolved when the cleanup task completes.
+ */
+ async runEnforcePendingPingsQuotaTask() {
+ // If there's a cleaning task already running, return it.
+ if (this._enforcePendingPingsQuotaTask) {
+ return this._enforcePendingPingsQuotaTask;
+ }
+
+ // Since there's no quota enforcing task running, start it.
+ try {
+ this._enforcePendingPingsQuotaTask = this._enforcePendingPingsQuota();
+ await this._enforcePendingPingsQuotaTask;
+ } finally {
+ this._enforcePendingPingsQuotaTask = null;
+ }
+ return undefined;
+ },
+
+ /**
+ * Enforce a disk quota for the pending pings.
+ * @return {Promise} Resolved when the quota check is complete.
+ */
+ async _enforcePendingPingsQuota() {
+ this._log.trace("_enforcePendingPingsQuota");
+ let startTimeStamp = Policy.now().getTime();
+
+ // Build an ordered list, from newer to older, of pending pings.
+ let pingList = Array.from(this._pendingPings, p => ({
+ id: p[0],
+ lastModified: p[1].lastModified,
+ }));
+
+ pingList.sort((a, b) => b.lastModified - a.lastModified);
+
+ // If our pending pings directory is too big, we should reduce it to reach 90% of the quota.
+ const SAFE_QUOTA = Policy.getPendingPingsQuota() * 0.9;
+ // The index of the last ping to keep. Pings older than this one will be deleted if
+ // the pending pings directory size exceeds the quota.
+ let lastPingIndexToKeep = null;
+ let pendingPingsSizeInBytes = 0;
+
+ // Find the disk size of the pending pings directory.
+ for (let i = 0; i < pingList.length; i++) {
+ if (this._shutdown) {
+ this._log.trace(
+ "_enforcePendingPingsQuota - Terminating the clean up task due to shutdown"
+ );
+ return;
+ }
+
+ let ping = pingList[i];
+
+ // Get the size for this ping.
+ const fileSize = await getPendingPingSize(ping.id);
+ if (!fileSize) {
+ this._log.warn(
+ "_enforcePendingPingsQuota - Unable to find the size of ping " +
+ ping.id
+ );
+ continue;
+ }
+
+ pendingPingsSizeInBytes += fileSize;
+ if (pendingPingsSizeInBytes < SAFE_QUOTA) {
+ // We save the index of the last ping which is ok to keep in order to speed up ping
+ // pruning.
+ lastPingIndexToKeep = i;
+ } else if (pendingPingsSizeInBytes > Policy.getPendingPingsQuota()) {
+ // Ouch, our pending pings directory size is too big. Bail out and start pruning!
+ break;
+ }
+ }
+
+ // Save the time it takes to check if the pending pings are over-quota.
+ Telemetry.getHistogramById("TELEMETRY_PENDING_CHECKING_OVER_QUOTA_MS").add(
+ Math.round(Policy.now().getTime() - startTimeStamp)
+ );
+
+ let recordHistograms = (sizeInMB, evictedPings, elapsedMs) => {
+ Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_SIZE_MB").add(
+ sizeInMB
+ );
+ Telemetry.getHistogramById(
+ "TELEMETRY_PENDING_PINGS_EVICTED_OVER_QUOTA"
+ ).add(evictedPings);
+ Telemetry.getHistogramById(
+ "TELEMETRY_PENDING_EVICTING_OVER_QUOTA_MS"
+ ).add(elapsedMs);
+ };
+
+ // Check if we're using too much space. If not, bail out.
+ if (pendingPingsSizeInBytes < Policy.getPendingPingsQuota()) {
+ recordHistograms(Math.round(pendingPingsSizeInBytes / 1024 / 1024), 0, 0);
+ return;
+ }
+
+ this._log.info(
+ "_enforcePendingPingsQuota - size: " +
+ pendingPingsSizeInBytes +
+ "bytes" +
+ ", safety quota: " +
+ SAFE_QUOTA +
+ "bytes"
+ );
+
+ startTimeStamp = Policy.now().getTime();
+ let pingsToPurge = pingList.slice(lastPingIndexToKeep + 1);
+
+ // Remove all the pings older than the last one which we are safe to keep.
+ for (let ping of pingsToPurge) {
+ if (this._shutdown) {
+ this._log.trace(
+ "_enforcePendingPingsQuota - Terminating the clean up task due to shutdown"
+ );
+ return;
+ }
+
+ // This list is guaranteed to be in order, so remove the pings at its
+ // beginning (oldest).
+ await this.removePendingPing(ping.id);
+ }
+
+ const endTimeStamp = Policy.now().getTime();
+ // We don't know the size of the pending pings directory if we are above the quota,
+ // since we stop scanning once we reach the quota. We use a special value to show
+ // this condition.
+ recordHistograms(
+ PENDING_PINGS_SIZE_PROBE_SPECIAL_VALUE,
+ pingsToPurge.length,
+ Math.ceil(endTimeStamp - startTimeStamp)
+ );
+ },
+
+ /**
+ * Reset the storage state in tests.
+ */
+ reset() {
+ this._shutdown = false;
+ this._scannedArchiveDirectory = false;
+ this._archivedPings = new Map();
+ this._scannedPendingDirectory = false;
+ this._pendingPings = new Map();
+ },
+
+ /**
+ * Get a list of info on the archived pings.
+ * This will scan the archive directory and grab basic data about the existing
+ * pings out of their filename.
+ *
+ * @return {promise<sequence<object>>}
+ */
+ async loadArchivedPingList() {
+ // If there's an archive loading task already running, return it.
+ if (this._scanArchiveTask) {
+ return this._scanArchiveTask;
+ }
+
+ await waitForAll(this._activelyArchiving);
+
+ if (this._scannedArchiveDirectory) {
+ this._log.trace(
+ "loadArchivedPingList - Archive already scanned, hitting cache."
+ );
+ return this._archivedPings;
+ }
+
+ // Since there's no archive loading task running, start it.
+ let result;
+ try {
+ this._scanArchiveTask = this._scanArchive();
+ result = await this._scanArchiveTask;
+ } finally {
+ this._scanArchiveTask = null;
+ }
+ return result;
+ },
+
+ async _scanArchive() {
+ this._log.trace("_scanArchive");
+
+ let submitProbes = (pingCount, dirCount) => {
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SCAN_PING_COUNT").add(
+ pingCount
+ );
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_DIRECTORIES_COUNT").add(
+ dirCount
+ );
+ };
+
+ if (!(await IOUtils.exists(lazy.gPingsArchivePath))) {
+ submitProbes(0, 0);
+ return new Map();
+ }
+
+ let subDirCount = 0;
+ // Walk through the monthly subdirs of the form <YYYY-MM>/
+ for (const path of await IOUtils.getChildren(lazy.gPingsArchivePath)) {
+ const info = await IOUtils.stat(path);
+
+ if (info.type !== "directory") {
+ continue;
+ }
+
+ const name = PathUtils.filename(path);
+ if (!isValidArchiveDir(name)) {
+ continue;
+ }
+
+ subDirCount++;
+
+ this._log.trace(`_scanArchive - checking in subdir: ${path}`);
+ const pingPaths = [];
+ for (const ping of await IOUtils.getChildren(path)) {
+ const info = await IOUtils.stat(ping);
+ if (info.type !== "directory") {
+ pingPaths.push(ping);
+ }
+ }
+
+ // Now process any ping files of the form "<timestamp>.<uuid>.<type>.[json|jsonlz4]".
+ for (const path of pingPaths) {
+ const filename = PathUtils.filename(path);
+ // data may be null if the filename doesn't match the above format.
+ let data = this._getArchivedPingDataFromFileName(filename);
+ if (!data) {
+ continue;
+ }
+
+ // In case of conflicts, overwrite only with newer pings.
+ if (this._archivedPings.has(data.id)) {
+ const overwrite =
+ data.timestamp > this._archivedPings.get(data.id).timestampCreated;
+ this._log.warn(
+ `_scanArchive - have seen this id before: ${data.id}, overwrite: ${overwrite}`
+ );
+ if (!overwrite) {
+ continue;
+ }
+
+ await this._removeArchivedPing(
+ data.id,
+ data.timestampCreated,
+ data.type
+ ).catch(e =>
+ this._log.warn("_scanArchive - failed to remove ping", e)
+ );
+ }
+
+ this._archivedPings.set(data.id, {
+ timestampCreated: data.timestamp,
+ type: internString(data.type),
+ });
+ }
+ }
+
+ // Mark the archive as scanned, so we no longer hit the disk.
+ this._scannedArchiveDirectory = true;
+ // Update the ping and directories count histograms.
+ submitProbes(this._archivedPings.size, subDirCount);
+ return this._archivedPings;
+ },
+
+ /**
+ * Save a single ping to a file.
+ *
+ * @param {object} ping The content of the ping to save.
+ * @param {string} file The destination file.
+ * @param {bool} overwrite If |true|, the file will be overwritten if it exists,
+ * if |false| the file will not be overwritten and no error will be reported if
+ * the file exists.
+ * @param {bool} [compress=false] If |true|, the file will use lz4 compression. Otherwise no
+ * compression will be used.
+ * @returns {promise}
+ */
+ async savePingToFile(ping, filePath, overwrite, compress = false) {
+ try {
+ this._log.trace("savePingToFile - path: " + filePath);
+ await IOUtils.writeJSON(filePath, ping, {
+ compress,
+ mode: overwrite ? "overwrite" : "create",
+ tmpPath: `${filePath}.tmp`,
+ });
+ } catch (e) {
+ if (
+ !DOMException.isInstance(e) ||
+ e.name !== "NoModificationAllowedError"
+ ) {
+ throw e;
+ }
+ }
+ },
+
+ /**
+ * Save a ping to its file.
+ *
+ * @param {object} ping The content of the ping to save.
+ * @param {bool} overwrite If |true|, the file will be overwritten
+ * if it exists.
+ * @returns {promise}
+ */
+ async savePing(ping, overwrite) {
+ await getPingDirectory();
+ let file = pingFilePath(ping);
+ await this.savePingToFile(ping, file, overwrite);
+ return file;
+ },
+
+ /**
+ * Remove the file for a ping
+ *
+ * @param {object} ping The ping.
+ * @returns {promise}
+ */
+ cleanupPingFile(ping) {
+ return IOUtils.remove(pingFilePath(ping));
+ },
+
+ savePendingPing(ping) {
+ let p = this.savePing(ping, true).then(path => {
+ this._pendingPings.set(ping.id, {
+ path,
+ lastModified: Policy.now().getTime(),
+ });
+ this._log.trace("savePendingPing - saved ping with id " + ping.id);
+ });
+ this._trackPendingPingSaveTask(p);
+ return p;
+ },
+
+ async loadPendingPing(id) {
+ this._log.trace("loadPendingPing - id: " + id);
+ let info = this._pendingPings.get(id);
+ if (!info) {
+ this._log.trace("loadPendingPing - unknown id " + id);
+ throw new Error(
+ "TelemetryStorage.loadPendingPing - no ping with id " + id
+ );
+ }
+
+ // Try to get the dimension of the ping. If that fails, update the histograms.
+ let fileSize = 0;
+ try {
+ fileSize = await IOUtils.stat(info.path).then(stat => stat.size);
+ } catch (e) {
+ if (!DOMException.isInstance(e) || e.name !== "NotFoundError") {
+ throw e;
+ }
+ // Fall through and let |loadPingFile| report the error.
+ }
+
+ // Purge pings which are too big.
+ if (fileSize > PING_FILE_MAXIMUM_SIZE_BYTES) {
+ await this.removePendingPing(id);
+ Telemetry.getHistogramById(
+ "TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB"
+ ).add(Math.floor(fileSize / 1024 / 1024));
+ Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_PENDING").add();
+
+ // Currently we don't have the ping type available without loading the ping from disk.
+ // Bug 1384903 will fix that.
+ lazy.TelemetryHealthPing.recordDiscardedPing("<unknown>");
+ throw new Error(
+ "loadPendingPing - exceeded the maximum ping size: " + fileSize
+ );
+ }
+
+ // Try to load the ping file. Update the related histograms on failure.
+ let ping;
+ try {
+ ping = await this.loadPingFile(info.path, false);
+ } catch (e) {
+ // If we failed to load the ping, check what happened and update the histogram.
+ if (e instanceof PingReadError) {
+ Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_READ").add();
+ } else if (e instanceof PingParseError) {
+ Telemetry.getHistogramById(
+ "TELEMETRY_PENDING_LOAD_FAILURE_PARSE"
+ ).add();
+ }
+
+ // Remove the ping from the cache, so we don't try to load it again.
+ this._pendingPings.delete(id);
+ // Then propagate the rejection.
+ throw e;
+ }
+
+ return ping;
+ },
+
+ removePendingPing(id) {
+ let info = this._pendingPings.get(id);
+ if (!info) {
+ this._log.trace("removePendingPing - unknown id " + id);
+ return Promise.resolve();
+ }
+
+ this._log.trace(
+ "removePendingPing - deleting ping with id: " +
+ id +
+ ", path: " +
+ info.path
+ );
+ this._pendingPings.delete(id);
+ return IOUtils.remove(info.path).catch(ex =>
+ this._log.error("removePendingPing - failed to remove ping", ex)
+ );
+ },
+
+ /**
+ * Track any pending ping save tasks through the promise passed here.
+ * This is needed to block on any outstanding ping save activity.
+ *
+ * @param {Object<Promise>} The save promise to track.
+ */
+ _trackPendingPingSaveTask(promise) {
+ let clear = () => this._activePendingPingSaves.delete(promise);
+ promise.then(clear, clear);
+ this._activePendingPingSaves.add(promise);
+ },
+
+ /**
+ * Return a promise that allows to wait on pending pings being saved.
+ * @return {Object<Promise>} A promise resolved when all the pending pings save promises
+ * are resolved.
+ */
+ promisePendingPingSaves() {
+ // Make sure to wait for all the promises, even if they reject. We don't need to log
+ // the failures here, as they are already logged elsewhere.
+ return waitForAll(this._activePendingPingSaves);
+ },
+
+ /**
+ * Run the task to remove all the pending pings
+ *
+ * @return {Promise} Resolved when the pings are removed.
+ */
+ async runRemovePendingPingsTask() {
+ // If we already have a pending pings removal task active, return that.
+ if (this._removePendingPingsTask) {
+ return this._removePendingPingsTask;
+ }
+
+ // Start the task to remove all pending pings. Also make sure to clear the task once done.
+ try {
+ this._removePendingPingsTask = this.removePendingPings();
+ await this._removePendingPingsTask;
+ } finally {
+ this._removePendingPingsTask = null;
+ }
+ return undefined;
+ },
+
+ async removePendingPings() {
+ this._log.trace("removePendingPings - removing all pending pings");
+
+ // Wait on pending pings still being saved, so so we don't miss removing them.
+ await this.promisePendingPingSaves();
+
+ // Individually remove existing pings, so we don't interfere with operations expecting
+ // the pending pings directory to exist.
+ const directory = TelemetryStorage.pingDirectoryPath;
+
+ if (!(await IOUtils.exists(directory))) {
+ this._log.trace(
+ "removePendingPings - the pending pings directory doesn't exist"
+ );
+ return;
+ }
+
+ for (const path of await IOUtils.getChildren(directory)) {
+ let info;
+ try {
+ info = await IOUtils.stat(path);
+ } catch (ex) {
+ // It is possible there is another task removing a ping in between
+ // reading the directory and calling stat.
+ //
+ // On Windows, attempting to call GetFileAttributesEx() on a file
+ // pending deletion will result in ERROR_ACCESS_DENIED, which will
+ // propagate to here as a NotAllowedError.
+ if (
+ DOMException.isInstance(ex) &&
+ (ex.name === "NotFoundError" || ex.name === "NotAllowedError")
+ ) {
+ continue;
+ }
+
+ throw ex;
+ }
+
+ if (info.type === "directory") {
+ continue;
+ }
+
+ try {
+ await IOUtils.remove(path);
+ } catch (ex) {
+ this._log.error(
+ `removePendingPings - failed to remove file ${path}`,
+ ex
+ );
+ continue;
+ }
+ }
+ },
+
+ /**
+ * Iterate through all pings in the userApplicationDataDir under the "Pending Pings" sub-directory
+ * and yield each file.
+ */
+ async *_iterateAppDataPings() {
+ this._log.trace("_iterateAppDataPings");
+
+ let uAppDataDir;
+ try {
+ uAppDataDir = Services.dirsvc.get("UAppData", Ci.nsIFile);
+ } catch (ex) {
+ // The test suites might not create and define the "UAppData" directory.
+ // We account for that here instead of manually going through each test using
+ // telemetry to manually create the directory and define the constant.
+ this._log.trace(
+ "_iterateAppDataPings - userApplicationDataDir is not defined. Is this a test?"
+ );
+ return;
+ }
+
+ const appDataPendingPings = PathUtils.join(
+ uAppDataDir.path,
+ "Pending Pings"
+ );
+
+ // Check if appDataPendingPings exists and bail out if it doesn't.
+ if (!(await IOUtils.exists(appDataPendingPings))) {
+ this._log.trace(
+ "_iterateAppDataPings - the AppData pending pings directory doesn't exist."
+ );
+ return;
+ }
+
+ // Iterate through the pending ping files.
+ for (const path of await IOUtils.getChildren(appDataPendingPings)) {
+ const info = await IOUtils.stat(path);
+ if (info.type !== "directory") {
+ yield path;
+ }
+ }
+ },
+
+ /**
+ * Remove all pings that are stored in the userApplicationDataDir
+ * under the "Pending Pings" sub-directory.
+ */
+ async removeAppDataPings() {
+ this._log.trace("removeAppDataPings");
+
+ for await (const path of this._iterateAppDataPings()) {
+ try {
+ await IOUtils.remove(path);
+ } catch (ex) {
+ this._log.error(
+ `removeAppDataPings - failed to remove file ${path}`,
+ ex
+ );
+ }
+ }
+ },
+
+ /**
+ * Migrate pings that are stored in the userApplicationDataDir
+ * under the "Pending Pings" sub-directory.
+ */
+ async _migrateAppDataPings() {
+ this._log.trace("_migrateAppDataPings");
+
+ for await (const path of this._iterateAppDataPings()) {
+ try {
+ // Load the ping data from the original file.
+ const pingData = await this.loadPingFile(path);
+
+ // Save it among the pending pings in the user profile, overwrite on
+ // ping id collision.
+ await TelemetryStorage.savePing(pingData, true);
+ } catch (ex) {
+ this._log.error(
+ `_migrateAppDataPings - failed to load or migrate file. Removing ${path}`,
+ ex
+ );
+ }
+
+ try {
+ // Finally remove the file.
+ await IOUtils.remove(path);
+ } catch (ex) {
+ this._log.error(
+ `_migrateAppDataPings - failed to remove file ${path}`,
+ ex
+ );
+ }
+ }
+ },
+
+ loadPendingPingList() {
+ // If we already have a pending scanning task active, return that.
+ if (this._scanPendingPingsTask) {
+ return this._scanPendingPingsTask;
+ }
+
+ if (this._scannedPendingDirectory) {
+ this._log.trace(
+ "loadPendingPingList - Pending already scanned, hitting cache."
+ );
+ return Promise.resolve(this._buildPingList());
+ }
+
+ // Since there's no pending pings scan task running, start it.
+ // Also make sure to clear the task once done.
+ this._scanPendingPingsTask = this._scanPendingPings().then(
+ pings => {
+ this._scanPendingPingsTask = null;
+ return pings;
+ },
+ ex => {
+ this._scanPendingPingsTask = null;
+ throw ex;
+ }
+ );
+ return this._scanPendingPingsTask;
+ },
+
+ getPendingPingList() {
+ return this._buildPingList();
+ },
+
+ async _scanPendingPings() {
+ this._log.trace("_scanPendingPings");
+
+ // Before pruning the pending pings, migrate over the ones from the user
+ // application data directory (mainly crash pings that failed to be sent).
+ await this._migrateAppDataPings();
+
+ const directory = TelemetryStorage.pingDirectoryPath;
+ if (!(await IOUtils.exists(directory))) {
+ return [];
+ }
+
+ const files = [];
+ for (const path of await IOUtils.getChildren(directory)) {
+ if (this._shutdown) {
+ return [];
+ }
+
+ try {
+ const info = await IOUtils.stat(path);
+ if (info.type !== "directory") {
+ files.push({ path, info });
+ }
+ } catch (ex) {
+ this._log.error(`_scanPendingPings - failed to stat file ${path}`, ex);
+ continue;
+ }
+ }
+
+ for (const { path, info } of files) {
+ if (this._shutdown) {
+ return [];
+ }
+
+ // Enforce a maximum file size limit on pending pings.
+ if (info.size > PING_FILE_MAXIMUM_SIZE_BYTES) {
+ this._log.error(
+ `_scanPendingPings - removing file exceeding size limit ${path}`
+ );
+ try {
+ await IOUtils.remove(path);
+ } catch (ex) {
+ this._log.error(
+ `_scanPendingPings - failed to remove file ${path}`,
+ ex
+ );
+ } finally {
+ Telemetry.getHistogramById(
+ "TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB"
+ ).add(Math.floor(info.size / 1024 / 1024));
+ Telemetry.getHistogramById(
+ "TELEMETRY_PING_SIZE_EXCEEDED_PENDING"
+ ).add();
+
+ // Currently we don't have the ping type available without loading the ping from disk.
+ // Bug 1384903 will fix that.
+ lazy.TelemetryHealthPing.recordDiscardedPing("<unknown>");
+ }
+ continue;
+ }
+
+ let id = PathUtils.filename(path);
+ if (!UUID_REGEX.test(id)) {
+ this._log.trace(`_scanPendingPings - filename is not a UUID: ${id}`);
+ id = Utils.generateUUID();
+ }
+
+ this._pendingPings.set(id, {
+ path,
+ lastModified: info.lastModified,
+ });
+ }
+
+ this._scannedPendingDirectory = true;
+ return this._buildPingList();
+ },
+
+ _buildPingList() {
+ const list = Array.from(this._pendingPings, p => ({
+ id: p[0],
+ lastModified: p[1].lastModified,
+ }));
+
+ list.sort((a, b) => b.lastModified - a.lastModified);
+ return list;
+ },
+
+ /**
+ * Loads a ping file.
+ * @param {String} aFilePath The path of the ping file.
+ * @param {Boolean} [aCompressed=false] If |true|, expects the file to be compressed using lz4.
+ * @return {Promise<Object>} A promise resolved with the ping content or rejected if the
+ * ping contains invalid data.
+ * @throws {PingReadError} There was an error while reading the ping file from the disk.
+ * @throws {PingParseError} There was an error while parsing the JSON content of the ping file.
+ */
+ async loadPingFile(aFilePath, aCompressed = false) {
+ let rawPing;
+ try {
+ rawPing = await IOUtils.readUTF8(aFilePath, { decompress: aCompressed });
+ } catch (e) {
+ this._log.trace(`loadPingfile - unreadable ping ${aFilePath}`, e);
+ throw new PingReadError(
+ e.message,
+ DOMException.isInstance(e) && e.name === "NotFoundError"
+ );
+ }
+
+ let ping;
+ try {
+ ping = JSON.parse(rawPing);
+ } catch (e) {
+ this._log.trace(`loadPingfile - unparseable ping ${aFilePath}`, e);
+ await IOUtils.remove(aFilePath).catch(ex => {
+ this._log.error(
+ `loadPingFile - failed removing unparseable ping file ${aFilePath}`,
+ ex
+ );
+ });
+ throw new PingParseError(e.message);
+ }
+
+ return ping;
+ },
+
+ /**
+ * Archived pings are saved with file names of the form:
+ * "<timestamp>.<uuid>.<type>.[json|jsonlz4]"
+ * This helper extracts that data from a given filename.
+ *
+ * @param fileName {String} The filename.
+ * @return {Object} Null if the filename didn't match the expected form.
+ * Otherwise an object with the extracted data in the form:
+ * { timestamp: <number>,
+ * id: <string>,
+ * type: <string> }
+ */
+ _getArchivedPingDataFromFileName(fileName) {
+ // Extract the parts.
+ let parts = fileName.split(".");
+ if (parts.length != 4) {
+ this._log.trace("_getArchivedPingDataFromFileName - should have 4 parts");
+ return null;
+ }
+
+ let [timestamp, uuid, type, extension] = parts;
+ if (extension != "json" && extension != "jsonlz4") {
+ this._log.trace(
+ "_getArchivedPingDataFromFileName - should have 'json' or 'jsonlz4' extension"
+ );
+ return null;
+ }
+
+ // Check for a valid timestamp.
+ timestamp = parseInt(timestamp);
+ if (Number.isNaN(timestamp)) {
+ this._log.trace(
+ "_getArchivedPingDataFromFileName - should have a valid timestamp"
+ );
+ return null;
+ }
+
+ // Check for a valid UUID.
+ if (!UUID_REGEX.test(uuid)) {
+ this._log.trace(
+ "_getArchivedPingDataFromFileName - should have a valid id"
+ );
+ return null;
+ }
+
+ // Check for a valid type string.
+ const typeRegex = /^[a-z0-9][a-z0-9-]+[a-z0-9]$/i;
+ if (!typeRegex.test(type)) {
+ this._log.trace(
+ "_getArchivedPingDataFromFileName - should have a valid type"
+ );
+ return null;
+ }
+
+ return {
+ timestamp,
+ id: uuid,
+ type,
+ };
+ },
+
+ async saveAbortedSessionPing(ping) {
+ this._log.trace(
+ "saveAbortedSessionPing - ping path: " + lazy.gAbortedSessionFilePath
+ );
+ await IOUtils.makeDirectory(lazy.gDataReportingDir);
+
+ return this._abortedSessionSerializer.enqueueTask(() =>
+ this.savePingToFile(ping, lazy.gAbortedSessionFilePath, true)
+ );
+ },
+
+ async loadAbortedSessionPing() {
+ let ping = null;
+ try {
+ ping = await this.loadPingFile(lazy.gAbortedSessionFilePath);
+ } catch (ex) {
+ if (ex.becauseNoSuchFile) {
+ this._log.trace("loadAbortedSessionPing - no such file");
+ } else {
+ this._log.error("loadAbortedSessionPing - error loading ping", ex);
+ }
+ }
+ return ping;
+ },
+
+ removeAbortedSessionPing() {
+ return this._abortedSessionSerializer.enqueueTask(async () => {
+ try {
+ await IOUtils.remove(lazy.gAbortedSessionFilePath, {
+ ignoreAbsent: false,
+ });
+ this._log.trace("removeAbortedSessionPing - success");
+ } catch (ex) {
+ if (DOMException.isInstance(ex) && ex.name === "NotFoundError") {
+ this._log.trace("removeAbortedSessionPing - no such file");
+ } else {
+ this._log.error("removeAbortedSessionPing - error removing ping", ex);
+ }
+ }
+ });
+ },
+
+ async saveUninstallPing(ping) {
+ if (AppConstants.platform != "win") {
+ return;
+ }
+
+ // Remove any old pings from this install first.
+ await this.removeUninstallPings();
+
+ let { directory: pingFile, file } = Policy.getUninstallPingPath(ping.id);
+ pingFile.append(file);
+
+ await this.savePingToFile(ping, pingFile.path, /* overwrite */ true);
+ },
+
+ async removeUninstallPings() {
+ if (AppConstants.platform != "win") {
+ return;
+ }
+
+ const { directory, file } = Policy.getUninstallPingPath("*");
+ const [prefix, suffix] = file.split("*");
+
+ for (const path of await IOUtils.getChildren(directory.path)) {
+ const filename = PathUtils.filename(path);
+ if (!filename.startsWith(prefix) || !filename.endsWith(suffix)) {
+ continue;
+ }
+
+ this._log.trace("removeUninstallPings - removing", path);
+ try {
+ await IOUtils.remove(path);
+ this._log.trace("removeUninstallPings - success");
+ } catch (ex) {
+ if (DOMException.isInstance(ex) && ex.name === "NotFoundError") {
+ this._log.trace("removeUninstallPings - no such file");
+ } else {
+ this._log.error("removeUninstallPings - error removing ping", ex);
+ }
+ }
+ }
+ },
+
+ /**
+ * Remove FHR database files. This is temporary and will be dropped in
+ * the future.
+ * @return {Promise} Resolved when the database files are deleted.
+ */
+ async removeFHRDatabase() {
+ this._log.trace("removeFHRDatabase");
+
+ // Let's try to remove the FHR DB with the default filename first.
+ const FHR_DB_DEFAULT_FILENAME = "healthreport.sqlite";
+
+ // Even if it's uncommon, there may be 2 additional files: - a "write ahead log"
+ // (-wal) file and a "shared memory file" (-shm). We need to remove them as well.
+ let FILES_TO_REMOVE = [
+ PathUtils.join(PathUtils.profileDir, FHR_DB_DEFAULT_FILENAME),
+ PathUtils.join(PathUtils.profileDir, FHR_DB_DEFAULT_FILENAME + "-wal"),
+ PathUtils.join(PathUtils.profileDir, FHR_DB_DEFAULT_FILENAME + "-shm"),
+ ];
+
+ // FHR could have used either the default DB file name or a custom one
+ // through this preference.
+ const FHR_DB_CUSTOM_FILENAME = Services.prefs.getStringPref(
+ "datareporting.healthreport.dbName",
+ undefined
+ );
+ if (FHR_DB_CUSTOM_FILENAME) {
+ FILES_TO_REMOVE.push(
+ PathUtils.join(PathUtils.profileDir, FHR_DB_CUSTOM_FILENAME),
+ PathUtils.join(PathUtils.profileDir, FHR_DB_CUSTOM_FILENAME + "-wal"),
+ PathUtils.join(PathUtils.profileDir, FHR_DB_CUSTOM_FILENAME + "-shm")
+ );
+ }
+
+ for (let f of FILES_TO_REMOVE) {
+ await IOUtils.remove(f).catch(e =>
+ this._log.error(`removeFHRDatabase - failed to remove ${f}`, e)
+ );
+ }
+ },
+};
+
+// Utility functions
+
+function pingFilePath(ping) {
+ // Support legacy ping formats, who don't have an "id" field, but a "slug" field.
+ let pingIdentifier = ping.slug ? ping.slug : ping.id;
+
+ if (typeof pingIdentifier === "undefined" || pingIdentifier === null) {
+ throw new Error(
+ "Incompatible ping format -- ping has no slug or id attribute"
+ );
+ }
+
+ return PathUtils.join(TelemetryStorage.pingDirectoryPath, pingIdentifier);
+}
+
+function getPingDirectory() {
+ return (async function () {
+ let directory = TelemetryStorage.pingDirectoryPath;
+
+ if (!(await IOUtils.exists(directory))) {
+ await IOUtils.makeDirectory(directory, { permissions: 0o700 });
+ }
+
+ return directory;
+ })();
+}
+
+/**
+ * Build the path to the archived ping.
+ * @param {String} aPingId The ping id.
+ * @param {Object} aDate The ping creation date.
+ * @param {String} aType The ping type.
+ * @return {String} The full path to the archived ping.
+ */
+function getArchivedPingPath(aPingId, aDate, aType) {
+ // Get the ping creation date and generate the archive directory to hold it. Note
+ // that getMonth returns a 0-based month, so we need to add an offset.
+ let month = String(aDate.getMonth() + 1);
+ let archivedPingDir = PathUtils.join(
+ lazy.gPingsArchivePath,
+ aDate.getFullYear() + "-" + month.padStart(2, "0")
+ );
+ // Generate the archived ping file path as YYYY-MM/<TIMESTAMP>.UUID.type.json
+ let fileName = [aDate.getTime(), aPingId, aType, "json"].join(".");
+ return PathUtils.join(archivedPingDir, fileName);
+}
+
+/**
+ * Get the size of the ping file on the disk.
+ * @return {Integer} The file size, in bytes, of the ping file or 0 on errors.
+ */
+var getArchivedPingSize = async function (aPingId, aDate, aType) {
+ const path = getArchivedPingPath(aPingId, aDate, aType);
+ let filePaths = [path + "lz4", path];
+
+ for (let path of filePaths) {
+ try {
+ return (await IOUtils.stat(path)).size;
+ } catch (e) {}
+ }
+
+ // That's odd, this ping doesn't seem to exist.
+ return 0;
+};
+
+/**
+ * Get the size of the pending ping file on the disk.
+ * @return {Integer} The file size, in bytes, of the ping file or 0 on errors.
+ */
+var getPendingPingSize = async function (aPingId) {
+ const path = PathUtils.join(TelemetryStorage.pingDirectoryPath, aPingId);
+ try {
+ return (await IOUtils.stat(path)).size;
+ } catch (e) {}
+
+ // That's odd, this ping doesn't seem to exist.
+ return 0;
+};
+
+/**
+ * Check if a directory name is in the "YYYY-MM" format.
+ * @param {String} aDirName The name of the pings archive directory.
+ * @return {Boolean} True if the directory name is in the right format, false otherwise.
+ */
+function isValidArchiveDir(aDirName) {
+ const dirRegEx = /^[0-9]{4}-[0-9]{2}$/;
+ return dirRegEx.test(aDirName);
+}
+
+/**
+ * Gets a date object from an archive directory name.
+ * @param {String} aDirName The name of the pings archive directory. Must be in the YYYY-MM
+ * format.
+ * @return {Object} A Date object or null if the dir name is not valid.
+ */
+function getDateFromArchiveDir(aDirName) {
+ let [year, month] = aDirName.split("-");
+ year = parseInt(year);
+ month = parseInt(month);
+ // Make sure to have sane numbers.
+ if (
+ !Number.isFinite(month) ||
+ !Number.isFinite(year) ||
+ month < 1 ||
+ month > 12
+ ) {
+ return null;
+ }
+ return new Date(year, month - 1, 1, 0, 0, 0);
+}
diff --git a/toolkit/components/telemetry/app/TelemetryTimestamps.sys.mjs b/toolkit/components/telemetry/app/TelemetryTimestamps.sys.mjs
new file mode 100644
index 0000000000..2ae5891596
--- /dev/null
+++ b/toolkit/components/telemetry/app/TelemetryTimestamps.sys.mjs
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * This module's purpose is to collect timestamps for important
+ * application-specific events.
+ *
+ * The TelemetryController component attaches the timestamps stored by this module to
+ * the telemetry submission, substracting the process lifetime so that the times
+ * are relative to process startup. The overall goal is to produce a basic
+ * timeline of the startup process.
+ */
+var timeStamps = {};
+
+export var TelemetryTimestamps = {
+ /**
+ * Adds a timestamp to the list. The addition of TimeStamps that already have
+ * a value stored is ignored.
+ *
+ * @param name must be a unique, generally "camelCase" descriptor of what the
+ * timestamp represents. e.g.: "delayedStartupStarted"
+ * @param value is a timeStamp in milliseconds since the epoch. If omitted,
+ * defaults to Date.now().
+ */
+ add: function TT_add(name, value) {
+ // Default to "now" if not specified
+ if (value == null) {
+ value = Date.now();
+ }
+
+ if (isNaN(value)) {
+ throw new Error("Value must be a timestamp");
+ }
+
+ // If there's an existing value, just ignore the new value.
+ if (timeStamps.hasOwnProperty(name)) {
+ return;
+ }
+
+ timeStamps[name] = value;
+ },
+
+ /**
+ * Returns a JS object containing all of the timeStamps as properties (can be
+ * easily serialized to JSON). Used by TelemetryController to retrieve the data
+ * to attach to the telemetry submission.
+ */
+ get: function TT_get() {
+ // Return a copy of the object.
+ return Cu.cloneInto(timeStamps, {});
+ },
+};
diff --git a/toolkit/components/telemetry/app/TelemetryUtils.sys.mjs b/toolkit/components/telemetry/app/TelemetryUtils.sys.mjs
new file mode 100644
index 0000000000..809460aebc
--- /dev/null
+++ b/toolkit/components/telemetry/app/TelemetryUtils.sys.mjs
@@ -0,0 +1,282 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { TelemetryControllerBase } from "resource://gre/modules/TelemetryControllerBase.sys.mjs";
+
+const lazy = {};
+ChromeUtils.defineESModuleGetters(lazy, {
+ UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
+});
+
+const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
+
+const IS_CONTENT_PROCESS = (function () {
+ // We cannot use Services.appinfo here because in telemetry xpcshell tests,
+ // appinfo is initially unavailable, and becomes available only later on.
+ // eslint-disable-next-line mozilla/use-services
+ let runtime = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULRuntime);
+ return runtime.processType == Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;
+})();
+
+export var TelemetryUtils = {
+ /**
+ * When telemetry is disabled, identifying information (such as client ID)
+ * should be removed. A topic event is emitted with a subject that matches
+ * this constant. When this happens, other systems that store identifying
+ * information about the client should delete that data. Please ask the
+ * Firefox Telemetry Team before relying on this topic.
+ *
+ * Here is an example of listening for that event:
+ *
+ * const { TelemetryUtils } = ChromeUtils.import("resource://gre/modules/TelemetryUtils.jsm");
+ *
+ * class YourClass {
+ * constructor() {
+ * Services.obs.addObserver(this, TelemetryUtils.TELEMETRY_UPLOAD_DISABLED_TOPIC);
+ * }
+ *
+ * observe(subject, topic, data) {
+ * if (topic == TelemetryUtils.TELEMETRY_UPLOAD_DISABLED_TOPIC) {
+ * // Telemetry was disabled
+ * // subject and data are both unused
+ * }
+ * }
+ * }
+ */
+ TELEMETRY_UPLOAD_DISABLED_TOPIC: "telemetry.upload.disabled",
+
+ Preferences: Object.freeze({
+ ...TelemetryControllerBase.Preferences,
+
+ // General Preferences
+ ArchiveEnabled: "toolkit.telemetry.archive.enabled",
+ CachedClientId: "toolkit.telemetry.cachedClientID",
+ DisableFuzzingDelay: "toolkit.telemetry.testing.disableFuzzingDelay",
+ FirstRun: "toolkit.telemetry.reportingpolicy.firstRun",
+ FirstShutdownPingEnabled: "toolkit.telemetry.firstShutdownPing.enabled",
+ HealthPingEnabled: "toolkit.telemetry.healthping.enabled",
+ IPCBatchTimeout: "toolkit.telemetry.ipcBatchTimeout",
+ OverrideOfficialCheck: "toolkit.telemetry.send.overrideOfficialCheck",
+ OverrideUpdateChannel: "toolkit.telemetry.overrideUpdateChannel",
+ Server: "toolkit.telemetry.server",
+ ShutdownPingSender: "toolkit.telemetry.shutdownPingSender.enabled",
+ ShutdownPingSenderFirstSession:
+ "toolkit.telemetry.shutdownPingSender.enabledFirstSession",
+ TelemetryEnabled: "toolkit.telemetry.enabled",
+ UntrustedModulesPingFrequency:
+ "toolkit.telemetry.untrustedModulesPing.frequency",
+ UpdatePing: "toolkit.telemetry.updatePing.enabled",
+ NewProfilePingEnabled: "toolkit.telemetry.newProfilePing.enabled",
+ NewProfilePingDelay: "toolkit.telemetry.newProfilePing.delay",
+ PreviousBuildID: "toolkit.telemetry.previousBuildID",
+
+ // Event Ping Preferences
+ EventPingMinimumFrequency: "toolkit.telemetry.eventping.minimumFrequency",
+ EventPingMaximumFrequency: "toolkit.telemetry.eventping.maximumFrequency",
+
+ // Data reporting Preferences
+ AcceptedPolicyDate: "datareporting.policy.dataSubmissionPolicyNotifiedTime",
+ AcceptedPolicyVersion:
+ "datareporting.policy.dataSubmissionPolicyAcceptedVersion",
+ BypassNotification:
+ "datareporting.policy.dataSubmissionPolicyBypassNotification",
+ CurrentPolicyVersion: "datareporting.policy.currentPolicyVersion",
+ DataSubmissionEnabled: "datareporting.policy.dataSubmissionEnabled",
+ FhrUploadEnabled: "datareporting.healthreport.uploadEnabled",
+ MinimumPolicyVersion: "datareporting.policy.minimumPolicyVersion",
+ FirstRunURL: "datareporting.policy.firstRunURL",
+ }),
+
+ /**
+ * A fixed valid client ID used when Telemetry upload is disabled.
+ */
+ get knownClientID() {
+ return "c0ffeec0-ffee-c0ff-eec0-ffeec0ffeec0";
+ },
+
+ /**
+ * True if this is a content process.
+ */
+ get isContentProcess() {
+ return IS_CONTENT_PROCESS;
+ },
+
+ /**
+ * Returns the state of the Telemetry enabled preference, making sure
+ * it correctly evaluates to a boolean type.
+ */
+ get isTelemetryEnabled() {
+ return TelemetryControllerBase.isTelemetryEnabled;
+ },
+
+ /**
+ * Turn a millisecond timestamp into a day timestamp.
+ *
+ * @param aMsec A number of milliseconds since Unix epoch.
+ * @return The number of whole days since Unix epoch.
+ */
+ millisecondsToDays(aMsec) {
+ return Math.floor(aMsec / MILLISECONDS_PER_DAY);
+ },
+
+ /**
+ * Takes a date and returns it truncated to a date with daily precision.
+ */
+ truncateToDays(date) {
+ return new Date(
+ date.getFullYear(),
+ date.getMonth(),
+ date.getDate(),
+ 0,
+ 0,
+ 0,
+ 0
+ );
+ },
+
+ /**
+ * Takes a date and returns it truncated to a date with hourly precision.
+ */
+ truncateToHours(date) {
+ return new Date(
+ date.getFullYear(),
+ date.getMonth(),
+ date.getDate(),
+ date.getHours(),
+ 0,
+ 0,
+ 0
+ );
+ },
+
+ /**
+ * Check if the difference between the times is within the provided tolerance.
+ * @param {Number} t1 A time in milliseconds.
+ * @param {Number} t2 A time in milliseconds.
+ * @param {Number} tolerance The tolerance, in milliseconds.
+ * @return {Boolean} True if the absolute time difference is within the tolerance, false
+ * otherwise.
+ */
+ areTimesClose(t1, t2, tolerance) {
+ return Math.abs(t1 - t2) <= tolerance;
+ },
+
+ /**
+ * Get the next midnight for a date.
+ * @param {Object} date The date object to check.
+ * @return {Object} The Date object representing the next midnight.
+ */
+ getNextMidnight(date) {
+ let nextMidnight = new Date(this.truncateToDays(date));
+ nextMidnight.setDate(nextMidnight.getDate() + 1);
+ return nextMidnight;
+ },
+
+ /**
+ * Get the midnight which is closer to the provided date.
+ * @param {Object} date The date object to check.
+ * @param {Number} tolerance The tolerance within we find the closest midnight.
+ * @return {Object} The Date object representing the closes midnight, or null if midnight
+ * is not within the midnight tolerance.
+ */
+ getNearestMidnight(date, tolerance) {
+ let lastMidnight = this.truncateToDays(date);
+ if (this.areTimesClose(date.getTime(), lastMidnight.getTime(), tolerance)) {
+ return lastMidnight;
+ }
+
+ const nextMidnightDate = this.getNextMidnight(date);
+ if (
+ this.areTimesClose(date.getTime(), nextMidnightDate.getTime(), tolerance)
+ ) {
+ return nextMidnightDate;
+ }
+ return null;
+ },
+
+ generateUUID() {
+ let str = Services.uuid.generateUUID().toString();
+ // strip {}
+ return str.substring(1, str.length - 1);
+ },
+
+ /**
+ * Find how many months passed between two dates.
+ * @param {Object} aStartDate The starting date.
+ * @param {Object} aEndDate The ending date.
+ * @return {Integer} The number of months between the two dates.
+ */
+ getElapsedTimeInMonths(aStartDate, aEndDate) {
+ return (
+ aEndDate.getMonth() -
+ aStartDate.getMonth() +
+ 12 * (aEndDate.getFullYear() - aStartDate.getFullYear())
+ );
+ },
+
+ /**
+ * Date.toISOString() gives us UTC times, this gives us local times in
+ * the ISO date format. See http://www.w3.org/TR/NOTE-datetime
+ * @param {Object} date The input date.
+ * @return {String} The local time ISO string.
+ */
+ toLocalTimeISOString(date) {
+ function padNumber(number, length) {
+ return number.toString().padStart(length, "0");
+ }
+
+ let sign = n => (n >= 0 ? "+" : "-");
+ // getTimezoneOffset counter-intuitively returns -60 for UTC+1.
+ let tzOffset = -date.getTimezoneOffset();
+
+ // YYYY-MM-DDThh:mm:ss.sTZD (eg 1997-07-16T19:20:30.45+01:00)
+ return (
+ padNumber(date.getFullYear(), 4) +
+ "-" +
+ padNumber(date.getMonth() + 1, 2) +
+ "-" +
+ padNumber(date.getDate(), 2) +
+ "T" +
+ padNumber(date.getHours(), 2) +
+ ":" +
+ padNumber(date.getMinutes(), 2) +
+ ":" +
+ padNumber(date.getSeconds(), 2) +
+ "." +
+ date.getMilliseconds() +
+ sign(tzOffset) +
+ padNumber(Math.floor(Math.abs(tzOffset / 60)), 2) +
+ ":" +
+ padNumber(Math.abs(tzOffset % 60), 2)
+ );
+ },
+
+ /**
+ * @returns {number} The monotonic time since the process start
+ * or (non-monotonic) Date value if this fails back.
+ */
+ monotonicNow() {
+ return Services.telemetry.msSinceProcessStart();
+ },
+
+ /**
+ * @returns {string} The name of the update channel to report
+ * in telemetry.
+ * By default, this is the same as the name of the channel that
+ * the browser uses to download its updates. However in certain
+ * situations, a single update channel provides multiple (distinct)
+ * build types, that need to be distinguishable on Telemetry.
+ */
+ getUpdateChannel() {
+ let overrideChannel = Services.prefs.getCharPref(
+ this.Preferences.OverrideUpdateChannel,
+ undefined
+ );
+ if (overrideChannel) {
+ return overrideChannel;
+ }
+
+ return lazy.UpdateUtils.getUpdateChannel(false);
+ },
+};
diff --git a/toolkit/components/telemetry/build_scripts/README.md b/toolkit/components/telemetry/build_scripts/README.md
new file mode 100644
index 0000000000..4823580735
--- /dev/null
+++ b/toolkit/components/telemetry/build_scripts/README.md
@@ -0,0 +1,5 @@
+# Telemetry Registries Parsers
+This package exports the parsers for Mozilla's probes registries. These registry file contains the definitions for the different probes (i.e. [scalars](https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/collection/scalars.html), [histograms](https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/collection/histograms.html) and [events](https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/collection/events.html)) that can be used to collect data.
+
+# License
+Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
diff --git a/toolkit/components/telemetry/build_scripts/gen_event_data.py b/toolkit/components/telemetry/build_scripts/gen_event_data.py
new file mode 100644
index 0000000000..2e321cea72
--- /dev/null
+++ b/toolkit/components/telemetry/build_scripts/gen_event_data.py
@@ -0,0 +1,227 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Write out event information for C++. The events are defined
+# in a file provided as a command-line argument.
+
+import itertools
+import json
+import sys
+from collections import OrderedDict
+from os import path
+
+from mozparsers import parse_events
+from mozparsers.shared_telemetry_utils import ParserError, static_assert
+
+COMPONENTS_PATH = path.abspath(
+ path.join(path.dirname(__file__), path.pardir, path.pardir)
+)
+sys.path.append(
+ path.join(COMPONENTS_PATH, "glean", "build_scripts", "glean_parser_ext")
+)
+from string_table import StringTable
+
+# The banner/text at the top of the generated file.
+banner = """/* This file is auto-generated, only for internal use in TelemetryEvent.h,
+ see gen_event_data.py. */
+"""
+
+file_header = """\
+#ifndef mozilla_TelemetryEventData_h
+#define mozilla_TelemetryEventData_h
+#include "core/EventInfo.h"
+#include "nsITelemetry.h"
+namespace {
+"""
+
+file_footer = """\
+} // namespace
+#endif // mozilla_TelemetryEventData_h
+"""
+
+
+def write_extra_table(events, output, string_table):
+ table_name = "gExtraKeysTable"
+ extra_table = []
+ extra_count = 0
+
+ print("#if defined(_MSC_VER) && !defined(__clang__)", file=output)
+ print("const uint32_t %s[] = {" % table_name, file=output)
+ print("#else", file=output)
+ print("constexpr uint32_t %s[] = {" % table_name, file=output)
+ print("#endif", file=output)
+
+ for e in events:
+ extra_index = 0
+ extra_keys = e.extra_keys
+ if len(extra_keys) > 0:
+ extra_index = extra_count
+ extra_count += len(extra_keys)
+ indexes = string_table.stringIndexes(extra_keys)
+
+ print(
+ " // %s, [%s], [%s]"
+ % (e.category, ", ".join(e.methods), ", ".join(e.objects)),
+ file=output,
+ )
+ print(" // extra_keys: %s" % ", ".join(extra_keys), file=output)
+ print(" %s," % ", ".join(map(str, indexes)), file=output)
+
+ extra_table.append((extra_index, len(extra_keys)))
+
+ print("};", file=output)
+ static_assert(output, "sizeof(%s) <= UINT32_MAX" % table_name, "index overflow")
+
+ return extra_table
+
+
+def write_common_event_table(events, output, string_table, extra_table):
+ table_name = "gCommonEventInfo"
+
+ print("#if defined(_MSC_VER) && !defined(__clang__)", file=output)
+ print("const CommonEventInfo %s[] = {" % table_name, file=output)
+ print("#else", file=output)
+ print("constexpr CommonEventInfo %s[] = {" % table_name, file=output)
+ print("#endif", file=output)
+
+ for e, extras in zip(events, extra_table):
+ # Write a comment to make the file human-readable.
+ print(" // category: %s" % e.category, file=output)
+ print(" // methods: [%s]" % ", ".join(e.methods), file=output)
+ print(" // objects: [%s]" % ", ".join(e.objects), file=output)
+
+ # Write the common info structure
+ print(
+ " {%d, %d, %d, %d, %s, %s, %s },"
+ % (
+ string_table.stringIndex(e.category),
+ string_table.stringIndex(e.expiry_version),
+ extras[0], # extra keys index
+ extras[1], # extra keys count
+ e.dataset,
+ " | ".join(e.record_in_processes_enum),
+ " | ".join(e.products_enum),
+ ),
+ file=output,
+ )
+
+ print("};", file=output)
+ static_assert(output, "sizeof(%s) <= UINT32_MAX" % table_name, "index overflow")
+
+
+def write_event_table(events, output, string_table):
+ table_name = "gEventInfo"
+
+ print("#if defined(_MSC_VER) && !defined(__clang__)", file=output)
+ print("const EventInfo %s[] = {" % table_name, file=output)
+ print("#else", file=output)
+ print("constexpr EventInfo %s[] = {" % table_name, file=output)
+ print("#endif", file=output)
+
+ for common_info_index, e in enumerate(events):
+ for method_name, object_name in itertools.product(e.methods, e.objects):
+ print(
+ " // category: %s, method: %s, object: %s"
+ % (e.category, method_name, object_name),
+ file=output,
+ )
+
+ print(
+ " {gCommonEventInfo[%d], %d, %d},"
+ % (
+ common_info_index,
+ string_table.stringIndex(method_name),
+ string_table.stringIndex(object_name),
+ ),
+ file=output,
+ )
+
+ print("};", file=output)
+ static_assert(output, "sizeof(%s) <= UINT32_MAX" % table_name, "index overflow")
+
+
+def generate_JSON_definitions(output, *filenames):
+ """Write the event definitions to a JSON file.
+
+ :param output: the file to write the content to.
+ :param filenames: a list of filenames provided by the build system.
+ We only support a single file.
+ """
+ # Load the event data.
+ events = []
+ for filename in filenames:
+ try:
+ batch = parse_events.load_events(filename, True)
+ events.extend(batch)
+ except ParserError as ex:
+ print("\nError processing %s:\n%s\n" % (filename, str(ex)), file=sys.stderr)
+ sys.exit(1)
+
+ event_definitions = OrderedDict()
+ for event in events:
+ category = event.category
+
+ if category not in event_definitions:
+ event_definitions[category] = OrderedDict()
+
+ event_definitions[category][event.name] = OrderedDict(
+ {
+ "methods": event.methods,
+ "objects": event.objects,
+ "extra_keys": event.extra_keys,
+ "record_on_release": True
+ if event.dataset_short == "opt-out"
+ else False,
+ # We don't expire dynamic-builtin scalars: they're only meant for
+ # use in local developer builds anyway. They will expire when rebuilding.
+ "expires": event.expiry_version,
+ "expired": False,
+ "products": event.products,
+ }
+ )
+
+ json.dump(event_definitions, output, sort_keys=True)
+
+
+def main(output, *filenames):
+ # Load the event data.
+ events = []
+ for filename in filenames:
+ try:
+ batch = parse_events.load_events(filename, True)
+ events.extend(batch)
+ except ParserError as ex:
+ print("\nError processing %s:\n%s\n" % (filename, str(ex)), file=sys.stderr)
+ sys.exit(1)
+
+ # Write the scalar data file.
+ print(banner, file=output)
+ print(file_header, file=output)
+
+ # Write the extra keys table.
+ string_table = StringTable()
+ extra_table = write_extra_table(events, output, string_table)
+ print("", file=output)
+
+ # Write a table with the common event data.
+ write_common_event_table(events, output, string_table, extra_table)
+ print("", file=output)
+
+ # Write the data for individual events.
+ write_event_table(events, output, string_table)
+ print("", file=output)
+
+ # Write the string table.
+ string_table_name = "gEventsStringTable"
+ string_table.writeDefinition(output, string_table_name)
+ static_assert(
+ output, "sizeof(%s) <= UINT32_MAX" % string_table_name, "index overflow"
+ )
+ print("", file=output)
+
+ print(file_footer, file=output)
+
+
+if __name__ == "__main__":
+ main(sys.stdout, *sys.argv[1:])
diff --git a/toolkit/components/telemetry/build_scripts/gen_event_enum.py b/toolkit/components/telemetry/build_scripts/gen_event_enum.py
new file mode 100644
index 0000000000..9dd418b3dd
--- /dev/null
+++ b/toolkit/components/telemetry/build_scripts/gen_event_enum.py
@@ -0,0 +1,81 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Write out C++ enum definitions that represent the different event types.
+#
+# The events are defined in files provided as command-line arguments.
+
+import sys
+
+import buildconfig
+from mozparsers import parse_events
+from mozparsers.shared_telemetry_utils import ParserError
+
+banner = """/* This file is auto-generated, see gen_event_enum.py. */
+"""
+
+file_header = """\
+#ifndef mozilla_TelemetryEventEnums_h
+#define mozilla_TelemetryEventEnums_h
+
+#include <stdint.h>
+
+namespace mozilla {
+namespace Telemetry {
+enum class EventID : uint32_t {\
+"""
+
+file_footer = """\
+};
+} // namespace mozilla
+} // namespace Telemetry
+#endif // mozilla_TelemetryEventEnums_h
+"""
+
+
+def main(output, *filenames):
+ # Load the events first.
+ events = []
+ for filename in filenames:
+ try:
+ batch = parse_events.load_events(filename, True)
+ events.extend(batch)
+ except ParserError as ex:
+ print("\nError processing %s:\n%s\n" % (filename, str(ex)), file=sys.stderr)
+ sys.exit(1)
+
+ grouped = dict()
+ index = 0
+ for e in events:
+ category = e.category
+ if category not in grouped:
+ grouped[category] = []
+ grouped[category].append((index, e))
+ index += len(e.enum_labels)
+
+ # Write the enum file.
+ print(banner, file=output)
+ print(file_header, file=output)
+
+ for category, indexed in sorted(grouped.items()):
+ category_cpp = indexed[0][1].category_cpp
+
+ print(" // category: %s" % category, file=output)
+
+ for event_index, e in indexed:
+ if e.record_on_os(buildconfig.substs["OS_TARGET"]):
+ for offset, label in enumerate(e.enum_labels):
+ print(
+ " %s_%s = %d," % (category_cpp, label, event_index + offset),
+ file=output,
+ )
+
+ print(" // meta", file=output)
+ print(" EventCount = %d," % index, file=output)
+
+ print(file_footer, file=output)
+
+
+if __name__ == "__main__":
+ main(sys.stdout, *sys.argv[1:])
diff --git a/toolkit/components/telemetry/build_scripts/gen_histogram_data.py b/toolkit/components/telemetry/build_scripts/gen_histogram_data.py
new file mode 100644
index 0000000000..a203dde9f9
--- /dev/null
+++ b/toolkit/components/telemetry/build_scripts/gen_histogram_data.py
@@ -0,0 +1,297 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Write out histogram information for C++. The histograms are defined
+# in a file provided as a command-line argument.
+
+import sys
+from os import path
+
+import buildconfig
+from mozparsers import parse_histograms
+from mozparsers.shared_telemetry_utils import ParserError, static_assert
+
+COMPONENTS_PATH = path.abspath(
+ path.join(path.dirname(__file__), path.pardir, path.pardir)
+)
+sys.path.append(
+ path.join(COMPONENTS_PATH, "glean", "build_scripts", "glean_parser_ext")
+)
+from string_table import StringTable
+
+banner = """/* This file is auto-generated, see gen_histogram_data.py. */
+"""
+
+
+def print_array_entry(
+ output,
+ histogram,
+ name_index,
+ exp_index,
+ label_index,
+ label_count,
+ key_index,
+ key_count,
+ store_index,
+ store_count,
+):
+ if histogram.record_on_os(buildconfig.substs["OS_TARGET"]):
+ print(
+ " { %d, %d, %d, %d, %d, %d, %d, %d, %d, %d, %s, %s, %s, %s, %s, %s },"
+ % (
+ histogram.low(),
+ histogram.high(),
+ histogram.n_buckets(),
+ name_index,
+ exp_index,
+ label_count,
+ key_count,
+ store_count,
+ label_index,
+ key_index,
+ store_index,
+ " | ".join(histogram.record_in_processes_enum()),
+ "true" if histogram.keyed() else "false",
+ histogram.nsITelemetry_kind(),
+ histogram.dataset(),
+ " | ".join(histogram.products_enum()),
+ ),
+ file=output,
+ )
+
+
+def write_histogram_table(output, histograms):
+ string_table = StringTable()
+
+ label_table = []
+ label_count = 0
+ keys_table = []
+ keys_count = 0
+ store_table = []
+ total_store_count = 0
+
+ print("constexpr HistogramInfo gHistogramInfos[] = {", file=output)
+ for histogram in histograms:
+ name_index = string_table.stringIndex(histogram.name())
+ exp_index = string_table.stringIndex(histogram.expiration())
+
+ labels = histogram.labels()
+ label_index = 0
+ if len(labels) > 0:
+ label_index = label_count
+ label_table.append((histogram.name(), string_table.stringIndexes(labels)))
+ label_count += len(labels)
+
+ keys = histogram.keys()
+ key_index = 0
+ if len(keys) > 0:
+ key_index = keys_count
+ keys_table.append((histogram.name(), string_table.stringIndexes(keys)))
+ keys_count += len(keys)
+
+ stores = histogram.record_into_store()
+ store_index = 0
+ if stores == ["main"]:
+ # if count == 1 && offset == UINT16_MAX -> only main store
+ store_index = "UINT16_MAX"
+ else:
+ store_index = total_store_count
+ store_table.append((histogram.name(), string_table.stringIndexes(stores)))
+ total_store_count += len(stores)
+
+ print_array_entry(
+ output,
+ histogram,
+ name_index,
+ exp_index,
+ label_index,
+ len(labels),
+ key_index,
+ len(keys),
+ store_index,
+ len(stores),
+ )
+ print("};\n", file=output)
+
+ strtab_name = "gHistogramStringTable"
+ string_table.writeDefinition(output, strtab_name)
+ static_assert(output, "sizeof(%s) <= UINT32_MAX" % strtab_name, "index overflow")
+
+ print("\n#if defined(_MSC_VER) && !defined(__clang__)", file=output)
+ print("const uint32_t gHistogramLabelTable[] = {", file=output)
+ print("#else", file=output)
+ print("constexpr uint32_t gHistogramLabelTable[] = {", file=output)
+ print("#endif", file=output)
+ for name, indexes in label_table:
+ print("/* %s */ %s," % (name, ", ".join(map(str, indexes))), file=output)
+ print("};", file=output)
+ static_assert(
+ output, "sizeof(gHistogramLabelTable) <= UINT16_MAX", "index overflow"
+ )
+
+ print("\n#if defined(_MSC_VER) && !defined(__clang__)", file=output)
+ print("const uint32_t gHistogramKeyTable[] = {", file=output)
+ print("#else", file=output)
+ print("constexpr uint32_t gHistogramKeyTable[] = {", file=output)
+ print("#endif", file=output)
+ for name, indexes in keys_table:
+ print("/* %s */ %s," % (name, ", ".join(map(str, indexes))), file=output)
+ print("};", file=output)
+ static_assert(output, "sizeof(gHistogramKeyTable) <= UINT16_MAX", "index overflow")
+
+ store_table_name = "gHistogramStoresTable"
+ print("\n#if defined(_MSC_VER) && !defined(__clang__)", file=output)
+ print("const uint32_t {}[] = {{".format(store_table_name), file=output)
+ print("#else", file=output)
+ print("constexpr uint32_t {}[] = {{".format(store_table_name), file=output)
+ print("#endif", file=output)
+ for name, indexes in store_table:
+ print("/* %s */ %s," % (name, ", ".join(map(str, indexes))), file=output)
+ print("};", file=output)
+ static_assert(
+ output, "sizeof(%s) <= UINT16_MAX" % store_table_name, "index overflow"
+ )
+
+
+# Write out static asserts for histogram data. We'd prefer to perform
+# these checks in this script itself, but since several histograms
+# (generally enumerated histograms) use compile-time constants for
+# their upper bounds, we have to let the compiler do the checking.
+
+
+def static_asserts_for_boolean(output, histogram):
+ pass
+
+
+def static_asserts_for_flag(output, histogram):
+ pass
+
+
+def static_asserts_for_count(output, histogram):
+ pass
+
+
+def static_asserts_for_enumerated(output, histogram):
+ n_values = histogram.high()
+ static_assert(
+ output, "%s > 2" % n_values, "Not enough values for %s" % histogram.name()
+ )
+
+
+def shared_static_asserts(output, histogram):
+ name = histogram.name()
+ low = histogram.low()
+ high = histogram.high()
+ n_buckets = histogram.n_buckets()
+ static_assert(output, "%s < %s" % (low, high), "low >= high for %s" % name)
+ static_assert(output, "%s > 2" % n_buckets, "Not enough values for %s" % name)
+ static_assert(output, "%s >= 1" % low, "Incorrect low value for %s" % name)
+ static_assert(
+ output,
+ "%s > %s" % (high, n_buckets),
+ "high must be > number of buckets for %s;"
+ " you may want an enumerated histogram" % name,
+ )
+
+
+def static_asserts_for_linear(output, histogram):
+ shared_static_asserts(output, histogram)
+
+
+def static_asserts_for_exponential(output, histogram):
+ shared_static_asserts(output, histogram)
+
+
+def write_histogram_static_asserts(output, histograms):
+ print(
+ """
+// Perform the checks at the beginning of HistogramGet at
+// compile time, so that incorrect histogram definitions
+// give compile-time errors, not runtime errors.""",
+ file=output,
+ )
+
+ table = {
+ "boolean": static_asserts_for_boolean,
+ "flag": static_asserts_for_flag,
+ "count": static_asserts_for_count,
+ "enumerated": static_asserts_for_enumerated,
+ "categorical": static_asserts_for_enumerated,
+ "linear": static_asserts_for_linear,
+ "exponential": static_asserts_for_exponential,
+ }
+
+ target_os = buildconfig.substs["OS_TARGET"]
+ for histogram in histograms:
+ kind = histogram.kind()
+ if not histogram.record_on_os(target_os):
+ continue
+
+ if kind not in table:
+ raise Exception(
+ 'Unknown kind "%s" for histogram "%s".' % (kind, histogram.name())
+ )
+ fn = table[kind]
+ fn(output, histogram)
+
+
+def write_histogram_ranges(output, histograms):
+ # This generates static data to avoid costly initialization of histograms
+ # (especially exponential ones which require log and exp calls) at runtime.
+ # The format must exactly match that required in histogram.cc, which is
+ # 0, buckets..., INT_MAX. Additionally, the list ends in a 0 to aid asserts
+ # that validate that the length of the ranges list is correct.U cache miss.
+ print("#if defined(_MSC_VER) && !defined(__clang__)", file=output)
+ print("const int gHistogramBucketLowerBounds[] = {", file=output)
+ print("#else", file=output)
+ print("constexpr int gHistogramBucketLowerBounds[] = {", file=output)
+ print("#endif", file=output)
+
+ # Print the dummy buckets for expired histograms, and set the offset to match.
+ print("0,1,2,INT_MAX,", file=output)
+ offset = 4
+ ranges_offsets = {}
+
+ for histogram in histograms:
+ ranges = tuple(histogram.ranges())
+ if ranges not in ranges_offsets:
+ ranges_offsets[ranges] = offset
+ # Suffix each ranges listing with INT_MAX, to match histogram.cc's
+ # expected format.
+ offset += len(ranges) + 1
+ print(",".join(map(str, ranges)), ",INT_MAX,", file=output)
+ print("0};", file=output)
+
+ if offset > 32767:
+ raise Exception("Histogram offsets exceeded maximum value for an int16_t.")
+
+ target_os = buildconfig.substs["OS_TARGET"]
+ print("#if defined(_MSC_VER) && !defined(__clang__)", file=output)
+ print("const int16_t gHistogramBucketLowerBoundIndex[] = {", file=output)
+ print("#else", file=output)
+ print("constexpr int16_t gHistogramBucketLowerBoundIndex[] = {", file=output)
+ print("#endif", file=output)
+ for histogram in histograms:
+ if histogram.record_on_os(target_os):
+ our_offset = ranges_offsets[tuple(histogram.ranges())]
+ print("%d," % our_offset, file=output)
+
+ print("};", file=output)
+
+
+def main(output, *filenames):
+ try:
+ histograms = list(parse_histograms.from_files(filenames))
+ except ParserError as ex:
+ print("\nError processing histograms:\n" + str(ex) + "\n")
+ sys.exit(1)
+
+ print(banner, file=output)
+ write_histogram_table(output, histograms)
+ write_histogram_ranges(output, histograms)
+ write_histogram_static_asserts(output, histograms)
+
+
+if __name__ == "__main__":
+ main(sys.stdout, *sys.argv[1:])
diff --git a/toolkit/components/telemetry/build_scripts/gen_histogram_enum.py b/toolkit/components/telemetry/build_scripts/gen_histogram_enum.py
new file mode 100644
index 0000000000..8d83e760c5
--- /dev/null
+++ b/toolkit/components/telemetry/build_scripts/gen_histogram_enum.py
@@ -0,0 +1,94 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Write out a C++ enum definition whose members are the names of
+# histograms as well as the following other members:
+#
+# - HistogramCount
+#
+# The histograms are defined in files provided as command-line arguments.
+
+import sys
+
+import buildconfig
+from mozparsers import parse_histograms
+from mozparsers.shared_telemetry_utils import ParserError
+
+banner = """/* This file is auto-generated, see gen_histogram_enum.py. */
+"""
+
+header = """
+#ifndef mozilla_TelemetryHistogramEnums_h
+#define mozilla_TelemetryHistogramEnums_h
+
+#include <cstdint>
+#include <type_traits>
+
+namespace mozilla {
+namespace Telemetry {
+"""
+
+footer = """
+} // namespace mozilla
+} // namespace Telemetry
+#endif // mozilla_TelemetryHistogramEnums_h"""
+
+
+def main(output, *filenames):
+ # Print header.
+ print(banner, file=output)
+ print(header, file=output)
+
+ # Load the histograms.
+ try:
+ all_histograms = list(parse_histograms.from_files(filenames))
+ except ParserError as ex:
+ print("\nError processing histograms:\n" + str(ex) + "\n")
+ sys.exit(1)
+
+ # Print the histogram enums.
+ print("enum HistogramID : uint32_t {", file=output)
+ for histogram in all_histograms:
+ if histogram.record_on_os(buildconfig.substs["OS_TARGET"]):
+ print(" %s," % histogram.name(), file=output)
+
+ print(" HistogramCount,", file=output)
+
+ print("};", file=output)
+
+ # Write categorical label enums.
+ categorical = filter(lambda h: h.kind() == "categorical", all_histograms)
+ categorical = filter(
+ lambda h: h.record_on_os(buildconfig.substs["OS_TARGET"]), categorical
+ )
+ enums = [("LABELS_" + h.name(), h.labels(), h.name()) for h in categorical]
+ for name, labels, _ in enums:
+ print("\nenum class %s : uint32_t {" % name, file=output)
+ print(" %s" % ",\n ".join(labels), file=output)
+ print("};", file=output)
+
+ print(
+ "\ntemplate<class T> struct IsCategoricalLabelEnum : std::false_type {};",
+ file=output,
+ )
+ for name, _, _ in enums:
+ print(
+ "template<> struct IsCategoricalLabelEnum<%s> : std::true_type {};" % name,
+ file=output,
+ )
+
+ print("\ntemplate<class T> struct CategoricalLabelId {};", file=output)
+ for name, _, id in enums:
+ print(
+ "template<> struct CategoricalLabelId<%s> : "
+ "std::integral_constant<uint32_t, %s> {};" % (name, id),
+ file=output,
+ )
+
+ # Footer.
+ print(footer, file=output)
+
+
+if __name__ == "__main__":
+ main(sys.stdout, *sys.argv[1:])
diff --git a/toolkit/components/telemetry/build_scripts/gen_histogram_phf.py b/toolkit/components/telemetry/build_scripts/gen_histogram_phf.py
new file mode 100644
index 0000000000..38c7245506
--- /dev/null
+++ b/toolkit/components/telemetry/build_scripts/gen_histogram_phf.py
@@ -0,0 +1,73 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from mozparsers.shared_telemetry_utils import ParserError
+from perfecthash import PerfectHash
+
+PHFSIZE = 1024
+
+import sys
+
+import buildconfig
+from mozparsers import parse_histograms
+
+banner = """/* This file is auto-generated, see gen_histogram_phf.py. */
+"""
+
+header = """
+#ifndef mozilla_TelemetryHistogramNameMap_h
+#define mozilla_TelemetryHistogramNameMap_h
+
+#include "mozilla/PerfectHash.h"
+
+namespace mozilla {
+namespace Telemetry {
+"""
+
+footer = """
+} // namespace mozilla
+} // namespace Telemetry
+#endif // mozilla_TelemetryHistogramNameMap_h
+"""
+
+
+def main(output, *filenames):
+ """
+ Generate a Perfect Hash Table for the Histogram name -> Histogram ID lookup.
+ The table is immutable once generated and we can avoid any dynamic memory allocation.
+ """
+
+ output.write(banner)
+ output.write(header)
+
+ try:
+ histograms = list(parse_histograms.from_files(filenames))
+ histograms = [
+ h for h in histograms if h.record_on_os(buildconfig.substs["OS_TARGET"])
+ ]
+ except ParserError as ex:
+ print("\nError processing histograms:\n" + str(ex) + "\n")
+ sys.exit(1)
+
+ histograms = [
+ (bytearray(hist.name(), "ascii"), idx) for (idx, hist) in enumerate(histograms)
+ ]
+ name_phf = PerfectHash(histograms, PHFSIZE)
+
+ output.write(
+ name_phf.cxx_codegen(
+ name="HistogramIDByNameLookup",
+ entry_type="uint32_t",
+ lower_entry=lambda x: str(x[1]),
+ key_type="const nsACString&",
+ key_bytes="aKey.BeginReading()",
+ key_length="aKey.Length()",
+ )
+ )
+
+ output.write(footer)
+
+
+if __name__ == "__main__":
+ main(sys.stdout, *sys.argv[1:])
diff --git a/toolkit/components/telemetry/build_scripts/gen_process_data.py b/toolkit/components/telemetry/build_scripts/gen_process_data.py
new file mode 100644
index 0000000000..2a494689ad
--- /dev/null
+++ b/toolkit/components/telemetry/build_scripts/gen_process_data.py
@@ -0,0 +1,80 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Write out processes data for C++. The processes are defined
+# in a file provided as a command-line argument.
+
+import collections
+import sys
+
+from mozparsers.shared_telemetry_utils import ParserError, load_yaml_file
+
+# The banner/text at the top of the generated file.
+banner = """/* This file is auto-generated from Telemetry build scripts,
+ see gen_process_data.py. */
+"""
+
+file_header = """\
+#ifndef mozilla_TelemetryProcessData_h
+#define mozilla_TelemetryProcessData_h
+
+#include "mozilla/TelemetryProcessEnums.h"
+
+namespace mozilla {
+namespace Telemetry {
+"""
+
+file_footer = """
+} // namespace Telemetry
+} // namespace mozilla
+#endif // mozilla_TelemetryProcessData_h"""
+
+
+def to_enum_label(name):
+ return name.title().replace("_", "")
+
+
+def write_processes_data(processes, output):
+ def p(line):
+ print(line, file=output)
+
+ processes = collections.OrderedDict(processes)
+
+ p("static GeckoProcessType ProcessIDToGeckoProcessType[%d] = {" % len(processes))
+ for i, (name, value) in enumerate(sorted(processes.items())):
+ p(
+ " /* %d: ProcessID::%s = */ %s,"
+ % (i, to_enum_label(name), value["gecko_enum"])
+ )
+ p("};")
+ p("")
+ p("#if defined(_MSC_VER) && !defined(__clang__)")
+ p("static const char* const ProcessIDToString[%d] = {" % len(processes))
+ p("#else")
+ p("static constexpr const char* ProcessIDToString[%d] = {" % len(processes))
+ p("#endif")
+ for i, (name, value) in enumerate(sorted(processes.items())):
+ p(' /* %d: ProcessID::%s = */ "%s",' % (i, to_enum_label(name), name))
+ p("};")
+
+
+def main(output, *filenames):
+ if len(filenames) > 1:
+ raise Exception("We don't support loading from more than one file.")
+
+ try:
+ processes = load_yaml_file(filenames[0])
+
+ # Write the process data file.
+ print(banner, file=output)
+ print(file_header, file=output)
+ write_processes_data(processes, output)
+ print(file_footer, file=output)
+ except ParserError as ex:
+ print("\nError generating processes data:\n" + str(ex) + "\n")
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main(sys.stdout, *sys.argv[1:])
diff --git a/toolkit/components/telemetry/build_scripts/gen_process_enum.py b/toolkit/components/telemetry/build_scripts/gen_process_enum.py
new file mode 100644
index 0000000000..bfe2d65e43
--- /dev/null
+++ b/toolkit/components/telemetry/build_scripts/gen_process_enum.py
@@ -0,0 +1,69 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Write out processes data for C++. The processes are defined
+# in a file provided as a command-line argument.
+
+import collections
+import sys
+
+from mozparsers.shared_telemetry_utils import ParserError, load_yaml_file
+
+# The banner/text at the top of the generated file.
+banner = """/* This file is auto-generated from Telemetry build scripts,
+ see gen_process_enum.py. */
+"""
+
+file_header = """\
+#ifndef mozilla_TelemetryProcessEnums_h
+#define mozilla_TelemetryProcessEnums_h
+
+#include <cstdint>
+
+namespace mozilla {
+namespace Telemetry {
+"""
+
+file_footer = """
+} // namespace Telemetry
+} // namespace mozilla
+#endif // mozilla_TelemetryProcessEnums_h"""
+
+
+def to_enum_label(name):
+ return name.title().replace("_", "")
+
+
+def write_processes_enum(processes, output):
+ def p(line):
+ print(line, file=output)
+
+ processes = collections.OrderedDict(processes)
+
+ p("enum class ProcessID : uint32_t {")
+ for i, (name, _) in enumerate(sorted(processes.items())):
+ p(" %s = %d," % (to_enum_label(name), i))
+ p(" Count = %d" % len(processes))
+ p("};")
+
+
+def main(output, *filenames):
+ if len(filenames) > 1:
+ raise Exception("We don't support loading from more than one file.")
+
+ try:
+ processes = load_yaml_file(filenames[0])
+
+ # Write the process data file.
+ print(banner, file=output)
+ print(file_header, file=output)
+ write_processes_enum(processes, output)
+ print(file_footer, file=output)
+ except ParserError as ex:
+ print("\nError generating processes enums:\n" + str(ex) + "\n")
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main(sys.stdout, *sys.argv[1:])
diff --git a/toolkit/components/telemetry/build_scripts/gen_scalar_data.py b/toolkit/components/telemetry/build_scripts/gen_scalar_data.py
new file mode 100644
index 0000000000..6ef1f457b5
--- /dev/null
+++ b/toolkit/components/telemetry/build_scripts/gen_scalar_data.py
@@ -0,0 +1,216 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Write out scalar information for C++. The scalars are defined
+# in a file provided as a command-line argument.
+
+import json
+import sys
+from collections import OrderedDict
+from os import path
+
+import buildconfig
+from mozparsers import parse_scalars
+from mozparsers.shared_telemetry_utils import ParserError, static_assert
+
+COMPONENTS_PATH = path.abspath(
+ path.join(path.dirname(__file__), path.pardir, path.pardir)
+)
+sys.path.append(
+ path.join(COMPONENTS_PATH, "glean", "build_scripts", "glean_parser_ext")
+)
+from string_table import StringTable
+
+# The banner/text at the top of the generated file.
+banner = """/* This file is auto-generated, only for internal use in TelemetryScalar.h,
+ see gen_scalar_data.py. */
+"""
+
+file_header = """\
+#ifndef mozilla_TelemetryScalarData_h
+#define mozilla_TelemetryScalarData_h
+#include "core/ScalarInfo.h"
+#include "nsITelemetry.h"
+namespace {
+"""
+
+file_footer = """\
+} // namespace
+#endif // mozilla_TelemetryScalarData_h
+"""
+
+
+def write_scalar_info(
+ scalar,
+ output,
+ name_index,
+ expiration_index,
+ store_index,
+ store_count,
+ key_count,
+ key_index,
+):
+ """Writes a scalar entry to the output file.
+
+ :param scalar: a ScalarType instance describing the scalar.
+ :param output: the output stream.
+ :param name_index: the index of the scalar name in the strings table.
+ :param expiration_index: the index of the expiration version in the strings table.
+ """
+ if scalar.record_on_os(buildconfig.substs["OS_TARGET"]):
+ print(
+ " {{ {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {} }},".format(
+ scalar.nsITelemetry_kind,
+ name_index,
+ expiration_index,
+ scalar.dataset,
+ " | ".join(scalar.record_in_processes_enum),
+ "true" if scalar.keyed else "false",
+ key_count,
+ key_index,
+ " | ".join(scalar.products_enum),
+ store_count,
+ store_index,
+ ),
+ file=output,
+ )
+
+
+def write_scalar_tables(scalars, output):
+ """Writes the scalar and strings tables to an header file.
+
+ :param scalars: a list of ScalarType instances describing the scalars.
+ :param output: the output stream.
+ """
+ string_table = StringTable()
+
+ store_table = []
+ total_store_count = 0
+
+ keys_table = []
+ total_key_count = 0
+
+ print("const ScalarInfo gScalars[] = {", file=output)
+ for s in scalars:
+ # We add both the scalar label and the expiration string to the strings
+ # table.
+ name_index = string_table.stringIndex(s.label)
+ exp_index = string_table.stringIndex(s.expires)
+
+ stores = s.record_into_store
+ store_index = 0
+ if stores == ["main"]:
+ # if count == 1 && offset == UINT16_MAX -> only main store
+ store_index = "UINT16_MAX"
+ else:
+ store_index = total_store_count
+ store_table.append((s.label, string_table.stringIndexes(stores)))
+ total_store_count += len(stores)
+
+ keys = s.keys
+ key_index = 0
+ if len(keys) > 0:
+ key_index = total_key_count
+ keys_table.append((s.label, string_table.stringIndexes(keys)))
+ total_key_count += len(keys)
+
+ # Write the scalar info entry.
+ write_scalar_info(
+ s,
+ output,
+ name_index,
+ exp_index,
+ store_index,
+ len(stores),
+ len(keys),
+ key_index,
+ )
+ print("};", file=output)
+
+ string_table_name = "gScalarsStringTable"
+ string_table.writeDefinition(output, string_table_name)
+ static_assert(
+ output, "sizeof(%s) <= UINT32_MAX" % string_table_name, "index overflow"
+ )
+
+ print("\nconstexpr uint32_t gScalarKeysTable[] = {", file=output)
+ for name, indexes in keys_table:
+ print("/* %s */ %s," % (name, ", ".join(map(str, indexes))), file=output)
+ print("};", file=output)
+
+ store_table_name = "gScalarStoresTable"
+ print("\n#if defined(_MSC_VER) && !defined(__clang__)", file=output)
+ print("const uint32_t {}[] = {{".format(store_table_name), file=output)
+ print("#else", file=output)
+ print("constexpr uint32_t {}[] = {{".format(store_table_name), file=output)
+ print("#endif", file=output)
+ for name, indexes in store_table:
+ print("/* %s */ %s," % (name, ", ".join(map(str, indexes))), file=output)
+ print("};", file=output)
+ static_assert(
+ output, "sizeof(%s) <= UINT16_MAX" % store_table_name, "index overflow"
+ )
+
+
+def parse_scalar_definitions(filenames):
+ scalars = []
+ for filename in filenames:
+ try:
+ batch = parse_scalars.load_scalars(filename)
+ scalars.extend(batch)
+ except ParserError as ex:
+ print("\nError processing %s:\n%s\n" % (filename, str(ex)), file=sys.stderr)
+ sys.exit(1)
+ return scalars
+
+
+def generate_JSON_definitions(output, *filenames):
+ """Write the scalar definitions to a JSON file.
+
+ :param output: the file to write the content to.
+ :param filenames: a list of filenames provided by the build system.
+ We only support a single file.
+ """
+ scalars = parse_scalar_definitions(filenames)
+
+ scalar_definitions = OrderedDict()
+ for scalar in scalars:
+ category = scalar.category
+
+ if category not in scalar_definitions:
+ scalar_definitions[category] = OrderedDict()
+
+ scalar_definitions[category][scalar.name] = OrderedDict(
+ {
+ "kind": scalar.nsITelemetry_kind,
+ "keyed": scalar.keyed,
+ "keys": scalar.keys,
+ "record_on_release": True
+ if scalar.dataset_short == "opt-out"
+ else False,
+ # We don't expire dynamic-builtin scalars: they're only meant for
+ # use in local developer builds anyway. They will expire when rebuilding.
+ "expired": False,
+ "stores": scalar.record_into_store,
+ "expires": scalar.expires,
+ "products": scalar.products,
+ }
+ )
+
+ json.dump(scalar_definitions, output)
+
+
+def main(output, *filenames):
+ # Load the scalars first.
+ scalars = parse_scalar_definitions(filenames)
+
+ # Write the scalar data file.
+ print(banner, file=output)
+ print(file_header, file=output)
+ write_scalar_tables(scalars, output)
+ print(file_footer, file=output)
+
+
+if __name__ == "__main__":
+ main(sys.stdout, *sys.argv[1:])
diff --git a/toolkit/components/telemetry/build_scripts/gen_scalar_enum.py b/toolkit/components/telemetry/build_scripts/gen_scalar_enum.py
new file mode 100644
index 0000000000..321cd047d7
--- /dev/null
+++ b/toolkit/components/telemetry/build_scripts/gen_scalar_enum.py
@@ -0,0 +1,60 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Write out a C++ enum definition whose members are the names of
+# scalar types.
+#
+# The scalars are defined in files provided as command-line arguments.
+
+import sys
+
+import buildconfig
+from mozparsers import parse_scalars
+from mozparsers.shared_telemetry_utils import ParserError
+
+banner = """/* This file is auto-generated, see gen_scalar_enum.py. */
+"""
+
+file_header = """\
+#ifndef mozilla_TelemetryScalarEnums_h
+#define mozilla_TelemetryScalarEnums_h
+namespace mozilla {
+namespace Telemetry {
+enum class ScalarID : uint32_t {\
+"""
+
+file_footer = """\
+};
+} // namespace mozilla
+} // namespace Telemetry
+#endif // mozilla_TelemetryScalarEnums_h
+"""
+
+
+def main(output, *filenames):
+ # Load the scalars first.
+ scalars = []
+ for filename in filenames:
+ try:
+ batch = parse_scalars.load_scalars(filename)
+ scalars.extend(batch)
+ except ParserError as ex:
+ print("\nError processing %s:\n%s\n" % (filename, str(ex)), file=sys.stderr)
+ sys.exit(1)
+
+ # Write the enum file.
+ print(banner, file=output)
+ print(file_header, file=output)
+
+ for s in scalars:
+ if s.record_on_os(buildconfig.substs["OS_TARGET"]):
+ print(" %s," % s.enum_label, file=output)
+
+ print(" ScalarCount,", file=output)
+
+ print(file_footer, file=output)
+
+
+if __name__ == "__main__":
+ main(sys.stdout, *sys.argv[1:])
diff --git a/toolkit/components/telemetry/build_scripts/gen_userinteraction_data.py b/toolkit/components/telemetry/build_scripts/gen_userinteraction_data.py
new file mode 100644
index 0000000000..b12cbde239
--- /dev/null
+++ b/toolkit/components/telemetry/build_scripts/gen_userinteraction_data.py
@@ -0,0 +1,105 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Write out UserInteraction information for C++. The UserInteractions are
+# defined in a file provided as a command-line argument.
+
+import sys
+from os import path
+
+from mozparsers import parse_user_interactions
+from mozparsers.shared_telemetry_utils import ParserError, static_assert
+
+COMPONENTS_PATH = path.abspath(
+ path.join(path.dirname(__file__), path.pardir, path.pardir)
+)
+sys.path.append(
+ path.join(COMPONENTS_PATH, "glean", "build_scripts", "glean_parser_ext")
+)
+import sys
+
+from string_table import StringTable
+
+# The banner/text at the top of the generated file.
+banner = """/* This file is auto-generated, only for internal use in
+ TelemetryUserInteraction.h, see gen_userinteraction_data.py. */
+"""
+
+file_header = """\
+#ifndef mozilla_TelemetryUserInteractionData_h
+#define mozilla_TelemetryUserInteractionData_h
+#include "core/UserInteractionInfo.h"
+"""
+
+file_footer = """\
+#endif // mozilla_TelemetryUserInteractionData_h
+"""
+
+
+def write_user_interaction_table(user_interactions, output, string_table):
+ head = """
+ namespace mozilla {
+ namespace Telemetry {
+ namespace UserInteractionID {
+ const static uint32_t UserInteractionCount = %d;
+ } // namespace UserInteractionID
+ } // namespace Telemetry
+ } // namespace mozilla
+ """
+
+ print(head % len(user_interactions), file=output)
+
+ print("namespace {", file=output)
+
+ table_name = "gUserInteractions"
+ print("constexpr UserInteractionInfo %s[] = {" % table_name, file=output)
+
+ for u in user_interactions:
+ name_index = string_table.stringIndex(u.label)
+ print(" UserInteractionInfo({}),".format(name_index), file=output)
+ print("};", file=output)
+
+ static_assert(
+ output,
+ "sizeof(%s) <= UINT32_MAX" % table_name,
+ "index overflow of UserInteractionInfo table %s" % table_name,
+ )
+
+ print("} // namespace", file=output)
+
+
+def main(output, *filenames):
+ # Load the UserInteraction data.
+ user_interactions = []
+ for filename in filenames:
+ try:
+ batch = parse_user_interactions.load_user_interactions(filename)
+ user_interactions.extend(batch)
+ except ParserError as ex:
+ print("\nError processing %s:\n%s\n" % (filename, str(ex)), file=sys.stderr)
+ sys.exit(1)
+
+ # Write the scalar data file.
+ print(banner, file=output)
+ print(file_header, file=output)
+
+ string_table = StringTable()
+
+ # Write the data for individual UserInteractions.
+ write_user_interaction_table(user_interactions, output, string_table)
+ print("", file=output)
+
+ # Write the string table.
+ string_table_name = "gUserInteractionsStringTable"
+ string_table.writeDefinition(output, string_table_name)
+ static_assert(
+ output, "sizeof(%s) <= UINT32_MAX" % string_table_name, "index overflow"
+ )
+ print("", file=output)
+
+ print(file_footer, file=output)
+
+
+if __name__ == "__main__":
+ main(sys.stdout, *sys.argv[1:])
diff --git a/toolkit/components/telemetry/build_scripts/gen_userinteraction_phf.py b/toolkit/components/telemetry/build_scripts/gen_userinteraction_phf.py
new file mode 100644
index 0000000000..f1c7256414
--- /dev/null
+++ b/toolkit/components/telemetry/build_scripts/gen_userinteraction_phf.py
@@ -0,0 +1,70 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from mozparsers.shared_telemetry_utils import ParserError
+from perfecthash import PerfectHash
+
+PHFSIZE = 1024
+
+import sys
+
+from mozparsers import parse_user_interactions
+
+banner = """/* This file is auto-generated, see gen_userinteraction_phf.py. */
+"""
+
+header = """
+#ifndef mozilla_TelemetryUserInteractionNameMap_h
+#define mozilla_TelemetryUserInteractionNameMap_h
+
+#include "mozilla/PerfectHash.h"
+
+namespace mozilla {
+namespace Telemetry {
+"""
+
+footer = """
+} // namespace mozilla
+} // namespace Telemetry
+#endif // mozilla_TelemetryUserInteractionNameMap_h
+"""
+
+
+def main(output, *filenames):
+ """
+ Generate a Perfect Hash Table for the UserInteraction name -> UserInteraction ID lookup.
+ The table is immutable once generated and we can avoid any dynamic memory allocation.
+ """
+
+ output.write(banner)
+ output.write(header)
+
+ try:
+ user_interactions = list(parse_user_interactions.from_files(filenames))
+ except ParserError as ex:
+ print("\nError processing UserInteractions:\n" + str(ex) + "\n")
+ sys.exit(1)
+
+ user_interactions = [
+ (bytearray(ui.label, "ascii"), idx)
+ for (idx, ui) in enumerate(user_interactions)
+ ]
+ name_phf = PerfectHash(user_interactions, PHFSIZE)
+
+ output.write(
+ name_phf.cxx_codegen(
+ name="UserInteractionIDByNameLookup",
+ entry_type="uint32_t",
+ lower_entry=lambda x: str(x[1]),
+ key_type="const nsACString&",
+ key_bytes="aKey.BeginReading()",
+ key_length="aKey.Length()",
+ )
+ )
+
+ output.write(footer)
+
+
+if __name__ == "__main__":
+ main(sys.stdout, *sys.argv[1:])
diff --git a/toolkit/components/telemetry/build_scripts/mozparsers/__init__.py b/toolkit/components/telemetry/build_scripts/mozparsers/__init__.py
new file mode 100644
index 0000000000..c580d191c1
--- /dev/null
+++ b/toolkit/components/telemetry/build_scripts/mozparsers/__init__.py
@@ -0,0 +1,3 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
diff --git a/toolkit/components/telemetry/build_scripts/mozparsers/parse_events.py b/toolkit/components/telemetry/build_scripts/mozparsers/parse_events.py
new file mode 100644
index 0000000000..09ed651917
--- /dev/null
+++ b/toolkit/components/telemetry/build_scripts/mozparsers/parse_events.py
@@ -0,0 +1,477 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import atexit
+import itertools
+import re
+import string
+
+import yaml
+
+from . import shared_telemetry_utils as utils
+from .shared_telemetry_utils import ParserError
+
+atexit.register(ParserError.exit_func)
+
+MAX_CATEGORY_NAME_LENGTH = 30
+MAX_METHOD_NAME_LENGTH = 20
+MAX_OBJECT_NAME_LENGTH = 20
+MAX_EXTRA_KEYS_COUNT = 10
+MAX_EXTRA_KEY_NAME_LENGTH = 15
+
+IDENTIFIER_PATTERN = r"^[a-zA-Z][a-zA-Z0-9_.]*[a-zA-Z0-9]$"
+
+
+def nice_type_name(t):
+ if issubclass(t, str):
+ return "string"
+ return t.__name__
+
+
+def convert_to_cpp_identifier(s, sep):
+ return string.capwords(s, sep).replace(sep, "")
+
+
+class OneOf:
+ """This is a placeholder type for the TypeChecker below.
+ It signals that the checked value should match one of the following arguments
+ passed to the TypeChecker constructor.
+ """
+
+ pass
+
+
+class AtomicTypeChecker:
+ """Validate a simple value against a given type"""
+
+ def __init__(self, instance_type):
+ self.instance_type = instance_type
+
+ def check(self, identifier, key, value):
+ if not isinstance(value, self.instance_type):
+ ParserError(
+ "%s: Failed type check for %s - expected %s, got %s."
+ % (
+ identifier,
+ key,
+ nice_type_name(self.instance_type),
+ nice_type_name(type(value)),
+ )
+ ).handle_later()
+
+
+class MultiTypeChecker:
+ """Validate a simple value against a list of possible types"""
+
+ def __init__(self, *instance_types):
+ if not instance_types:
+ raise Exception("At least one instance type is required.")
+ self.instance_types = instance_types
+
+ def check(self, identifier, key, value):
+ if not any(isinstance(value, i) for i in self.instance_types):
+ ParserError(
+ "%s: Failed type check for %s - got %s, expected one of:\n%s"
+ % (
+ identifier,
+ key,
+ nice_type_name(type(value)),
+ " or ".join(map(nice_type_name, self.instance_types)),
+ )
+ ).handle_later()
+
+
+class ListTypeChecker:
+ """Validate a list of values against a given type"""
+
+ def __init__(self, instance_type):
+ self.instance_type = instance_type
+
+ def check(self, identifier, key, value):
+ if len(value) < 1:
+ ParserError(
+ "%s: Failed check for %s - list should not be empty."
+ % (identifier, key)
+ ).handle_now()
+
+ for x in value:
+ if not isinstance(x, self.instance_type):
+ ParserError(
+ "%s: Failed type check for %s - expected list value type %s, got"
+ " %s."
+ % (
+ identifier,
+ key,
+ nice_type_name(self.instance_type),
+ nice_type_name(type(x)),
+ )
+ ).handle_later()
+
+
+class DictTypeChecker:
+ """Validate keys and values of a dict against a given type"""
+
+ def __init__(self, keys_instance_type, values_instance_type):
+ self.keys_instance_type = keys_instance_type
+ self.values_instance_type = values_instance_type
+
+ def check(self, identifier, key, value):
+ if len(value.keys()) < 1:
+ ParserError(
+ "%s: Failed check for %s - dict should not be empty."
+ % (identifier, key)
+ ).handle_now()
+ for x in value.keys():
+ if not isinstance(x, self.keys_instance_type):
+ ParserError(
+ "%s: Failed dict type check for %s - expected key type %s, got "
+ "%s."
+ % (
+ identifier,
+ key,
+ nice_type_name(self.keys_instance_type),
+ nice_type_name(type(x)),
+ )
+ ).handle_later()
+ for k, v in value.items():
+ if not isinstance(v, self.values_instance_type):
+ ParserError(
+ "%s: Failed dict type check for %s - "
+ "expected value type %s for key %s, got %s."
+ % (
+ identifier,
+ key,
+ nice_type_name(self.values_instance_type),
+ k,
+ nice_type_name(type(v)),
+ )
+ ).handle_later()
+
+
+def type_check_event_fields(identifier, name, definition):
+ """Perform a type/schema check on the event definition."""
+ REQUIRED_FIELDS = {
+ "objects": ListTypeChecker(str),
+ "bug_numbers": ListTypeChecker(int),
+ "notification_emails": ListTypeChecker(str),
+ "record_in_processes": ListTypeChecker(str),
+ "description": AtomicTypeChecker(str),
+ "products": ListTypeChecker(str),
+ }
+ OPTIONAL_FIELDS = {
+ "methods": ListTypeChecker(str),
+ "release_channel_collection": AtomicTypeChecker(str),
+ "expiry_version": AtomicTypeChecker(str),
+ "extra_keys": DictTypeChecker(str, str),
+ "operating_systems": ListTypeChecker(str),
+ }
+ ALL_FIELDS = REQUIRED_FIELDS.copy()
+ ALL_FIELDS.update(OPTIONAL_FIELDS)
+
+ # Check that all the required fields are available.
+ missing_fields = [f for f in REQUIRED_FIELDS.keys() if f not in definition]
+ if len(missing_fields) > 0:
+ ParserError(
+ identifier + ": Missing required fields: " + ", ".join(missing_fields)
+ ).handle_now()
+
+ # Is there any unknown field?
+ unknown_fields = [f for f in definition.keys() if f not in ALL_FIELDS]
+ if len(unknown_fields) > 0:
+ ParserError(
+ identifier + ": Unknown fields: " + ", ".join(unknown_fields)
+ ).handle_later()
+
+ # Type-check fields.
+ for k, v in definition.items():
+ ALL_FIELDS[k].check(identifier, k, v)
+
+
+def string_check(identifier, field, value, min_length=1, max_length=None, regex=None):
+ # Length check.
+ if len(value) < min_length:
+ ParserError(
+ "%s: Value '%s' for field %s is less than minimum length of %d."
+ % (identifier, value, field, min_length)
+ ).handle_later()
+ if max_length and len(value) > max_length:
+ ParserError(
+ "%s: Value '%s' for field %s is greater than maximum length of %d."
+ % (identifier, value, field, max_length)
+ ).handle_later()
+ # Regex check.
+ if regex and not re.match(regex, value):
+ ParserError(
+ '%s: String value "%s" for %s is not matching pattern "%s".'
+ % (identifier, value, field, regex)
+ ).handle_later()
+
+
+class EventData:
+ """A class representing one event."""
+
+ def __init__(self, category, name, definition, strict_type_checks=False):
+ self._category = category
+ self._name = name
+ self._definition = definition
+ self._strict_type_checks = strict_type_checks
+
+ type_check_event_fields(self.identifier, name, definition)
+
+ # Check method & object string patterns.
+ if strict_type_checks:
+ for method in self.methods:
+ string_check(
+ self.identifier,
+ field="methods",
+ value=method,
+ min_length=1,
+ max_length=MAX_METHOD_NAME_LENGTH,
+ regex=IDENTIFIER_PATTERN,
+ )
+ for obj in self.objects:
+ string_check(
+ self.identifier,
+ field="objects",
+ value=obj,
+ min_length=1,
+ max_length=MAX_OBJECT_NAME_LENGTH,
+ regex=IDENTIFIER_PATTERN,
+ )
+
+ # Check release_channel_collection
+ rcc_key = "release_channel_collection"
+ rcc = definition.get(rcc_key, "opt-in")
+ allowed_rcc = ["opt-in", "opt-out"]
+ if rcc not in allowed_rcc:
+ ParserError(
+ "%s: Value for %s should be one of: %s"
+ % (self.identifier, rcc_key, ", ".join(allowed_rcc))
+ ).handle_later()
+
+ # Check record_in_processes.
+ record_in_processes = definition.get("record_in_processes")
+ for proc in record_in_processes:
+ if not utils.is_valid_process_name(proc):
+ ParserError(
+ self.identifier + ": Unknown value in record_in_processes: " + proc
+ ).handle_later()
+
+ # Check products.
+ products = definition.get("products")
+ for product in products:
+ if not utils.is_valid_product(product) and self._strict_type_checks:
+ ParserError(
+ self.identifier + ": Unknown value in products: " + product
+ ).handle_later()
+ if utils.is_geckoview_streaming_product(product):
+ ParserError(
+ "{}: Product `{}` unsupported for Event Telemetry".format(
+ self.identifier, product
+ )
+ ).handle_later()
+
+ # Check operating_systems.
+ operating_systems = definition.get("operating_systems", [])
+ for operating_system in operating_systems:
+ if not utils.is_valid_os(operating_system):
+ ParserError(
+ self.identifier
+ + ": Unknown value in operating_systems: "
+ + operating_system
+ ).handle_later()
+
+ # Check extra_keys.
+ extra_keys = definition.get("extra_keys", {})
+ if len(extra_keys.keys()) > MAX_EXTRA_KEYS_COUNT:
+ ParserError(
+ "%s: Number of extra_keys exceeds limit %d."
+ % (self.identifier, MAX_EXTRA_KEYS_COUNT)
+ ).handle_later()
+ for key in extra_keys.keys():
+ string_check(
+ self.identifier,
+ field="extra_keys",
+ value=key,
+ min_length=1,
+ max_length=MAX_EXTRA_KEY_NAME_LENGTH,
+ regex=IDENTIFIER_PATTERN,
+ )
+
+ # Check expiry.
+ if "expiry_version" not in definition:
+ ParserError(
+ "%s: event is missing required field expiry_version" % (self.identifier)
+ ).handle_later()
+
+ # Finish setup.
+ # Historical versions of Events.yaml may contain expiration versions
+ # using the deprecated format 'N.Na1'. Those scripts set
+ # self._strict_type_checks to false.
+ expiry_version = definition.get("expiry_version", "never")
+ if (
+ not utils.validate_expiration_version(expiry_version)
+ and self._strict_type_checks
+ ):
+ ParserError(
+ "{}: invalid expiry_version: {}.".format(
+ self.identifier, expiry_version
+ )
+ ).handle_now()
+ definition["expiry_version"] = utils.add_expiration_postfix(expiry_version)
+
+ @property
+ def category(self):
+ return self._category
+
+ @property
+ def category_cpp(self):
+ # Transform e.g. category.example into CategoryExample.
+ return convert_to_cpp_identifier(self._category, ".")
+
+ @property
+ def name(self):
+ return self._name
+
+ @property
+ def identifier(self):
+ return self.category + "#" + self.name
+
+ @property
+ def methods(self):
+ return self._definition.get("methods", [self.name])
+
+ @property
+ def objects(self):
+ return self._definition.get("objects")
+
+ @property
+ def record_in_processes(self):
+ return self._definition.get("record_in_processes")
+
+ @property
+ def record_in_processes_enum(self):
+ """Get the non-empty list of flags representing the processes to record data in"""
+ return [utils.process_name_to_enum(p) for p in self.record_in_processes]
+
+ @property
+ def products(self):
+ """Get the non-empty list of products to record data on"""
+ return self._definition.get("products")
+
+ @property
+ def products_enum(self):
+ """Get the non-empty list of flags representing products to record data on"""
+ return [utils.product_name_to_enum(p) for p in self.products]
+
+ @property
+ def expiry_version(self):
+ return self._definition.get("expiry_version")
+
+ @property
+ def operating_systems(self):
+ """Get the list of operating systems to record data on"""
+ return self._definition.get("operating_systems", ["all"])
+
+ def record_on_os(self, target_os):
+ """Check if this probe should be recorded on the passed os."""
+ os = self.operating_systems
+ if "all" in os:
+ return True
+
+ canonical_os = utils.canonical_os(target_os)
+
+ if "unix" in os and canonical_os in utils.UNIX_LIKE_OS:
+ return True
+
+ return canonical_os in os
+
+ @property
+ def enum_labels(self):
+ def enum(method_name, object_name):
+ m = convert_to_cpp_identifier(method_name, "_")
+ o = convert_to_cpp_identifier(object_name, "_")
+ return m + "_" + o
+
+ combinations = itertools.product(self.methods, self.objects)
+ return [enum(t[0], t[1]) for t in combinations]
+
+ @property
+ def dataset(self):
+ """Get the nsITelemetry constant equivalent for release_channel_collection."""
+ rcc = self.dataset_short
+ if rcc == "opt-out":
+ return "nsITelemetry::DATASET_ALL_CHANNELS"
+ return "nsITelemetry::DATASET_PRERELEASE_CHANNELS"
+
+ @property
+ def dataset_short(self):
+ """Get the short name of the chosen release channel collection policy for the event."""
+ # The collection policy is optional, but we still define a default
+ # behaviour for it.
+ return self._definition.get("release_channel_collection", "opt-in")
+
+ @property
+ def extra_keys(self):
+ return list(sorted(self._definition.get("extra_keys", {}).keys()))
+
+
+def load_events(filename, strict_type_checks):
+ """Parses a YAML file containing the event definitions.
+
+ :param filename: the YAML file containing the event definitions.
+ :strict_type_checks A boolean indicating whether to use the stricter type checks.
+ :raises ParserError: if the event file cannot be opened or parsed.
+ """
+
+ # Parse the event definitions from the YAML file.
+ events = None
+ try:
+ with open(filename, "r") as f:
+ events = yaml.safe_load(f)
+ except IOError as e:
+ ParserError("Error opening " + filename + ": " + str(e) + ".").handle_now()
+ except ParserError as e:
+ ParserError(
+ "Error parsing events in " + filename + ": " + str(e) + "."
+ ).handle_now()
+
+ event_list = []
+
+ # Events are defined in a fixed two-level hierarchy within the definition file.
+ # The first level contains the category (group name), while the second level contains
+ # the event names and definitions, e.g.:
+ # category.name:
+ # event_name:
+ # <event definition>
+ # ...
+ # ...
+ for category_name, category in sorted(events.items()):
+ string_check(
+ "top level structure",
+ field="category",
+ value=category_name,
+ min_length=1,
+ max_length=MAX_CATEGORY_NAME_LENGTH,
+ regex=IDENTIFIER_PATTERN,
+ )
+
+ # Make sure that the category has at least one entry in it.
+ if not category or len(category) == 0:
+ ParserError(
+ "Category " + category_name + " must contain at least one entry."
+ ).handle_now()
+
+ for name, entry in sorted(category.items()):
+ string_check(
+ category_name,
+ field="event name",
+ value=name,
+ min_length=1,
+ max_length=MAX_METHOD_NAME_LENGTH,
+ regex=IDENTIFIER_PATTERN,
+ )
+ event_list.append(EventData(category_name, name, entry, strict_type_checks))
+
+ return event_list
diff --git a/toolkit/components/telemetry/build_scripts/mozparsers/parse_histograms.py b/toolkit/components/telemetry/build_scripts/mozparsers/parse_histograms.py
new file mode 100644
index 0000000000..626188bf06
--- /dev/null
+++ b/toolkit/components/telemetry/build_scripts/mozparsers/parse_histograms.py
@@ -0,0 +1,836 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import atexit
+import collections
+import itertools
+import json
+import math
+import os
+import re
+from collections import OrderedDict
+from ctypes import c_int
+
+from . import shared_telemetry_utils as utils
+from .shared_telemetry_utils import ParserError
+
+atexit.register(ParserError.exit_func)
+
+# Constants.
+MAX_LABEL_LENGTH = 20
+MAX_LABEL_COUNT = 100
+MAX_KEY_COUNT = 30
+MAX_KEY_LENGTH = 20
+MIN_CATEGORICAL_BUCKET_COUNT = 50
+CPP_IDENTIFIER_PATTERN = "^[a-z][a-z0-9_]+[a-z0-9]$"
+
+ALWAYS_ALLOWED_KEYS = [
+ "kind",
+ "description",
+ "operating_systems",
+ "expires_in_version",
+ "alert_emails",
+ "keyed",
+ "releaseChannelCollection",
+ "bug_numbers",
+ "keys",
+ "record_in_processes",
+ "record_into_store",
+ "products",
+]
+
+BASE_DOC_URL = (
+ "https://firefox-source-docs.mozilla.org/toolkit/components/" "telemetry/telemetry/"
+)
+HISTOGRAMS_DOC_URL = BASE_DOC_URL + "collection/histograms.html"
+SCALARS_DOC_URL = BASE_DOC_URL + "collection/scalars.html"
+
+GECKOVIEW_STREAMING_SUPPORTED_KINDS = [
+ "linear",
+ "exponential",
+ "categorical",
+]
+
+
+def linear_buckets(dmin, dmax, n_buckets):
+ ret_array = [0] * n_buckets
+ dmin = float(dmin)
+ dmax = float(dmax)
+ for i in range(1, n_buckets):
+ linear_range = (dmin * (n_buckets - 1 - i) + dmax * (i - 1)) / (n_buckets - 2)
+ ret_array[i] = int(linear_range + 0.5)
+ return ret_array
+
+
+def exponential_buckets(dmin, dmax, n_buckets):
+ log_max = math.log(dmax)
+ bucket_index = 2
+ ret_array = [0] * n_buckets
+ current = dmin
+ ret_array[1] = current
+ for bucket_index in range(2, n_buckets):
+ log_current = math.log(current)
+ log_ratio = (log_max - log_current) / (n_buckets - bucket_index)
+ log_next = log_current + log_ratio
+ next_value = int(math.floor(math.exp(log_next) + 0.5))
+ if next_value > current:
+ current = next_value
+ else:
+ current = current + 1
+ ret_array[bucket_index] = current
+ return ret_array
+
+
+allowlists = None
+
+
+def load_allowlist():
+ global allowlists
+ try:
+ parsers_path = os.path.realpath(os.path.dirname(__file__))
+ # The parsers live in build_scripts/parsers in the Telemetry module, while
+ # the histogram-allowlists file lives in the root of the module. Account
+ # for that when looking for the allowlist.
+ # NOTE: if the parsers are moved, this logic will need to be updated.
+ telemetry_module_path = os.path.abspath(
+ os.path.join(parsers_path, os.pardir, os.pardir)
+ )
+ allowlist_path = os.path.join(
+ telemetry_module_path, "histogram-allowlists.json"
+ )
+ with open(allowlist_path, "r") as f:
+ try:
+ allowlists = json.load(f)
+ for name, allowlist in allowlists.items():
+ allowlists[name] = set(allowlist)
+ except ValueError:
+ ParserError("Error parsing allowlist: %s" % allowlist_path).handle_now()
+ except IOError:
+ allowlists = None
+ ParserError("Unable to parse allowlist: %s." % allowlist_path).handle_now()
+
+
+class Histogram:
+ """A class for representing a histogram definition."""
+
+ def __init__(self, name, definition, strict_type_checks=False):
+ """Initialize a histogram named name with the given definition.
+ definition is a dict-like object that must contain at least the keys:
+
+ - 'kind': The kind of histogram. Must be one of 'boolean', 'flag',
+ 'count', 'enumerated', 'linear', or 'exponential'.
+ - 'description': A textual description of the histogram.
+ - 'strict_type_checks': A boolean indicating whether to use the new, stricter type checks.
+ The server-side still has to deal with old, oddly typed
+ submissions, so we have to skip them there by default.
+ """
+ self._strict_type_checks = strict_type_checks
+ self.verify_attributes(name, definition)
+ self._name = name
+ self._description = definition["description"]
+ self._kind = definition["kind"]
+ self._keys = definition.get("keys", [])
+ self._keyed = definition.get("keyed", False)
+ self._expiration = definition.get("expires_in_version")
+ self._labels = definition.get("labels", [])
+ self._record_in_processes = definition.get("record_in_processes")
+ self._record_into_store = definition.get("record_into_store", ["main"])
+ self._products = definition.get("products")
+ self._operating_systems = definition.get("operating_systems", ["all"])
+
+ self.compute_bucket_parameters(definition)
+ self.set_nsITelemetry_kind()
+ self.set_dataset(definition)
+
+ def name(self):
+ """Return the name of the histogram."""
+ return self._name
+
+ def description(self):
+ """Return the description of the histogram."""
+ return self._description
+
+ def kind(self):
+ """Return the kind of the histogram.
+ Will be one of 'boolean', 'flag', 'count', 'enumerated', 'categorical', 'linear',
+ or 'exponential'."""
+ return self._kind
+
+ def expiration(self):
+ """Return the expiration version of the histogram."""
+ return self._expiration
+
+ def nsITelemetry_kind(self):
+ """Return the nsITelemetry constant corresponding to the kind of
+ the histogram."""
+ return self._nsITelemetry_kind
+
+ def low(self):
+ """Return the lower bound of the histogram."""
+ return self._low
+
+ def high(self):
+ """Return the high bound of the histogram."""
+ return self._high
+
+ def n_buckets(self):
+ """Return the number of buckets in the histogram."""
+ return self._n_buckets
+
+ def keyed(self):
+ """Returns True if this a keyed histogram, false otherwise."""
+ return self._keyed
+
+ def keys(self):
+ """Returns a list of allowed keys for keyed histogram, [] for others."""
+ return self._keys
+
+ def dataset(self):
+ """Returns the dataset this histogram belongs into."""
+ return self._dataset
+
+ def labels(self):
+ """Returns a list of labels for a categorical histogram, [] for others."""
+ return self._labels
+
+ def record_in_processes(self):
+ """Returns a list of processes this histogram is permitted to record in."""
+ return self._record_in_processes
+
+ def record_in_processes_enum(self):
+ """Get the non-empty list of flags representing the processes to record data in"""
+ return [utils.process_name_to_enum(p) for p in self.record_in_processes()]
+
+ def products(self):
+ """Get the non-empty list of products to record data on"""
+ return self._products
+
+ def products_enum(self):
+ """Get the non-empty list of flags representing products to record data on"""
+ return [utils.product_name_to_enum(p) for p in self.products()]
+
+ def operating_systems(self):
+ """Get the list of operating systems to record data on"""
+ return self._operating_systems
+
+ def record_on_os(self, target_os):
+ """Check if this probe should be recorded on the passed os."""
+ os = self.operating_systems()
+ if "all" in os:
+ return True
+
+ canonical_os = utils.canonical_os(target_os)
+
+ if "unix" in os and canonical_os in utils.UNIX_LIKE_OS:
+ return True
+
+ return canonical_os in os
+
+ def record_into_store(self):
+ """Get the non-empty list of stores to record into"""
+ return self._record_into_store
+
+ def ranges(self):
+ """Return an array of lower bounds for each bucket in the histogram."""
+ bucket_fns = {
+ "boolean": linear_buckets,
+ "flag": linear_buckets,
+ "count": linear_buckets,
+ "enumerated": linear_buckets,
+ "categorical": linear_buckets,
+ "linear": linear_buckets,
+ "exponential": exponential_buckets,
+ }
+
+ if self._kind not in bucket_fns:
+ ParserError(
+ 'Unknown kind "%s" for histogram "%s".' % (self._kind, self._name)
+ ).handle_later()
+
+ fn = bucket_fns[self._kind]
+ return fn(self.low(), self.high(), self.n_buckets())
+
+ def compute_bucket_parameters(self, definition):
+ bucket_fns = {
+ "boolean": Histogram.boolean_flag_bucket_parameters,
+ "flag": Histogram.boolean_flag_bucket_parameters,
+ "count": Histogram.boolean_flag_bucket_parameters,
+ "enumerated": Histogram.enumerated_bucket_parameters,
+ "categorical": Histogram.categorical_bucket_parameters,
+ "linear": Histogram.linear_bucket_parameters,
+ "exponential": Histogram.exponential_bucket_parameters,
+ }
+
+ if self._kind not in bucket_fns:
+ ParserError(
+ 'Unknown kind "%s" for histogram "%s".' % (self._kind, self._name)
+ ).handle_later()
+
+ fn = bucket_fns[self._kind]
+ self.set_bucket_parameters(*fn(definition))
+
+ def verify_attributes(self, name, definition):
+ general_keys = ALWAYS_ALLOWED_KEYS + ["low", "high", "n_buckets"]
+
+ table = {
+ "boolean": ALWAYS_ALLOWED_KEYS,
+ "flag": ALWAYS_ALLOWED_KEYS,
+ "count": ALWAYS_ALLOWED_KEYS,
+ "enumerated": ALWAYS_ALLOWED_KEYS + ["n_values"],
+ "categorical": ALWAYS_ALLOWED_KEYS + ["labels", "n_values"],
+ "linear": general_keys,
+ "exponential": general_keys,
+ }
+ # We removed extended_statistics_ok on the client, but the server-side,
+ # where _strict_type_checks==False, has to deal with historical data.
+ if not self._strict_type_checks:
+ table["exponential"].append("extended_statistics_ok")
+
+ kind = definition["kind"]
+ if kind not in table:
+ ParserError(
+ 'Unknown kind "%s" for histogram "%s".' % (kind, name)
+ ).handle_later()
+ allowed_keys = table[kind]
+
+ self.check_name(name)
+ self.check_keys(name, definition, allowed_keys)
+ self.check_keys_field(name, definition)
+ self.check_field_types(name, definition)
+ self.check_allowlisted_kind(name, definition)
+ self.check_allowlistable_fields(name, definition)
+ self.check_expiration(name, definition)
+ self.check_label_values(name, definition)
+ self.check_record_in_processes(name, definition)
+ self.check_products(name, definition)
+ self.check_operating_systems(name, definition)
+ self.check_record_into_store(name, definition)
+
+ def check_name(self, name):
+ if "#" in name:
+ ParserError(
+ 'Error for histogram name "%s": "#" is not allowed.' % (name)
+ ).handle_later()
+
+ # Avoid C++ identifier conflicts between histogram enums and label enum names.
+ if name.startswith("LABELS_"):
+ ParserError(
+ 'Error for histogram name "%s": can not start with "LABELS_".' % (name)
+ ).handle_later()
+
+ # To make it easier to generate C++ identifiers from this etc., we restrict
+ # the histogram names to a strict pattern.
+ # We skip this on the server to avoid failures with old Histogram.json revisions.
+ if self._strict_type_checks:
+ if not re.match(CPP_IDENTIFIER_PATTERN, name, re.IGNORECASE):
+ ParserError(
+ 'Error for histogram name "%s": name does not conform to "%s"'
+ % (name, CPP_IDENTIFIER_PATTERN)
+ ).handle_later()
+
+ def check_expiration(self, name, definition):
+ field = "expires_in_version"
+ expiration = definition.get(field)
+
+ if not expiration:
+ return
+
+ # We forbid new probes from using "expires_in_version" : "default" field/value pair.
+ # Old ones that use this are added to the allowlist.
+ if (
+ expiration == "default"
+ and allowlists is not None
+ and name not in allowlists["expiry_default"]
+ ):
+ ParserError(
+ 'New histogram "%s" cannot have "default" %s value.' % (name, field)
+ ).handle_later()
+
+ # Historical editions of Histograms.json can have the deprecated
+ # expiration format 'N.Na1'. Fortunately, those scripts set
+ # self._strict_type_checks to false.
+ if (
+ expiration != "default"
+ and not utils.validate_expiration_version(expiration)
+ and self._strict_type_checks
+ ):
+ ParserError(
+ (
+ "Error for histogram {} - invalid {}: {}."
+ "\nSee: {}#expires-in-version"
+ ).format(name, field, expiration, HISTOGRAMS_DOC_URL)
+ ).handle_later()
+
+ expiration = utils.add_expiration_postfix(expiration)
+
+ definition[field] = expiration
+
+ def check_label_values(self, name, definition):
+ labels = definition.get("labels")
+ if not labels:
+ return
+
+ invalid = filter(lambda l: len(l) > MAX_LABEL_LENGTH, labels)
+ if len(list(invalid)) > 0:
+ ParserError(
+ 'Label values for "%s" exceed length limit of %d: %s'
+ % (name, MAX_LABEL_LENGTH, ", ".join(invalid))
+ ).handle_later()
+
+ if len(labels) > MAX_LABEL_COUNT:
+ ParserError(
+ 'Label count for "%s" exceeds limit of %d' % (name, MAX_LABEL_COUNT)
+ ).handle_now()
+
+ # To make it easier to generate C++ identifiers from this etc., we restrict
+ # the label values to a strict pattern.
+ invalid = filter(
+ lambda l: not re.match(CPP_IDENTIFIER_PATTERN, l, re.IGNORECASE), labels
+ )
+ if len(list(invalid)) > 0:
+ ParserError(
+ 'Label values for %s are not matching pattern "%s": %s'
+ % (name, CPP_IDENTIFIER_PATTERN, ", ".join(invalid))
+ ).handle_later()
+
+ def check_record_in_processes(self, name, definition):
+ if not self._strict_type_checks:
+ return
+
+ field = "record_in_processes"
+ rip = definition.get(field)
+
+ DOC_URL = HISTOGRAMS_DOC_URL + "#record-in-processes"
+
+ if not rip:
+ ParserError(
+ 'Histogram "%s" must have a "%s" field:\n%s' % (name, field, DOC_URL)
+ ).handle_later()
+
+ for process in rip:
+ if not utils.is_valid_process_name(process):
+ ParserError(
+ 'Histogram "%s" has unknown process "%s" in %s.\n%s'
+ % (name, process, field, DOC_URL)
+ ).handle_later()
+
+ def check_products(self, name, definition):
+ if not self._strict_type_checks:
+ return
+
+ field = "products"
+ products = definition.get(field)
+
+ DOC_URL = HISTOGRAMS_DOC_URL + "#products"
+
+ if not products:
+ ParserError(
+ 'Histogram "%s" must have a "%s" field:\n%s' % (name, field, DOC_URL)
+ ).handle_now()
+
+ for product in products:
+ if not utils.is_valid_product(product):
+ ParserError(
+ 'Histogram "%s" has unknown product "%s" in %s.\n%s'
+ % (name, product, field, DOC_URL)
+ ).handle_later()
+ if utils.is_geckoview_streaming_product(product):
+ kind = definition.get("kind")
+ if kind not in GECKOVIEW_STREAMING_SUPPORTED_KINDS:
+ ParserError(
+ (
+ 'Histogram "%s" is of kind "%s" which is unsupported for '
+ 'product "%s".'
+ )
+ % (name, kind, product)
+ ).handle_later()
+ keyed = definition.get("keyed")
+ if keyed:
+ ParserError(
+ 'Keyed histograms like "%s" are unsupported for product "%s"'
+ % (name, product)
+ ).handle_later()
+
+ def check_operating_systems(self, name, definition):
+ if not self._strict_type_checks:
+ return
+
+ field = "operating_systems"
+ operating_systems = definition.get(field)
+
+ DOC_URL = HISTOGRAMS_DOC_URL + "#operating-systems"
+
+ if not operating_systems:
+ # operating_systems is optional
+ return
+
+ for operating_system in operating_systems:
+ if not utils.is_valid_os(operating_system):
+ ParserError(
+ 'Histogram "%s" has unknown operating system "%s" in %s.\n%s'
+ % (name, operating_system, field, DOC_URL)
+ ).handle_later()
+
+ def check_record_into_store(self, name, definition):
+ if not self._strict_type_checks:
+ return
+
+ field = "record_into_store"
+ DOC_URL = HISTOGRAMS_DOC_URL + "#record-into-store"
+
+ if field not in definition:
+ # record_into_store is optional
+ return
+
+ record_into_store = definition.get(field)
+ # record_into_store should not be empty
+ if not record_into_store:
+ ParserError(
+ 'Histogram "%s" has empty list of stores, which is not allowed.\n%s'
+ % (name, DOC_URL)
+ ).handle_later()
+
+ def check_keys_field(self, name, definition):
+ keys = definition.get("keys")
+ if not self._strict_type_checks or keys is None:
+ return
+
+ if not definition.get("keyed", False):
+ raise ValueError(
+ "'keys' field is not valid for %s; only allowed for keyed histograms."
+ % (name)
+ )
+
+ if len(keys) == 0:
+ raise ValueError("The key list for %s cannot be empty" % (name))
+
+ if len(keys) > MAX_KEY_COUNT:
+ raise ValueError(
+ "Label count for %s exceeds limit of %d" % (name, MAX_KEY_COUNT)
+ )
+
+ invalid = filter(lambda k: len(k) > MAX_KEY_LENGTH, keys)
+ if len(list(invalid)) > 0:
+ raise ValueError(
+ '"keys" values for %s are exceeding length "%d": %s'
+ % (name, MAX_KEY_LENGTH, ", ".join(invalid))
+ )
+
+ def check_allowlisted_kind(self, name, definition):
+ # We don't need to run any of these checks on the server.
+ if not self._strict_type_checks or allowlists is None:
+ return
+
+ # Disallow "flag" and "count" histograms on desktop, suggest to use
+ # scalars instead. Allow using these histograms on Android, as we
+ # don't support scalars there yet.
+ hist_kind = definition.get("kind")
+ android_target = "android" in definition.get("operating_systems", [])
+
+ if (
+ not android_target
+ and hist_kind in ["flag", "count"]
+ and name not in allowlists["kind"]
+ ):
+ ParserError(
+ (
+ 'Unsupported kind "%s" for histogram "%s":\n'
+ 'New "%s" histograms are not supported on Desktop, you should'
+ " use scalars instead:\n"
+ "%s\n"
+ "Are you trying to add a histogram on Android?"
+ ' Add "operating_systems": ["android"] to your histogram definition.'
+ )
+ % (hist_kind, name, hist_kind, SCALARS_DOC_URL)
+ ).handle_now()
+
+ # Check for the presence of fields that old histograms are allowlisted for.
+ def check_allowlistable_fields(self, name, definition):
+ # We don't need to run any of these checks on the server.
+ if not self._strict_type_checks:
+ return
+
+ # In the pipeline we don't have allowlists available.
+ if allowlists is None:
+ return
+
+ for field in ["alert_emails", "bug_numbers"]:
+ if field not in definition and name not in allowlists[field]:
+ ParserError(
+ 'New histogram "%s" must have a "%s" field.' % (name, field)
+ ).handle_later()
+ if field in definition and name in allowlists[field]:
+ msg = (
+ 'Histogram "%s" should be removed from the allowlist for "%s" in '
+ "histogram-allowlists.json."
+ )
+ ParserError(msg % (name, field)).handle_later()
+
+ def check_field_types(self, name, definition):
+ # Define expected types for the histogram properties.
+ type_checked_fields = {
+ "n_buckets": int,
+ "n_values": int,
+ "low": int,
+ "high": int,
+ "keyed": bool,
+ "expires_in_version": str,
+ "kind": str,
+ "description": str,
+ "releaseChannelCollection": str,
+ }
+
+ # For list fields we check the items types.
+ type_checked_list_fields = {
+ "bug_numbers": int,
+ "alert_emails": str,
+ "labels": str,
+ "record_in_processes": str,
+ "keys": str,
+ "products": str,
+ "operating_systems": str,
+ "record_into_store": str,
+ }
+
+ # For the server-side, where _strict_type_checks==False, we want to
+ # skip the stricter type checks for these fields for dealing with
+ # historical data.
+ coerce_fields = ["low", "high", "n_values", "n_buckets"]
+ if not self._strict_type_checks:
+ # This handles some old non-numeric expressions.
+ EXPRESSIONS = {
+ "JS::GCReason::NUM_TELEMETRY_REASONS": 101,
+ "mozilla::StartupTimeline::MAX_EVENT_ID": 12,
+ }
+
+ def try_to_coerce_to_number(v):
+ if v in EXPRESSIONS:
+ return EXPRESSIONS[v]
+ try:
+ return eval(v, {})
+ except Exception:
+ return v
+
+ for key in [k for k in coerce_fields if k in definition]:
+ definition[key] = try_to_coerce_to_number(definition[key])
+ # This handles old "keyed":"true" definitions (bug 1271986).
+ if definition.get("keyed", None) == "true":
+ definition["keyed"] = True
+
+ def nice_type_name(t):
+ if t is str:
+ return "string"
+ return t.__name__
+
+ for key, key_type in type_checked_fields.items():
+ if key not in definition:
+ continue
+ if not isinstance(definition[key], key_type):
+ ParserError(
+ 'Value for key "{0}" in histogram "{1}" should be {2}.'.format(
+ key, name, nice_type_name(key_type)
+ )
+ ).handle_later()
+
+ # Make sure the max range is lower than or equal to INT_MAX
+ if "high" in definition and not c_int(definition["high"]).value > 0:
+ ParserError(
+ 'Value for high in histogram "{0}" should be lower or equal to INT_MAX.'.format(
+ nice_type_name(c_int)
+ )
+ ).handle_later()
+
+ for key, key_type in type_checked_list_fields.items():
+ if key not in definition:
+ continue
+ if not all(isinstance(x, key_type) for x in definition[key]):
+ ParserError(
+ 'All values for list "{0}" in histogram "{1}" should be of type'
+ " {2}.".format(key, name, nice_type_name(key_type))
+ ).handle_later()
+
+ def check_keys(self, name, definition, allowed_keys):
+ if not self._strict_type_checks:
+ return
+ for key in iter(definition.keys()):
+ if key not in allowed_keys:
+ ParserError(
+ 'Key "%s" is not allowed for histogram "%s".' % (key, name)
+ ).handle_later()
+
+ def set_bucket_parameters(self, low, high, n_buckets):
+ self._low = low
+ self._high = high
+ self._n_buckets = n_buckets
+ max_n_buckets = 101 if self._kind in ["enumerated", "categorical"] else 100
+ if (
+ allowlists is not None
+ and self._n_buckets > max_n_buckets
+ and type(self._n_buckets) is int
+ ):
+ if self._name not in allowlists["n_buckets"]:
+ ParserError(
+ 'New histogram "%s" is not permitted to have more than 100 buckets.\n'
+ "Histograms with large numbers of buckets use disproportionately high"
+ " amounts of resources. Contact a Telemetry peer (e.g. in #telemetry)"
+ " if you think an exception ought to be made:\n"
+ "https://wiki.mozilla.org/Modules/Toolkit#Telemetry" % self._name
+ ).handle_later()
+
+ @staticmethod
+ def boolean_flag_bucket_parameters(definition):
+ return (1, 2, 3)
+
+ @staticmethod
+ def linear_bucket_parameters(definition):
+ return (definition.get("low", 1), definition["high"], definition["n_buckets"])
+
+ @staticmethod
+ def enumerated_bucket_parameters(definition):
+ n_values = definition["n_values"]
+ return (1, n_values, n_values + 1)
+
+ @staticmethod
+ def categorical_bucket_parameters(definition):
+ # Categorical histograms default to 50 buckets to make working with them easier.
+ # Otherwise when adding labels later we run into problems with the pipeline not
+ # supporting bucket changes.
+ # This can be overridden using the n_values field.
+ n_values = max(
+ len(definition["labels"]),
+ definition.get("n_values", 0),
+ MIN_CATEGORICAL_BUCKET_COUNT,
+ )
+ return (1, n_values, n_values + 1)
+
+ @staticmethod
+ def exponential_bucket_parameters(definition):
+ return (definition.get("low", 1), definition["high"], definition["n_buckets"])
+
+ def set_nsITelemetry_kind(self):
+ # Pick a Telemetry implementation type.
+ types = {
+ "boolean": "BOOLEAN",
+ "flag": "FLAG",
+ "count": "COUNT",
+ "enumerated": "LINEAR",
+ "categorical": "CATEGORICAL",
+ "linear": "LINEAR",
+ "exponential": "EXPONENTIAL",
+ }
+
+ if self._kind not in types:
+ ParserError(
+ 'Unknown kind "%s" for histogram "%s".' % (self._kind, self._name)
+ ).handle_later()
+
+ self._nsITelemetry_kind = "nsITelemetry::HISTOGRAM_%s" % types[self._kind]
+
+ def set_dataset(self, definition):
+ datasets = {
+ "opt-in": "DATASET_PRERELEASE_CHANNELS",
+ "opt-out": "DATASET_ALL_CHANNELS",
+ }
+
+ value = definition.get("releaseChannelCollection", "opt-in")
+ if value not in datasets:
+ ParserError(
+ "Unknown value for releaseChannelCollection"
+ ' policy for histogram "%s".' % self._name
+ ).handle_later()
+
+ self._dataset = "nsITelemetry::" + datasets[value]
+
+
+# This hook function loads the histograms into an OrderedDict.
+# It will raise a ParserError if duplicate keys are found.
+def load_histograms_into_dict(ordered_pairs, strict_type_checks):
+ d = collections.OrderedDict()
+ for key, value in ordered_pairs:
+ if strict_type_checks and key in d:
+ ParserError(
+ "Found duplicate key in Histograms file: %s" % key
+ ).handle_later()
+ d[key] = value
+ return d
+
+
+# We support generating histograms from multiple different input files, not
+# just Histograms.json. For each file's basename, we have a specific
+# routine to parse that file, and return a dictionary mapping histogram
+# names to histogram parameters.
+def from_json(filename, strict_type_checks):
+ with open(filename, "r") as f:
+ try:
+
+ def hook(ps):
+ return load_histograms_into_dict(ps, strict_type_checks)
+
+ histograms = json.load(f, object_pairs_hook=hook)
+ except ValueError as e:
+ ParserError(
+ "error parsing histograms in %s: %s" % (filename, e)
+ ).handle_now()
+ return histograms
+
+
+def to_camel_case(property_name):
+ return re.sub(
+ "(^|_|-)([a-z0-9])",
+ lambda m: m.group(2).upper(),
+ property_name.strip("_").strip("-"),
+ )
+
+
+FILENAME_PARSERS = [
+ (lambda x: from_json if x.endswith(".json") else None),
+]
+
+
+def from_files(filenames, strict_type_checks=True):
+ """Return an iterator that provides a sequence of Histograms for
+ the histograms defined in filenames.
+ """
+ if strict_type_checks:
+ load_allowlist()
+
+ all_histograms = OrderedDict()
+ for filename in filenames:
+ parser = None
+ for checkFn in FILENAME_PARSERS:
+ parser = checkFn(os.path.basename(filename))
+ if parser is not None:
+ break
+
+ if parser is None:
+ ParserError("Don't know how to parse %s." % filename).handle_now()
+
+ histograms = parser(filename, strict_type_checks)
+
+ # OrderedDicts are important, because then the iteration order over
+ # the parsed histograms is stable, which makes the insertion into
+ # all_histograms stable, which makes ordering in generated files
+ # stable, which makes builds more deterministic.
+ if not isinstance(histograms, OrderedDict):
+ ParserError("Histogram parser did not provide an OrderedDict.").handle_now()
+
+ for name, definition in histograms.items():
+ if name in all_histograms:
+ ParserError('Duplicate histogram name "%s".' % name).handle_later()
+ all_histograms[name] = definition
+
+ # Check that histograms that were removed from Histograms.json etc.
+ # are also removed from the allowlists.
+ if allowlists is not None:
+ all_allowlist_entries = itertools.chain.from_iterable(iter(allowlists.values()))
+ orphaned = set(all_allowlist_entries) - set(all_histograms.keys())
+ if len(orphaned) > 0:
+ msg = (
+ "The following entries are orphaned and should be removed from "
+ "histogram-allowlists.json:\n%s"
+ )
+ ParserError(msg % (", ".join(sorted(orphaned)))).handle_later()
+
+ for name, definition in all_histograms.items():
+ yield Histogram(name, definition, strict_type_checks=strict_type_checks)
diff --git a/toolkit/components/telemetry/build_scripts/mozparsers/parse_scalars.py b/toolkit/components/telemetry/build_scripts/mozparsers/parse_scalars.py
new file mode 100644
index 0000000000..5ec591b393
--- /dev/null
+++ b/toolkit/components/telemetry/build_scripts/mozparsers/parse_scalars.py
@@ -0,0 +1,503 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import atexit
+import io
+import re
+
+import yaml
+
+from . import shared_telemetry_utils as utils
+from .shared_telemetry_utils import ParserError
+
+atexit.register(ParserError.exit_func)
+
+# The map of containing the allowed scalar types and their mapping to
+# nsITelemetry::SCALAR_TYPE_* type constants.
+
+BASE_DOC_URL = (
+ "https://firefox-source-docs.mozilla.org/toolkit/components/"
+ + "telemetry/telemetry/collection/scalars.html"
+)
+
+SCALAR_TYPES_MAP = {
+ "uint": "nsITelemetry::SCALAR_TYPE_COUNT",
+ "string": "nsITelemetry::SCALAR_TYPE_STRING",
+ "boolean": "nsITelemetry::SCALAR_TYPE_BOOLEAN",
+}
+
+
+class ScalarType:
+ """A class for representing a scalar definition."""
+
+ def __init__(self, category_name, probe_name, definition, strict_type_checks):
+ # Validate and set the name, so we don't need to pass it to the other
+ # validation functions.
+ self._strict_type_checks = strict_type_checks
+ self.validate_names(category_name, probe_name)
+ self._name = probe_name
+ self._category_name = category_name
+
+ # Validating the scalar definition.
+ self.validate_types(definition)
+ self.validate_values(definition)
+
+ # Everything is ok, set the rest of the data.
+ self._definition = definition
+ self._expires = utils.add_expiration_postfix(definition["expires"])
+
+ def validate_names(self, category_name, probe_name):
+ """Validate the category and probe name:
+ - Category name must be alpha-numeric + '.', no leading/trailing digit or '.'.
+ - Probe name must be alpha-numeric + '_', no leading/trailing digit or '_'.
+
+ :param category_name: the name of the category the probe is in.
+ :param probe_name: the name of the scalar probe.
+ :raises ParserError: if the length of the names exceeds the limit or they don't
+ conform our name specification.
+ """
+
+ # Enforce a maximum length on category and probe names.
+ MAX_NAME_LENGTH = 40
+ for n in [category_name, probe_name]:
+ if len(n) > MAX_NAME_LENGTH:
+ ParserError(
+ (
+ "Name '{}' exceeds maximum name length of {} characters.\n"
+ "See: {}#the-yaml-definition-file"
+ ).format(n, MAX_NAME_LENGTH, BASE_DOC_URL)
+ ).handle_later()
+
+ def check_name(name, error_msg_prefix, allowed_char_regexp):
+ # Check if we only have the allowed characters.
+ chars_regxp = r"^[a-zA-Z0-9" + allowed_char_regexp + r"]+$"
+ if not re.search(chars_regxp, name):
+ ParserError(
+ (
+ error_msg_prefix + " name must be alpha-numeric. Got: '{}'.\n"
+ "See: {}#the-yaml-definition-file"
+ ).format(name, BASE_DOC_URL)
+ ).handle_later()
+
+ # Don't allow leading/trailing digits, '.' or '_'.
+ if re.search(r"(^[\d\._])|([\d\._])$", name):
+ ParserError(
+ (
+ error_msg_prefix + " name must not have a leading/trailing "
+ "digit, a dot or underscore. Got: '{}'.\n"
+ " See: {}#the-yaml-definition-file"
+ ).format(name, BASE_DOC_URL)
+ ).handle_later()
+
+ check_name(category_name, "Category", r"\.")
+ check_name(probe_name, "Probe", r"_")
+
+ def validate_types(self, definition):
+ """This function performs some basic sanity checks on the scalar definition:
+ - Checks that all the required fields are available.
+ - Checks that all the fields have the expected types.
+
+ :param definition: the dictionary containing the scalar properties.
+ :raises ParserError: if a scalar definition field is of the wrong type.
+ :raises ParserError: if a required field is missing or unknown fields are present.
+ """
+
+ if not self._strict_type_checks:
+ return
+
+ def validate_notification_email(notification_email):
+ # Perform simple email validation to make sure it doesn't contain spaces or commas.
+ return not any(c in notification_email for c in [",", " "])
+
+ # The required and optional fields in a scalar type definition.
+ REQUIRED_FIELDS = {
+ "bug_numbers": list, # This contains ints. See LIST_FIELDS_CONTENT.
+ "description": str,
+ "expires": str,
+ "kind": str,
+ "notification_emails": list, # This contains strings. See LIST_FIELDS_CONTENT.
+ "record_in_processes": list,
+ "products": list,
+ }
+
+ OPTIONAL_FIELDS = {
+ "release_channel_collection": str,
+ "keyed": bool,
+ "keys": list,
+ "operating_systems": list,
+ "record_into_store": list,
+ }
+
+ # The types for the data within the fields that hold lists.
+ LIST_FIELDS_CONTENT = {
+ "bug_numbers": int,
+ "notification_emails": str,
+ "record_in_processes": str,
+ "products": str,
+ "keys": str,
+ "operating_systems": str,
+ "record_into_store": str,
+ }
+
+ # Concatenate the required and optional field definitions.
+ ALL_FIELDS = REQUIRED_FIELDS.copy()
+ ALL_FIELDS.update(OPTIONAL_FIELDS)
+
+ # Checks that all the required fields are available.
+ missing_fields = [f for f in REQUIRED_FIELDS.keys() if f not in definition]
+ if len(missing_fields) > 0:
+ ParserError(
+ self._name
+ + " - missing required fields: "
+ + ", ".join(missing_fields)
+ + ".\nSee: {}#required-fields".format(BASE_DOC_URL)
+ ).handle_later()
+
+ # Do we have any unknown field?
+ unknown_fields = [f for f in definition.keys() if f not in ALL_FIELDS]
+ if len(unknown_fields) > 0:
+ ParserError(
+ self._name
+ + " - unknown fields: "
+ + ", ".join(unknown_fields)
+ + ".\nSee: {}#required-fields".format(BASE_DOC_URL)
+ ).handle_later()
+
+ # Checks the type for all the fields.
+ wrong_type_names = [
+ "{} must be {}".format(f, str(ALL_FIELDS[f]))
+ for f in definition.keys()
+ if not isinstance(definition[f], ALL_FIELDS[f])
+ ]
+ if len(wrong_type_names) > 0:
+ ParserError(
+ self._name
+ + " - "
+ + ", ".join(wrong_type_names)
+ + ".\nSee: {}#required-fields".format(BASE_DOC_URL)
+ ).handle_later()
+
+ # Check that the email addresses doesn't contain spaces or commas
+ notification_emails = definition.get("notification_emails")
+ for notification_email in notification_emails:
+ if not validate_notification_email(notification_email):
+ ParserError(
+ self._name
+ + " - invalid email address: "
+ + notification_email
+ + ".\nSee: {}".format(BASE_DOC_URL)
+ ).handle_later()
+
+ # Check that the lists are not empty and that data in the lists
+ # have the correct types.
+ list_fields = [f for f in definition if isinstance(definition[f], list)]
+ for field in list_fields:
+ # Check for empty lists.
+ if len(definition[field]) == 0:
+ ParserError(
+ (
+ "Field '{}' for probe '{}' must not be empty"
+ + ".\nSee: {}#required-fields)"
+ ).format(field, self._name, BASE_DOC_URL)
+ ).handle_later()
+ # Check the type of the list content.
+ broken_types = [
+ not isinstance(v, LIST_FIELDS_CONTENT[field]) for v in definition[field]
+ ]
+ if any(broken_types):
+ ParserError(
+ (
+ "Field '{}' for probe '{}' must only contain values of type {}"
+ ".\nSee: {}#the-yaml-definition-file)"
+ ).format(
+ field,
+ self._name,
+ str(LIST_FIELDS_CONTENT[field]),
+ BASE_DOC_URL,
+ )
+ ).handle_later()
+
+ # Check that keys are only added to keyed scalars and that their values are valid
+ MAX_KEY_COUNT = 100
+ MAX_KEY_LENGTH = 72
+ keys = definition.get("keys")
+ if keys is not None:
+ if not definition.get("keyed", False):
+ ParserError(
+ self._name
+ + "- invalid field: "
+ + "\n`keys` field only valid for keyed histograms"
+ ).handle_later()
+
+ if len(keys) > MAX_KEY_COUNT:
+ ParserError(
+ self._name
+ + " - exceeding key count: "
+ + "\n`keys` values count must not exceed {}".format(MAX_KEY_COUNT)
+ ).handle_later()
+
+ invalid = list(filter(lambda k: len(k) > MAX_KEY_LENGTH, keys))
+ if len(invalid) > 0:
+ ParserError(
+ self._name
+ + " - invalid key value"
+ + "\n `keys` values are exceeding length {}:".format(MAX_KEY_LENGTH)
+ + ", ".join(invalid)
+ ).handle_later()
+
+ def validate_values(self, definition):
+ """This function checks that the fields have the correct values.
+
+ :param definition: the dictionary containing the scalar properties.
+ :raises ParserError: if a scalar definition field contains an unexpected value.
+ """
+
+ if not self._strict_type_checks:
+ return
+
+ # Validate the scalar kind.
+ scalar_kind = definition.get("kind")
+ if scalar_kind not in SCALAR_TYPES_MAP.keys():
+ ParserError(
+ self._name
+ + " - unknown scalar kind: "
+ + scalar_kind
+ + ".\nSee: {}".format(BASE_DOC_URL)
+ ).handle_later()
+
+ # Validate the collection policy.
+ collection_policy = definition.get("release_channel_collection", None)
+ if collection_policy and collection_policy not in ["opt-in", "opt-out"]:
+ ParserError(
+ self._name
+ + " - unknown collection policy: "
+ + collection_policy
+ + ".\nSee: {}#optional-fields".format(BASE_DOC_URL)
+ ).handle_later()
+
+ # Validate operating_systems.
+ operating_systems = definition.get("operating_systems", [])
+ for operating_system in operating_systems:
+ if not utils.is_valid_os(operating_system):
+ ParserError(
+ self._name
+ + " - invalid entry in operating_systems: "
+ + operating_system
+ + ".\nSee: {}#optional-fields".format(BASE_DOC_URL)
+ ).handle_later()
+
+ # Validate record_in_processes.
+ record_in_processes = definition.get("record_in_processes", [])
+ for proc in record_in_processes:
+ if not utils.is_valid_process_name(proc):
+ ParserError(
+ self._name
+ + " - unknown value in record_in_processes: "
+ + proc
+ + ".\nSee: {}".format(BASE_DOC_URL)
+ ).handle_later()
+
+ # Validate product.
+ products = definition.get("products", [])
+ for product in products:
+ if not utils.is_valid_product(product):
+ ParserError(
+ self._name
+ + " - unknown value in products: "
+ + product
+ + ".\nSee: {}".format(BASE_DOC_URL)
+ ).handle_later()
+ if utils.is_geckoview_streaming_product(product):
+ keyed = definition.get("keyed")
+ if keyed:
+ ParserError(
+ "%s - keyed Scalars not supported for product %s"
+ % (self._name, product)
+ ).handle_later()
+
+ # Validate the expiration version.
+ # Historical versions of Scalars.json may contain expiration versions
+ # using the deprecated format 'N.Na1'. Those scripts set
+ # self._strict_type_checks to false.
+ expires = definition.get("expires")
+ if not utils.validate_expiration_version(expires) and self._strict_type_checks:
+ ParserError(
+ "{} - invalid expires: {}.\nSee: {}#required-fields".format(
+ self._name, expires, BASE_DOC_URL
+ )
+ ).handle_later()
+
+ @property
+ def category(self):
+ """Get the category name"""
+ return self._category_name
+
+ @property
+ def name(self):
+ """Get the scalar name"""
+ return self._name
+
+ @property
+ def label(self):
+ """Get the scalar label generated from the scalar and category names."""
+ return self._category_name + "." + self._name
+
+ @property
+ def enum_label(self):
+ """Get the enum label generated from the scalar and category names. This is used to
+ generate the enum tables."""
+
+ # The scalar name can contain informations about its hierarchy (e.g. 'a.b.scalar').
+ # We can't have dots in C++ enums, replace them with an underscore. Also, make the
+ # label upper case for consistency with the histogram enums.
+ return self.label.replace(".", "_").upper()
+
+ @property
+ def bug_numbers(self):
+ """Get the list of related bug numbers"""
+ return self._definition["bug_numbers"]
+
+ @property
+ def description(self):
+ """Get the scalar description"""
+ return self._definition["description"]
+
+ @property
+ def expires(self):
+ """Get the scalar expiration"""
+ return self._expires
+
+ @property
+ def kind(self):
+ """Get the scalar kind"""
+ return self._definition["kind"]
+
+ @property
+ def keys(self):
+ """Get the allowed keys for this scalar or [] if there aren't any'"""
+ return self._definition.get("keys", [])
+
+ @property
+ def keyed(self):
+ """Boolean indicating whether this is a keyed scalar"""
+ return self._definition.get("keyed", False)
+
+ @property
+ def nsITelemetry_kind(self):
+ """Get the scalar kind constant defined in nsITelemetry"""
+ return SCALAR_TYPES_MAP.get(self.kind)
+
+ @property
+ def notification_emails(self):
+ """Get the list of notification emails"""
+ return self._definition["notification_emails"]
+
+ @property
+ def record_in_processes(self):
+ """Get the non-empty list of processes to record data in"""
+ # Before we added content process support in bug 1278556, we only recorded in the
+ # main process.
+ return self._definition.get("record_in_processes", ["main"])
+
+ @property
+ def record_in_processes_enum(self):
+ """Get the non-empty list of flags representing the processes to record data in"""
+ return [utils.process_name_to_enum(p) for p in self.record_in_processes]
+
+ @property
+ def products(self):
+ """Get the non-empty list of products to record data on"""
+ return self._definition.get("products")
+
+ @property
+ def products_enum(self):
+ """Get the non-empty list of flags representing products to record data on"""
+ return [utils.product_name_to_enum(p) for p in self.products]
+
+ @property
+ def dataset(self):
+ """Get the nsITelemetry constant equivalent to the chosen release channel collection
+ policy for the scalar.
+ """
+ rcc = self.dataset_short
+ table = {
+ "opt-in": "DATASET_PRERELEASE_CHANNELS",
+ "opt-out": "DATASET_ALL_CHANNELS",
+ }
+ return "nsITelemetry::" + table[rcc]
+
+ @property
+ def dataset_short(self):
+ """Get the short name of the chosen release channel collection policy for the scalar."""
+ # The collection policy is optional, but we still define a default
+ # behaviour for it.
+ return self._definition.get("release_channel_collection", "opt-in")
+
+ @property
+ def operating_systems(self):
+ """Get the list of operating systems to record data on"""
+ return self._definition.get("operating_systems", ["all"])
+
+ def record_on_os(self, target_os):
+ """Check if this probe should be recorded on the passed os."""
+ os = self.operating_systems
+ if "all" in os:
+ return True
+
+ canonical_os = utils.canonical_os(target_os)
+
+ if "unix" in os and canonical_os in utils.UNIX_LIKE_OS:
+ return True
+
+ return canonical_os in os
+
+ @property
+ def record_into_store(self):
+ """Get the list of stores this probe should be recorded into"""
+ return self._definition.get("record_into_store", ["main"])
+
+
+def load_scalars(filename, strict_type_checks=True):
+ """Parses a YAML file containing the scalar definition.
+
+ :param filename: the YAML file containing the scalars definition.
+ :raises ParserError: if the scalar file cannot be opened or parsed.
+ """
+
+ # Parse the scalar definitions from the YAML file.
+ scalars = None
+ try:
+ with io.open(filename, "r", encoding="utf-8") as f:
+ scalars = yaml.safe_load(f)
+ except IOError as e:
+ ParserError("Error opening " + filename + ": " + str(e)).handle_now()
+ except ValueError as e:
+ ParserError(
+ "Error parsing scalars in {}: {}"
+ ".\nSee: {}".format(filename, e, BASE_DOC_URL)
+ ).handle_now()
+
+ scalar_list = []
+
+ # Scalars are defined in a fixed two-level hierarchy within the definition file.
+ # The first level contains the category name, while the second level contains the
+ # probe name (e.g. "category.name: probe: ...").
+ for category_name in sorted(scalars):
+ category = scalars[category_name]
+
+ # Make sure that the category has at least one probe in it.
+ if not category or len(category) == 0:
+ ParserError(
+ 'Category "{}" must have at least one probe in it'
+ ".\nSee: {}".format(category_name, BASE_DOC_URL)
+ ).handle_later()
+
+ for probe_name in sorted(category):
+ # We found a scalar type. Go ahead and parse it.
+ scalar_info = category[probe_name]
+ scalar_list.append(
+ ScalarType(category_name, probe_name, scalar_info, strict_type_checks)
+ )
+
+ return scalar_list
diff --git a/toolkit/components/telemetry/build_scripts/mozparsers/parse_user_interactions.py b/toolkit/components/telemetry/build_scripts/mozparsers/parse_user_interactions.py
new file mode 100644
index 0000000000..6863d67ec4
--- /dev/null
+++ b/toolkit/components/telemetry/build_scripts/mozparsers/parse_user_interactions.py
@@ -0,0 +1,256 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import atexit
+import io
+import re
+
+import yaml
+
+from .shared_telemetry_utils import ParserError
+
+atexit.register(ParserError.exit_func)
+
+BASE_DOC_URL = (
+ "https://firefox-source-docs.mozilla.org/toolkit/components/"
+ + "telemetry/telemetry/collection/user_interactions.html"
+)
+
+
+class UserInteractionType:
+ """A class for representing a UserInteraction definition."""
+
+ def __init__(self, category_name, user_interaction_name, definition):
+ # Validate and set the name, so we don't need to pass it to the other
+ # validation functions.
+ self.validate_names(category_name, user_interaction_name)
+ self._name = user_interaction_name
+ self._category_name = category_name
+
+ # Validating the UserInteraction definition.
+ self.validate_types(definition)
+
+ # Everything is ok, set the rest of the data.
+ self._definition = definition
+
+ def validate_names(self, category_name, user_interaction_name):
+ """Validate the category and UserInteraction name:
+ - Category name must be alpha-numeric + '.', no leading/trailing digit or '.'.
+ - UserInteraction name must be alpha-numeric + '_', no leading/trailing digit or '_'.
+
+ :param category_name: the name of the category the UserInteraction is in.
+ :param user_interaction_name: the name of the UserInteraction.
+ :raises ParserError: if the length of the names exceeds the limit or they don't
+ conform our name specification.
+ """
+
+ # Enforce a maximum length on category and UserInteraction names.
+ MAX_NAME_LENGTH = 40
+ for n in [category_name, user_interaction_name]:
+ if len(n) > MAX_NAME_LENGTH:
+ ParserError(
+ (
+ "Name '{}' exceeds maximum name length of {} characters.\n"
+ "See: {}#the-yaml-definition-file"
+ ).format(n, MAX_NAME_LENGTH, BASE_DOC_URL)
+ ).handle_later()
+
+ def check_name(name, error_msg_prefix, allowed_char_regexp):
+ # Check if we only have the allowed characters.
+ chars_regxp = r"^[a-zA-Z0-9" + allowed_char_regexp + r"]+$"
+ if not re.search(chars_regxp, name):
+ ParserError(
+ (
+ error_msg_prefix + " name must be alpha-numeric. Got: '{}'.\n"
+ "See: {}#the-yaml-definition-file"
+ ).format(name, BASE_DOC_URL)
+ ).handle_later()
+
+ # Don't allow leading/trailing digits, '.' or '_'.
+ if re.search(r"(^[\d\._])|([\d\._])$", name):
+ ParserError(
+ (
+ error_msg_prefix + " name must not have a leading/trailing "
+ "digit, a dot or underscore. Got: '{}'.\n"
+ " See: {}#the-yaml-definition-file"
+ ).format(name, BASE_DOC_URL)
+ ).handle_later()
+
+ check_name(category_name, "Category", r"\.")
+ check_name(user_interaction_name, "UserInteraction", r"_")
+
+ def validate_types(self, definition):
+ """This function performs some basic sanity checks on the UserInteraction
+ definition:
+ - Checks that all the required fields are available.
+ - Checks that all the fields have the expected types.
+
+ :param definition: the dictionary containing the UserInteraction
+ properties.
+ :raises ParserError: if a UserInteraction definition field is of the
+ wrong type.
+ :raises ParserError: if a required field is missing or unknown fields are present.
+ """
+
+ # The required and optional fields in a UserInteraction definition.
+ REQUIRED_FIELDS = {
+ "bug_numbers": list, # This contains ints. See LIST_FIELDS_CONTENT.
+ "description": str,
+ }
+
+ # The types for the data within the fields that hold lists.
+ LIST_FIELDS_CONTENT = {
+ "bug_numbers": int,
+ }
+
+ ALL_FIELDS = REQUIRED_FIELDS.copy()
+
+ # Checks that all the required fields are available.
+ missing_fields = [f for f in REQUIRED_FIELDS.keys() if f not in definition]
+ if len(missing_fields) > 0:
+ ParserError(
+ self._name
+ + " - missing required fields: "
+ + ", ".join(missing_fields)
+ + ".\nSee: {}#required-fields".format(BASE_DOC_URL)
+ ).handle_later()
+
+ # Do we have any unknown field?
+ unknown_fields = [f for f in definition.keys() if f not in ALL_FIELDS]
+ if len(unknown_fields) > 0:
+ ParserError(
+ self._name
+ + " - unknown fields: "
+ + ", ".join(unknown_fields)
+ + ".\nSee: {}#required-fields".format(BASE_DOC_URL)
+ ).handle_later()
+
+ # Checks the type for all the fields.
+ wrong_type_names = [
+ "{} must be {}".format(f, str(ALL_FIELDS[f]))
+ for f in definition.keys()
+ if not isinstance(definition[f], ALL_FIELDS[f])
+ ]
+ if len(wrong_type_names) > 0:
+ ParserError(
+ self._name
+ + " - "
+ + ", ".join(wrong_type_names)
+ + ".\nSee: {}#required-fields".format(BASE_DOC_URL)
+ ).handle_later()
+
+ # Check that the lists are not empty and that data in the lists
+ # have the correct types.
+ list_fields = [f for f in definition if isinstance(definition[f], list)]
+ for field in list_fields:
+ # Check for empty lists.
+ if len(definition[field]) == 0:
+ ParserError(
+ (
+ "Field '{}' for probe '{}' must not be empty"
+ + ".\nSee: {}#required-fields)"
+ ).format(field, self._name, BASE_DOC_URL)
+ ).handle_later()
+ # Check the type of the list content.
+ broken_types = [
+ not isinstance(v, LIST_FIELDS_CONTENT[field]) for v in definition[field]
+ ]
+ if any(broken_types):
+ ParserError(
+ (
+ "Field '{}' for probe '{}' must only contain values of type {}"
+ ".\nSee: {}#the-yaml-definition-file)"
+ ).format(
+ field,
+ self._name,
+ str(LIST_FIELDS_CONTENT[field]),
+ BASE_DOC_URL,
+ )
+ ).handle_later()
+
+ @property
+ def category(self):
+ """Get the category name"""
+ return self._category_name
+
+ @property
+ def name(self):
+ """Get the UserInteraction name"""
+ return self._name
+
+ @property
+ def label(self):
+ """Get the UserInteraction label generated from the UserInteraction
+ and category names.
+ """
+ return self._category_name + "." + self._name
+
+ @property
+ def bug_numbers(self):
+ """Get the list of related bug numbers"""
+ return self._definition["bug_numbers"]
+
+ @property
+ def description(self):
+ """Get the UserInteraction description"""
+ return self._definition["description"]
+
+
+def load_user_interactions(filename):
+ """Parses a YAML file containing the UserInteraction definition.
+
+ :param filename: the YAML file containing the UserInteraction definition.
+ :raises ParserError: if the UserInteraction file cannot be opened or
+ parsed.
+ """
+
+ # Parse the UserInteraction definitions from the YAML file.
+ user_interactions = None
+ try:
+ with io.open(filename, "r", encoding="utf-8") as f:
+ user_interactions = yaml.safe_load(f)
+ except IOError as e:
+ ParserError("Error opening " + filename + ": " + str(e)).handle_now()
+ except ValueError as e:
+ ParserError(
+ "Error parsing UserInteractions in {}: {}"
+ ".\nSee: {}".format(filename, e, BASE_DOC_URL)
+ ).handle_now()
+
+ user_interaction_list = []
+
+ # UserInteractions are defined in a fixed two-level hierarchy within the
+ # definition file. The first level contains the category name, while the
+ # second level contains the UserInteraction name
+ # (e.g. "category.name: user.interaction: ...").
+ for category_name in sorted(user_interactions):
+ category = user_interactions[category_name]
+
+ # Make sure that the category has at least one UserInteraction in it.
+ if not category or len(category) == 0:
+ ParserError(
+ 'Category "{}" must have at least one UserInteraction in it'
+ ".\nSee: {}".format(category_name, BASE_DOC_URL)
+ ).handle_later()
+
+ for user_interaction_name in sorted(category):
+ # We found a UserInteraction type. Go ahead and parse it.
+ user_interaction_info = category[user_interaction_name]
+ user_interaction_list.append(
+ UserInteractionType(
+ category_name, user_interaction_name, user_interaction_info
+ )
+ )
+
+ return user_interaction_list
+
+
+def from_files(filenames):
+ all_user_interactions = []
+
+ for filename in filenames:
+ all_user_interactions += load_user_interactions(filename)
+
+ for user_interaction in all_user_interactions:
+ yield user_interaction
diff --git a/toolkit/components/telemetry/build_scripts/mozparsers/shared_telemetry_utils.py b/toolkit/components/telemetry/build_scripts/mozparsers/shared_telemetry_utils.py
new file mode 100644
index 0000000000..4b4cc9f685
--- /dev/null
+++ b/toolkit/components/telemetry/build_scripts/mozparsers/shared_telemetry_utils.py
@@ -0,0 +1,185 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# This file contains utility functions shared by the scalars and the histogram generation
+# scripts.
+
+import os
+import re
+import sys
+
+import yaml
+
+# This is a list of flags that determine which process a measurement is allowed
+# to record from.
+KNOWN_PROCESS_FLAGS = {
+ "all": "All",
+ "all_children": "AllChildren",
+ "main": "Main",
+ "content": "Content",
+ "gpu": "Gpu",
+ "rdd": "Rdd",
+ "socket": "Socket",
+ "utility": "Utility",
+ # Historical Values
+ "all_childs": "AllChildren", # Supporting files from before bug 1363725
+}
+
+GECKOVIEW_STREAMING_PRODUCT = "geckoview_streaming"
+
+SUPPORTED_PRODUCTS = {
+ "firefox": "Firefox",
+ "fennec": "Fennec",
+ GECKOVIEW_STREAMING_PRODUCT: "GeckoviewStreaming",
+ "thunderbird": "Thunderbird",
+ # Historical, deprecated values:
+ # 'geckoview': 'Geckoview',
+}
+
+SUPPORTED_OPERATING_SYSTEMS = [
+ "mac",
+ "linux",
+ "windows",
+ "android",
+ "unix",
+ "all",
+]
+
+# mozinfo identifies linux, BSD variants, Solaris and SunOS as unix
+# Solaris and SunOS are identified as "unix" OS.
+UNIX_LIKE_OS = [
+ "unix",
+ "linux",
+ "bsd",
+]
+
+CANONICAL_OPERATING_SYSTEMS = {
+ "darwin": "mac",
+ "linux": "linux",
+ "winnt": "windows",
+ "android": "android",
+ # for simplicity we treat all BSD and Solaris systems as unix
+ "gnu/kfreebsd": "unix",
+ "sunos": "unix",
+ "dragonfly": "unix",
+ "freeunix": "unix",
+ "netunix": "unix",
+ "openunix": "unix",
+}
+
+PROCESS_ENUM_PREFIX = "mozilla::Telemetry::Common::RecordedProcessType::"
+PRODUCT_ENUM_PREFIX = "mozilla::Telemetry::Common::SupportedProduct::"
+
+
+class ParserError(Exception):
+ """Thrown by different probe parsers. Errors are partitioned into
+ 'immediately fatal' and 'eventually fatal' so that the parser can print
+ multiple error messages at a time. See bug 1401612 ."""
+
+ eventual_errors = []
+
+ def __init__(self, *args):
+ Exception.__init__(self, *args)
+
+ def handle_later(self):
+ ParserError.eventual_errors.append(self)
+
+ def handle_now(self):
+ ParserError.print_eventuals()
+ print(str(self), file=sys.stderr)
+ sys.stderr.flush()
+ os._exit(1)
+
+ @classmethod
+ def print_eventuals(cls):
+ while cls.eventual_errors:
+ print(str(cls.eventual_errors.pop(0)), file=sys.stderr)
+
+ @classmethod
+ def exit_func(cls):
+ if cls.eventual_errors:
+ cls("Some errors occurred").handle_now()
+
+
+def is_valid_process_name(name):
+ return name in KNOWN_PROCESS_FLAGS
+
+
+def process_name_to_enum(name):
+ return PROCESS_ENUM_PREFIX + KNOWN_PROCESS_FLAGS.get(name)
+
+
+def is_valid_product(name):
+ return name in SUPPORTED_PRODUCTS
+
+
+def is_geckoview_streaming_product(name):
+ return name == GECKOVIEW_STREAMING_PRODUCT
+
+
+def is_valid_os(name):
+ return name in SUPPORTED_OPERATING_SYSTEMS
+
+
+def canonical_os(os):
+ """Translate possible OS_TARGET names to their canonical value."""
+
+ return CANONICAL_OPERATING_SYSTEMS.get(os.lower()) or "unknown"
+
+
+def product_name_to_enum(product):
+ if not is_valid_product(product):
+ raise ParserError("Invalid product {}".format(product))
+ return PRODUCT_ENUM_PREFIX + SUPPORTED_PRODUCTS.get(product)
+
+
+def static_assert(output, expression, message):
+ """Writes a C++ compile-time assertion expression to a file.
+ :param output: the output stream.
+ :param expression: the expression to check.
+ :param message: the string literal that will appear if the expression evaluates to
+ false.
+ """
+ print('static_assert(%s, "%s");' % (expression, message), file=output)
+
+
+def validate_expiration_version(expiration):
+ """Makes sure the expiration version has the expected format.
+
+ Allowed examples: "10", "20", "60", "never"
+ Disallowed examples: "Never", "asd", "4000000", "60a1", "30.5a1"
+
+ :param expiration: the expiration version string.
+ :return: True if the expiration validates correctly, False otherwise.
+ """
+ if expiration != "never" and not re.match(r"^\d{1,3}$", expiration):
+ return False
+
+ return True
+
+
+def add_expiration_postfix(expiration):
+ """Formats the expiration version and adds a version postfix if needed.
+
+ :param expiration: the expiration version string.
+ :return: the modified expiration string.
+ """
+ if re.match(r"^[1-9][0-9]*$", expiration):
+ return expiration + ".0a1"
+
+ if re.match(r"^[1-9][0-9]*\.0$", expiration):
+ return expiration + "a1"
+
+ return expiration
+
+
+def load_yaml_file(filename):
+ """Load a YAML file from disk, throw a ParserError on failure."""
+ try:
+ with open(filename, "r") as f:
+ return yaml.safe_load(f)
+ except IOError as e:
+ raise ParserError("Error opening " + filename + ": " + str(e))
+ except ValueError as e:
+ raise ParserError("Error parsing processes in {}: {}".format(filename, e))
diff --git a/toolkit/components/telemetry/build_scripts/run_glean_parser.py b/toolkit/components/telemetry/build_scripts/run_glean_parser.py
new file mode 100644
index 0000000000..e71206e9b0
--- /dev/null
+++ b/toolkit/components/telemetry/build_scripts/run_glean_parser.py
@@ -0,0 +1,17 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import sys
+from pathlib import Path
+
+from glean_parser import lint
+
+
+def main(output, *filenames):
+ if lint.glinter([Path(x) for x in filenames], {"allow_reserved": False}):
+ sys.exit(1)
+
+
+if __name__ == "__main__":
+ main(sys.stdout, *sys.argv[1:])
diff --git a/toolkit/components/telemetry/build_scripts/setup.py b/toolkit/components/telemetry/build_scripts/setup.py
new file mode 100644
index 0000000000..bd8967aec5
--- /dev/null
+++ b/toolkit/components/telemetry/build_scripts/setup.py
@@ -0,0 +1,32 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from setuptools import find_packages, setup
+
+VERSION = "1.0.0"
+
+with open("README.md", "r") as fh:
+ long_description = fh.read()
+
+setup(
+ author="Mozilla Telemetry Team",
+ author_email="telemetry-client-dev@mozilla.com",
+ url=(
+ "https://firefox-source-docs.mozilla.org/"
+ "toolkit/components/telemetry/telemetry/collection/index.html"
+ ),
+ name="mozparsers",
+ description="Shared parsers for the Telemetry probe regitries.",
+ long_description=long_description,
+ long_description_content_type="text/markdown",
+ license="MPL 2.0",
+ packages=find_packages(),
+ version=VERSION,
+ classifiers=[
+ "Topic :: Software Development :: Build Tools",
+ "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
+ "Programming Language :: Python :: 2.7",
+ ],
+ keywords=["mozilla", "telemetry", "parsers"],
+)
diff --git a/toolkit/components/telemetry/components.conf b/toolkit/components/telemetry/components.conf
new file mode 100644
index 0000000000..1a129f3729
--- /dev/null
+++ b/toolkit/components/telemetry/components.conf
@@ -0,0 +1,23 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Classes = [
+ {
+ 'cid': '{117b219f-92fe-4bd2-a21b-95a342a9d474}',
+ 'contract_ids': ['@mozilla.org/base/telemetry-startup;1'],
+ 'esModule': 'resource://gre/modules/TelemetryStartup.sys.mjs',
+ 'constructor': 'TelemetryStartup',
+ 'processes': ProcessSelector.MAIN_PROCESS_ONLY,
+ },
+ {
+ 'cid': '{efc1415c-5708-41cc-8226-82bf1d3bee16}',
+ 'contract_ids': ['@mozilla.org/base/telemetry-controller-content;1'],
+ 'esModule': 'resource://gre/modules/TelemetryControllerContent.sys.mjs',
+ 'processes': ProcessSelector.CONTENT_PROCESS_ONLY,
+ 'constructor': 'getTelemetryController',
+ 'categories': {'content-process-ready-for-script': 'TelemetryControllerContent'},
+ },
+]
diff --git a/toolkit/components/telemetry/core/EventInfo.h b/toolkit/components/telemetry/core/EventInfo.h
new file mode 100644
index 0000000000..b80f85af92
--- /dev/null
+++ b/toolkit/components/telemetry/core/EventInfo.h
@@ -0,0 +1,57 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TelemetryEventInfo_h__
+#define TelemetryEventInfo_h__
+
+#include "TelemetryCommon.h"
+
+// This module is internal to Telemetry. The structures here hold data that
+// describe events.
+// It should only be used by TelemetryEventData.h and TelemetryEvent.cpp.
+//
+// For the public interface to Telemetry functionality, see Telemetry.h.
+
+namespace {
+
+struct CommonEventInfo {
+ // Indices for the category and expiration strings.
+ uint32_t category_offset;
+ uint32_t expiration_version_offset;
+
+ // The index and count for the extra key offsets in the extra table.
+ uint32_t extra_index;
+ uint32_t extra_count;
+
+ // The dataset this event is recorded in.
+ uint32_t dataset;
+
+ // Which processes to record this event in.
+ mozilla::Telemetry::Common::RecordedProcessType record_in_processes;
+
+ // Which products to record this event on.
+ mozilla::Telemetry::Common::SupportedProduct products;
+
+ // Convenience functions for accessing event strings.
+ const nsDependentCString expiration_version() const;
+ const nsDependentCString category() const;
+ const nsDependentCString extra_key(uint32_t index) const;
+};
+
+struct EventInfo {
+ // The corresponding CommonEventInfo.
+ const CommonEventInfo& common_info;
+
+ // Indices for the method & object strings.
+ uint32_t method_offset;
+ uint32_t object_offset;
+
+ const nsDependentCString method() const;
+ const nsDependentCString object() const;
+};
+
+} // namespace
+
+#endif // TelemetryEventInfo_h__
diff --git a/toolkit/components/telemetry/core/ScalarInfo.h b/toolkit/components/telemetry/core/ScalarInfo.h
new file mode 100644
index 0000000000..125ea7e88e
--- /dev/null
+++ b/toolkit/components/telemetry/core/ScalarInfo.h
@@ -0,0 +1,94 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TelemetryScalarInfo_h__
+#define TelemetryScalarInfo_h__
+
+#include "TelemetryCommon.h"
+
+// This module is internal to Telemetry. It defines a structure that holds the
+// scalar info. It should only be used by TelemetryScalarData.h automatically
+// generated file and TelemetryScalar.cpp. This should not be used anywhere
+// else. For the public interface to Telemetry functionality, see Telemetry.h.
+
+namespace {
+
+/**
+ * Base scalar information, common to both "static" and dynamic scalars.
+ */
+struct BaseScalarInfo {
+ uint32_t kind;
+ uint32_t dataset;
+ mozilla::Telemetry::Common::RecordedProcessType record_in_processes;
+ bool keyed;
+ uint32_t key_count;
+ uint32_t key_offset;
+ mozilla::Telemetry::Common::SupportedProduct products;
+ bool builtin;
+
+ constexpr BaseScalarInfo(
+ uint32_t aKind, uint32_t aDataset,
+ mozilla::Telemetry::Common::RecordedProcessType aRecordInProcess,
+ bool aKeyed, uint32_t aKeyCount, uint32_t aKeyOffset,
+ mozilla::Telemetry::Common::SupportedProduct aProducts,
+ bool aBuiltin = true)
+ : kind(aKind),
+ dataset(aDataset),
+ record_in_processes(aRecordInProcess),
+ keyed(aKeyed),
+ key_count(aKeyCount),
+ key_offset(aKeyOffset),
+ products(aProducts),
+ builtin(aBuiltin) {}
+ virtual ~BaseScalarInfo() = default;
+
+ virtual const char* name() const = 0;
+ virtual const char* expiration() const = 0;
+
+ virtual uint32_t storeOffset() const = 0;
+ virtual uint32_t storeCount() const = 0;
+};
+
+/**
+ * "Static" scalar definition: these are the ones riding
+ * the trains.
+ */
+struct ScalarInfo : BaseScalarInfo {
+ uint32_t name_offset;
+ uint32_t expiration_offset;
+ uint32_t store_count;
+ uint16_t store_offset;
+
+ // In order to cleanly support dynamic scalars in TelemetryScalar.cpp, we need
+ // to use virtual functions for |name| and |expiration|, as they won't be
+ // looked up in the static tables in that case. However, using virtual
+ // functions makes |ScalarInfo| non-aggregate and prevents from using
+ // aggregate initialization (curly brackets) in the generated
+ // TelemetryScalarData.h. To work around this problem we define a constructor
+ // that takes the exact number of parameters we need.
+ constexpr ScalarInfo(
+ uint32_t aKind, uint32_t aNameOffset, uint32_t aExpirationOffset,
+ uint32_t aDataset,
+ mozilla::Telemetry::Common::RecordedProcessType aRecordInProcess,
+ bool aKeyed, uint32_t aKeyCount, uint32_t aKeyOffset,
+ mozilla::Telemetry::Common::SupportedProduct aProducts,
+ uint32_t aStoreCount, uint32_t aStoreOffset)
+ : BaseScalarInfo(aKind, aDataset, aRecordInProcess, aKeyed, aKeyCount,
+ aKeyOffset, aProducts),
+ name_offset(aNameOffset),
+ expiration_offset(aExpirationOffset),
+ store_count(aStoreCount),
+ store_offset(aStoreOffset) {}
+
+ const char* name() const override;
+ const char* expiration() const override;
+
+ uint32_t storeOffset() const override { return store_offset; };
+ uint32_t storeCount() const override { return store_count; };
+};
+
+} // namespace
+
+#endif // TelemetryScalarInfo_h__
diff --git a/toolkit/components/telemetry/core/Stopwatch.cpp b/toolkit/components/telemetry/core/Stopwatch.cpp
new file mode 100644
index 0000000000..fe6191aed3
--- /dev/null
+++ b/toolkit/components/telemetry/core/Stopwatch.cpp
@@ -0,0 +1,752 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "mozilla/telemetry/Stopwatch.h"
+
+#include "TelemetryHistogram.h"
+#include "TelemetryUserInteraction.h"
+
+#include "js/MapAndSet.h"
+#include "js/WeakMap.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/BackgroundHangMonitor.h"
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/HangAnnotations.h"
+#include "mozilla/ProfilerMarkers.h"
+#include "mozilla/DataMutex.h"
+#include "mozilla/TimeStamp.h"
+#include "nsHashKeys.h"
+#include "nsContentUtils.h"
+#include "nsPrintfCString.h"
+#include "nsQueryObject.h"
+#include "nsString.h"
+#include "xpcpublic.h"
+
+using mozilla::DataMutex;
+using mozilla::dom::AutoJSAPI;
+
+#define USER_INTERACTION_VALUE_MAX_LENGTH 50 // bytes
+
+static inline nsQueryObject<nsISupports> do_QueryReflector(
+ JSObject* aReflector) {
+ // None of the types we query to are implemented by Window or Location.
+ nsCOMPtr<nsISupports> reflector = xpc::ReflectorToISupportsStatic(aReflector);
+ return do_QueryObject(reflector);
+}
+
+static inline nsQueryObject<nsISupports> do_QueryReflector(
+ const JS::Value& aReflector) {
+ return do_QueryReflector(&aReflector.toObject());
+}
+
+static void LogError(JSContext* aCx, const nsCString& aMessage) {
+ // This is a bit of a hack to report an error with the current JS caller's
+ // location. We create an AutoJSAPI object bound to the current caller
+ // global, report a JS error, and then let AutoJSAPI's destructor report the
+ // error.
+ //
+ // Unfortunately, there isn't currently a more straightforward way to do
+ // this from C++.
+ JS::Rooted<JSObject*> global(aCx, JS::CurrentGlobalOrNull(aCx));
+
+ AutoJSAPI jsapi;
+ if (jsapi.Init(global)) {
+ JS_ReportErrorUTF8(jsapi.cx(), "%s", aMessage.get());
+ }
+}
+
+namespace mozilla::telemetry {
+
+class Timer final : public mozilla::LinkedListElement<RefPtr<Timer>> {
+ public:
+ NS_INLINE_DECL_REFCOUNTING(Timer)
+
+ Timer() = default;
+
+ void Start(bool aInSeconds) {
+ mStartTime = TimeStamp::Now();
+ mInSeconds = aInSeconds;
+ }
+
+ bool Started() { return !mStartTime.IsNull(); }
+
+ uint32_t Elapsed() {
+ auto delta = TimeStamp::Now() - mStartTime;
+ return mInSeconds ? delta.ToSeconds() : delta.ToMilliseconds();
+ }
+
+ TimeStamp& StartTime() { return mStartTime; }
+
+ bool& InSeconds() { return mInSeconds; }
+
+ /**
+ * Note that these values will want to be read from the
+ * BackgroundHangAnnotator thread. Callers should take a lock
+ * on Timers::mBHRAnnotationTimers before calling this.
+ */
+ void SetBHRAnnotation(const nsAString& aBHRAnnotationKey,
+ const nsACString& aBHRAnnotationValue) {
+ mBHRAnnotationKey = aBHRAnnotationKey;
+ mBHRAnnotationValue = aBHRAnnotationValue;
+ }
+
+ const nsString& GetBHRAnnotationKey() const { return mBHRAnnotationKey; }
+ const nsCString& GetBHRAnnotationValue() const { return mBHRAnnotationValue; }
+
+ private:
+ ~Timer() = default;
+ TimeStamp mStartTime{};
+ nsString mBHRAnnotationKey;
+ nsCString mBHRAnnotationValue;
+ bool mInSeconds;
+};
+
+#define TIMER_KEYS_IID \
+ { \
+ 0xef707178, 0x1544, 0x46e2, { \
+ 0xa3, 0xf5, 0x98, 0x38, 0xba, 0x60, 0xfd, 0x8f \
+ } \
+ }
+
+class TimerKeys final : public nsISupports {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECLARE_STATIC_IID_ACCESSOR(TIMER_KEYS_IID)
+
+ Timer* Get(const nsAString& aKey, bool aCreate = true);
+
+ already_AddRefed<Timer> GetAndDelete(const nsAString& aKey) {
+ RefPtr<Timer> timer;
+ mTimers.Remove(aKey, getter_AddRefs(timer));
+ return timer.forget();
+ }
+
+ bool Delete(const nsAString& aKey) { return mTimers.Remove(aKey); }
+
+ private:
+ ~TimerKeys() = default;
+
+ nsRefPtrHashtable<nsStringHashKey, Timer> mTimers;
+};
+
+NS_DEFINE_STATIC_IID_ACCESSOR(TimerKeys, TIMER_KEYS_IID)
+
+NS_IMPL_ISUPPORTS(TimerKeys, TimerKeys)
+
+Timer* TimerKeys::Get(const nsAString& aKey, bool aCreate) {
+ if (aCreate) {
+ return mTimers.GetOrInsertNew(aKey);
+ }
+ return mTimers.GetWeak(aKey);
+}
+
+class Timers final : public BackgroundHangAnnotator {
+ public:
+ Timers();
+
+ static Timers& Singleton();
+
+ NS_INLINE_DECL_REFCOUNTING(Timers)
+
+ JSObject* Get(JSContext* aCx, const nsAString& aHistogram,
+ bool aCreate = true);
+
+ TimerKeys* Get(JSContext* aCx, const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj, bool aCreate = true);
+
+ Timer* Get(JSContext* aCx, const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj, const nsAString& aKey,
+ bool aCreate = true);
+
+ already_AddRefed<Timer> GetAndDelete(JSContext* aCx,
+ const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj,
+ const nsAString& aKey);
+
+ bool Delete(JSContext* aCx, const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj, const nsAString& aKey);
+
+ int32_t TimeElapsed(JSContext* aCx, const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj, const nsAString& aKey,
+ bool aCanceledOkay = false);
+
+ bool Start(JSContext* aCx, const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj, const nsAString& aKey,
+ bool aInSeconds = false);
+
+ int32_t Finish(JSContext* aCx, const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj, const nsAString& aKey,
+ bool aCanceledOkay = false);
+
+ bool& SuppressErrors() { return mSuppressErrors; }
+
+ bool StartUserInteraction(JSContext* aCx, const nsAString& aUserInteraction,
+ const nsACString& aValue,
+ JS::Handle<JSObject*> aObj);
+ bool RunningUserInteraction(JSContext* aCx, const nsAString& aUserInteraction,
+ JS::Handle<JSObject*> aObj);
+ bool UpdateUserInteraction(JSContext* aCx, const nsAString& aUserInteraction,
+ const nsACString& aValue,
+ JS::Handle<JSObject*> aObj);
+ bool FinishUserInteraction(JSContext* aCx, const nsAString& aUserInteraction,
+ JS::Handle<JSObject*> aObj,
+ const dom::Optional<nsACString>& aAdditionalText);
+ bool CancelUserInteraction(JSContext* aCx, const nsAString& aUserInteraction,
+ JS::Handle<JSObject*> aObj);
+
+ void AnnotateHang(BackgroundHangAnnotations& aAnnotations) final;
+
+ private:
+ ~Timers();
+
+ JS::PersistentRooted<JSObject*> mTimers;
+ DataMutex<mozilla::LinkedList<RefPtr<Timer>>> mBHRAnnotationTimers;
+ bool mSuppressErrors = false;
+
+ static StaticRefPtr<Timers> sSingleton;
+};
+
+StaticRefPtr<Timers> Timers::sSingleton;
+
+/* static */ Timers& Timers::Singleton() {
+ if (!sSingleton) {
+ sSingleton = new Timers();
+ ClearOnShutdown(&sSingleton);
+ }
+ return *sSingleton;
+}
+
+Timers::Timers()
+ : mTimers(dom::RootingCx()), mBHRAnnotationTimers("BHRAnnotationTimers") {
+ AutoJSAPI jsapi;
+ MOZ_ALWAYS_TRUE(jsapi.Init(xpc::PrivilegedJunkScope()));
+
+ mTimers = JS::NewMapObject(jsapi.cx());
+ MOZ_RELEASE_ASSERT(mTimers);
+
+ BackgroundHangMonitor::RegisterAnnotator(*this);
+}
+
+Timers::~Timers() {
+ // We use a scope here to prevent a deadlock with the mutex that locks
+ // inside of ::UnregisterAnnotator.
+ {
+ auto annotationTimers = mBHRAnnotationTimers.Lock();
+ annotationTimers->clear();
+ }
+ BackgroundHangMonitor::UnregisterAnnotator(*this);
+}
+
+JSObject* Timers::Get(JSContext* aCx, const nsAString& aHistogram,
+ bool aCreate) {
+ JSAutoRealm ar(aCx, mTimers);
+
+ JS::Rooted<JS::Value> histogram(aCx);
+ JS::Rooted<JS::Value> objs(aCx);
+
+ if (!xpc::NonVoidStringToJsval(aCx, aHistogram, &histogram) ||
+ !JS::MapGet(aCx, mTimers, histogram, &objs)) {
+ return nullptr;
+ }
+ if (!objs.isObject()) {
+ if (aCreate) {
+ objs = JS::ObjectOrNullValue(JS::NewWeakMapObject(aCx));
+ }
+ if (!objs.isObject() || !JS::MapSet(aCx, mTimers, histogram, objs)) {
+ return nullptr;
+ }
+ }
+
+ return &objs.toObject();
+}
+
+TimerKeys* Timers::Get(JSContext* aCx, const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj, bool aCreate) {
+ JSAutoRealm ar(aCx, mTimers);
+
+ JS::Rooted<JSObject*> objs(aCx, Get(aCx, aHistogram, aCreate));
+ if (!objs) {
+ return nullptr;
+ }
+
+ // If no object is passed, use mTimers as a stand-in for a null object
+ // (which cannot be used as a weak map key).
+ JS::Rooted<JSObject*> obj(aCx, aObj ? aObj : mTimers);
+ if (!JS_WrapObject(aCx, &obj)) {
+ return nullptr;
+ }
+
+ RefPtr<TimerKeys> keys;
+ JS::Rooted<JS::Value> keysObj(aCx);
+ JS::Rooted<JS::Value> objVal(aCx, JS::ObjectValue(*obj));
+ if (!JS::GetWeakMapEntry(aCx, objs, objVal, &keysObj)) {
+ return nullptr;
+ }
+ if (!keysObj.isObject()) {
+ if (aCreate) {
+ keys = new TimerKeys();
+ Unused << nsContentUtils::WrapNative(aCx, keys, &keysObj);
+ }
+ if (!keysObj.isObject() ||
+ !JS::SetWeakMapEntry(aCx, objs, objVal, keysObj)) {
+ return nullptr;
+ }
+ }
+
+ keys = do_QueryReflector(keysObj);
+ return keys;
+}
+
+Timer* Timers::Get(JSContext* aCx, const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj, const nsAString& aKey,
+ bool aCreate) {
+ if (RefPtr<TimerKeys> keys = Get(aCx, aHistogram, aObj, aCreate)) {
+ return keys->Get(aKey, aCreate);
+ }
+ return nullptr;
+}
+
+already_AddRefed<Timer> Timers::GetAndDelete(JSContext* aCx,
+ const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj,
+ const nsAString& aKey) {
+ if (RefPtr<TimerKeys> keys = Get(aCx, aHistogram, aObj, false)) {
+ return keys->GetAndDelete(aKey);
+ }
+ return nullptr;
+}
+
+bool Timers::Delete(JSContext* aCx, const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj, const nsAString& aKey) {
+ if (RefPtr<TimerKeys> keys = Get(aCx, aHistogram, aObj, false)) {
+ return keys->Delete(aKey);
+ }
+ return false;
+}
+
+int32_t Timers::TimeElapsed(JSContext* aCx, const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj, const nsAString& aKey,
+ bool aCanceledOkay) {
+ RefPtr<Timer> timer = Get(aCx, aHistogram, aObj, aKey, false);
+ if (!timer) {
+ if (!aCanceledOkay && !mSuppressErrors) {
+ LogError(aCx, nsPrintfCString(
+ "TelemetryStopwatch: requesting elapsed time for "
+ "nonexisting stopwatch. Histogram: \"%s\", key: \"%s\"",
+ NS_ConvertUTF16toUTF8(aHistogram).get(),
+ NS_ConvertUTF16toUTF8(aKey).get()));
+ }
+ return -1;
+ }
+
+ return timer->Elapsed();
+}
+
+bool Timers::Start(JSContext* aCx, const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj, const nsAString& aKey,
+ bool aInSeconds) {
+ if (RefPtr<Timer> timer = Get(aCx, aHistogram, aObj, aKey)) {
+ if (timer->Started()) {
+ if (!mSuppressErrors) {
+ LogError(aCx,
+ nsPrintfCString(
+ "TelemetryStopwatch: key \"%s\" was already initialized",
+ NS_ConvertUTF16toUTF8(aHistogram).get()));
+ }
+ Delete(aCx, aHistogram, aObj, aKey);
+ } else {
+ timer->Start(aInSeconds);
+ return true;
+ }
+ }
+ return false;
+}
+
+int32_t Timers::Finish(JSContext* aCx, const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj, const nsAString& aKey,
+ bool aCanceledOkay) {
+ RefPtr<Timer> timer = GetAndDelete(aCx, aHistogram, aObj, aKey);
+ if (!timer) {
+ if (!aCanceledOkay && !mSuppressErrors) {
+ LogError(aCx, nsPrintfCString(
+ "TelemetryStopwatch: finishing nonexisting stopwatch. "
+ "Histogram: \"%s\", key: \"%s\"",
+ NS_ConvertUTF16toUTF8(aHistogram).get(),
+ NS_ConvertUTF16toUTF8(aKey).get()));
+ }
+ return -1;
+ }
+
+ int32_t delta = timer->Elapsed();
+ NS_ConvertUTF16toUTF8 histogram(aHistogram);
+ nsresult rv;
+ if (!aKey.IsVoid()) {
+ NS_ConvertUTF16toUTF8 key(aKey);
+ rv = TelemetryHistogram::Accumulate(histogram.get(), key, delta);
+ } else {
+ rv = TelemetryHistogram::Accumulate(histogram.get(), delta);
+ }
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ nsCString markerText = histogram;
+ if (!aKey.IsVoid()) {
+ markerText.AppendLiteral(":");
+ markerText.Append(NS_ConvertUTF16toUTF8(aKey));
+ }
+ PROFILER_MARKER_TEXT("TelemetryStopwatch", OTHER,
+ MarkerTiming::IntervalUntilNowFrom(timer->StartTime()),
+ markerText);
+ }
+ if (NS_FAILED(rv) && rv != NS_ERROR_NOT_AVAILABLE && !mSuppressErrors) {
+ LogError(aCx, nsPrintfCString(
+ "TelemetryStopwatch: failed to update the Histogram "
+ "\"%s\", using key: \"%s\"",
+ NS_ConvertUTF16toUTF8(aHistogram).get(),
+ NS_ConvertUTF16toUTF8(aKey).get()));
+ }
+ return NS_SUCCEEDED(rv) ? delta : -1;
+}
+
+bool Timers::StartUserInteraction(JSContext* aCx,
+ const nsAString& aUserInteraction,
+ const nsACString& aValue,
+ JS::Handle<JSObject*> aObj) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Ensure that this ID maps to a UserInteraction that can be recorded
+ // for this product.
+ if (!TelemetryUserInteraction::CanRecord(aUserInteraction)) {
+ if (!mSuppressErrors) {
+ LogError(aCx, nsPrintfCString(
+ "UserInteraction with name \"%s\" cannot be recorded.",
+ NS_ConvertUTF16toUTF8(aUserInteraction).get()));
+ }
+ return false;
+ }
+
+ if (aValue.Length() > USER_INTERACTION_VALUE_MAX_LENGTH) {
+ if (!mSuppressErrors) {
+ LogError(aCx,
+ nsPrintfCString(
+ "UserInteraction with name \"%s\" cannot be recorded with"
+ "a value of length greater than %d (%s)",
+ NS_ConvertUTF16toUTF8(aUserInteraction).get(),
+ USER_INTERACTION_VALUE_MAX_LENGTH,
+ PromiseFlatCString(aValue).get()));
+ }
+ return false;
+ }
+
+ if (RefPtr<Timer> timer = Get(aCx, aUserInteraction, aObj, VoidString())) {
+ auto annotationTimers = mBHRAnnotationTimers.Lock();
+
+ if (timer->Started()) {
+ if (!mSuppressErrors) {
+ LogError(aCx,
+ nsPrintfCString(
+ "UserInteraction with name \"%s\" was already initialized",
+ NS_ConvertUTF16toUTF8(aUserInteraction).get()));
+ }
+ timer->removeFrom(*annotationTimers);
+ Delete(aCx, aUserInteraction, aObj, VoidString());
+ timer = Get(aCx, aUserInteraction, aObj, VoidString());
+
+ nsAutoString clobberText(aUserInteraction);
+ clobberText.AppendLiteral(u" (clobbered)");
+ timer->SetBHRAnnotation(clobberText, aValue);
+ } else {
+ timer->SetBHRAnnotation(aUserInteraction, aValue);
+ }
+
+ annotationTimers->insertBack(timer);
+ timer->Start(false);
+ return true;
+ }
+ return false;
+}
+
+bool Timers::RunningUserInteraction(JSContext* aCx,
+ const nsAString& aUserInteraction,
+ JS::Handle<JSObject*> aObj) {
+ if (RefPtr<Timer> timer =
+ Get(aCx, aUserInteraction, aObj, VoidString(), false /* aCreate */)) {
+ return timer->Started();
+ }
+ return false;
+}
+
+bool Timers::UpdateUserInteraction(JSContext* aCx,
+ const nsAString& aUserInteraction,
+ const nsACString& aValue,
+ JS::Handle<JSObject*> aObj) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Ensure that this ID maps to a UserInteraction that can be recorded
+ // for this product.
+ if (!TelemetryUserInteraction::CanRecord(aUserInteraction)) {
+ if (!mSuppressErrors) {
+ LogError(aCx, nsPrintfCString(
+ "UserInteraction with name \"%s\" cannot be recorded.",
+ NS_ConvertUTF16toUTF8(aUserInteraction).get()));
+ }
+ return false;
+ }
+
+ auto lock = mBHRAnnotationTimers.Lock();
+ if (RefPtr<Timer> timer = Get(aCx, aUserInteraction, aObj, VoidString())) {
+ if (!timer->Started()) {
+ if (!mSuppressErrors) {
+ LogError(aCx, nsPrintfCString(
+ "UserInteraction with id \"%s\" was not initialized",
+ NS_ConvertUTF16toUTF8(aUserInteraction).get()));
+ }
+ return false;
+ }
+ timer->SetBHRAnnotation(aUserInteraction, aValue);
+ return true;
+ }
+ return false;
+}
+
+bool Timers::FinishUserInteraction(
+ JSContext* aCx, const nsAString& aUserInteraction,
+ JS::Handle<JSObject*> aObj,
+ const dom::Optional<nsACString>& aAdditionalText) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Ensure that this ID maps to a UserInteraction that can be recorded
+ // for this product.
+ if (!TelemetryUserInteraction::CanRecord(aUserInteraction)) {
+ if (!mSuppressErrors) {
+ LogError(aCx, nsPrintfCString(
+ "UserInteraction with id \"%s\" cannot be recorded.",
+ NS_ConvertUTF16toUTF8(aUserInteraction).get()));
+ }
+ return false;
+ }
+
+ RefPtr<Timer> timer = GetAndDelete(aCx, aUserInteraction, aObj, VoidString());
+ if (!timer) {
+ if (!mSuppressErrors) {
+ LogError(aCx, nsPrintfCString(
+ "UserInteraction: finishing nonexisting stopwatch. "
+ "name: \"%s\"",
+ NS_ConvertUTF16toUTF8(aUserInteraction).get()));
+ }
+ return false;
+ }
+
+ if (profiler_thread_is_being_profiled_for_markers()) {
+ nsAutoCString markerText(timer->GetBHRAnnotationValue());
+ if (aAdditionalText.WasPassed()) {
+ markerText.Append(",");
+ markerText.Append(aAdditionalText.Value());
+ }
+
+ PROFILER_MARKER_TEXT(NS_ConvertUTF16toUTF8(aUserInteraction), OTHER,
+ MarkerTiming::IntervalUntilNowFrom(timer->StartTime()),
+ markerText);
+ }
+
+ // The Timer will be held alive by the RefPtr that's still in the LinkedList,
+ // so the automatic removal from the LinkedList from the LinkedListElement
+ // destructor will not occur. We must remove it manually from the LinkedList
+ // instead.
+ {
+ auto annotationTimers = mBHRAnnotationTimers.Lock();
+ timer->removeFrom(*annotationTimers);
+ }
+
+ return true;
+}
+
+bool Timers::CancelUserInteraction(JSContext* aCx,
+ const nsAString& aUserInteraction,
+ JS::Handle<JSObject*> aObj) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Ensure that this ID maps to a UserInteraction that can be recorded
+ // for this product.
+ if (!TelemetryUserInteraction::CanRecord(aUserInteraction)) {
+ if (!mSuppressErrors) {
+ LogError(aCx, nsPrintfCString(
+ "UserInteraction with id \"%s\" cannot be recorded.",
+ NS_ConvertUTF16toUTF8(aUserInteraction).get()));
+ }
+ return false;
+ }
+
+ RefPtr<Timer> timer = GetAndDelete(aCx, aUserInteraction, aObj, VoidString());
+ if (!timer) {
+ if (!mSuppressErrors) {
+ LogError(aCx, nsPrintfCString(
+ "UserInteraction: cancelling nonexisting stopwatch. "
+ "name: \"%s\"",
+ NS_ConvertUTF16toUTF8(aUserInteraction).get()));
+ }
+ return false;
+ }
+
+ // The Timer will be held alive by the RefPtr that's still in the LinkedList,
+ // so the automatic removal from the LinkedList from the LinkedListElement
+ // destructor will not occur. We must remove it manually from the LinkedList
+ // instead.
+ {
+ auto annotationTimers = mBHRAnnotationTimers.Lock();
+ timer->removeFrom(*annotationTimers);
+ }
+
+ return true;
+}
+
+void Timers::AnnotateHang(mozilla::BackgroundHangAnnotations& aAnnotations) {
+ auto annotationTimers = mBHRAnnotationTimers.Lock();
+ for (Timer* bhrAnnotationTimer : *annotationTimers) {
+ aAnnotations.AddAnnotation(bhrAnnotationTimer->GetBHRAnnotationKey(),
+ bhrAnnotationTimer->GetBHRAnnotationValue());
+ }
+}
+
+/* static */
+bool Stopwatch::Start(const dom::GlobalObject& aGlobal,
+ const nsAString& aHistogram, JS::Handle<JSObject*> aObj,
+ const dom::TelemetryStopwatchOptions& aOptions) {
+ return StartKeyed(aGlobal, aHistogram, VoidString(), aObj, aOptions);
+}
+/* static */
+bool Stopwatch::StartKeyed(const dom::GlobalObject& aGlobal,
+ const nsAString& aHistogram, const nsAString& aKey,
+ JS::Handle<JSObject*> aObj,
+ const dom::TelemetryStopwatchOptions& aOptions) {
+ return Timers::Singleton().Start(aGlobal.Context(), aHistogram, aObj, aKey,
+ aOptions.mInSeconds);
+}
+
+/* static */
+bool Stopwatch::Running(const dom::GlobalObject& aGlobal,
+ const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj) {
+ return RunningKeyed(aGlobal, aHistogram, VoidString(), aObj);
+}
+
+/* static */
+bool Stopwatch::RunningKeyed(const dom::GlobalObject& aGlobal,
+ const nsAString& aHistogram, const nsAString& aKey,
+ JS::Handle<JSObject*> aObj) {
+ return TimeElapsedKeyed(aGlobal, aHistogram, aKey, aObj, true) != -1;
+}
+
+/* static */
+int32_t Stopwatch::TimeElapsed(const dom::GlobalObject& aGlobal,
+ const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj, bool aCanceledOkay) {
+ return TimeElapsedKeyed(aGlobal, aHistogram, VoidString(), aObj,
+ aCanceledOkay);
+}
+
+/* static */
+int32_t Stopwatch::TimeElapsedKeyed(const dom::GlobalObject& aGlobal,
+ const nsAString& aHistogram,
+ const nsAString& aKey,
+ JS::Handle<JSObject*> aObj,
+ bool aCanceledOkay) {
+ return Timers::Singleton().TimeElapsed(aGlobal.Context(), aHistogram, aObj,
+ aKey, aCanceledOkay);
+}
+
+/* static */
+bool Stopwatch::Finish(const dom::GlobalObject& aGlobal,
+ const nsAString& aHistogram, JS::Handle<JSObject*> aObj,
+ bool aCanceledOkay) {
+ return FinishKeyed(aGlobal, aHistogram, VoidString(), aObj, aCanceledOkay);
+}
+
+/* static */
+bool Stopwatch::FinishKeyed(const dom::GlobalObject& aGlobal,
+ const nsAString& aHistogram, const nsAString& aKey,
+ JS::Handle<JSObject*> aObj, bool aCanceledOkay) {
+ return Timers::Singleton().Finish(aGlobal.Context(), aHistogram, aObj, aKey,
+ aCanceledOkay) != -1;
+}
+
+/* static */
+bool Stopwatch::Cancel(const dom::GlobalObject& aGlobal,
+ const nsAString& aHistogram,
+ JS::Handle<JSObject*> aObj) {
+ return CancelKeyed(aGlobal, aHistogram, VoidString(), aObj);
+}
+
+/* static */
+bool Stopwatch::CancelKeyed(const dom::GlobalObject& aGlobal,
+ const nsAString& aHistogram, const nsAString& aKey,
+ JS::Handle<JSObject*> aObj) {
+ return Timers::Singleton().Delete(aGlobal.Context(), aHistogram, aObj, aKey);
+}
+
+/* static */
+void Stopwatch::SetTestModeEnabled(const dom::GlobalObject& aGlobal,
+ bool aTesting) {
+ Timers::Singleton().SuppressErrors() = aTesting;
+}
+
+/* static */
+bool UserInteractionStopwatch::Start(const dom::GlobalObject& aGlobal,
+ const nsAString& aUserInteraction,
+ const nsACString& aValue,
+ JS::Handle<JSObject*> aObj) {
+ if (!NS_IsMainThread()) {
+ return false;
+ }
+ return Timers::Singleton().StartUserInteraction(
+ aGlobal.Context(), aUserInteraction, aValue, aObj);
+}
+
+/* static */
+bool UserInteractionStopwatch::Running(const dom::GlobalObject& aGlobal,
+ const nsAString& aUserInteraction,
+ JS::Handle<JSObject*> aObj) {
+ if (!NS_IsMainThread()) {
+ return false;
+ }
+ return Timers::Singleton().RunningUserInteraction(aGlobal.Context(),
+ aUserInteraction, aObj);
+}
+
+/* static */
+bool UserInteractionStopwatch::Update(const dom::GlobalObject& aGlobal,
+ const nsAString& aUserInteraction,
+ const nsACString& aValue,
+ JS::Handle<JSObject*> aObj) {
+ if (!NS_IsMainThread()) {
+ return false;
+ }
+ return Timers::Singleton().UpdateUserInteraction(
+ aGlobal.Context(), aUserInteraction, aValue, aObj);
+}
+
+/* static */
+bool UserInteractionStopwatch::Cancel(const dom::GlobalObject& aGlobal,
+ const nsAString& aUserInteraction,
+ JS::Handle<JSObject*> aObj) {
+ if (!NS_IsMainThread()) {
+ return false;
+ }
+ return Timers::Singleton().CancelUserInteraction(aGlobal.Context(),
+ aUserInteraction, aObj);
+}
+
+/* static */
+bool UserInteractionStopwatch::Finish(
+ const dom::GlobalObject& aGlobal, const nsAString& aUserInteraction,
+ JS::Handle<JSObject*> aObj,
+ const dom::Optional<nsACString>& aAdditionalText) {
+ if (!NS_IsMainThread()) {
+ return false;
+ }
+ return Timers::Singleton().FinishUserInteraction(
+ aGlobal.Context(), aUserInteraction, aObj, aAdditionalText);
+}
+
+} // namespace mozilla::telemetry
diff --git a/toolkit/components/telemetry/core/Stopwatch.h b/toolkit/components/telemetry/core/Stopwatch.h
new file mode 100644
index 0000000000..070872e250
--- /dev/null
+++ b/toolkit/components/telemetry/core/Stopwatch.h
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef Stopwatch_h__
+#define Stopwatch_h__
+
+#include "mozilla/dom/TelemetryStopwatchBinding.h"
+
+namespace mozilla {
+namespace telemetry {
+
+class Stopwatch {
+ using GlobalObject = mozilla::dom::GlobalObject;
+
+ public:
+ static bool Start(const GlobalObject& global, const nsAString& histogram,
+ JS::Handle<JSObject*> obj,
+ const dom::TelemetryStopwatchOptions& options);
+
+ static bool Running(const GlobalObject& global, const nsAString& histogram,
+ JS::Handle<JSObject*> obj);
+
+ static bool Cancel(const GlobalObject& global, const nsAString& histogram,
+ JS::Handle<JSObject*> obj);
+
+ static int32_t TimeElapsed(const GlobalObject& global,
+ const nsAString& histogram,
+ JS::Handle<JSObject*> obj, bool canceledOkay);
+
+ static bool Finish(const GlobalObject& global, const nsAString& histogram,
+ JS::Handle<JSObject*> obj, bool canceledOkay);
+
+ static bool StartKeyed(const GlobalObject& global, const nsAString& histogram,
+ const nsAString& key, JS::Handle<JSObject*> obj,
+ const dom::TelemetryStopwatchOptions& options);
+
+ static bool RunningKeyed(const GlobalObject& global,
+ const nsAString& histogram, const nsAString& key,
+ JS::Handle<JSObject*> obj);
+
+ static bool CancelKeyed(const GlobalObject& global,
+ const nsAString& histogram, const nsAString& key,
+ JS::Handle<JSObject*> obj);
+
+ static int32_t TimeElapsedKeyed(const GlobalObject& global,
+ const nsAString& histogram,
+ const nsAString& key,
+ JS::Handle<JSObject*> obj, bool canceledOkay);
+
+ static bool FinishKeyed(const GlobalObject& global,
+ const nsAString& histogram, const nsAString& key,
+ JS::Handle<JSObject*> obj, bool canceledOkay);
+
+ static void SetTestModeEnabled(const GlobalObject& global, bool testing);
+};
+
+class UserInteractionStopwatch {
+ using GlobalObject = mozilla::dom::GlobalObject;
+
+ public:
+ static bool Start(const GlobalObject& aGlobal,
+ const nsAString& aUserInteraction, const nsACString& aValue,
+ JS::Handle<JSObject*> aObj);
+ static bool Running(const GlobalObject& aGlobal,
+ const nsAString& aUserInteraction,
+ JS::Handle<JSObject*> aObj);
+ static bool Update(const GlobalObject& aGlobal,
+ const nsAString& aUserInteraction,
+ const nsACString& aValue, JS::Handle<JSObject*> aObj);
+ static bool Cancel(const GlobalObject& aGlobal,
+ const nsAString& aUserInteraction,
+ JS::Handle<JSObject*> aObj);
+ static bool Finish(const GlobalObject& aGlobal,
+ const nsAString& aUserInteraction,
+ JS::Handle<JSObject*> aObj,
+ const dom::Optional<nsACString>& aAdditionalText);
+};
+
+} // namespace telemetry
+} // namespace mozilla
+
+#endif // Stopwatch_h__
diff --git a/toolkit/components/telemetry/core/Telemetry.cpp b/toolkit/components/telemetry/core/Telemetry.cpp
new file mode 100644
index 0000000000..a0effb02bb
--- /dev/null
+++ b/toolkit/components/telemetry/core/Telemetry.cpp
@@ -0,0 +1,2035 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Telemetry.h"
+
+#include <algorithm>
+#include <prio.h>
+#include <prproces.h>
+#if defined(XP_UNIX) && !defined(XP_DARWIN)
+# include <time.h>
+#else
+# include <chrono>
+#endif
+#include "base/pickle.h"
+#include "base/process_util.h"
+#if defined(MOZ_TELEMETRY_GECKOVIEW)
+# include "geckoview/TelemetryGeckoViewPersistence.h"
+#endif
+#include "ipc/TelemetryIPCAccumulator.h"
+#include "jsapi.h"
+#include "jsfriendapi.h"
+#include "js/Array.h" // JS::NewArrayObject
+#include "js/GCAPI.h"
+#include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineProperty
+#include "mozilla/dom/ToJSValue.h"
+#include "mozilla/dom/Promise.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/Attributes.h"
+#include "mozilla/BackgroundHangMonitor.h"
+#ifdef MOZ_BACKGROUNDTASKS
+# include "mozilla/BackgroundTasks.h"
+#endif
+#include "mozilla/Components.h"
+#include "mozilla/DataMutex.h"
+#include "mozilla/DebugOnly.h"
+#include "mozilla/FStream.h"
+#include "mozilla/IOInterposer.h"
+#include "mozilla/Likely.h"
+#include "mozilla/MathAlgorithms.h"
+#include "mozilla/MemoryReporting.h"
+#include "mozilla/MemoryTelemetry.h"
+#include "mozilla/ModuleUtils.h"
+#include "mozilla/Mutex.h"
+#include "mozilla/PoisonIOInterposer.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StartupTimeline.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/Unused.h"
+#if defined(XP_WIN)
+# include "mozilla/WinDllServices.h"
+#endif
+#include "nsAppDirectoryServiceDefs.h"
+#include "nsBaseHashtable.h"
+#include "nsClassHashtable.h"
+#include "nsCOMArray.h"
+#include "nsCOMPtr.h"
+#include "nsTHashMap.h"
+#include "nsHashKeys.h"
+#include "nsIDirectoryEnumerator.h"
+#include "nsDirectoryServiceDefs.h"
+#include "nsIFileStreams.h"
+#include "nsIMemoryReporter.h"
+#include "nsISeekableStream.h"
+#include "nsITelemetry.h"
+#if defined(XP_WIN)
+# include "other/UntrustedModules.h"
+#endif
+#include "nsJSUtils.h"
+#include "nsLocalFile.h"
+#include "nsNativeCharsetUtils.h"
+#include "nsNetCID.h"
+#include "nsNetUtil.h"
+#include "nsProxyRelease.h"
+#include "nsReadableUtils.h"
+#include "nsString.h"
+#include "nsTHashtable.h"
+#include "nsThreadUtils.h"
+#if defined(XP_WIN)
+# include "nsUnicharUtils.h"
+#endif
+#include "nsVersionComparator.h"
+#include "nsXPCOMCIDInternal.h"
+#include "other/CombinedStacks.h"
+#include "other/TelemetryIOInterposeObserver.h"
+#include "TelemetryCommon.h"
+#include "TelemetryEvent.h"
+#include "TelemetryHistogram.h"
+#include "TelemetryScalar.h"
+#include "TelemetryUserInteraction.h"
+
+namespace {
+
+using namespace mozilla;
+using mozilla::dom::AutoJSAPI;
+using mozilla::dom::Promise;
+using mozilla::Telemetry::CombinedStacks;
+using mozilla::Telemetry::EventExtraEntry;
+using mozilla::Telemetry::TelemetryIOInterposeObserver;
+using Telemetry::Common::AutoHashtable;
+using Telemetry::Common::GetCurrentProduct;
+using Telemetry::Common::StringHashSet;
+using Telemetry::Common::SupportedProduct;
+using Telemetry::Common::ToJSString;
+
+// This is not a member of TelemetryImpl because we want to record I/O during
+// startup.
+StaticAutoPtr<TelemetryIOInterposeObserver> sTelemetryIOObserver;
+
+void ClearIOReporting() {
+ if (!sTelemetryIOObserver) {
+ return;
+ }
+ IOInterposer::Unregister(IOInterposeObserver::OpAllWithStaging,
+ sTelemetryIOObserver);
+ sTelemetryIOObserver = nullptr;
+}
+
+class TelemetryImpl final : public nsITelemetry, public nsIMemoryReporter {
+ NS_DECL_THREADSAFE_ISUPPORTS
+ NS_DECL_NSITELEMETRY
+ NS_DECL_NSIMEMORYREPORTER
+
+ public:
+ void InitMemoryReporter();
+
+ static already_AddRefed<nsITelemetry> CreateTelemetryInstance();
+ static void ShutdownTelemetry();
+ static void RecordSlowStatement(const nsACString& sql,
+ const nsACString& dbName, uint32_t delay);
+ struct Stat {
+ uint32_t hitCount;
+ uint32_t totalTime;
+ };
+ struct StmtStats {
+ struct Stat mainThread;
+ struct Stat otherThreads;
+ };
+ typedef nsBaseHashtableET<nsCStringHashKey, StmtStats> SlowSQLEntryType;
+
+ static void RecordIceCandidates(const uint32_t iceCandidateBitmask,
+ const bool success);
+ static bool CanRecordBase();
+ static bool CanRecordExtended();
+ static bool CanRecordReleaseData();
+ static bool CanRecordPrereleaseData();
+
+ private:
+ TelemetryImpl();
+ ~TelemetryImpl();
+
+ static nsCString SanitizeSQL(const nsACString& sql);
+
+ enum SanitizedState { Sanitized, Unsanitized };
+
+ static void StoreSlowSQL(const nsACString& offender, uint32_t delay,
+ SanitizedState state);
+
+ static bool ReflectMainThreadSQL(SlowSQLEntryType* entry, JSContext* cx,
+ JS::Handle<JSObject*> obj);
+ static bool ReflectOtherThreadsSQL(SlowSQLEntryType* entry, JSContext* cx,
+ JS::Handle<JSObject*> obj);
+ static bool ReflectSQL(const SlowSQLEntryType* entry, const Stat* stat,
+ JSContext* cx, JS::Handle<JSObject*> obj);
+
+ bool AddSQLInfo(JSContext* cx, JS::Handle<JSObject*> rootObj, bool mainThread,
+ bool privateSQL);
+ bool GetSQLStats(JSContext* cx, JS::MutableHandle<JS::Value> ret,
+ bool includePrivateSql);
+
+ void ReadLateWritesStacks(nsIFile* aProfileDir);
+
+ static StaticDataMutex<TelemetryImpl*> sTelemetry;
+ AutoHashtable<SlowSQLEntryType> mPrivateSQL;
+ AutoHashtable<SlowSQLEntryType> mSanitizedSQL;
+ Mutex mHashMutex MOZ_UNANNOTATED;
+ Atomic<bool, SequentiallyConsistent> mCanRecordBase;
+ Atomic<bool, SequentiallyConsistent> mCanRecordExtended;
+
+ CombinedStacks
+ mLateWritesStacks; // This is collected out of the main thread.
+ bool mCachedTelemetryData;
+ uint32_t mLastShutdownTime;
+ uint32_t mFailedLockCount;
+ nsCOMArray<nsIFetchTelemetryDataCallback> mCallbacks;
+ friend class nsFetchTelemetryData;
+};
+
+StaticDataMutex<TelemetryImpl*> TelemetryImpl::sTelemetry(nullptr, nullptr);
+
+MOZ_DEFINE_MALLOC_SIZE_OF(TelemetryMallocSizeOf)
+
+NS_IMETHODIMP
+TelemetryImpl::CollectReports(nsIHandleReportCallback* aHandleReport,
+ nsISupports* aData, bool aAnonymize) {
+ mozilla::MallocSizeOf aMallocSizeOf = TelemetryMallocSizeOf;
+
+#define COLLECT_REPORT(name, size, desc) \
+ MOZ_COLLECT_REPORT(name, KIND_HEAP, UNITS_BYTES, size, desc)
+
+ COLLECT_REPORT("explicit/telemetry/impl", aMallocSizeOf(this),
+ "Memory used by the Telemetry core implemenation");
+
+ COLLECT_REPORT(
+ "explicit/telemetry/scalar/shallow",
+ TelemetryScalar::GetMapShallowSizesOfExcludingThis(aMallocSizeOf),
+ "Memory used by the Telemetry Scalar implemenation");
+
+ { // Scope for mHashMutex lock
+ MutexAutoLock lock(mHashMutex);
+ COLLECT_REPORT("explicit/telemetry/PrivateSQL",
+ mPrivateSQL.SizeOfExcludingThis(aMallocSizeOf),
+ "Memory used by the PrivateSQL Telemetry");
+
+ COLLECT_REPORT("explicit/telemetry/SanitizedSQL",
+ mSanitizedSQL.SizeOfExcludingThis(aMallocSizeOf),
+ "Memory used by the SanitizedSQL Telemetry");
+ }
+
+ if (sTelemetryIOObserver) {
+ COLLECT_REPORT("explicit/telemetry/IOObserver",
+ sTelemetryIOObserver->SizeOfIncludingThis(aMallocSizeOf),
+ "Memory used by the Telemetry IO Observer");
+ }
+
+ COLLECT_REPORT("explicit/telemetry/LateWritesStacks",
+ mLateWritesStacks.SizeOfExcludingThis(),
+ "Memory used by the Telemetry LateWrites Stack capturer");
+
+ COLLECT_REPORT("explicit/telemetry/Callbacks",
+ mCallbacks.ShallowSizeOfExcludingThis(aMallocSizeOf),
+ "Memory used by the Telemetry Callbacks array (shallow)");
+
+ COLLECT_REPORT(
+ "explicit/telemetry/histogram/data",
+ TelemetryHistogram::GetHistogramSizesOfIncludingThis(aMallocSizeOf),
+ "Memory used by Telemetry Histogram data");
+
+ COLLECT_REPORT("explicit/telemetry/scalar/data",
+ TelemetryScalar::GetScalarSizesOfIncludingThis(aMallocSizeOf),
+ "Memory used by Telemetry Scalar data");
+
+ COLLECT_REPORT("explicit/telemetry/event/data",
+ TelemetryEvent::SizeOfIncludingThis(aMallocSizeOf),
+ "Memory used by Telemetry Event data");
+
+#undef COLLECT_REPORT
+
+ return NS_OK;
+}
+
+void InitHistogramRecordingEnabled() {
+ TelemetryHistogram::InitHistogramRecordingEnabled();
+}
+
+using PathChar = filesystem::Path::value_type;
+using PathCharPtr = const PathChar*;
+
+static uint32_t ReadLastShutdownDuration(PathCharPtr filename) {
+ RefPtr<nsLocalFile> file =
+ new nsLocalFile(nsTDependentString<PathChar>(filename));
+ FILE* f;
+ if (NS_FAILED(file->OpenANSIFileDesc("r", &f)) || !f) {
+ return 0;
+ }
+
+ int shutdownTime;
+ int r = fscanf(f, "%d\n", &shutdownTime);
+ fclose(f);
+ if (r != 1) {
+ return 0;
+ }
+
+ return shutdownTime;
+}
+
+const int32_t kMaxFailedProfileLockFileSize = 10;
+
+bool GetFailedLockCount(nsIInputStream* inStream, uint32_t aCount,
+ unsigned int& result) {
+ nsAutoCString bufStr;
+ nsresult rv;
+ rv = NS_ReadInputStreamToString(inStream, bufStr, aCount);
+ NS_ENSURE_SUCCESS(rv, false);
+ result = bufStr.ToInteger(&rv);
+ return NS_SUCCEEDED(rv) && result > 0;
+}
+
+nsresult GetFailedProfileLockFile(nsIFile** aFile, nsIFile* aProfileDir) {
+ NS_ENSURE_ARG_POINTER(aProfileDir);
+
+ nsresult rv = aProfileDir->Clone(aFile);
+ NS_ENSURE_SUCCESS(rv, rv);
+
+ (*aFile)->AppendNative("Telemetry.FailedProfileLocks.txt"_ns);
+ return NS_OK;
+}
+
+class nsFetchTelemetryData : public Runnable {
+ public:
+ nsFetchTelemetryData(PathCharPtr aShutdownTimeFilename,
+ nsIFile* aFailedProfileLockFile, nsIFile* aProfileDir)
+ : mozilla::Runnable("nsFetchTelemetryData"),
+ mShutdownTimeFilename(aShutdownTimeFilename),
+ mFailedProfileLockFile(aFailedProfileLockFile),
+ mProfileDir(aProfileDir) {}
+
+ private:
+ PathCharPtr mShutdownTimeFilename;
+ nsCOMPtr<nsIFile> mFailedProfileLockFile;
+ nsCOMPtr<nsIFile> mProfileDir;
+
+ public:
+ void MainThread() {
+ auto lock = TelemetryImpl::sTelemetry.Lock();
+ auto telemetry = lock.ref();
+ telemetry->mCachedTelemetryData = true;
+ for (unsigned int i = 0, n = telemetry->mCallbacks.Count(); i < n; ++i) {
+ telemetry->mCallbacks[i]->Complete();
+ }
+ telemetry->mCallbacks.Clear();
+ }
+
+ NS_IMETHOD Run() override {
+ uint32_t failedLockCount = 0;
+ uint32_t lastShutdownDuration = 0;
+ LoadFailedLockCount(failedLockCount);
+ lastShutdownDuration = ReadLastShutdownDuration(mShutdownTimeFilename);
+ {
+ auto lock = TelemetryImpl::sTelemetry.Lock();
+ auto telemetry = lock.ref();
+ telemetry->mFailedLockCount = failedLockCount;
+ telemetry->mLastShutdownTime = lastShutdownDuration;
+ telemetry->ReadLateWritesStacks(mProfileDir);
+ }
+
+ TelemetryScalar::Set(Telemetry::ScalarID::BROWSER_TIMINGS_LAST_SHUTDOWN,
+ lastShutdownDuration);
+
+ nsCOMPtr<nsIRunnable> e =
+ NewRunnableMethod("nsFetchTelemetryData::MainThread", this,
+ &nsFetchTelemetryData::MainThread);
+ NS_ENSURE_STATE(e);
+ NS_DispatchToMainThread(e);
+ return NS_OK;
+ }
+
+ private:
+ nsresult LoadFailedLockCount(uint32_t& failedLockCount) {
+ failedLockCount = 0;
+ int64_t fileSize = 0;
+ nsresult rv = mFailedProfileLockFile->GetFileSize(&fileSize);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ NS_ENSURE_TRUE(fileSize <= kMaxFailedProfileLockFileSize,
+ NS_ERROR_UNEXPECTED);
+ nsCOMPtr<nsIInputStream> inStream;
+ rv = NS_NewLocalFileInputStream(getter_AddRefs(inStream),
+ mFailedProfileLockFile, PR_RDONLY);
+ NS_ENSURE_SUCCESS(rv, rv);
+ NS_ENSURE_TRUE(GetFailedLockCount(inStream, fileSize, failedLockCount),
+ NS_ERROR_UNEXPECTED);
+ inStream->Close();
+
+ mFailedProfileLockFile->Remove(false);
+ return NS_OK;
+ }
+};
+
+static TimeStamp gRecordedShutdownStartTime;
+static bool gAlreadyFreedShutdownTimeFileName = false;
+static PathCharPtr gRecordedShutdownTimeFileName = nullptr;
+
+static PathCharPtr GetShutdownTimeFileName() {
+ if (gAlreadyFreedShutdownTimeFileName) {
+ return nullptr;
+ }
+
+ if (!gRecordedShutdownTimeFileName) {
+ nsCOMPtr<nsIFile> mozFile;
+ NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR, getter_AddRefs(mozFile));
+ if (!mozFile) return nullptr;
+
+ mozFile->AppendNative("Telemetry.ShutdownTime.txt"_ns);
+
+ gRecordedShutdownTimeFileName = NS_xstrdup(mozFile->NativePath().get());
+ }
+
+ return gRecordedShutdownTimeFileName;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetLastShutdownDuration(uint32_t* aResult) {
+ // The user must call AsyncFetchTelemetryData first. We return zero instead of
+ // reporting a failure so that the rest of telemetry can uniformly handle
+ // the read not being available yet.
+ if (!mCachedTelemetryData) {
+ *aResult = 0;
+ return NS_OK;
+ }
+
+ *aResult = mLastShutdownTime;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetFailedProfileLockCount(uint32_t* aResult) {
+ // The user must call AsyncFetchTelemetryData first. We return zero instead of
+ // reporting a failure so that the rest of telemetry can uniformly handle
+ // the read not being available yet.
+ if (!mCachedTelemetryData) {
+ *aResult = 0;
+ return NS_OK;
+ }
+
+ *aResult = mFailedLockCount;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::AsyncFetchTelemetryData(
+ nsIFetchTelemetryDataCallback* aCallback) {
+ // We have finished reading the data already, just call the callback.
+ if (mCachedTelemetryData) {
+ aCallback->Complete();
+ return NS_OK;
+ }
+
+ // We already have a read request running, just remember the callback.
+ if (mCallbacks.Count() != 0) {
+ mCallbacks.AppendObject(aCallback);
+ return NS_OK;
+ }
+
+ // We make this check so that GetShutdownTimeFileName() doesn't get
+ // called; calling that function without telemetry enabled violates
+ // assumptions that the write-the-shutdown-timestamp machinery makes.
+ if (!Telemetry::CanRecordExtended()) {
+ mCachedTelemetryData = true;
+ aCallback->Complete();
+ return NS_OK;
+ }
+
+ // Send the read to a background thread provided by the stream transport
+ // service to avoid a read in the main thread.
+ nsCOMPtr<nsIEventTarget> targetThread =
+ do_GetService(NS_STREAMTRANSPORTSERVICE_CONTRACTID);
+ if (!targetThread) {
+ mCachedTelemetryData = true;
+ aCallback->Complete();
+ return NS_OK;
+ }
+
+ // We have to get the filename from the main thread.
+ PathCharPtr shutdownTimeFilename = GetShutdownTimeFileName();
+ if (!shutdownTimeFilename) {
+ mCachedTelemetryData = true;
+ aCallback->Complete();
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIFile> profileDir;
+ nsresult rv = NS_GetSpecialDirectory(NS_APP_USER_PROFILE_50_DIR,
+ getter_AddRefs(profileDir));
+ if (NS_FAILED(rv)) {
+ mCachedTelemetryData = true;
+ aCallback->Complete();
+ return NS_OK;
+ }
+
+ nsCOMPtr<nsIFile> failedProfileLockFile;
+ rv = GetFailedProfileLockFile(getter_AddRefs(failedProfileLockFile),
+ profileDir);
+ if (NS_FAILED(rv)) {
+ mCachedTelemetryData = true;
+ aCallback->Complete();
+ return NS_OK;
+ }
+
+ mCallbacks.AppendObject(aCallback);
+
+ nsCOMPtr<nsIRunnable> event = new nsFetchTelemetryData(
+ shutdownTimeFilename, failedProfileLockFile, profileDir);
+
+ targetThread->Dispatch(event, NS_DISPATCH_NORMAL);
+ return NS_OK;
+}
+
+TelemetryImpl::TelemetryImpl()
+ : mHashMutex("Telemetry::mHashMutex"),
+ mCanRecordBase(false),
+ mCanRecordExtended(false),
+ mCachedTelemetryData(false),
+ mLastShutdownTime(0),
+ mFailedLockCount(0) {
+ // We expect TelemetryHistogram::InitializeGlobalState() to have been
+ // called before we get to this point.
+ MOZ_ASSERT(TelemetryHistogram::GlobalStateHasBeenInitialized());
+}
+
+TelemetryImpl::~TelemetryImpl() {
+ UnregisterWeakMemoryReporter(this);
+
+ // This is still racey as access to these collections is guarded using
+ // sTelemetry. We will fix this in bug 1367344.
+ MutexAutoLock hashLock(mHashMutex);
+}
+
+void TelemetryImpl::InitMemoryReporter() { RegisterWeakMemoryReporter(this); }
+
+bool TelemetryImpl::ReflectSQL(const SlowSQLEntryType* entry, const Stat* stat,
+ JSContext* cx, JS::Handle<JSObject*> obj) {
+ if (stat->hitCount == 0) return true;
+
+ const nsACString& sql = entry->GetKey();
+
+ JS::Rooted<JSObject*> arrayObj(cx, JS::NewArrayObject(cx, 0));
+ if (!arrayObj) {
+ return false;
+ }
+ return (
+ JS_DefineElement(cx, arrayObj, 0, stat->hitCount, JSPROP_ENUMERATE) &&
+ JS_DefineElement(cx, arrayObj, 1, stat->totalTime, JSPROP_ENUMERATE) &&
+ JS_DefineProperty(cx, obj, sql.BeginReading(), arrayObj,
+ JSPROP_ENUMERATE));
+}
+
+bool TelemetryImpl::ReflectMainThreadSQL(SlowSQLEntryType* entry, JSContext* cx,
+ JS::Handle<JSObject*> obj) {
+ return ReflectSQL(entry, &entry->GetModifiableData()->mainThread, cx, obj);
+}
+
+bool TelemetryImpl::ReflectOtherThreadsSQL(SlowSQLEntryType* entry,
+ JSContext* cx,
+ JS::Handle<JSObject*> obj) {
+ return ReflectSQL(entry, &entry->GetModifiableData()->otherThreads, cx, obj);
+}
+
+bool TelemetryImpl::AddSQLInfo(JSContext* cx, JS::Handle<JSObject*> rootObj,
+ bool mainThread, bool privateSQL) {
+ JS::Rooted<JSObject*> statsObj(cx, JS_NewPlainObject(cx));
+ if (!statsObj) return false;
+
+ AutoHashtable<SlowSQLEntryType>& sqlMap =
+ (privateSQL ? mPrivateSQL : mSanitizedSQL);
+ AutoHashtable<SlowSQLEntryType>::ReflectEntryFunc reflectFunction =
+ (mainThread ? ReflectMainThreadSQL : ReflectOtherThreadsSQL);
+ if (!sqlMap.ReflectIntoJS(reflectFunction, cx, statsObj)) {
+ return false;
+ }
+
+ return JS_DefineProperty(cx, rootObj,
+ mainThread ? "mainThread" : "otherThreads", statsObj,
+ JSPROP_ENUMERATE);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::SetHistogramRecordingEnabled(const nsACString& id,
+ bool aEnabled) {
+ return TelemetryHistogram::SetHistogramRecordingEnabled(id, aEnabled);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetSnapshotForHistograms(const nsACString& aStoreName,
+ bool aClearStore, bool aFilterTest,
+ JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult) {
+ constexpr auto defaultStore = "main"_ns;
+ unsigned int dataset = mCanRecordExtended
+ ? nsITelemetry::DATASET_PRERELEASE_CHANNELS
+ : nsITelemetry::DATASET_ALL_CHANNELS;
+ return TelemetryHistogram::CreateHistogramSnapshots(
+ aCx, aResult, aStoreName.IsVoid() ? defaultStore : aStoreName, dataset,
+ aClearStore, aFilterTest);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetSnapshotForKeyedHistograms(
+ const nsACString& aStoreName, bool aClearStore, bool aFilterTest,
+ JSContext* aCx, JS::MutableHandle<JS::Value> aResult) {
+ constexpr auto defaultStore = "main"_ns;
+ unsigned int dataset = mCanRecordExtended
+ ? nsITelemetry::DATASET_PRERELEASE_CHANNELS
+ : nsITelemetry::DATASET_ALL_CHANNELS;
+ return TelemetryHistogram::GetKeyedHistogramSnapshots(
+ aCx, aResult, aStoreName.IsVoid() ? defaultStore : aStoreName, dataset,
+ aClearStore, aFilterTest);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetCategoricalLabels(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult) {
+ return TelemetryHistogram::GetCategoricalHistogramLabels(aCx, aResult);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetSnapshotForScalars(const nsACString& aStoreName,
+ bool aClearStore, bool aFilterTest,
+ JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult) {
+ constexpr auto defaultStore = "main"_ns;
+ unsigned int dataset = mCanRecordExtended
+ ? nsITelemetry::DATASET_PRERELEASE_CHANNELS
+ : nsITelemetry::DATASET_ALL_CHANNELS;
+ return TelemetryScalar::CreateSnapshots(
+ dataset, aClearStore, aCx, 1, aResult, aFilterTest,
+ aStoreName.IsVoid() ? defaultStore : aStoreName);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetSnapshotForKeyedScalars(
+ const nsACString& aStoreName, bool aClearStore, bool aFilterTest,
+ JSContext* aCx, JS::MutableHandle<JS::Value> aResult) {
+ constexpr auto defaultStore = "main"_ns;
+ unsigned int dataset = mCanRecordExtended
+ ? nsITelemetry::DATASET_PRERELEASE_CHANNELS
+ : nsITelemetry::DATASET_ALL_CHANNELS;
+ return TelemetryScalar::CreateKeyedSnapshots(
+ dataset, aClearStore, aCx, 1, aResult, aFilterTest,
+ aStoreName.IsVoid() ? defaultStore : aStoreName);
+}
+
+bool TelemetryImpl::GetSQLStats(JSContext* cx, JS::MutableHandle<JS::Value> ret,
+ bool includePrivateSql) {
+ JS::Rooted<JSObject*> root_obj(cx, JS_NewPlainObject(cx));
+ if (!root_obj) return false;
+ ret.setObject(*root_obj);
+
+ MutexAutoLock hashMutex(mHashMutex);
+ // Add info about slow SQL queries on the main thread
+ if (!AddSQLInfo(cx, root_obj, true, includePrivateSql)) return false;
+ // Add info about slow SQL queries on other threads
+ if (!AddSQLInfo(cx, root_obj, false, includePrivateSql)) return false;
+
+ return true;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetSlowSQL(JSContext* cx, JS::MutableHandle<JS::Value> ret) {
+ if (GetSQLStats(cx, ret, false)) return NS_OK;
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetDebugSlowSQL(JSContext* cx,
+ JS::MutableHandle<JS::Value> ret) {
+ bool revealPrivateSql =
+ Preferences::GetBool("toolkit.telemetry.debugSlowSql", false);
+ if (GetSQLStats(cx, ret, revealPrivateSql)) return NS_OK;
+ return NS_ERROR_FAILURE;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetUntrustedModuleLoadEvents(uint32_t aFlags, JSContext* cx,
+ Promise** aPromise) {
+#if defined(XP_WIN)
+ return Telemetry::GetUntrustedModuleLoadEvents(aFlags, cx, aPromise);
+#else
+ return NS_ERROR_NOT_IMPLEMENTED;
+#endif
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetAreUntrustedModuleLoadEventsReady(bool* ret) {
+#if defined(XP_WIN)
+ *ret = DllServices::Get()->IsReadyForBackgroundProcessing();
+ return NS_OK;
+#else
+ return NS_ERROR_NOT_IMPLEMENTED;
+#endif
+}
+
+#if defined(MOZ_GECKO_PROFILER)
+class GetLoadedModulesResultRunnable final : public Runnable {
+ nsMainThreadPtrHandle<Promise> mPromise;
+ SharedLibraryInfo mRawModules;
+ nsCOMPtr<nsIThread> mWorkerThread;
+# if defined(XP_WIN)
+ nsTHashMap<nsStringHashKey, nsString> mCertSubjects;
+# endif // defined(XP_WIN)
+
+ public:
+ GetLoadedModulesResultRunnable(const nsMainThreadPtrHandle<Promise>& aPromise,
+ const SharedLibraryInfo& rawModules)
+ : mozilla::Runnable("GetLoadedModulesResultRunnable"),
+ mPromise(aPromise),
+ mRawModules(rawModules),
+ mWorkerThread(do_GetCurrentThread()) {
+ MOZ_ASSERT(!NS_IsMainThread());
+# if defined(XP_WIN)
+ ObtainCertSubjects();
+# endif // defined(XP_WIN)
+ }
+
+ NS_IMETHOD
+ Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ mWorkerThread->Shutdown();
+
+ AutoJSAPI jsapi;
+ if (NS_WARN_IF(!jsapi.Init(mPromise->GetGlobalObject()))) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+
+ JSContext* cx = jsapi.cx();
+
+ JS::Rooted<JSObject*> moduleArray(cx, JS::NewArrayObject(cx, 0));
+ if (!moduleArray) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+
+ for (unsigned int i = 0, n = mRawModules.GetSize(); i != n; i++) {
+ const SharedLibrary& info = mRawModules.GetEntry(i);
+
+ JS::Rooted<JSObject*> moduleObj(cx, JS_NewPlainObject(cx));
+ if (!moduleObj) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+
+ // Module name.
+ JS::Rooted<JSString*> moduleName(
+ cx, JS_NewUCStringCopyZ(cx, info.GetModuleName().get()));
+ if (!moduleName || !JS_DefineProperty(cx, moduleObj, "name", moduleName,
+ JSPROP_ENUMERATE)) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+
+ // Module debug name.
+ JS::Rooted<JS::Value> moduleDebugName(cx);
+
+ if (!info.GetDebugName().IsEmpty()) {
+ JS::Rooted<JSString*> str_moduleDebugName(
+ cx, JS_NewUCStringCopyZ(cx, info.GetDebugName().get()));
+ if (!str_moduleDebugName) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+ moduleDebugName.setString(str_moduleDebugName);
+ } else {
+ moduleDebugName.setNull();
+ }
+
+ if (!JS_DefineProperty(cx, moduleObj, "debugName", moduleDebugName,
+ JSPROP_ENUMERATE)) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+
+ // Module Breakpad identifier.
+ JS::Rooted<JS::Value> id(cx);
+
+ if (!info.GetBreakpadId().IsEmpty()) {
+ JS::Rooted<JSString*> str_id(
+ cx, JS_NewStringCopyZ(cx, info.GetBreakpadId().get()));
+ if (!str_id) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+ id.setString(str_id);
+ } else {
+ id.setNull();
+ }
+
+ if (!JS_DefineProperty(cx, moduleObj, "debugID", id, JSPROP_ENUMERATE)) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+
+ // Module version.
+ JS::Rooted<JS::Value> version(cx);
+
+ if (!info.GetVersion().IsEmpty()) {
+ JS::Rooted<JSString*> v(
+ cx, JS_NewStringCopyZ(cx, info.GetVersion().BeginReading()));
+ if (!v) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+ version.setString(v);
+ } else {
+ version.setNull();
+ }
+
+ if (!JS_DefineProperty(cx, moduleObj, "version", version,
+ JSPROP_ENUMERATE)) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+
+# if defined(XP_WIN)
+ // Cert Subject.
+ if (auto subject = mCertSubjects.Lookup(info.GetModulePath())) {
+ JS::Rooted<JSString*> jsOrg(cx, ToJSString(cx, *subject));
+ if (!jsOrg) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+
+ JS::Rooted<JS::Value> certSubject(cx);
+ certSubject.setString(jsOrg);
+
+ if (!JS_DefineProperty(cx, moduleObj, "certSubject", certSubject,
+ JSPROP_ENUMERATE)) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+ }
+# endif // defined(XP_WIN)
+
+ if (!JS_DefineElement(cx, moduleArray, i, moduleObj, JSPROP_ENUMERATE)) {
+ mPromise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+ }
+
+ mPromise->MaybeResolve(moduleArray);
+ return NS_OK;
+ }
+
+ private:
+# if defined(XP_WIN)
+ void ObtainCertSubjects() {
+ MOZ_ASSERT(!NS_IsMainThread());
+
+ // NB: Currently we cannot lower this down to the profiler layer due to
+ // differing startup dependencies between the profiler and DllServices.
+ RefPtr<DllServices> dllSvc(DllServices::Get());
+
+ for (unsigned int i = 0, n = mRawModules.GetSize(); i != n; i++) {
+ const SharedLibrary& info = mRawModules.GetEntry(i);
+
+ auto orgName = dllSvc->GetBinaryOrgName(info.GetModulePath().get());
+ if (orgName) {
+ mCertSubjects.InsertOrUpdate(info.GetModulePath(),
+ nsDependentString(orgName.get()));
+ }
+ }
+ }
+# endif // defined(XP_WIN)
+};
+
+class GetLoadedModulesRunnable final : public Runnable {
+ nsMainThreadPtrHandle<Promise> mPromise;
+
+ public:
+ explicit GetLoadedModulesRunnable(
+ const nsMainThreadPtrHandle<Promise>& aPromise)
+ : mozilla::Runnable("GetLoadedModulesRunnable"), mPromise(aPromise) {}
+
+ NS_IMETHOD
+ Run() override {
+ nsCOMPtr<nsIRunnable> resultRunnable = new GetLoadedModulesResultRunnable(
+ mPromise, SharedLibraryInfo::GetInfoForSelf());
+ return NS_DispatchToMainThread(resultRunnable);
+ }
+};
+#endif // MOZ_GECKO_PROFILER
+
+NS_IMETHODIMP
+TelemetryImpl::GetLoadedModules(JSContext* cx, Promise** aPromise) {
+#if defined(MOZ_GECKO_PROFILER)
+ nsIGlobalObject* global = xpc::CurrentNativeGlobal(cx);
+ if (NS_WARN_IF(!global)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult result;
+ RefPtr<Promise> promise = Promise::Create(global, result);
+ if (NS_WARN_IF(result.Failed())) {
+ return result.StealNSResult();
+ }
+
+ nsCOMPtr<nsIThread> getModulesThread;
+ nsresult rv =
+ NS_NewNamedThread("TelemetryModule", getter_AddRefs(getModulesThread));
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ promise->MaybeReject(NS_ERROR_FAILURE);
+ return NS_OK;
+ }
+
+ nsMainThreadPtrHandle<Promise> mainThreadPromise(
+ new nsMainThreadPtrHolder<Promise>(
+ "TelemetryImpl::GetLoadedModules::Promise", promise));
+ nsCOMPtr<nsIRunnable> runnable =
+ new GetLoadedModulesRunnable(mainThreadPromise);
+ promise.forget(aPromise);
+
+ return getModulesThread->Dispatch(runnable, nsIEventTarget::DISPATCH_NORMAL);
+#else // MOZ_GECKO_PROFILER
+ return NS_ERROR_NOT_IMPLEMENTED;
+#endif // MOZ_GECKO_PROFILER
+}
+
+static bool IsValidBreakpadId(const std::string& breakpadId) {
+ if (breakpadId.size() < 33) {
+ return false;
+ }
+ for (char c : breakpadId) {
+ if ((c < '0' || c > '9') && (c < 'A' || c > 'F')) {
+ return false;
+ }
+ }
+ return true;
+}
+
+// Read a stack from the given file name. In case of any error, aStack is
+// unchanged.
+static void ReadStack(PathCharPtr aFileName,
+ Telemetry::ProcessedStack& aStack) {
+ IFStream file(aFileName);
+
+ size_t numModules;
+ file >> numModules;
+ if (file.fail()) {
+ return;
+ }
+
+ char newline = file.get();
+ if (file.fail() || newline != '\n') {
+ return;
+ }
+
+ Telemetry::ProcessedStack stack;
+ for (size_t i = 0; i < numModules; ++i) {
+ std::string breakpadId;
+ file >> breakpadId;
+ if (file.fail() || !IsValidBreakpadId(breakpadId)) {
+ return;
+ }
+
+ char space = file.get();
+ if (file.fail() || space != ' ') {
+ return;
+ }
+
+ std::string moduleName;
+ getline(file, moduleName);
+ if (file.fail() || moduleName[0] == ' ') {
+ return;
+ }
+
+ Telemetry::ProcessedStack::Module module = {
+ NS_ConvertUTF8toUTF16(moduleName.c_str()),
+ nsCString(breakpadId.c_str(), breakpadId.size()),
+ };
+ stack.AddModule(module);
+ }
+
+ size_t numFrames;
+ file >> numFrames;
+ if (file.fail()) {
+ return;
+ }
+
+ newline = file.get();
+ if (file.fail() || newline != '\n') {
+ return;
+ }
+
+ for (size_t i = 0; i < numFrames; ++i) {
+ uint16_t index;
+ file >> index;
+ uintptr_t offset;
+ file >> std::hex >> offset >> std::dec;
+ if (file.fail()) {
+ return;
+ }
+
+ Telemetry::ProcessedStack::Frame frame = {offset, index};
+ stack.AddFrame(frame);
+ }
+
+ aStack = stack;
+}
+
+void TelemetryImpl::ReadLateWritesStacks(nsIFile* aProfileDir) {
+ nsCOMPtr<nsIDirectoryEnumerator> files;
+ if (NS_FAILED(aProfileDir->GetDirectoryEntries(getter_AddRefs(files)))) {
+ return;
+ }
+
+ constexpr auto prefix = u"Telemetry.LateWriteFinal-"_ns;
+ nsCOMPtr<nsIFile> file;
+ while (NS_SUCCEEDED(files->GetNextFile(getter_AddRefs(file))) && file) {
+ nsAutoString leafName;
+ if (NS_FAILED(file->GetLeafName(leafName)) ||
+ !StringBeginsWith(leafName, prefix)) {
+ continue;
+ }
+
+ Telemetry::ProcessedStack stack;
+ ReadStack(file->NativePath().get(), stack);
+ if (stack.GetStackSize() != 0) {
+ mLateWritesStacks.AddStack(stack);
+ }
+ // Delete the file so that we don't report it again on the next run.
+ file->Remove(false);
+ }
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetLateWrites(JSContext* cx, JS::MutableHandle<JS::Value> ret) {
+ // The user must call AsyncReadTelemetryData first. We return an empty list
+ // instead of reporting a failure so that the rest of telemetry can uniformly
+ // handle the read not being available yet.
+
+ // FIXME: we allocate the js object again and again in the getter. We should
+ // figure out a way to cache it. In order to do that we have to call
+ // JS_AddNamedObjectRoot. A natural place to do so is in the TelemetryImpl
+ // constructor, but it is not clear how to get a JSContext in there.
+ // Another option would be to call it in here when we first call
+ // CreateJSStackObject, but we would still need to figure out where to call
+ // JS_RemoveObjectRoot. Would it be ok to never call JS_RemoveObjectRoot
+ // and just set the pointer to nullptr is the telemetry destructor?
+
+ JSObject* report;
+ if (!mCachedTelemetryData) {
+ CombinedStacks empty;
+ report = CreateJSStackObject(cx, empty);
+ } else {
+ report = CreateJSStackObject(cx, mLateWritesStacks);
+ }
+
+ if (report == nullptr) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ret.setObject(*report);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetHistogramById(const nsACString& name, JSContext* cx,
+ JS::MutableHandle<JS::Value> ret) {
+ return TelemetryHistogram::GetHistogramById(name, cx, ret);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetKeyedHistogramById(const nsACString& name, JSContext* cx,
+ JS::MutableHandle<JS::Value> ret) {
+ return TelemetryHistogram::GetKeyedHistogramById(name, cx, ret);
+}
+
+/**
+ * Indicates if Telemetry can record base data (FHR data). This is true if the
+ * FHR data reporting service or self-support are enabled.
+ *
+ * In the unlikely event that adding a new base probe is needed, please check
+ * the data collection wiki at https://wiki.mozilla.org/Firefox/Data_Collection
+ * and talk to the Telemetry team.
+ */
+NS_IMETHODIMP
+TelemetryImpl::GetCanRecordBase(bool* ret) {
+ *ret = mCanRecordBase;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::SetCanRecordBase(bool canRecord) {
+#ifndef FUZZING
+ if (canRecord != mCanRecordBase) {
+ TelemetryHistogram::SetCanRecordBase(canRecord);
+ TelemetryScalar::SetCanRecordBase(canRecord);
+ TelemetryEvent::SetCanRecordBase(canRecord);
+ mCanRecordBase = canRecord;
+ }
+#endif
+ return NS_OK;
+}
+
+/**
+ * Indicates if Telemetry is allowed to record extended data. Returns false if
+ * the user hasn't opted into "extended Telemetry" on the Release channel, when
+ * the user has explicitly opted out of Telemetry on Nightly/Aurora/Beta or if
+ * manually set to false during tests. If the returned value is false, gathering
+ * of extended telemetry statistics is disabled.
+ */
+NS_IMETHODIMP
+TelemetryImpl::GetCanRecordExtended(bool* ret) {
+ *ret = mCanRecordExtended;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::SetCanRecordExtended(bool canRecord) {
+#ifndef FUZZING
+ if (canRecord != mCanRecordExtended) {
+ TelemetryHistogram::SetCanRecordExtended(canRecord);
+ TelemetryScalar::SetCanRecordExtended(canRecord);
+ TelemetryEvent::SetCanRecordExtended(canRecord);
+ mCanRecordExtended = canRecord;
+ }
+#endif
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetCanRecordReleaseData(bool* ret) {
+ *ret = mCanRecordBase;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetCanRecordPrereleaseData(bool* ret) {
+ *ret = mCanRecordExtended;
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetIsOfficialTelemetry(bool* ret) {
+#if defined(MOZILLA_OFFICIAL) && defined(MOZ_TELEMETRY_REPORTING) && \
+ !defined(DEBUG)
+ *ret = true;
+#else
+ *ret = false;
+#endif
+ return NS_OK;
+}
+
+already_AddRefed<nsITelemetry> TelemetryImpl::CreateTelemetryInstance() {
+ {
+ auto lock = sTelemetry.Lock();
+ MOZ_ASSERT(
+ *lock == nullptr,
+ "CreateTelemetryInstance may only be called once, via GetService()");
+ }
+
+ bool useTelemetry = false;
+#ifndef FUZZING
+ if (XRE_IsParentProcess() || XRE_IsContentProcess() || XRE_IsGPUProcess() ||
+ XRE_IsRDDProcess() || XRE_IsSocketProcess() || XRE_IsUtilityProcess()) {
+ useTelemetry = true;
+ }
+#endif
+#ifdef MOZ_BACKGROUNDTASKS
+ if (BackgroundTasks::IsBackgroundTaskMode()) {
+ // Background tasks collect per-task metrics with Glean.
+ useTelemetry = false;
+ }
+#endif
+
+ // First, initialize the TelemetryHistogram and TelemetryScalar global states.
+ TelemetryHistogram::InitializeGlobalState(useTelemetry, useTelemetry);
+ TelemetryScalar::InitializeGlobalState(useTelemetry, useTelemetry);
+
+ // Only record events from the parent process.
+ TelemetryEvent::InitializeGlobalState(XRE_IsParentProcess(),
+ XRE_IsParentProcess());
+
+ // Currently, only UserInteractions from the parent process are recorded.
+ TelemetryUserInteraction::InitializeGlobalState(useTelemetry, useTelemetry);
+
+ // Now, create and initialize the Telemetry global state.
+ TelemetryImpl* telemetry = new TelemetryImpl();
+ {
+ auto lock = sTelemetry.Lock();
+ *lock = telemetry;
+ // AddRef for the local reference before releasing the lock.
+ NS_ADDREF(telemetry);
+ }
+
+ // AddRef for the caller
+ nsCOMPtr<nsITelemetry> ret = telemetry;
+
+ telemetry->mCanRecordBase = useTelemetry;
+ telemetry->mCanRecordExtended = useTelemetry;
+
+ telemetry->InitMemoryReporter();
+ InitHistogramRecordingEnabled(); // requires sTelemetry to exist
+
+ return ret.forget();
+}
+
+void TelemetryImpl::ShutdownTelemetry() {
+ // No point in collecting IO beyond this point
+ ClearIOReporting();
+ {
+ auto lock = sTelemetry.Lock();
+ NS_IF_RELEASE(lock.ref());
+ }
+
+ // Lastly, de-initialise the TelemetryHistogram and TelemetryScalar global
+ // states, so as to release any heap storage that would otherwise be kept
+ // alive by it.
+ TelemetryHistogram::DeInitializeGlobalState();
+ TelemetryScalar::DeInitializeGlobalState();
+ TelemetryEvent::DeInitializeGlobalState();
+
+ TelemetryUserInteraction::DeInitializeGlobalState();
+ TelemetryIPCAccumulator::DeInitializeGlobalState();
+}
+
+void TelemetryImpl::StoreSlowSQL(const nsACString& sql, uint32_t delay,
+ SanitizedState state) {
+ auto lock = sTelemetry.Lock();
+ auto telemetry = lock.ref();
+ AutoHashtable<SlowSQLEntryType>* slowSQLMap = nullptr;
+ if (state == Sanitized)
+ slowSQLMap = &(telemetry->mSanitizedSQL);
+ else
+ slowSQLMap = &(telemetry->mPrivateSQL);
+
+ MutexAutoLock hashMutex(telemetry->mHashMutex);
+
+ SlowSQLEntryType* entry = slowSQLMap->GetEntry(sql);
+ if (!entry) {
+ entry = slowSQLMap->PutEntry(sql);
+ if (MOZ_UNLIKELY(!entry)) return;
+ entry->GetModifiableData()->mainThread.hitCount = 0;
+ entry->GetModifiableData()->mainThread.totalTime = 0;
+ entry->GetModifiableData()->otherThreads.hitCount = 0;
+ entry->GetModifiableData()->otherThreads.totalTime = 0;
+ }
+
+ if (NS_IsMainThread()) {
+ entry->GetModifiableData()->mainThread.hitCount++;
+ entry->GetModifiableData()->mainThread.totalTime += delay;
+ } else {
+ entry->GetModifiableData()->otherThreads.hitCount++;
+ entry->GetModifiableData()->otherThreads.totalTime += delay;
+ }
+}
+
+/**
+ * This method replaces string literals in SQL strings with the word :private
+ *
+ * States used in this state machine:
+ *
+ * NORMAL:
+ * - This is the active state when not iterating over a string literal or
+ * comment
+ *
+ * SINGLE_QUOTE:
+ * - Defined here: http://www.sqlite.org/lang_expr.html
+ * - This state represents iterating over a string literal opened with
+ * a single quote.
+ * - A single quote within the string can be encoded by putting 2 single quotes
+ * in a row, e.g. 'This literal contains an escaped quote '''
+ * - Any double quotes found within a single-quoted literal are ignored
+ * - This state covers BLOB literals, e.g. X'ABC123'
+ * - The string literal and the enclosing quotes will be replaced with
+ * the text :private
+ *
+ * DOUBLE_QUOTE:
+ * - Same rules as the SINGLE_QUOTE state.
+ * - According to http://www.sqlite.org/lang_keywords.html,
+ * SQLite interprets text in double quotes as an identifier unless it's used in
+ * a context where it cannot be resolved to an identifier and a string literal
+ * is allowed. This method removes text in double-quotes for safety.
+ *
+ * DASH_COMMENT:
+ * - http://www.sqlite.org/lang_comment.html
+ * - A dash comment starts with two dashes in a row,
+ * e.g. DROP TABLE foo -- a comment
+ * - Any text following two dashes in a row is interpreted as a comment until
+ * end of input or a newline character
+ * - Any quotes found within the comment are ignored and no replacements made
+ *
+ * C_STYLE_COMMENT:
+ * - http://www.sqlite.org/lang_comment.html
+ * - A C-style comment starts with a forward slash and an asterisk, and ends
+ * with an asterisk and a forward slash
+ * - Any text following comment start is interpreted as a comment up to end of
+ * input or comment end
+ * - Any quotes found within the comment are ignored and no replacements made
+ */
+nsCString TelemetryImpl::SanitizeSQL(const nsACString& sql) {
+ nsCString output;
+ int length = sql.Length();
+
+ typedef enum {
+ NORMAL,
+ SINGLE_QUOTE,
+ DOUBLE_QUOTE,
+ DASH_COMMENT,
+ C_STYLE_COMMENT,
+ } State;
+
+ State state = NORMAL;
+ int fragmentStart = 0;
+ for (int i = 0; i < length; i++) {
+ char character = sql[i];
+ char nextCharacter = (i + 1 < length) ? sql[i + 1] : '\0';
+
+ switch (character) {
+ case '\'':
+ case '"':
+ if (state == NORMAL) {
+ state = (character == '\'') ? SINGLE_QUOTE : DOUBLE_QUOTE;
+ output +=
+ nsDependentCSubstring(sql, fragmentStart, i - fragmentStart);
+ output += ":private";
+ fragmentStart = -1;
+ } else if ((state == SINGLE_QUOTE && character == '\'') ||
+ (state == DOUBLE_QUOTE && character == '"')) {
+ if (nextCharacter == character) {
+ // Two consecutive quotes within a string literal are a single
+ // escaped quote
+ i++;
+ } else {
+ state = NORMAL;
+ fragmentStart = i + 1;
+ }
+ }
+ break;
+ case '-':
+ if (state == NORMAL) {
+ if (nextCharacter == '-') {
+ state = DASH_COMMENT;
+ i++;
+ }
+ }
+ break;
+ case '\n':
+ if (state == DASH_COMMENT) {
+ state = NORMAL;
+ }
+ break;
+ case '/':
+ if (state == NORMAL) {
+ if (nextCharacter == '*') {
+ state = C_STYLE_COMMENT;
+ i++;
+ }
+ }
+ break;
+ case '*':
+ if (state == C_STYLE_COMMENT) {
+ if (nextCharacter == '/') {
+ state = NORMAL;
+ }
+ }
+ break;
+ default:
+ continue;
+ }
+ }
+
+ if ((fragmentStart >= 0) && fragmentStart < length)
+ output += nsDependentCSubstring(sql, fragmentStart, length - fragmentStart);
+
+ return output;
+}
+
+// An allowlist mechanism to prevent Telemetry reporting on Addon & Thunderbird
+// DBs.
+struct TrackedDBEntry {
+ const char* mName;
+ const uint32_t mNameLength;
+
+ // This struct isn't meant to be used beyond the static arrays below.
+ constexpr TrackedDBEntry(const char* aName, uint32_t aNameLength)
+ : mName(aName), mNameLength(aNameLength) {}
+
+ TrackedDBEntry() = delete;
+ TrackedDBEntry(TrackedDBEntry&) = delete;
+};
+
+#define TRACKEDDB_ENTRY(_name) \
+ { _name, (sizeof(_name) - 1) }
+
+// An allowlist of database names. If the database name exactly matches one of
+// these then its SQL statements will always be recorded.
+static constexpr TrackedDBEntry kTrackedDBs[] = {
+ // IndexedDB for about:home, see aboutHome.js
+ TRACKEDDB_ENTRY("818200132aebmoouht.sqlite"),
+ TRACKEDDB_ENTRY("addons.sqlite"),
+ TRACKEDDB_ENTRY("content-prefs.sqlite"),
+ TRACKEDDB_ENTRY("cookies.sqlite"),
+ TRACKEDDB_ENTRY("extensions.sqlite"),
+ TRACKEDDB_ENTRY("favicons.sqlite"),
+ TRACKEDDB_ENTRY("formhistory.sqlite"),
+ TRACKEDDB_ENTRY("index.sqlite"),
+ TRACKEDDB_ENTRY("netpredictions.sqlite"),
+ TRACKEDDB_ENTRY("permissions.sqlite"),
+ TRACKEDDB_ENTRY("places.sqlite"),
+ TRACKEDDB_ENTRY("reading-list.sqlite"),
+ TRACKEDDB_ENTRY("search.sqlite"),
+ TRACKEDDB_ENTRY("urlclassifier3.sqlite"),
+ TRACKEDDB_ENTRY("webappsstore.sqlite")};
+
+// An allowlist of database name prefixes. If the database name begins with
+// one of these prefixes then its SQL statements will always be recorded.
+static const TrackedDBEntry kTrackedDBPrefixes[] = {
+ TRACKEDDB_ENTRY("indexedDB-")};
+
+#undef TRACKEDDB_ENTRY
+
+// Slow SQL statements will be automatically
+// trimmed to kMaxSlowStatementLength characters.
+// This limit doesn't include the ellipsis and DB name,
+// that are appended at the end of the stored statement.
+const uint32_t kMaxSlowStatementLength = 1000;
+
+void TelemetryImpl::RecordSlowStatement(const nsACString& sql,
+ const nsACString& dbName,
+ uint32_t delay) {
+ MOZ_ASSERT(!sql.IsEmpty());
+ MOZ_ASSERT(!dbName.IsEmpty());
+
+ {
+ auto lock = sTelemetry.Lock();
+ if (!lock.ref() || !TelemetryHistogram::CanRecordExtended()) {
+ return;
+ }
+ }
+
+ bool recordStatement = false;
+
+ for (const TrackedDBEntry& nameEntry : kTrackedDBs) {
+ MOZ_ASSERT(nameEntry.mNameLength);
+ const nsDependentCString name(nameEntry.mName, nameEntry.mNameLength);
+ if (dbName == name) {
+ recordStatement = true;
+ break;
+ }
+ }
+
+ if (!recordStatement) {
+ for (const TrackedDBEntry& prefixEntry : kTrackedDBPrefixes) {
+ MOZ_ASSERT(prefixEntry.mNameLength);
+ const nsDependentCString prefix(prefixEntry.mName,
+ prefixEntry.mNameLength);
+ if (StringBeginsWith(dbName, prefix)) {
+ recordStatement = true;
+ break;
+ }
+ }
+ }
+
+ if (recordStatement) {
+ nsAutoCString sanitizedSQL(SanitizeSQL(sql));
+ if (sanitizedSQL.Length() > kMaxSlowStatementLength) {
+ sanitizedSQL.SetLength(kMaxSlowStatementLength);
+ sanitizedSQL += "...";
+ }
+ sanitizedSQL.AppendPrintf(" /* %s */", nsPromiseFlatCString(dbName).get());
+ StoreSlowSQL(sanitizedSQL, delay, Sanitized);
+ } else {
+ // Report aggregate DB-level statistics for addon DBs
+ nsAutoCString aggregate;
+ aggregate.AppendPrintf("Untracked SQL for %s",
+ nsPromiseFlatCString(dbName).get());
+ StoreSlowSQL(aggregate, delay, Sanitized);
+ }
+
+ nsAutoCString fullSQL;
+ fullSQL.AppendPrintf("%s /* %s */", nsPromiseFlatCString(sql).get(),
+ nsPromiseFlatCString(dbName).get());
+ StoreSlowSQL(fullSQL, delay, Unsanitized);
+}
+
+bool TelemetryImpl::CanRecordBase() {
+ auto lock = sTelemetry.Lock();
+ auto telemetry = lock.ref();
+ if (!telemetry) {
+ return false;
+ }
+ bool canRecordBase;
+ nsresult rv = telemetry->GetCanRecordBase(&canRecordBase);
+ return NS_SUCCEEDED(rv) && canRecordBase;
+}
+
+bool TelemetryImpl::CanRecordExtended() {
+ auto lock = sTelemetry.Lock();
+ auto telemetry = lock.ref();
+ if (!telemetry) {
+ return false;
+ }
+ bool canRecordExtended;
+ nsresult rv = telemetry->GetCanRecordExtended(&canRecordExtended);
+ return NS_SUCCEEDED(rv) && canRecordExtended;
+}
+
+bool TelemetryImpl::CanRecordReleaseData() { return CanRecordBase(); }
+
+bool TelemetryImpl::CanRecordPrereleaseData() { return CanRecordExtended(); }
+
+NS_IMPL_ISUPPORTS(TelemetryImpl, nsITelemetry, nsIMemoryReporter)
+
+NS_IMETHODIMP
+TelemetryImpl::GetFileIOReports(JSContext* cx,
+ JS::MutableHandle<JS::Value> ret) {
+ if (sTelemetryIOObserver) {
+ JS::Rooted<JSObject*> obj(cx, JS_NewPlainObject(cx));
+ if (!obj) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!sTelemetryIOObserver->ReflectIntoJS(cx, obj)) {
+ return NS_ERROR_FAILURE;
+ }
+ ret.setObject(*obj);
+ return NS_OK;
+ }
+ ret.setNull();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::MsSinceProcessStart(double* aResult) {
+ return Telemetry::Common::MsSinceProcessStart(aResult);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::MsSinceProcessStartIncludingSuspend(double* aResult) {
+ return Telemetry::Common::MsSinceProcessStartIncludingSuspend(aResult);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::MsSinceProcessStartExcludingSuspend(double* aResult) {
+ return Telemetry::Common::MsSinceProcessStartExcludingSuspend(aResult);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::MsSystemNow(double* aResult) {
+#if defined(XP_UNIX) && !defined(XP_DARWIN)
+ timespec ts;
+ clock_gettime(CLOCK_REALTIME, &ts);
+ *aResult = ts.tv_sec * 1000 + ts.tv_nsec / 1000000;
+#else
+ using namespace std::chrono;
+ milliseconds ms =
+ duration_cast<milliseconds>(system_clock::now().time_since_epoch());
+ *aResult = static_cast<double>(ms.count());
+#endif // XP_UNIX && !XP_DARWIN
+
+ return NS_OK;
+}
+
+// Telemetry Scalars IDL Implementation
+
+NS_IMETHODIMP
+TelemetryImpl::ScalarAdd(const nsACString& aName, JS::Handle<JS::Value> aVal,
+ JSContext* aCx) {
+ return TelemetryScalar::Add(aName, aVal, aCx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::ScalarSet(const nsACString& aName, JS::Handle<JS::Value> aVal,
+ JSContext* aCx) {
+ return TelemetryScalar::Set(aName, aVal, aCx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::ScalarSetMaximum(const nsACString& aName,
+ JS::Handle<JS::Value> aVal, JSContext* aCx) {
+ return TelemetryScalar::SetMaximum(aName, aVal, aCx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::KeyedScalarAdd(const nsACString& aName, const nsAString& aKey,
+ JS::Handle<JS::Value> aVal, JSContext* aCx) {
+ return TelemetryScalar::Add(aName, aKey, aVal, aCx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::KeyedScalarSet(const nsACString& aName, const nsAString& aKey,
+ JS::Handle<JS::Value> aVal, JSContext* aCx) {
+ return TelemetryScalar::Set(aName, aKey, aVal, aCx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::KeyedScalarSetMaximum(const nsACString& aName,
+ const nsAString& aKey,
+ JS::Handle<JS::Value> aVal,
+ JSContext* aCx) {
+ return TelemetryScalar::SetMaximum(aName, aKey, aVal, aCx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::RegisterScalars(const nsACString& aCategoryName,
+ JS::Handle<JS::Value> aScalarData,
+ JSContext* cx) {
+ return TelemetryScalar::RegisterScalars(aCategoryName, aScalarData, false,
+ cx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::RegisterBuiltinScalars(const nsACString& aCategoryName,
+ JS::Handle<JS::Value> aScalarData,
+ JSContext* cx) {
+ return TelemetryScalar::RegisterScalars(aCategoryName, aScalarData, true, cx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::ClearScalars() {
+ TelemetryScalar::ClearScalars();
+ return NS_OK;
+}
+
+// Telemetry Event IDL implementation.
+
+NS_IMETHODIMP
+TelemetryImpl::RecordEvent(const nsACString& aCategory,
+ const nsACString& aMethod, const nsACString& aObject,
+ JS::Handle<JS::Value> aValue,
+ JS::Handle<JS::Value> aExtra, JSContext* aCx,
+ uint8_t optional_argc) {
+ return TelemetryEvent::RecordEvent(aCategory, aMethod, aObject, aValue,
+ aExtra, aCx, optional_argc);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::SnapshotEvents(uint32_t aDataset, bool aClear,
+ uint32_t aEventLimit, JSContext* aCx,
+ uint8_t optional_argc,
+ JS::MutableHandle<JS::Value> aResult) {
+ return TelemetryEvent::CreateSnapshots(aDataset, aClear, aEventLimit, aCx,
+ optional_argc, aResult);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::RegisterEvents(const nsACString& aCategory,
+ JS::Handle<JS::Value> aEventData, JSContext* cx) {
+ return TelemetryEvent::RegisterEvents(aCategory, aEventData, false, cx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::RegisterBuiltinEvents(const nsACString& aCategory,
+ JS::Handle<JS::Value> aEventData,
+ JSContext* cx) {
+ return TelemetryEvent::RegisterEvents(aCategory, aEventData, true, cx);
+}
+
+NS_IMETHODIMP
+TelemetryImpl::ClearEvents() {
+ TelemetryEvent::ClearEvents();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::SetEventRecordingEnabled(const nsACString& aCategory,
+ bool aEnabled) {
+ TelemetryEvent::SetEventRecordingEnabled(aCategory, aEnabled);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::FlushBatchedChildTelemetry() {
+ TelemetryIPCAccumulator::IPCTimerFired(nullptr, nullptr);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::EarlyInit() {
+ Unused << MemoryTelemetry::Get();
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::DelayedInit() {
+ MemoryTelemetry::Get().DelayedInit();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::Shutdown() {
+ MemoryTelemetry::Get().Shutdown();
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GatherMemory(JSContext* aCx, Promise** aResult) {
+ ErrorResult rv;
+ RefPtr<Promise> promise = Promise::Create(xpc::CurrentNativeGlobal(aCx), rv);
+ if (rv.Failed()) {
+ return rv.StealNSResult();
+ }
+
+ MemoryTelemetry::Get().GatherReports(
+ [promise]() { promise->MaybeResolve(JS::UndefinedHandleValue); });
+
+ promise.forget(aResult);
+ return NS_OK;
+}
+
+NS_IMETHODIMP
+TelemetryImpl::GetAllStores(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult) {
+ StringHashSet stores;
+ nsresult rv;
+
+ rv = TelemetryHistogram::GetAllStores(stores);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ rv = TelemetryScalar::GetAllStores(stores);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ JS::RootedVector<JS::Value> allStores(aCx);
+ if (!allStores.reserve(stores.Count())) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (const auto& value : stores) {
+ JS::Rooted<JS::Value> store(aCx);
+
+ store.setString(ToJSString(aCx, value));
+ if (!allStores.append(store)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ JS::Rooted<JSObject*> rarray(aCx, JS::NewArrayObject(aCx, allStores));
+ if (rarray == nullptr) {
+ return NS_ERROR_FAILURE;
+ }
+ aResult.setObject(*rarray);
+
+ return NS_OK;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// EXTERNALLY VISIBLE FUNCTIONS in no name space
+// These are NOT listed in Telemetry.h
+
+/**
+ * The XRE_TelemetryAdd function is to be used by embedding applications
+ * that can't use mozilla::Telemetry::Accumulate() directly.
+ */
+void XRE_TelemetryAccumulate(int aID, uint32_t aSample) {
+ mozilla::Telemetry::Accumulate((mozilla::Telemetry::HistogramID)aID, aSample);
+}
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// EXTERNALLY VISIBLE FUNCTIONS in mozilla::
+// These are NOT listed in Telemetry.h
+
+namespace mozilla {
+
+void RecordShutdownStartTimeStamp() {
+#ifdef DEBUG
+ // FIXME: this function should only be called once, since it should be called
+ // at the earliest point we *know* we are shutting down. Unfortunately
+ // this assert has been firing. Given that if we are called multiple times
+ // we just keep the last timestamp, the assert is commented for now.
+ static bool recorded = false;
+ // MOZ_ASSERT(!recorded);
+ (void)
+ recorded; // Silence unused-var warnings (remove when assert re-enabled)
+ recorded = true;
+#endif
+
+ if (!Telemetry::CanRecordExtended()) return;
+
+ gRecordedShutdownStartTime = TimeStamp::Now();
+
+ GetShutdownTimeFileName();
+}
+
+void RecordShutdownEndTimeStamp() {
+ if (!gRecordedShutdownTimeFileName || gAlreadyFreedShutdownTimeFileName)
+ return;
+
+ PathString name(gRecordedShutdownTimeFileName);
+ free(const_cast<PathChar*>(gRecordedShutdownTimeFileName));
+ gRecordedShutdownTimeFileName = nullptr;
+ gAlreadyFreedShutdownTimeFileName = true;
+
+ if (gRecordedShutdownStartTime.IsNull()) {
+ // If |CanRecordExtended()| is true before |AsyncFetchTelemetryData| is
+ // called and then disabled before shutdown, |RecordShutdownStartTimeStamp|
+ // will bail out and we will end up with a null |gRecordedShutdownStartTime|
+ // here. This can happen during tests.
+ return;
+ }
+
+ nsTAutoString<PathChar> tmpName(name);
+ tmpName.AppendLiteral(".tmp");
+ RefPtr<nsLocalFile> tmpFile = new nsLocalFile(tmpName);
+ FILE* f;
+ if (NS_FAILED(tmpFile->OpenANSIFileDesc("w", &f)) || !f) return;
+ // On a normal release build this should be called just before
+ // calling _exit, but on a debug build or when the user forces a full
+ // shutdown this is called as late as possible, so we have to
+ // allow this write as write poisoning will be enabled.
+ MozillaRegisterDebugFILE(f);
+
+ TimeStamp now = TimeStamp::Now();
+ MOZ_ASSERT(now >= gRecordedShutdownStartTime);
+ TimeDuration diff = now - gRecordedShutdownStartTime;
+ uint32_t diff2 = diff.ToMilliseconds();
+ int written = fprintf(f, "%d\n", diff2);
+ MozillaUnRegisterDebugFILE(f);
+ int rv = fclose(f);
+ if (written < 0 || rv != 0) {
+ tmpFile->Remove(false);
+ return;
+ }
+ RefPtr<nsLocalFile> file = new nsLocalFile(name);
+ nsAutoString leafName;
+ file->GetLeafName(leafName);
+ tmpFile->RenameTo(nullptr, leafName);
+}
+
+} // namespace mozilla
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// EXTERNALLY VISIBLE FUNCTIONS in mozilla::Telemetry::
+// These are listed in Telemetry.h
+
+namespace mozilla::Telemetry {
+
+// The external API for controlling recording state
+void SetHistogramRecordingEnabled(HistogramID aID, bool aEnabled) {
+ TelemetryHistogram::SetHistogramRecordingEnabled(aID, aEnabled);
+}
+
+void Accumulate(HistogramID aHistogram, uint32_t aSample) {
+ TelemetryHistogram::Accumulate(aHistogram, aSample);
+}
+
+void Accumulate(HistogramID aHistogram, const nsTArray<uint32_t>& aSamples) {
+ TelemetryHistogram::Accumulate(aHistogram, aSamples);
+}
+
+void Accumulate(HistogramID aID, const nsCString& aKey, uint32_t aSample) {
+ TelemetryHistogram::Accumulate(aID, aKey, aSample);
+}
+
+void Accumulate(HistogramID aID, const nsCString& aKey,
+ const nsTArray<uint32_t>& aSamples) {
+ TelemetryHistogram::Accumulate(aID, aKey, aSamples);
+}
+
+void Accumulate(const char* name, uint32_t sample) {
+ TelemetryHistogram::Accumulate(name, sample);
+}
+
+void Accumulate(const char* name, const nsCString& key, uint32_t sample) {
+ TelemetryHistogram::Accumulate(name, key, sample);
+}
+
+void AccumulateCategorical(HistogramID id, const nsCString& label) {
+ TelemetryHistogram::AccumulateCategorical(id, label);
+}
+
+void AccumulateCategorical(HistogramID id, const nsTArray<nsCString>& labels) {
+ TelemetryHistogram::AccumulateCategorical(id, labels);
+}
+
+void AccumulateTimeDelta(HistogramID aHistogram, TimeStamp start,
+ TimeStamp end) {
+ if (start > end) {
+ Accumulate(aHistogram, 0);
+ return;
+ }
+ Accumulate(aHistogram, static_cast<uint32_t>((end - start).ToMilliseconds()));
+}
+
+void AccumulateTimeDelta(HistogramID aHistogram, const nsCString& key,
+ TimeStamp start, TimeStamp end) {
+ if (start > end) {
+ Accumulate(aHistogram, key, 0);
+ return;
+ }
+ Accumulate(aHistogram, key,
+ static_cast<uint32_t>((end - start).ToMilliseconds()));
+}
+const char* GetHistogramName(HistogramID id) {
+ return TelemetryHistogram::GetHistogramName(id);
+}
+
+bool CanRecordBase() { return TelemetryImpl::CanRecordBase(); }
+
+bool CanRecordExtended() { return TelemetryImpl::CanRecordExtended(); }
+
+bool CanRecordReleaseData() { return TelemetryImpl::CanRecordReleaseData(); }
+
+bool CanRecordPrereleaseData() {
+ return TelemetryImpl::CanRecordPrereleaseData();
+}
+
+void RecordSlowSQLStatement(const nsACString& statement,
+ const nsACString& dbName, uint32_t delay) {
+ TelemetryImpl::RecordSlowStatement(statement, dbName, delay);
+}
+
+void Init() {
+ // Make the service manager hold a long-lived reference to the service
+ nsCOMPtr<nsITelemetry> telemetryService =
+ do_GetService("@mozilla.org/base/telemetry;1");
+ MOZ_ASSERT(telemetryService);
+}
+
+void WriteFailedProfileLock(nsIFile* aProfileDir) {
+ nsCOMPtr<nsIFile> file;
+ nsresult rv = GetFailedProfileLockFile(getter_AddRefs(file), aProfileDir);
+ NS_ENSURE_SUCCESS_VOID(rv);
+ int64_t fileSize = 0;
+ rv = file->GetFileSize(&fileSize);
+ // It's expected that the file might not exist yet
+ if (NS_FAILED(rv) && rv != NS_ERROR_FILE_NOT_FOUND) {
+ return;
+ }
+ nsCOMPtr<nsIRandomAccessStream> fileRandomAccessStream;
+ rv = NS_NewLocalFileRandomAccessStream(getter_AddRefs(fileRandomAccessStream),
+ file, PR_RDWR | PR_CREATE_FILE, 0640);
+ NS_ENSURE_SUCCESS_VOID(rv);
+ NS_ENSURE_TRUE_VOID(fileSize <= kMaxFailedProfileLockFileSize);
+ unsigned int failedLockCount = 0;
+ if (fileSize > 0) {
+ nsCOMPtr<nsIInputStream> inStream =
+ do_QueryInterface(fileRandomAccessStream);
+ NS_ENSURE_TRUE_VOID(inStream);
+ if (!GetFailedLockCount(inStream, fileSize, failedLockCount)) {
+ failedLockCount = 0;
+ }
+ }
+ ++failedLockCount;
+ nsAutoCString bufStr;
+ bufStr.AppendInt(static_cast<int>(failedLockCount));
+ // If we read in an existing failed lock count, we need to reset the file ptr
+ if (fileSize > 0) {
+ rv = fileRandomAccessStream->Seek(nsISeekableStream::NS_SEEK_SET, 0);
+ NS_ENSURE_SUCCESS_VOID(rv);
+ }
+ nsCOMPtr<nsIOutputStream> outStream =
+ do_QueryInterface(fileRandomAccessStream);
+ uint32_t bytesLeft = bufStr.Length();
+ const char* bytes = bufStr.get();
+ do {
+ uint32_t written = 0;
+ rv = outStream->Write(bytes, bytesLeft, &written);
+ if (NS_FAILED(rv)) {
+ break;
+ }
+ bytes += written;
+ bytesLeft -= written;
+ } while (bytesLeft > 0);
+ fileRandomAccessStream->SetEOF();
+}
+
+void InitIOReporting(nsIFile* aXreDir) {
+ // Never initialize twice
+ if (sTelemetryIOObserver) {
+ return;
+ }
+
+ sTelemetryIOObserver = new TelemetryIOInterposeObserver(aXreDir);
+ IOInterposer::Register(IOInterposeObserver::OpAllWithStaging,
+ sTelemetryIOObserver);
+}
+
+void SetProfileDir(nsIFile* aProfD) {
+ if (!sTelemetryIOObserver || !aProfD) {
+ return;
+ }
+ nsAutoString profDirPath;
+ nsresult rv = aProfD->GetPath(profDirPath);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+ sTelemetryIOObserver->AddPath(profDirPath, u"{profile}"_ns);
+}
+
+// Scalar API C++ Endpoints
+
+void ScalarAdd(mozilla::Telemetry::ScalarID aId, uint32_t aVal) {
+ TelemetryScalar::Add(aId, aVal);
+}
+
+void ScalarSet(mozilla::Telemetry::ScalarID aId, uint32_t aVal) {
+ TelemetryScalar::Set(aId, aVal);
+}
+
+void ScalarSet(mozilla::Telemetry::ScalarID aId, bool aVal) {
+ TelemetryScalar::Set(aId, aVal);
+}
+
+void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aVal) {
+ TelemetryScalar::Set(aId, aVal);
+}
+
+void ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, uint32_t aVal) {
+ TelemetryScalar::SetMaximum(aId, aVal);
+}
+
+void ScalarAdd(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+ uint32_t aVal) {
+ TelemetryScalar::Add(aId, aKey, aVal);
+}
+
+void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+ uint32_t aVal) {
+ TelemetryScalar::Set(aId, aKey, aVal);
+}
+
+void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+ bool aVal) {
+ TelemetryScalar::Set(aId, aKey, aVal);
+}
+
+void ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+ uint32_t aVal) {
+ TelemetryScalar::SetMaximum(aId, aKey, aVal);
+}
+
+void RecordEvent(
+ mozilla::Telemetry::EventID aId, const mozilla::Maybe<nsCString>& aValue,
+ const mozilla::Maybe<CopyableTArray<EventExtraEntry>>& aExtra) {
+ TelemetryEvent::RecordEventNative(aId, aValue, aExtra);
+}
+
+void SetEventRecordingEnabled(const nsACString& aCategory, bool aEnabled) {
+ TelemetryEvent::SetEventRecordingEnabled(aCategory, aEnabled);
+}
+
+void ShutdownTelemetry() { TelemetryImpl::ShutdownTelemetry(); }
+
+} // namespace mozilla::Telemetry
+
+NS_IMPL_COMPONENT_FACTORY(nsITelemetry) {
+ return TelemetryImpl::CreateTelemetryInstance().downcast<nsISupports>();
+}
diff --git a/toolkit/components/telemetry/core/Telemetry.h b/toolkit/components/telemetry/core/Telemetry.h
new file mode 100644
index 0000000000..d0fb76a24e
--- /dev/null
+++ b/toolkit/components/telemetry/core/Telemetry.h
@@ -0,0 +1,577 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef Telemetry_h__
+#define Telemetry_h__
+
+#include "mozilla/Maybe.h"
+#include "mozilla/TelemetryEventEnums.h"
+#include "mozilla/TelemetryHistogramEnums.h"
+#include "mozilla/TelemetryScalarEnums.h"
+#include "mozilla/TimeStamp.h"
+#include "nsString.h"
+#include "nsTArray.h"
+#include "nsXULAppAPI.h"
+
+/******************************************************************************
+ * This implements the Telemetry system.
+ * It allows recording into histograms as well some more specialized data
+ * points and gives access to the data.
+ *
+ * For documentation on how to add and use new Telemetry probes, see:
+ * https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/start/adding-a-new-probe.html
+ *
+ * For more general information on Telemetry see:
+ * https://wiki.mozilla.org/Telemetry
+ *****************************************************************************/
+
+namespace mozilla {
+namespace Telemetry {
+
+struct HistogramAccumulation;
+struct KeyedHistogramAccumulation;
+struct ScalarAction;
+struct KeyedScalarAction;
+struct ChildEventData;
+
+struct EventExtraEntry {
+ nsCString key;
+ nsCString value;
+};
+
+/**
+ * Initialize the Telemetry service on the main thread at startup.
+ */
+void Init();
+
+/**
+ * Shutdown the Telemetry service.
+ */
+void ShutdownTelemetry();
+
+/**
+ * Adds sample to a histogram defined in TelemetryHistogramEnums.h
+ *
+ * @param id - histogram id
+ * @param sample - value to record.
+ */
+void Accumulate(HistogramID id, uint32_t sample);
+
+/**
+ * Adds an array of samples to a histogram defined in TelemetryHistograms.h
+ * @param id - histogram id
+ * @param samples - values to record.
+ */
+void Accumulate(HistogramID id, const nsTArray<uint32_t>& samples);
+
+/**
+ * Adds sample to a keyed histogram defined in TelemetryHistogramEnums.h
+ *
+ * @param id - keyed histogram id
+ * @param key - the string key
+ * @param sample - (optional) value to record, defaults to 1.
+ */
+void Accumulate(HistogramID id, const nsCString& key, uint32_t sample = 1);
+
+/**
+ * Adds an array of samples to a histogram defined in TelemetryHistograms.h
+ * @param id - histogram id
+ * @param samples - values to record.
+ * @param key - the string key
+ */
+void Accumulate(HistogramID id, const nsCString& key,
+ const nsTArray<uint32_t>& samples);
+
+/**
+ * Adds a sample to a histogram defined in TelemetryHistogramEnums.h.
+ * This function is here to support telemetry measurements from Java,
+ * where we have only names and not numeric IDs. You should almost
+ * certainly be using the by-enum-id version instead of this one.
+ *
+ * @param name - histogram name
+ * @param sample - value to record
+ */
+void Accumulate(const char* name, uint32_t sample);
+
+/**
+ * Adds a sample to a histogram defined in TelemetryHistogramEnums.h.
+ * This function is here to support telemetry measurements from Java,
+ * where we have only names and not numeric IDs. You should almost
+ * certainly be using the by-enum-id version instead of this one.
+ *
+ * @param name - histogram name
+ * @param key - the string key
+ * @param sample - sample - (optional) value to record, defaults to 1.
+ */
+void Accumulate(const char* name, const nsCString& key, uint32_t sample = 1);
+
+/**
+ * Adds sample to a categorical histogram defined in TelemetryHistogramEnums.h
+ * This is the typesafe - and preferred - way to use the categorical histograms
+ * by passing values from the corresponding Telemetry::LABELS_* enum.
+ *
+ * @param enumValue - Label value from one of the Telemetry::LABELS_* enums.
+ */
+template <class E>
+void AccumulateCategorical(E enumValue) {
+ static_assert(IsCategoricalLabelEnum<E>::value,
+ "Only categorical label enum types are supported.");
+ Accumulate(static_cast<HistogramID>(CategoricalLabelId<E>::value),
+ static_cast<uint32_t>(enumValue));
+};
+
+/**
+ * Adds an array of samples to categorical histograms defined in
+ * TelemetryHistogramEnums.h This is the typesafe - and preferred - way to use
+ * the categorical histograms by passing values from the corresponding
+ * Telemetry::LABELS_* enums.
+ *
+ * @param enumValues - Array of labels from Telemetry::LABELS_* enums.
+ */
+template <class E>
+void AccumulateCategorical(const nsTArray<E>& enumValues) {
+ static_assert(IsCategoricalLabelEnum<E>::value,
+ "Only categorical label enum types are supported.");
+ nsTArray<uint32_t> intSamples(enumValues.Length());
+
+ for (E aValue : enumValues) {
+ intSamples.AppendElement(static_cast<uint32_t>(aValue));
+ }
+
+ HistogramID categoricalId =
+ static_cast<HistogramID>(CategoricalLabelId<E>::value);
+
+ Accumulate(categoricalId, intSamples);
+}
+
+/**
+ * Adds sample to a keyed categorical histogram defined in
+ * TelemetryHistogramEnums.h This is the typesafe - and preferred - way to use
+ * the keyed categorical histograms by passing values from the corresponding
+ * Telemetry::LABELS_* enum.
+ *
+ * @param key - the string key
+ * @param enumValue - Label value from one of the Telemetry::LABELS_* enums.
+ */
+template <class E>
+void AccumulateCategoricalKeyed(const nsCString& key, E enumValue) {
+ static_assert(IsCategoricalLabelEnum<E>::value,
+ "Only categorical label enum types are supported.");
+ Accumulate(static_cast<HistogramID>(CategoricalLabelId<E>::value), key,
+ static_cast<uint32_t>(enumValue));
+};
+
+/**
+ * Adds an array of samples to a keyed categorical histogram defined in
+ * TelemetryHistogramEnums.h. This is the typesafe - and preferred - way to use
+ * the keyed categorical histograms by passing values from the corresponding
+ * Telemetry::LABELS_*enum.
+ *
+ * @param key - the string key
+ * @param enumValue - Label value from one of the Telemetry::LABELS_* enums.
+ */
+template <class E>
+void AccumulateCategoricalKeyed(const nsCString& key,
+ const nsTArray<E>& enumValues) {
+ static_assert(IsCategoricalLabelEnum<E>::value,
+ "Only categorical label enum types are supported.");
+ nsTArray<uint32_t> intSamples(enumValues.Length());
+
+ for (E aValue : enumValues) {
+ intSamples.AppendElement(static_cast<uint32_t>(aValue));
+ }
+
+ Accumulate(static_cast<HistogramID>(CategoricalLabelId<E>::value), key,
+ intSamples);
+};
+
+/**
+ * Adds sample to a categorical histogram defined in TelemetryHistogramEnums.h
+ * This string will be matched against the labels defined in Histograms.json.
+ * If the string does not match a label defined for the histogram, nothing will
+ * be recorded.
+ *
+ * @param id - The histogram id.
+ * @param label - A string label value that is defined in Histograms.json for
+ * this histogram.
+ */
+void AccumulateCategorical(HistogramID id, const nsCString& label);
+
+/**
+ * Adds an array of samples to a categorical histogram defined in
+ * Histograms.json
+ *
+ * @param id - The histogram id
+ * @param labels - The array of labels to accumulate
+ */
+void AccumulateCategorical(HistogramID id, const nsTArray<nsCString>& labels);
+
+/**
+ * Adds time delta in milliseconds to a histogram defined in
+ * TelemetryHistogramEnums.h
+ *
+ * @param id - histogram id
+ * @param start - start time
+ * @param end - end time
+ */
+void AccumulateTimeDelta(HistogramID id, TimeStamp start,
+ TimeStamp end = TimeStamp::Now());
+
+/**
+ * Adds time delta in milliseconds to a keyed histogram defined in
+ * TelemetryHistogramEnums.h
+ *
+ * @param id - histogram id
+ * @param key - the string key
+ * @param start - start time
+ * @param end - end time
+ */
+void AccumulateTimeDelta(HistogramID id, const nsCString& key, TimeStamp start,
+ TimeStamp end = TimeStamp::Now());
+
+/**
+ * Enable/disable recording for this histogram in this process at runtime.
+ * Recording is enabled by default, unless listed at
+ * kRecordingInitiallyDisabledIDs[]. id must be a valid telemetry enum,
+ *
+ * @param id - histogram id
+ * @param enabled - whether or not to enable recording from now on.
+ */
+void SetHistogramRecordingEnabled(HistogramID id, bool enabled);
+
+const char* GetHistogramName(HistogramID id);
+
+class MOZ_RAII RuntimeAutoTimer {
+ public:
+ explicit RuntimeAutoTimer(Telemetry::HistogramID aId,
+ TimeStamp aStart = TimeStamp::Now())
+ : id(aId), start(aStart) {}
+ explicit RuntimeAutoTimer(Telemetry::HistogramID aId, const nsCString& aKey,
+ TimeStamp aStart = TimeStamp::Now())
+ : id(aId), key(aKey), start(aStart) {
+ MOZ_ASSERT(!aKey.IsEmpty(), "The key must not be empty.");
+ }
+
+ ~RuntimeAutoTimer() {
+ if (key.IsEmpty()) {
+ AccumulateTimeDelta(id, start);
+ } else {
+ AccumulateTimeDelta(id, key, start);
+ }
+ }
+
+ private:
+ Telemetry::HistogramID id;
+ const nsCString key;
+ const TimeStamp start;
+};
+
+template <HistogramID id>
+class MOZ_RAII AutoTimer {
+ public:
+ explicit AutoTimer(TimeStamp aStart = TimeStamp::Now()) : start(aStart) {}
+
+ explicit AutoTimer(const nsCString& aKey, TimeStamp aStart = TimeStamp::Now())
+ : start(aStart), key(aKey) {
+ MOZ_ASSERT(!aKey.IsEmpty(), "The key must not be empty.");
+ }
+
+ ~AutoTimer() {
+ if (key.IsEmpty()) {
+ AccumulateTimeDelta(id, start);
+ } else {
+ AccumulateTimeDelta(id, key, start);
+ }
+ }
+
+ private:
+ const TimeStamp start;
+ const nsCString key;
+};
+
+class MOZ_RAII RuntimeAutoCounter {
+ public:
+ explicit RuntimeAutoCounter(HistogramID aId, uint32_t counterStart = 0)
+ : id(aId), counter(counterStart) {}
+
+ ~RuntimeAutoCounter() { Accumulate(id, counter); }
+
+ // Prefix increment only, to encourage good habits.
+ void operator++() {
+ if (NS_WARN_IF(counter == std::numeric_limits<uint32_t>::max())) {
+ return;
+ }
+ ++counter;
+ }
+
+ // Chaining doesn't make any sense, don't return anything.
+ void operator+=(int increment) {
+ if (NS_WARN_IF(increment > 0 &&
+ static_cast<uint32_t>(increment) >
+ (std::numeric_limits<uint32_t>::max() - counter))) {
+ counter = std::numeric_limits<uint32_t>::max();
+ return;
+ }
+ if (NS_WARN_IF(increment < 0 &&
+ static_cast<uint32_t>(-increment) > counter)) {
+ counter = std::numeric_limits<uint32_t>::min();
+ return;
+ }
+ counter += increment;
+ }
+
+ private:
+ HistogramID id;
+ uint32_t counter;
+};
+
+template <HistogramID id>
+class MOZ_RAII AutoCounter {
+ public:
+ explicit AutoCounter(uint32_t counterStart = 0) : counter(counterStart) {}
+
+ ~AutoCounter() { Accumulate(id, counter); }
+
+ // Prefix increment only, to encourage good habits.
+ void operator++() {
+ if (NS_WARN_IF(counter == std::numeric_limits<uint32_t>::max())) {
+ return;
+ }
+ ++counter;
+ }
+
+ // Chaining doesn't make any sense, don't return anything.
+ void operator+=(int increment) {
+ if (NS_WARN_IF(increment > 0 &&
+ static_cast<uint32_t>(increment) >
+ (std::numeric_limits<uint32_t>::max() - counter))) {
+ counter = std::numeric_limits<uint32_t>::max();
+ return;
+ }
+ if (NS_WARN_IF(increment < 0 &&
+ static_cast<uint32_t>(-increment) > counter)) {
+ counter = std::numeric_limits<uint32_t>::min();
+ return;
+ }
+ counter += increment;
+ }
+
+ private:
+ uint32_t counter;
+};
+
+/**
+ * Indicates whether Telemetry base data recording is turned on. Added for
+ * future uses.
+ */
+bool CanRecordBase();
+
+/**
+ * Indicates whether Telemetry extended data recording is turned on. This is
+ * intended to guard calls to Accumulate when the statistic being recorded is
+ * expensive to compute.
+ */
+bool CanRecordExtended();
+
+/**
+ * Indicates whether Telemetry release data recording is turned on. Usually
+ * true.
+ *
+ * @see nsITelemetry.canRecordReleaseData
+ */
+bool CanRecordReleaseData();
+
+/**
+ * Indicates whether Telemetry pre-release data recording is turned on. Tends
+ * to be true on pre-release channels.
+ *
+ * @see nsITelemetry.canRecordPrereleaseData
+ */
+bool CanRecordPrereleaseData();
+
+/**
+ * Records slow SQL statements for Telemetry reporting.
+ *
+ * @param statement - offending SQL statement to record
+ * @param dbName - DB filename
+ * @param delay - execution time in milliseconds
+ */
+void RecordSlowSQLStatement(const nsACString& statement,
+ const nsACString& dbName, uint32_t delay);
+
+/**
+ * Initialize I/O Reporting
+ * Initially this only records I/O for files in the binary directory.
+ *
+ * @param aXreDir - XRE directory
+ */
+void InitIOReporting(nsIFile* aXreDir);
+
+/**
+ * Set the profile directory. Once called, files in the profile directory will
+ * be included in I/O reporting. We can't use the directory
+ * service to obtain this information because it isn't running yet.
+ */
+void SetProfileDir(nsIFile* aProfD);
+
+/**
+ * Called to inform Telemetry that startup has completed.
+ */
+void LeavingStartupStage();
+
+/**
+ * Called to inform Telemetry that shutdown is commencing.
+ */
+void EnteringShutdownStage();
+
+/**
+ * Thresholds for a statement to be considered slow, in milliseconds
+ */
+const uint32_t kSlowSQLThresholdForMainThread = 50;
+const uint32_t kSlowSQLThresholdForHelperThreads = 100;
+
+/**
+ * Record a failed attempt at locking the user's profile.
+ *
+ * @param aProfileDir The profile directory whose lock attempt failed
+ */
+void WriteFailedProfileLock(nsIFile* aProfileDir);
+
+/**
+ * Adds the value to the given scalar.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The value to add to the scalar.
+ */
+void ScalarAdd(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
+
+/**
+ * Sets the scalar to the given value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The value to set the scalar to.
+ */
+void ScalarSet(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
+
+/**
+ * Sets the scalar to the given value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The value to set the scalar to.
+ */
+void ScalarSet(mozilla::Telemetry::ScalarID aId, bool aValue);
+
+/**
+ * Sets the scalar to the given value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The value to set the scalar to, truncated to
+ * 50 characters if exceeding that length.
+ */
+void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aValue);
+
+/**
+ * Sets the scalar to the maximum of the current and the passed value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The value the scalar is set to if its greater
+ * than the current value.
+ */
+void ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
+
+/**
+ * Adds the value to the given scalar.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The scalar key.
+ * @param aValue The value to add to the scalar.
+ */
+void ScalarAdd(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+ uint32_t aValue);
+
+/**
+ * Sets the scalar to the given value.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The scalar key.
+ * @param aValue The value to set the scalar to.
+ */
+void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+ uint32_t aValue);
+
+/**
+ * Sets the scalar to the given value.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The scalar key.
+ * @param aValue The value to set the scalar to.
+ */
+void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+ bool aValue);
+
+/**
+ * Sets the scalar to the maximum of the current and the passed value.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The scalar key.
+ * @param aValue The value the scalar is set to if its greater
+ * than the current value.
+ */
+void ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+ uint32_t aValue);
+
+template <ScalarID id>
+class MOZ_RAII AutoScalarTimer {
+ public:
+ explicit AutoScalarTimer(TimeStamp aStart = TimeStamp::Now())
+ : start(aStart) {}
+
+ explicit AutoScalarTimer(const nsAString& aKey,
+ TimeStamp aStart = TimeStamp::Now())
+ : start(aStart), key(aKey) {
+ MOZ_ASSERT(!aKey.IsEmpty(), "The key must not be empty.");
+ }
+
+ ~AutoScalarTimer() {
+ TimeStamp end = TimeStamp::Now();
+ uint32_t delta = static_cast<uint32_t>((end - start).ToMilliseconds());
+ if (key.IsEmpty()) {
+ mozilla::Telemetry::ScalarSet(id, delta);
+ } else {
+ mozilla::Telemetry::ScalarSet(id, key, delta);
+ }
+ }
+
+ private:
+ const TimeStamp start;
+ const nsString key;
+};
+
+/**
+ * Records an event. See the Event documentation for more information:
+ * https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/collection/events.html
+ *
+ * @param aId The event enum id.
+ * @param aValue Optional. The event value.
+ * @param aExtra Optional. The event's extra key/value pairs.
+ */
+void RecordEvent(mozilla::Telemetry::EventID aId,
+ const mozilla::Maybe<nsCString>& aValue,
+ const mozilla::Maybe<CopyableTArray<EventExtraEntry>>& aExtra);
+
+/**
+ * Enables recording of events in a category.
+ * Events default to recording disabled.
+ * This toggles recording for all events in the specified category.
+ *
+ * @param aCategory The category name.
+ * @param aEnabled Whether recording should be enabled or disabled.
+ */
+void SetEventRecordingEnabled(const nsACString& aCategory, bool aEnabled);
+
+} // namespace Telemetry
+} // namespace mozilla
+
+#endif // Telemetry_h__
diff --git a/toolkit/components/telemetry/core/TelemetryCommon.cpp b/toolkit/components/telemetry/core/TelemetryCommon.cpp
new file mode 100644
index 0000000000..7113a682c9
--- /dev/null
+++ b/toolkit/components/telemetry/core/TelemetryCommon.cpp
@@ -0,0 +1,209 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TelemetryCommon.h"
+
+#include <cstring>
+#include "js/String.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/StaticPrefs_toolkit.h"
+#include "nsComponentManagerUtils.h"
+#include "nsIConsoleService.h"
+#include "nsITelemetry.h"
+#include "nsServiceManagerUtils.h"
+#include "nsThreadUtils.h"
+#include "nsVersionComparator.h"
+#include "TelemetryProcessData.h"
+#include "Telemetry.h"
+#include "mozilla/Uptime.h"
+
+namespace mozilla::Telemetry::Common {
+
+bool IsExpiredVersion(const char* aExpiration) {
+ MOZ_ASSERT(aExpiration);
+ // Note: We intentionally don't construct a static Version object here as we
+ // saw odd crashes around this (see bug 1334105).
+ return strcmp(aExpiration, "never") && strcmp(aExpiration, "default") &&
+ (mozilla::Version(aExpiration) <= MOZ_APP_VERSION);
+}
+
+bool IsInDataset(uint32_t aDataset, uint32_t aContainingDataset) {
+ if (aDataset == aContainingDataset) {
+ return true;
+ }
+
+ // The "optin on release channel" dataset is a superset of the
+ // "optout on release channel one".
+ if (aContainingDataset == nsITelemetry::DATASET_PRERELEASE_CHANNELS &&
+ aDataset == nsITelemetry::DATASET_ALL_CHANNELS) {
+ return true;
+ }
+
+ return false;
+}
+
+bool CanRecordDataset(uint32_t aDataset, bool aCanRecordBase,
+ bool aCanRecordExtended) {
+ // If we are extended telemetry is enabled, we are allowed to record
+ // regardless of the dataset.
+ if (aCanRecordExtended) {
+ return true;
+ }
+
+ // If base telemetry data is enabled and we're trying to record base
+ // telemetry, allow it.
+ if (aCanRecordBase &&
+ IsInDataset(aDataset, nsITelemetry::DATASET_ALL_CHANNELS)) {
+ return true;
+ }
+
+ // We're not recording extended telemetry or this is not the base
+ // dataset. Bail out.
+ return false;
+}
+
+bool CanRecordInProcess(RecordedProcessType processes,
+ GeckoProcessType processType) {
+ // We can use (1 << ProcessType) due to the way RecordedProcessType is
+ // defined.
+ bool canRecordProcess =
+ !!(processes & static_cast<RecordedProcessType>(1 << processType));
+
+ return canRecordProcess;
+}
+
+bool CanRecordInProcess(RecordedProcessType processes, ProcessID processId) {
+ return CanRecordInProcess(processes, GetGeckoProcessType(processId));
+}
+
+bool CanRecordProduct(SupportedProduct aProducts) {
+ return mozilla::StaticPrefs::
+ toolkit_telemetry_testing_overrideProductsCheck() ||
+ !!(aProducts & GetCurrentProduct());
+}
+
+nsresult MsSinceProcessStart(double* aResult) {
+ *aResult =
+ (TimeStamp::NowLoRes() - TimeStamp::ProcessCreation()).ToMilliseconds();
+ return NS_OK;
+}
+
+nsresult MsSinceProcessStartIncludingSuspend(double* aResult) {
+ auto rv = mozilla::ProcessUptimeMs();
+ if (rv) {
+ *aResult = rv.value();
+ return NS_OK;
+ }
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+nsresult MsSinceProcessStartExcludingSuspend(double* aResult) {
+ auto rv = mozilla::ProcessUptimeExcludingSuspendMs();
+ if (rv) {
+ *aResult = rv.value();
+ return NS_OK;
+ }
+ return NS_ERROR_NOT_AVAILABLE;
+}
+
+void LogToBrowserConsole(uint32_t aLogLevel, const nsAString& aMsg) {
+ if (!NS_IsMainThread()) {
+ nsString msg(aMsg);
+ nsCOMPtr<nsIRunnable> task = NS_NewRunnableFunction(
+ "Telemetry::Common::LogToBrowserConsole",
+ [aLogLevel, msg]() { LogToBrowserConsole(aLogLevel, msg); });
+ NS_DispatchToMainThread(task.forget(), NS_DISPATCH_NORMAL);
+ return;
+ }
+
+ nsCOMPtr<nsIConsoleService> console(
+ do_GetService("@mozilla.org/consoleservice;1"));
+ if (!console) {
+ NS_WARNING("Failed to log message to console.");
+ return;
+ }
+
+ nsCOMPtr<nsIScriptError> error(do_CreateInstance(NS_SCRIPTERROR_CONTRACTID));
+ error->Init(aMsg, u""_ns, u""_ns, 0, 0, aLogLevel, "chrome javascript"_ns,
+ false /* from private window */, true /* from chrome context */);
+ console->LogMessage(error);
+}
+
+const char* GetNameForProcessID(ProcessID process) {
+ MOZ_ASSERT(process < ProcessID::Count);
+ return ProcessIDToString[static_cast<uint32_t>(process)];
+}
+
+ProcessID GetIDForProcessName(const char* aProcessName) {
+ for (uint32_t id = 0; id < static_cast<uint32_t>(ProcessID::Count); id++) {
+ if (!strcmp(GetNameForProcessID(ProcessID(id)), aProcessName)) {
+ return ProcessID(id);
+ }
+ }
+
+ return ProcessID::Count;
+}
+
+GeckoProcessType GetGeckoProcessType(ProcessID process) {
+ MOZ_ASSERT(process < ProcessID::Count);
+ return ProcessIDToGeckoProcessType[static_cast<uint32_t>(process)];
+}
+
+bool IsStringCharValid(const char aChar, const bool aAllowInfixPeriod,
+ const bool aAllowInfixUnderscore) {
+ return (aChar >= 'A' && aChar <= 'Z') || (aChar >= 'a' && aChar <= 'z') ||
+ (aChar >= '0' && aChar <= '9') ||
+ (aAllowInfixPeriod && (aChar == '.')) ||
+ (aAllowInfixUnderscore && (aChar == '_'));
+}
+
+bool IsValidIdentifierString(const nsACString& aStr, const size_t aMaxLength,
+ const bool aAllowInfixPeriod,
+ const bool aAllowInfixUnderscore) {
+ // Check string length.
+ if (aStr.Length() > aMaxLength) {
+ return false;
+ }
+
+ // Check string characters.
+ const char* first = aStr.BeginReading();
+ const char* end = aStr.EndReading();
+
+ for (const char* cur = first; cur < end; ++cur) {
+ const bool infix = (cur != first) && (cur != (end - 1));
+ if (!IsStringCharValid(*cur, aAllowInfixPeriod && infix,
+ aAllowInfixUnderscore && infix)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+JSString* ToJSString(JSContext* cx, const nsACString& aStr) {
+ const NS_ConvertUTF8toUTF16 wide(aStr);
+ return JS_NewUCStringCopyN(cx, wide.Data(), wide.Length());
+}
+
+JSString* ToJSString(JSContext* cx, const nsAString& aStr) {
+ return JS_NewUCStringCopyN(cx, aStr.Data(), aStr.Length());
+}
+
+SupportedProduct GetCurrentProduct() {
+#if defined(MOZ_WIDGET_ANDROID)
+ if (mozilla::StaticPrefs::toolkit_telemetry_geckoview_streaming()) {
+ return SupportedProduct::GeckoviewStreaming;
+ } else {
+ return SupportedProduct::Fennec;
+ }
+#elif defined(MOZ_THUNDERBIRD)
+ return SupportedProduct::Thunderbird;
+#else
+ return SupportedProduct::Firefox;
+#endif
+}
+
+} // namespace mozilla::Telemetry::Common
diff --git a/toolkit/components/telemetry/core/TelemetryCommon.h b/toolkit/components/telemetry/core/TelemetryCommon.h
new file mode 100644
index 0000000000..141410f150
--- /dev/null
+++ b/toolkit/components/telemetry/core/TelemetryCommon.h
@@ -0,0 +1,198 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TelemetryCommon_h__
+#define TelemetryCommon_h__
+
+#include "PLDHashTable.h"
+#include "js/RootingAPI.h"
+#include "js/TypeDecls.h"
+#include "mozilla/TypedEnumBits.h"
+#include "mozilla/TelemetryProcessEnums.h"
+#include "nsHashtablesFwd.h"
+#include "nsTHashSet.h"
+#include "nsTHashtable.h"
+#include "nsIScriptError.h"
+#include "nsXULAppAPI.h"
+
+namespace mozilla {
+namespace Telemetry {
+namespace Common {
+
+typedef nsTHashSet<nsCString> StringHashSet;
+
+enum class RecordedProcessType : uint16_t {
+ Main = (1 << GeckoProcessType_Default), // Also known as "parent process"
+ Content = (1 << GeckoProcessType_Content),
+ Gpu = (1 << GeckoProcessType_GPU),
+ Rdd = (1 << GeckoProcessType_RDD),
+ Socket = (1 << GeckoProcessType_Socket),
+ Utility = (1 << GeckoProcessType_Utility),
+ AllChildren = 0xFFFF - 1, // All the child processes (i.e. content, gpu, ...)
+ // Always `All-Main` to allow easy matching.
+ All = 0xFFFF // All the processes
+};
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(RecordedProcessType);
+static_assert(static_cast<uint16_t>(RecordedProcessType::Main) == 1,
+ "Main process type must be equal to 1 to allow easy matching in "
+ "CanRecordInProcess");
+
+enum class SupportedProduct : uint8_t {
+ Firefox = (1 << 0),
+ Fennec = (1 << 1),
+ // Note that `1 << 2` (former GeckoView) is missing in the representation
+ // but isn't necessary to be maintained, but we see no point in filling it
+ // at this time.
+ GeckoviewStreaming = (1 << 3),
+ Thunderbird = (1 << 4),
+};
+MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(SupportedProduct);
+
+template <class EntryType>
+class AutoHashtable : public nsTHashtable<EntryType> {
+ public:
+ explicit AutoHashtable(
+ uint32_t initLength = PLDHashTable::kDefaultInitialLength);
+ typedef bool (*ReflectEntryFunc)(EntryType* entry, JSContext* cx,
+ JS::Handle<JSObject*> obj);
+ bool ReflectIntoJS(ReflectEntryFunc entryFunc, JSContext* cx,
+ JS::Handle<JSObject*> obj);
+};
+
+template <class EntryType>
+AutoHashtable<EntryType>::AutoHashtable(uint32_t initLength)
+ : nsTHashtable<EntryType>(initLength) {}
+
+/**
+ * Reflect the individual entries of table into JS, usually by defining
+ * some property and value of obj. entryFunc is called for each entry.
+ */
+template <typename EntryType>
+bool AutoHashtable<EntryType>::ReflectIntoJS(ReflectEntryFunc entryFunc,
+ JSContext* cx,
+ JS::Handle<JSObject*> obj) {
+ for (auto iter = this->Iter(); !iter.Done(); iter.Next()) {
+ if (!entryFunc(iter.Get(), cx, obj)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+bool IsExpiredVersion(const char* aExpiration);
+bool IsInDataset(uint32_t aDataset, uint32_t aContainingDataset);
+bool CanRecordDataset(uint32_t aDataset, bool aCanRecordBase,
+ bool aCanRecordExtended);
+bool CanRecordInProcess(RecordedProcessType aProcesses,
+ GeckoProcessType aProcess);
+bool CanRecordInProcess(RecordedProcessType aProcesses, ProcessID aProcess);
+bool CanRecordProduct(SupportedProduct aProducts);
+
+/**
+ * Return the number of milliseconds since process start using monotonic
+ * timestamps (unaffected by system clock changes). Depending on the platform,
+ * this can include the time the device was suspended (Windows) or not (Linux,
+ * macOS).
+ *
+ * @return NS_OK on success.
+ */
+nsresult MsSinceProcessStart(double* aResult);
+
+/**
+ * Return the number of milliseconds since process start using monotonic
+ * timestamps (unaffected by system clock changes), including the time the
+ * system was suspended.
+ *
+ * @return NS_OK on success, NS_ERROR_NOT_AVAILABLE if the data is unavailable
+ * (this can happen on old operating systems).
+ */
+nsresult MsSinceProcessStartIncludingSuspend(double* aResult);
+
+/**
+ * Return the number of milliseconds since process start using monotonic
+ * timestamps (unaffected by system clock changes), excluding the time the
+ * system was suspended.
+ *
+ * @return NS_OK on success, NS_ERROR_NOT_AVAILABLE if the data is unavailable
+ * (this can happen on old operating systems).
+ */
+nsresult MsSinceProcessStartExcludingSuspend(double* aResult);
+
+/**
+ * Dumps a log message to the Browser Console using the provided level.
+ *
+ * @param aLogLevel The level to use when displaying the message in the browser
+ * console (e.g. nsIScriptError::warningFlag, ...).
+ * @param aMsg The text message to print to the console.
+ */
+void LogToBrowserConsole(uint32_t aLogLevel, const nsAString& aMsg);
+
+/**
+ * Get the name string for a ProcessID.
+ * This is the name we use for the Telemetry payloads.
+ */
+const char* GetNameForProcessID(ProcessID process);
+
+/**
+ * Get the process id give a process name.
+ *
+ * @param aProcessName - the name of the process.
+ * @returns {ProcessID} one value from ProcessID::* or ProcessID::Count if the
+ * name of the process was not found.
+ */
+ProcessID GetIDForProcessName(const char* aProcessName);
+
+/**
+ * Get the GeckoProcessType for a ProcessID.
+ * Telemetry distinguishes between more process types than the GeckoProcessType,
+ * so the mapping is not direct.
+ */
+GeckoProcessType GetGeckoProcessType(ProcessID process);
+
+/**
+ * Check if the passed telemetry identifier is valid.
+ *
+ * @param aStr The string identifier.
+ * @param aMaxLength The maximum length of the identifier.
+ * @param aAllowInfixPeriod Whether or not to allow infix dots.
+ * @param aAllowInfixUnderscore Whether or not to allow infix underscores.
+ * @returns true if the string validates correctly, false otherwise.
+ */
+bool IsValidIdentifierString(const nsACString& aStr, const size_t aMaxLength,
+ const bool aAllowInfixPeriod,
+ const bool aAllowInfixUnderscore);
+
+/**
+ * Convert the given UTF8 string to a JavaScript string. The returned
+ * string's contents will be the UTF16 conversion of the given string.
+ *
+ * @param cx The JS context.
+ * @param aStr The UTF8 string.
+ * @returns a JavaScript string.
+ */
+JSString* ToJSString(JSContext* cx, const nsACString& aStr);
+
+/**
+ * Convert the given UTF16 string to a JavaScript string.
+ *
+ * @param cx The JS context.
+ * @param aStr The UTF16 string.
+ * @returns a JavaScript string.
+ */
+JSString* ToJSString(JSContext* cx, const nsAString& aStr);
+
+/**
+ * Get an identifier for the currently-running product.
+ * This is not stable over time and may change.
+ *
+ * @returns the product identifier
+ */
+SupportedProduct GetCurrentProduct();
+
+} // namespace Common
+} // namespace Telemetry
+} // namespace mozilla
+
+#endif // TelemetryCommon_h__
diff --git a/toolkit/components/telemetry/core/TelemetryEvent.cpp b/toolkit/components/telemetry/core/TelemetryEvent.cpp
new file mode 100644
index 0000000000..ad6a3fd2c7
--- /dev/null
+++ b/toolkit/components/telemetry/core/TelemetryEvent.cpp
@@ -0,0 +1,1387 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "Telemetry.h"
+#include "TelemetryEvent.h"
+#include <limits>
+#include "ipc/TelemetryIPCAccumulator.h"
+#include "jsapi.h"
+#include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject, JS::NewArrayObject
+#include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineProperty, JS_Enumerate, JS_GetElement, JS_GetProperty, JS_GetPropertyById, JS_HasProperty
+#include "mozilla/Maybe.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPtr.h"
+#include "nsClassHashtable.h"
+#include "nsHashKeys.h"
+#include "nsIObserverService.h"
+#include "nsITelemetry.h"
+#include "nsJSUtils.h"
+#include "nsPrintfCString.h"
+#include "nsTArray.h"
+#include "nsUTF8Utils.h"
+#include "nsXULAppAPI.h"
+#include "TelemetryCommon.h"
+#include "TelemetryEventData.h"
+#include "TelemetryScalar.h"
+
+using mozilla::MakeUnique;
+using mozilla::Maybe;
+using mozilla::StaticAutoPtr;
+using mozilla::StaticMutex;
+using mozilla::StaticMutexAutoLock;
+using mozilla::TimeStamp;
+using mozilla::UniquePtr;
+using mozilla::Telemetry::ChildEventData;
+using mozilla::Telemetry::EventExtraEntry;
+using mozilla::Telemetry::LABELS_TELEMETRY_EVENT_RECORDING_ERROR;
+using mozilla::Telemetry::LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR;
+using mozilla::Telemetry::ProcessID;
+using mozilla::Telemetry::Common::CanRecordDataset;
+using mozilla::Telemetry::Common::CanRecordInProcess;
+using mozilla::Telemetry::Common::CanRecordProduct;
+using mozilla::Telemetry::Common::GetNameForProcessID;
+using mozilla::Telemetry::Common::IsExpiredVersion;
+using mozilla::Telemetry::Common::IsInDataset;
+using mozilla::Telemetry::Common::IsValidIdentifierString;
+using mozilla::Telemetry::Common::LogToBrowserConsole;
+using mozilla::Telemetry::Common::MsSinceProcessStart;
+using mozilla::Telemetry::Common::ToJSString;
+
+namespace TelemetryIPCAccumulator = mozilla::TelemetryIPCAccumulator;
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// Naming: there are two kinds of functions in this file:
+//
+// * Functions taking a StaticMutexAutoLock: these can only be reached via
+// an interface function (TelemetryEvent::*). They expect the interface
+// function to have acquired |gTelemetryEventsMutex|, so they do not
+// have to be thread-safe.
+//
+// * Functions named TelemetryEvent::*. This is the external interface.
+// Entries and exits to these functions are serialised using
+// |gTelemetryEventsMutex|.
+//
+// Avoiding races and deadlocks:
+//
+// All functions in the external interface (TelemetryEvent::*) are
+// serialised using the mutex |gTelemetryEventsMutex|. This means
+// that the external interface is thread-safe, and the internal
+// functions can ignore thread safety. But it also brings a danger
+// of deadlock if any function in the external interface can get back
+// to that interface. That is, we will deadlock on any call chain like
+// this:
+//
+// TelemetryEvent::* -> .. any functions .. -> TelemetryEvent::*
+//
+// To reduce the danger of that happening, observe the following rules:
+//
+// * No function in TelemetryEvent::* may directly call, nor take the
+// address of, any other function in TelemetryEvent::*.
+//
+// * No internal function may call, nor take the address
+// of, any function in TelemetryEvent::*.
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE TYPES
+
+namespace {
+
+const uint32_t kEventCount =
+ static_cast<uint32_t>(mozilla::Telemetry::EventID::EventCount);
+// This is a special event id used to mark expired events, to make expiry checks
+// cheap at runtime.
+const uint32_t kExpiredEventId = std::numeric_limits<uint32_t>::max();
+static_assert(kExpiredEventId > kEventCount,
+ "Built-in event count should be less than the expired event id.");
+
+// Maximum length of any passed value string, in UTF8 byte sequence length.
+const uint32_t kMaxValueByteLength = 80;
+// Maximum length of any string value in the extra dictionary, in UTF8 byte
+// sequence length.
+const uint32_t kMaxExtraValueByteLength = 80;
+// Maximum length of dynamic method names, in UTF8 byte sequence length.
+const uint32_t kMaxMethodNameByteLength = 20;
+// Maximum length of dynamic object names, in UTF8 byte sequence length.
+const uint32_t kMaxObjectNameByteLength = 20;
+// Maximum length of extra key names, in UTF8 byte sequence length.
+const uint32_t kMaxExtraKeyNameByteLength = 15;
+// The maximum number of valid extra keys for an event.
+const uint32_t kMaxExtraKeyCount = 10;
+// The number of event records allowed in an event ping.
+const uint32_t kEventPingLimit = 1000;
+
+struct EventKey {
+ uint32_t id;
+ bool dynamic;
+
+ EventKey() : id(kExpiredEventId), dynamic(false) {}
+ EventKey(uint32_t id_, bool dynamic_) : id(id_), dynamic(dynamic_) {}
+};
+
+struct DynamicEventInfo {
+ DynamicEventInfo(const nsACString& category, const nsACString& method,
+ const nsACString& object, nsTArray<nsCString>& extra_keys,
+ bool recordOnRelease, bool builtin)
+ : category(category),
+ method(method),
+ object(object),
+ extra_keys(extra_keys.Clone()),
+ recordOnRelease(recordOnRelease),
+ builtin(builtin) {}
+
+ DynamicEventInfo(const DynamicEventInfo&) = default;
+ DynamicEventInfo& operator=(const DynamicEventInfo&) = delete;
+
+ const nsCString category;
+ const nsCString method;
+ const nsCString object;
+ const CopyableTArray<nsCString> extra_keys;
+ const bool recordOnRelease;
+ const bool builtin;
+
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t n = 0;
+
+ n += category.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ n += method.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ n += object.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ n += extra_keys.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (auto& key : extra_keys) {
+ n += key.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+
+ return n;
+ }
+};
+
+enum class RecordEventResult {
+ Ok,
+ UnknownEvent,
+ InvalidExtraKey,
+ StorageLimitReached,
+ ExpiredEvent,
+ WrongProcess,
+ CannotRecord,
+};
+
+typedef CopyableTArray<EventExtraEntry> ExtraArray;
+
+class EventRecord {
+ public:
+ EventRecord(double timestamp, const EventKey& key,
+ const Maybe<nsCString>& value, const ExtraArray& extra)
+ : mTimestamp(timestamp),
+ mEventKey(key),
+ mValue(value),
+ mExtra(extra.Clone()) {}
+
+ EventRecord(const EventRecord& other) = default;
+
+ EventRecord& operator=(const EventRecord& other) = delete;
+
+ double Timestamp() const { return mTimestamp; }
+ const EventKey& GetEventKey() const { return mEventKey; }
+ const Maybe<nsCString>& Value() const { return mValue; }
+ const ExtraArray& Extra() const { return mExtra; }
+
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ private:
+ const double mTimestamp;
+ const EventKey mEventKey;
+ const Maybe<nsCString> mValue;
+ const ExtraArray mExtra;
+};
+
+// Implements the methods for EventInfo.
+const nsDependentCString EventInfo::method() const {
+ return nsDependentCString(&gEventsStringTable[this->method_offset]);
+}
+
+const nsDependentCString EventInfo::object() const {
+ return nsDependentCString(&gEventsStringTable[this->object_offset]);
+}
+
+// Implements the methods for CommonEventInfo.
+const nsDependentCString CommonEventInfo::category() const {
+ return nsDependentCString(&gEventsStringTable[this->category_offset]);
+}
+
+const nsDependentCString CommonEventInfo::expiration_version() const {
+ return nsDependentCString(
+ &gEventsStringTable[this->expiration_version_offset]);
+}
+
+const nsDependentCString CommonEventInfo::extra_key(uint32_t index) const {
+ MOZ_ASSERT(index < this->extra_count);
+ uint32_t key_index = gExtraKeysTable[this->extra_index + index];
+ return nsDependentCString(&gEventsStringTable[key_index]);
+}
+
+// Implementation for the EventRecord class.
+size_t EventRecord::SizeOfExcludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t n = 0;
+
+ if (mValue) {
+ n += mValue.value().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+
+ n += mExtra.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (uint32_t i = 0; i < mExtra.Length(); ++i) {
+ n += mExtra[i].key.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ n += mExtra[i].value.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+
+ return n;
+}
+
+nsCString UniqueEventName(const nsACString& category, const nsACString& method,
+ const nsACString& object) {
+ nsCString name;
+ name.Append(category);
+ name.AppendLiteral("#");
+ name.Append(method);
+ name.AppendLiteral("#");
+ name.Append(object);
+ return name;
+}
+
+nsCString UniqueEventName(const EventInfo& info) {
+ return UniqueEventName(info.common_info.category(), info.method(),
+ info.object());
+}
+
+nsCString UniqueEventName(const DynamicEventInfo& info) {
+ return UniqueEventName(info.category, info.method, info.object);
+}
+
+void TruncateToByteLength(nsCString& str, uint32_t length) {
+ // last will be the index of the first byte of the current multi-byte
+ // sequence.
+ uint32_t last = RewindToPriorUTF8Codepoint(str.get(), length);
+ str.Truncate(last);
+}
+
+} // anonymous namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE STATE, SHARED BY ALL THREADS
+
+namespace {
+
+// Set to true once this global state has been initialized.
+bool gInitDone = false;
+
+bool gCanRecordBase;
+bool gCanRecordExtended;
+
+// The EventName -> EventKey cache map.
+nsTHashMap<nsCStringHashKey, EventKey> gEventNameIDMap(kEventCount);
+
+// The CategoryName set.
+nsTHashSet<nsCString> gCategoryNames;
+
+// This tracks the IDs of the categories for which recording is enabled.
+nsTHashSet<nsCString> gEnabledCategories;
+
+// The main event storage. Events are inserted here, keyed by process id and
+// in recording order.
+typedef nsUint32HashKey ProcessIDHashKey;
+typedef nsTArray<EventRecord> EventRecordArray;
+typedef nsClassHashtable<ProcessIDHashKey, EventRecordArray>
+ EventRecordsMapType;
+
+EventRecordsMapType gEventRecords;
+
+// The details on dynamic events that are recorded from addons are registered
+// here.
+StaticAutoPtr<nsTArray<DynamicEventInfo>> gDynamicEventInfo;
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: thread-safe helpers for event recording.
+
+namespace {
+
+unsigned int GetDataset(const StaticMutexAutoLock& lock,
+ const EventKey& eventKey) {
+ if (!eventKey.dynamic) {
+ return gEventInfo[eventKey.id].common_info.dataset;
+ }
+
+ if (!gDynamicEventInfo) {
+ return nsITelemetry::DATASET_PRERELEASE_CHANNELS;
+ }
+
+ return (*gDynamicEventInfo)[eventKey.id].recordOnRelease
+ ? nsITelemetry::DATASET_ALL_CHANNELS
+ : nsITelemetry::DATASET_PRERELEASE_CHANNELS;
+}
+
+nsCString GetCategory(const StaticMutexAutoLock& lock,
+ const EventKey& eventKey) {
+ if (!eventKey.dynamic) {
+ return gEventInfo[eventKey.id].common_info.category();
+ }
+
+ if (!gDynamicEventInfo) {
+ return ""_ns;
+ }
+
+ return (*gDynamicEventInfo)[eventKey.id].category;
+}
+
+bool CanRecordEvent(const StaticMutexAutoLock& lock, const EventKey& eventKey,
+ ProcessID process) {
+ if (!gCanRecordBase) {
+ return false;
+ }
+
+ if (!CanRecordDataset(GetDataset(lock, eventKey), gCanRecordBase,
+ gCanRecordExtended)) {
+ return false;
+ }
+
+ // We don't allow specifying a process to record in for dynamic events.
+ if (!eventKey.dynamic) {
+ const CommonEventInfo& info = gEventInfo[eventKey.id].common_info;
+
+ if (!CanRecordProduct(info.products)) {
+ return false;
+ }
+
+ if (!CanRecordInProcess(info.record_in_processes, process)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+bool IsExpired(const EventKey& key) { return key.id == kExpiredEventId; }
+
+EventRecordArray* GetEventRecordsForProcess(const StaticMutexAutoLock& lock,
+ ProcessID processType) {
+ return gEventRecords.GetOrInsertNew(uint32_t(processType));
+}
+
+bool GetEventKey(const StaticMutexAutoLock& lock, const nsACString& category,
+ const nsACString& method, const nsACString& object,
+ EventKey* aEventKey) {
+ const nsCString& name = UniqueEventName(category, method, object);
+ return gEventNameIDMap.Get(name, aEventKey);
+}
+
+static bool CheckExtraKeysValid(const EventKey& eventKey,
+ const ExtraArray& extra) {
+ nsTHashSet<nsCString> validExtraKeys;
+ if (!eventKey.dynamic) {
+ const CommonEventInfo& common = gEventInfo[eventKey.id].common_info;
+ for (uint32_t i = 0; i < common.extra_count; ++i) {
+ validExtraKeys.Insert(common.extra_key(i));
+ }
+ } else if (gDynamicEventInfo) {
+ const DynamicEventInfo& info = (*gDynamicEventInfo)[eventKey.id];
+ for (uint32_t i = 0, len = info.extra_keys.Length(); i < len; ++i) {
+ validExtraKeys.Insert(info.extra_keys[i]);
+ }
+ }
+
+ for (uint32_t i = 0; i < extra.Length(); ++i) {
+ if (!validExtraKeys.Contains(extra[i].key)) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+RecordEventResult RecordEvent(const StaticMutexAutoLock& lock,
+ ProcessID processType, double timestamp,
+ const nsACString& category,
+ const nsACString& method,
+ const nsACString& object,
+ const Maybe<nsCString>& value,
+ const ExtraArray& extra) {
+ // Look up the event id.
+ EventKey eventKey;
+ if (!GetEventKey(lock, category, method, object, &eventKey)) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::UnknownEvent);
+ return RecordEventResult::UnknownEvent;
+ }
+
+ // If the event is expired or not enabled for this process, we silently drop
+ // this call. We don't want recording for expired probes to be an error so
+ // code doesn't have to be removed at a specific time or version. Even logging
+ // warnings would become very noisy.
+ if (IsExpired(eventKey)) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Expired);
+ return RecordEventResult::ExpiredEvent;
+ }
+
+ // Fixup the process id only for non-builtin (e.g. supporting build faster)
+ // dynamic events.
+ auto dynamicNonBuiltin =
+ eventKey.dynamic && !(*gDynamicEventInfo)[eventKey.id].builtin;
+ if (dynamicNonBuiltin) {
+ processType = ProcessID::Dynamic;
+ }
+
+ // Check whether the extra keys passed are valid.
+ if (!CheckExtraKeysValid(eventKey, extra)) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::ExtraKey);
+ return RecordEventResult::InvalidExtraKey;
+ }
+
+ // Check whether we can record this event.
+ if (!CanRecordEvent(lock, eventKey, processType)) {
+ return RecordEventResult::CannotRecord;
+ }
+
+ // Count the number of times this event has been recorded, even if its
+ // category does not have recording enabled.
+ TelemetryScalar::SummarizeEvent(UniqueEventName(category, method, object),
+ processType, dynamicNonBuiltin);
+
+ // Check whether this event's category has recording enabled
+ if (!gEnabledCategories.Contains(GetCategory(lock, eventKey))) {
+ return RecordEventResult::Ok;
+ }
+
+ EventRecordArray* eventRecords = GetEventRecordsForProcess(lock, processType);
+ eventRecords->AppendElement(EventRecord(timestamp, eventKey, value, extra));
+
+ // Notify observers when we hit the "event" ping event record limit.
+ if (eventRecords->Length() == kEventPingLimit) {
+ return RecordEventResult::StorageLimitReached;
+ }
+
+ return RecordEventResult::Ok;
+}
+
+RecordEventResult ShouldRecordChildEvent(const StaticMutexAutoLock& lock,
+ const nsACString& category,
+ const nsACString& method,
+ const nsACString& object) {
+ EventKey eventKey;
+ if (!GetEventKey(lock, category, method, object, &eventKey)) {
+ // This event is unknown in this process, but it might be a dynamic event
+ // that was registered in the parent process.
+ return RecordEventResult::Ok;
+ }
+
+ if (IsExpired(eventKey)) {
+ return RecordEventResult::ExpiredEvent;
+ }
+
+ const auto processes =
+ gEventInfo[eventKey.id].common_info.record_in_processes;
+ if (!CanRecordInProcess(processes, XRE_GetProcessType())) {
+ return RecordEventResult::WrongProcess;
+ }
+
+ return RecordEventResult::Ok;
+}
+
+void RegisterEvents(const StaticMutexAutoLock& lock, const nsACString& category,
+ const nsTArray<DynamicEventInfo>& eventInfos,
+ const nsTArray<bool>& eventExpired, bool aBuiltin) {
+ MOZ_ASSERT(eventInfos.Length() == eventExpired.Length(),
+ "Event data array sizes should match.");
+
+ // Register the new events.
+ if (!gDynamicEventInfo) {
+ gDynamicEventInfo = new nsTArray<DynamicEventInfo>();
+ }
+
+ for (uint32_t i = 0, len = eventInfos.Length(); i < len; ++i) {
+ const nsCString& eventName = UniqueEventName(eventInfos[i]);
+
+ // Re-registering events can happen for two reasons and we don't print
+ // warnings:
+ //
+ // * When add-ons update.
+ // We don't support changing their definition, but the expiry might have
+ // changed.
+ // * When dynamic builtins ("build faster") events are registered.
+ // The dynamic definition takes precedence then.
+ EventKey existing;
+ if (!aBuiltin && gEventNameIDMap.Get(eventName, &existing)) {
+ if (eventExpired[i]) {
+ existing.id = kExpiredEventId;
+ }
+ continue;
+ }
+
+ gDynamicEventInfo->AppendElement(eventInfos[i]);
+ uint32_t eventId =
+ eventExpired[i] ? kExpiredEventId : gDynamicEventInfo->Length() - 1;
+ gEventNameIDMap.InsertOrUpdate(eventName, EventKey{eventId, true});
+ }
+
+ // If it is a builtin, add the category name in order to enable it later.
+ if (aBuiltin) {
+ gCategoryNames.Insert(category);
+ }
+
+ if (!aBuiltin) {
+ // Now after successful registration enable recording for this category
+ // (if not a dynamic builtin).
+ gEnabledCategories.Insert(category);
+ }
+}
+
+} // anonymous namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: thread-unsafe helpers for event handling.
+
+namespace {
+
+nsresult SerializeEventsArray(const EventRecordArray& events, JSContext* cx,
+ JS::MutableHandle<JSObject*> result,
+ unsigned int dataset) {
+ // We serialize the events to a JS array.
+ JS::Rooted<JSObject*> eventsArray(cx,
+ JS::NewArrayObject(cx, events.Length()));
+ if (!eventsArray) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (uint32_t i = 0; i < events.Length(); ++i) {
+ const EventRecord& record = events[i];
+
+ // Each entry is an array of one of the forms:
+ // [timestamp, category, method, object, value]
+ // [timestamp, category, method, object, null, extra]
+ // [timestamp, category, method, object, value, extra]
+ JS::RootedVector<JS::Value> items(cx);
+
+ // Add timestamp.
+ JS::Rooted<JS::Value> val(cx);
+ if (!items.append(JS::NumberValue(floor(record.Timestamp())))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Add category, method, object.
+ auto addCategoryMethodObjectValues = [&](const nsACString& category,
+ const nsACString& method,
+ const nsACString& object) -> bool {
+ return items.append(JS::StringValue(ToJSString(cx, category))) &&
+ items.append(JS::StringValue(ToJSString(cx, method))) &&
+ items.append(JS::StringValue(ToJSString(cx, object)));
+ };
+
+ const EventKey& eventKey = record.GetEventKey();
+ if (!eventKey.dynamic) {
+ const EventInfo& info = gEventInfo[eventKey.id];
+ if (!addCategoryMethodObjectValues(info.common_info.category(),
+ info.method(), info.object())) {
+ return NS_ERROR_FAILURE;
+ }
+ } else if (gDynamicEventInfo) {
+ const DynamicEventInfo& info = (*gDynamicEventInfo)[eventKey.id];
+ if (!addCategoryMethodObjectValues(info.category, info.method,
+ info.object)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Add the optional string value only when needed.
+ // When the value field is empty and extra is not set, we can save a little
+ // space that way. We still need to submit a null value if extra is set, to
+ // match the form: [ts, category, method, object, null, extra]
+ if (record.Value()) {
+ if (!items.append(
+ JS::StringValue(ToJSString(cx, record.Value().value())))) {
+ return NS_ERROR_FAILURE;
+ }
+ } else if (!record.Extra().IsEmpty()) {
+ if (!items.append(JS::NullValue())) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Add the optional extra dictionary.
+ // To save a little space, only add it when it is not empty.
+ if (!record.Extra().IsEmpty()) {
+ JS::Rooted<JSObject*> obj(cx, JS_NewPlainObject(cx));
+ if (!obj) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Add extra key & value entries.
+ const ExtraArray& extra = record.Extra();
+ for (uint32_t i = 0; i < extra.Length(); ++i) {
+ JS::Rooted<JS::Value> value(cx);
+ value.setString(ToJSString(cx, extra[i].value));
+
+ if (!JS_DefineProperty(cx, obj, extra[i].key.get(), value,
+ JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ val.setObject(*obj);
+
+ if (!items.append(val)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Add the record to the events array.
+ JS::Rooted<JSObject*> itemsArray(cx, JS::NewArrayObject(cx, items));
+ if (!JS_DefineElement(cx, eventsArray, i, itemsArray, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ result.set(eventsArray);
+ return NS_OK;
+}
+
+} // anonymous namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// EXTERNALLY VISIBLE FUNCTIONS in namespace TelemetryEvents::
+
+// This is a StaticMutex rather than a plain Mutex (1) so that
+// it gets initialised in a thread-safe manner the first time
+// it is used, and (2) because it is never de-initialised, and
+// a normal Mutex would show up as a leak in BloatView. StaticMutex
+// also has the "OffTheBooks" property, so it won't show as a leak
+// in BloatView.
+// Another reason to use a StaticMutex instead of a plain Mutex is
+// that, due to the nature of Telemetry, we cannot rely on having a
+// mutex initialized in InitializeGlobalState. Unfortunately, we
+// cannot make sure that no other function is called before this point.
+static StaticMutex gTelemetryEventsMutex MOZ_UNANNOTATED;
+
+void TelemetryEvent::InitializeGlobalState(bool aCanRecordBase,
+ bool aCanRecordExtended) {
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+ MOZ_ASSERT(!gInitDone,
+ "TelemetryEvent::InitializeGlobalState "
+ "may only be called once");
+
+ gCanRecordBase = aCanRecordBase;
+ gCanRecordExtended = aCanRecordExtended;
+
+ // Populate the static event name->id cache. Note that the event names are
+ // statically allocated and come from the automatically generated
+ // TelemetryEventData.h.
+ const uint32_t eventCount =
+ static_cast<uint32_t>(mozilla::Telemetry::EventID::EventCount);
+ for (uint32_t i = 0; i < eventCount; ++i) {
+ const EventInfo& info = gEventInfo[i];
+ uint32_t eventId = i;
+
+ // If this event is expired or not recorded in this process, mark it with
+ // a special event id.
+ // This avoids doing repeated checks at runtime.
+ if (IsExpiredVersion(info.common_info.expiration_version().get())) {
+ eventId = kExpiredEventId;
+ }
+
+ gEventNameIDMap.InsertOrUpdate(UniqueEventName(info),
+ EventKey{eventId, false});
+ gCategoryNames.Insert(info.common_info.category());
+ }
+
+ // A hack until bug 1691156 is fixed
+ gEnabledCategories.Insert("avif"_ns);
+
+ gInitDone = true;
+}
+
+void TelemetryEvent::DeInitializeGlobalState() {
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+ MOZ_ASSERT(gInitDone);
+
+ gCanRecordBase = false;
+ gCanRecordExtended = false;
+
+ gEventNameIDMap.Clear();
+ gCategoryNames.Clear();
+ gEnabledCategories.Clear();
+ gEventRecords.Clear();
+
+ gDynamicEventInfo = nullptr;
+
+ gInitDone = false;
+}
+
+void TelemetryEvent::SetCanRecordBase(bool b) {
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+ gCanRecordBase = b;
+}
+
+void TelemetryEvent::SetCanRecordExtended(bool b) {
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+ gCanRecordExtended = b;
+}
+
+nsresult TelemetryEvent::RecordChildEvents(
+ ProcessID aProcessType,
+ const nsTArray<mozilla::Telemetry::ChildEventData>& aEvents) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+ for (uint32_t i = 0; i < aEvents.Length(); ++i) {
+ const mozilla::Telemetry::ChildEventData& e = aEvents[i];
+
+ // Timestamps from child processes are absolute. We fix them up here to be
+ // relative to the main process start time.
+ // This allows us to put events from all processes on the same timeline.
+ double relativeTimestamp =
+ (e.timestamp - TimeStamp::ProcessCreation()).ToMilliseconds();
+
+ ::RecordEvent(locker, aProcessType, relativeTimestamp, e.category, e.method,
+ e.object, e.value, e.extra);
+ }
+ return NS_OK;
+}
+
+nsresult TelemetryEvent::RecordEvent(const nsACString& aCategory,
+ const nsACString& aMethod,
+ const nsACString& aObject,
+ JS::Handle<JS::Value> aValue,
+ JS::Handle<JS::Value> aExtra,
+ JSContext* cx, uint8_t optional_argc) {
+ // Check value argument.
+ if ((optional_argc > 0) && !aValue.isNull() && !aValue.isString()) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ u"Invalid type for value parameter."_ns);
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Value);
+ return NS_OK;
+ }
+
+ // Extract value parameter.
+ Maybe<nsCString> value;
+ if (aValue.isString()) {
+ nsAutoJSString jsStr;
+ if (!jsStr.init(cx, aValue)) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ u"Invalid string value for value parameter."_ns);
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Value);
+ return NS_OK;
+ }
+
+ nsCString str = NS_ConvertUTF16toUTF8(jsStr);
+ if (str.Length() > kMaxValueByteLength) {
+ LogToBrowserConsole(
+ nsIScriptError::warningFlag,
+ nsLiteralString(
+ u"Value parameter exceeds maximum string length, truncating."));
+ TruncateToByteLength(str, kMaxValueByteLength);
+ }
+ value = mozilla::Some(str);
+ }
+
+ // Check extra argument.
+ if ((optional_argc > 1) && !aExtra.isNull() && !aExtra.isObject()) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ u"Invalid type for extra parameter."_ns);
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Extra);
+ return NS_OK;
+ }
+
+ // Extract extra dictionary.
+ ExtraArray extra;
+ if (aExtra.isObject()) {
+ JS::Rooted<JSObject*> obj(cx, &aExtra.toObject());
+ JS::Rooted<JS::IdVector> ids(cx, JS::IdVector(cx));
+ if (!JS_Enumerate(cx, obj, &ids)) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ u"Failed to enumerate object."_ns);
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Extra);
+ return NS_OK;
+ }
+
+ for (size_t i = 0, n = ids.length(); i < n; i++) {
+ nsAutoJSString key;
+ if (!key.init(cx, ids[i])) {
+ LogToBrowserConsole(
+ nsIScriptError::warningFlag,
+ nsLiteralString(
+ u"Extra dictionary should only contain string keys."));
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Extra);
+ return NS_OK;
+ }
+
+ JS::Rooted<JS::Value> value(cx);
+ if (!JS_GetPropertyById(cx, obj, ids[i], &value)) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ u"Failed to get extra property."_ns);
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Extra);
+ return NS_OK;
+ }
+
+ nsAutoJSString jsStr;
+ if (!value.isString() || !jsStr.init(cx, value)) {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ u"Extra properties should have string values."_ns);
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_RECORDING_ERROR::Extra);
+ return NS_OK;
+ }
+
+ nsCString str = NS_ConvertUTF16toUTF8(jsStr);
+ if (str.Length() > kMaxExtraValueByteLength) {
+ LogToBrowserConsole(
+ nsIScriptError::warningFlag,
+ nsLiteralString(
+ u"Extra value exceeds maximum string length, truncating."));
+ TruncateToByteLength(str, kMaxExtraValueByteLength);
+ }
+
+ extra.AppendElement(EventExtraEntry{NS_ConvertUTF16toUTF8(key), str});
+ }
+ }
+
+ // Lock for accessing internal data.
+ // While the lock is being held, no complex calls like JS calls can be made,
+ // as all of these could record Telemetry, which would result in deadlock.
+ RecordEventResult res;
+ if (!XRE_IsParentProcess()) {
+ {
+ StaticMutexAutoLock lock(gTelemetryEventsMutex);
+ res = ::ShouldRecordChildEvent(lock, aCategory, aMethod, aObject);
+ }
+
+ if (res == RecordEventResult::Ok) {
+ TelemetryIPCAccumulator::RecordChildEvent(
+ TimeStamp::NowLoRes(), aCategory, aMethod, aObject, value, extra);
+ }
+ } else {
+ StaticMutexAutoLock lock(gTelemetryEventsMutex);
+
+ if (!gInitDone) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Get the current time.
+ double timestamp = -1;
+ if (NS_WARN_IF(NS_FAILED(MsSinceProcessStart(&timestamp)))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ res = ::RecordEvent(lock, ProcessID::Parent, timestamp, aCategory, aMethod,
+ aObject, value, extra);
+ }
+
+ // Trigger warnings or errors where needed.
+ switch (res) {
+ case RecordEventResult::UnknownEvent: {
+ nsPrintfCString msg(R"(Unknown event: ["%s", "%s", "%s"])",
+ PromiseFlatCString(aCategory).get(),
+ PromiseFlatCString(aMethod).get(),
+ PromiseFlatCString(aObject).get());
+ LogToBrowserConsole(nsIScriptError::errorFlag,
+ NS_ConvertUTF8toUTF16(msg));
+ return NS_OK;
+ }
+ case RecordEventResult::InvalidExtraKey: {
+ nsPrintfCString msg(R"(Invalid extra key for event ["%s", "%s", "%s"].)",
+ PromiseFlatCString(aCategory).get(),
+ PromiseFlatCString(aMethod).get(),
+ PromiseFlatCString(aObject).get());
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ NS_ConvertUTF8toUTF16(msg));
+ return NS_OK;
+ }
+ case RecordEventResult::StorageLimitReached: {
+ LogToBrowserConsole(nsIScriptError::warningFlag,
+ u"Event storage limit reached."_ns);
+ nsCOMPtr<nsIObserverService> serv =
+ mozilla::services::GetObserverService();
+ if (serv) {
+ serv->NotifyObservers(nullptr, "event-telemetry-storage-limit-reached",
+ nullptr);
+ }
+ return NS_OK;
+ }
+ default:
+ return NS_OK;
+ }
+}
+
+void TelemetryEvent::RecordEventNative(
+ mozilla::Telemetry::EventID aId, const mozilla::Maybe<nsCString>& aValue,
+ const mozilla::Maybe<ExtraArray>& aExtra) {
+ // Truncate aValue if present and necessary.
+ mozilla::Maybe<nsCString> value;
+ if (aValue) {
+ nsCString valueStr = aValue.ref();
+ if (valueStr.Length() > kMaxValueByteLength) {
+ TruncateToByteLength(valueStr, kMaxValueByteLength);
+ }
+ value = mozilla::Some(valueStr);
+ }
+
+ // Truncate any over-long extra values.
+ ExtraArray extra;
+ if (aExtra) {
+ extra = aExtra.value();
+ for (auto& item : extra) {
+ if (item.value.Length() > kMaxExtraValueByteLength) {
+ TruncateToByteLength(item.value, kMaxExtraValueByteLength);
+ }
+ }
+ }
+
+ const EventInfo& info = gEventInfo[static_cast<uint32_t>(aId)];
+ const nsCString category(info.common_info.category());
+ const nsCString method(info.method());
+ const nsCString object(info.object());
+ if (!XRE_IsParentProcess()) {
+ RecordEventResult res;
+ {
+ StaticMutexAutoLock lock(gTelemetryEventsMutex);
+ res = ::ShouldRecordChildEvent(lock, category, method, object);
+ }
+
+ if (res == RecordEventResult::Ok) {
+ TelemetryIPCAccumulator::RecordChildEvent(TimeStamp::NowLoRes(), category,
+ method, object, value, extra);
+ }
+ } else {
+ StaticMutexAutoLock lock(gTelemetryEventsMutex);
+
+ if (!gInitDone) {
+ return;
+ }
+
+ // Get the current time.
+ double timestamp = -1;
+ if (NS_WARN_IF(NS_FAILED(MsSinceProcessStart(&timestamp)))) {
+ return;
+ }
+
+ ::RecordEvent(lock, ProcessID::Parent, timestamp, category, method, object,
+ value, extra);
+ }
+}
+
+static bool GetArrayPropertyValues(JSContext* cx, JS::Handle<JSObject*> obj,
+ const char* property,
+ nsTArray<nsCString>* results) {
+ JS::Rooted<JS::Value> value(cx);
+ if (!JS_GetProperty(cx, obj, property, &value)) {
+ JS_ReportErrorASCII(cx, R"(Missing required property "%s" for event)",
+ property);
+ return false;
+ }
+
+ bool isArray = false;
+ if (!JS::IsArrayObject(cx, value, &isArray) || !isArray) {
+ JS_ReportErrorASCII(cx, R"(Property "%s" for event should be an array)",
+ property);
+ return false;
+ }
+
+ JS::Rooted<JSObject*> arrayObj(cx, &value.toObject());
+ uint32_t arrayLength;
+ if (!JS::GetArrayLength(cx, arrayObj, &arrayLength)) {
+ return false;
+ }
+
+ for (uint32_t arrayIdx = 0; arrayIdx < arrayLength; ++arrayIdx) {
+ JS::Rooted<JS::Value> element(cx);
+ if (!JS_GetElement(cx, arrayObj, arrayIdx, &element)) {
+ return false;
+ }
+
+ if (!element.isString()) {
+ JS_ReportErrorASCII(
+ cx, R"(Array entries for event property "%s" should be strings)",
+ property);
+ return false;
+ }
+
+ nsAutoJSString jsStr;
+ if (!jsStr.init(cx, element)) {
+ return false;
+ }
+
+ results->AppendElement(NS_ConvertUTF16toUTF8(jsStr));
+ }
+
+ return true;
+}
+
+nsresult TelemetryEvent::RegisterEvents(const nsACString& aCategory,
+ JS::Handle<JS::Value> aEventData,
+ bool aBuiltin, JSContext* cx) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Events can only be registered in the parent process");
+
+ if (!IsValidIdentifierString(aCategory, 30, true, true)) {
+ JS_ReportErrorASCII(
+ cx, "Category parameter should match the identifier pattern.");
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Category);
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (!aEventData.isObject()) {
+ JS_ReportErrorASCII(cx, "Event data parameter should be an object");
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ JS::Rooted<JSObject*> obj(cx, &aEventData.toObject());
+ JS::Rooted<JS::IdVector> eventPropertyIds(cx, JS::IdVector(cx));
+ if (!JS_Enumerate(cx, obj, &eventPropertyIds)) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
+ return NS_ERROR_FAILURE;
+ }
+
+ // Collect the event data into local storage first.
+ // Only after successfully validating all contained events will we register
+ // them into global storage.
+ nsTArray<DynamicEventInfo> newEventInfos;
+ nsTArray<bool> newEventExpired;
+
+ for (size_t i = 0, n = eventPropertyIds.length(); i < n; i++) {
+ nsAutoJSString eventName;
+ if (!eventName.init(cx, eventPropertyIds[i])) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!IsValidIdentifierString(NS_ConvertUTF16toUTF8(eventName),
+ kMaxMethodNameByteLength, false, true)) {
+ JS_ReportErrorASCII(cx,
+ "Event names should match the identifier pattern.");
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Name);
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ JS::Rooted<JS::Value> value(cx);
+ if (!JS_GetPropertyById(cx, obj, eventPropertyIds[i], &value) ||
+ !value.isObject()) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
+ return NS_ERROR_FAILURE;
+ }
+ JS::Rooted<JSObject*> eventObj(cx, &value.toObject());
+
+ // Extract the event registration data.
+ nsTArray<nsCString> methods;
+ nsTArray<nsCString> objects;
+ nsTArray<nsCString> extra_keys;
+ bool expired = false;
+ bool recordOnRelease = false;
+
+ // The methods & objects properties are required.
+ if (!GetArrayPropertyValues(cx, eventObj, "methods", &methods)) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!GetArrayPropertyValues(cx, eventObj, "objects", &objects)) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
+ return NS_ERROR_FAILURE;
+ }
+
+ // extra_keys is optional.
+ bool hasProperty = false;
+ if (JS_HasProperty(cx, eventObj, "extra_keys", &hasProperty) &&
+ hasProperty) {
+ if (!GetArrayPropertyValues(cx, eventObj, "extra_keys", &extra_keys)) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // expired is optional.
+ if (JS_HasProperty(cx, eventObj, "expired", &hasProperty) && hasProperty) {
+ JS::Rooted<JS::Value> temp(cx);
+ if (!JS_GetProperty(cx, eventObj, "expired", &temp) ||
+ !temp.isBoolean()) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
+ return NS_ERROR_FAILURE;
+ }
+
+ expired = temp.toBoolean();
+ }
+
+ // record_on_release is optional.
+ if (JS_HasProperty(cx, eventObj, "record_on_release", &hasProperty) &&
+ hasProperty) {
+ JS::Rooted<JS::Value> temp(cx);
+ if (!JS_GetProperty(cx, eventObj, "record_on_release", &temp) ||
+ !temp.isBoolean()) {
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Other);
+ return NS_ERROR_FAILURE;
+ }
+
+ recordOnRelease = temp.toBoolean();
+ }
+
+ // Validate methods.
+ for (auto& method : methods) {
+ if (!IsValidIdentifierString(method, kMaxMethodNameByteLength, false,
+ true)) {
+ JS_ReportErrorASCII(
+ cx, "Method names should match the identifier pattern.");
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Method);
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+
+ // Validate objects.
+ for (auto& object : objects) {
+ if (!IsValidIdentifierString(object, kMaxObjectNameByteLength, false,
+ true)) {
+ JS_ReportErrorASCII(
+ cx, "Object names should match the identifier pattern.");
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::Object);
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+
+ // Validate extra keys.
+ if (extra_keys.Length() > kMaxExtraKeyCount) {
+ JS_ReportErrorASCII(cx, "No more than 10 extra keys can be registered.");
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::ExtraKeys);
+ return NS_ERROR_INVALID_ARG;
+ }
+ for (auto& key : extra_keys) {
+ if (!IsValidIdentifierString(key, kMaxExtraKeyNameByteLength, false,
+ true)) {
+ JS_ReportErrorASCII(
+ cx, "Extra key names should match the identifier pattern.");
+ mozilla::Telemetry::AccumulateCategorical(
+ LABELS_TELEMETRY_EVENT_REGISTRATION_ERROR::ExtraKeys);
+ return NS_ERROR_INVALID_ARG;
+ }
+ }
+
+ // Append event infos to be registered.
+ for (auto& method : methods) {
+ for (auto& object : objects) {
+ // We defer the actual registration here in case any other event
+ // description is invalid. In that case we don't need to roll back any
+ // partial registration.
+ DynamicEventInfo info{aCategory, method, object,
+ extra_keys, recordOnRelease, aBuiltin};
+ newEventInfos.AppendElement(info);
+ newEventExpired.AppendElement(expired);
+ }
+ }
+ }
+
+ {
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+ RegisterEvents(locker, aCategory, newEventInfos, newEventExpired, aBuiltin);
+ }
+
+ return NS_OK;
+}
+
+nsresult TelemetryEvent::CreateSnapshots(uint32_t aDataset, bool aClear,
+ uint32_t aEventLimit, JSContext* cx,
+ uint8_t optional_argc,
+ JS::MutableHandle<JS::Value> aResult) {
+ if (!XRE_IsParentProcess()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Creating a JS snapshot of the events is a two-step process:
+ // (1) Lock the storage and copy the events into function-local storage.
+ // (2) Serialize the events into JS.
+ // We can't hold a lock for (2) because we will run into deadlocks otherwise
+ // from JS recording Telemetry.
+
+ // (1) Extract the events from storage with a lock held.
+ nsTArray<std::pair<const char*, EventRecordArray>> processEvents;
+ nsTArray<std::pair<uint32_t, EventRecordArray>> leftovers;
+ {
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+
+ if (!gInitDone) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // The snapshotting function is the same for both static and dynamic builtin
+ // events. We can use the same function and store the events in the same
+ // output storage.
+ auto snapshotter = [aDataset, &locker, &processEvents, &leftovers, aClear,
+ optional_argc,
+ aEventLimit](EventRecordsMapType& aProcessStorage) {
+ for (const auto& entry : aProcessStorage) {
+ const EventRecordArray* eventStorage = entry.GetWeak();
+ EventRecordArray events;
+ EventRecordArray leftoverEvents;
+
+ const uint32_t len = eventStorage->Length();
+ for (uint32_t i = 0; i < len; ++i) {
+ const EventRecord& record = (*eventStorage)[i];
+ if (IsInDataset(GetDataset(locker, record.GetEventKey()), aDataset)) {
+ // If we have a limit, adhere to it. If we have a limit and are
+ // going to clear, save the leftovers for later.
+ if (optional_argc < 2 || events.Length() < aEventLimit) {
+ events.AppendElement(record);
+ } else if (aClear) {
+ leftoverEvents.AppendElement(record);
+ }
+ }
+ }
+
+ if (events.Length()) {
+ const char* processName =
+ GetNameForProcessID(ProcessID(entry.GetKey()));
+ processEvents.EmplaceBack(processName, std::move(events));
+ if (leftoverEvents.Length()) {
+ leftovers.EmplaceBack(entry.GetKey(), std::move(leftoverEvents));
+ }
+ }
+ }
+ };
+
+ // Take a snapshot of the plain and dynamic builtin events.
+ snapshotter(gEventRecords);
+ if (aClear) {
+ gEventRecords.Clear();
+ for (auto& pair : leftovers) {
+ gEventRecords.InsertOrUpdate(
+ pair.first, MakeUnique<EventRecordArray>(std::move(pair.second)));
+ }
+ leftovers.Clear();
+ }
+ }
+
+ // (2) Serialize the events to a JS object.
+ JS::Rooted<JSObject*> rootObj(cx, JS_NewPlainObject(cx));
+ if (!rootObj) {
+ return NS_ERROR_FAILURE;
+ }
+
+ const uint32_t processLength = processEvents.Length();
+ for (uint32_t i = 0; i < processLength; ++i) {
+ JS::Rooted<JSObject*> eventsArray(cx);
+ if (NS_FAILED(SerializeEventsArray(processEvents[i].second, cx,
+ &eventsArray, aDataset))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!JS_DefineProperty(cx, rootObj, processEvents[i].first, eventsArray,
+ JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ aResult.setObject(*rootObj);
+ return NS_OK;
+}
+
+/**
+ * Resets all the stored events. This is intended to be only used in tests.
+ */
+void TelemetryEvent::ClearEvents() {
+ StaticMutexAutoLock lock(gTelemetryEventsMutex);
+
+ if (!gInitDone) {
+ return;
+ }
+
+ gEventRecords.Clear();
+}
+
+void TelemetryEvent::SetEventRecordingEnabled(const nsACString& category,
+ bool enabled) {
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+
+ if (!gCategoryNames.Contains(category)) {
+ LogToBrowserConsole(
+ nsIScriptError::warningFlag,
+ NS_ConvertUTF8toUTF16(
+ nsLiteralCString(
+ "Unknown category for SetEventRecordingEnabled: ") +
+ category));
+ return;
+ }
+
+ if (enabled) {
+ gEnabledCategories.Insert(category);
+ } else {
+ gEnabledCategories.Remove(category);
+ }
+}
+
+size_t TelemetryEvent::SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) {
+ StaticMutexAutoLock locker(gTelemetryEventsMutex);
+ size_t n = 0;
+
+ auto getSizeOfRecords = [aMallocSizeOf](auto& storageMap) {
+ size_t partial = storageMap.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (const auto& eventRecords : storageMap.Values()) {
+ partial += eventRecords->ShallowSizeOfIncludingThis(aMallocSizeOf);
+
+ const uint32_t len = eventRecords->Length();
+ for (uint32_t i = 0; i < len; ++i) {
+ partial += (*eventRecords)[i].SizeOfExcludingThis(aMallocSizeOf);
+ }
+ }
+ return partial;
+ };
+
+ n += getSizeOfRecords(gEventRecords);
+
+ n += gEventNameIDMap.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (auto iter = gEventNameIDMap.ConstIter(); !iter.Done(); iter.Next()) {
+ n += iter.Key().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+
+ n += gCategoryNames.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ n += gEnabledCategories.ShallowSizeOfExcludingThis(aMallocSizeOf);
+
+ if (gDynamicEventInfo) {
+ n += gDynamicEventInfo->ShallowSizeOfIncludingThis(aMallocSizeOf);
+ for (auto& info : *gDynamicEventInfo) {
+ n += info.SizeOfExcludingThis(aMallocSizeOf);
+ }
+ }
+
+ return n;
+}
diff --git a/toolkit/components/telemetry/core/TelemetryEvent.h b/toolkit/components/telemetry/core/TelemetryEvent.h
new file mode 100644
index 0000000000..8734df1174
--- /dev/null
+++ b/toolkit/components/telemetry/core/TelemetryEvent.h
@@ -0,0 +1,71 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TelemetryEvent_h__
+#define TelemetryEvent_h__
+
+#include <stdint.h>
+#include "js/TypeDecls.h"
+#include "mozilla/Maybe.h"
+#include "mozilla/TelemetryEventEnums.h"
+#include "mozilla/TelemetryProcessEnums.h"
+#include "nsTArray.h"
+#include "nsStringFwd.h"
+
+namespace mozilla {
+namespace Telemetry {
+struct ChildEventData;
+struct EventExtraEntry;
+} // namespace Telemetry
+} // namespace mozilla
+
+using mozilla::Telemetry::EventExtraEntry;
+
+// This module is internal to Telemetry. It encapsulates Telemetry's
+// event recording and storage logic. It should only be used by
+// Telemetry.cpp. These functions should not be used anywhere else.
+// For the public interface to Telemetry functionality, see Telemetry.h.
+
+namespace TelemetryEvent {
+
+void InitializeGlobalState(bool canRecordBase, bool canRecordExtended);
+void DeInitializeGlobalState();
+
+void SetCanRecordBase(bool b);
+void SetCanRecordExtended(bool b);
+
+// C++ API Endpoint.
+void RecordEventNative(
+ mozilla::Telemetry::EventID aId, const mozilla::Maybe<nsCString>& aValue,
+ const mozilla::Maybe<CopyableTArray<EventExtraEntry>>& aExtra);
+
+// JS API Endpoints.
+nsresult RecordEvent(const nsACString& aCategory, const nsACString& aMethod,
+ const nsACString& aObject, JS::Handle<JS::Value> aValue,
+ JS::Handle<JS::Value> aExtra, JSContext* aCx,
+ uint8_t optional_argc);
+
+void SetEventRecordingEnabled(const nsACString& aCategory, bool aEnabled);
+nsresult RegisterEvents(const nsACString& aCategory,
+ JS::Handle<JS::Value> aEventData, bool aBuiltin,
+ JSContext* cx);
+
+nsresult CreateSnapshots(uint32_t aDataset, bool aClear, uint32_t aEventLimit,
+ JSContext* aCx, uint8_t optional_argc,
+ JS::MutableHandle<JS::Value> aResult);
+
+// Record events from child processes.
+nsresult RecordChildEvents(
+ mozilla::Telemetry::ProcessID aProcessType,
+ const nsTArray<mozilla::Telemetry::ChildEventData>& aEvents);
+
+// Only to be used for testing.
+void ClearEvents();
+
+size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
+
+} // namespace TelemetryEvent
+
+#endif // TelemetryEvent_h__
diff --git a/toolkit/components/telemetry/core/TelemetryHistogram.cpp b/toolkit/components/telemetry/core/TelemetryHistogram.cpp
new file mode 100644
index 0000000000..88ac88eb9e
--- /dev/null
+++ b/toolkit/components/telemetry/core/TelemetryHistogram.cpp
@@ -0,0 +1,3677 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TelemetryHistogram.h"
+
+#include <limits>
+#include "base/histogram.h"
+#include "geckoview/streaming/GeckoViewStreamingTelemetry.h"
+#include "ipc/TelemetryIPCAccumulator.h"
+#include "jsapi.h"
+#include "jsfriendapi.h"
+#include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject, JS::NewArrayObject
+#include "js/GCAPI.h"
+#include "js/Object.h" // JS::GetClass, JS::GetMaybePtrFromReservedSlot, JS::SetReservedSlot
+#include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineFunction, JS_DefineProperty, JS_DefineUCProperty, JS_Enumerate, JS_GetElement, JS_GetProperty, JS_GetPropertyById
+#include "mozilla/dom/ToJSValue.h"
+#include "mozilla/gfx/GPUProcessManager.h"
+#include "mozilla/Atomics.h"
+#include "mozilla/JSONWriter.h"
+#include "mozilla/StartupTimeline.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/Unused.h"
+#include "nsClassHashtable.h"
+#include "nsString.h"
+#include "nsHashKeys.h"
+#include "nsITelemetry.h"
+#include "nsPrintfCString.h"
+#include "TelemetryHistogramNameMap.h"
+#include "TelemetryScalar.h"
+
+using base::BooleanHistogram;
+using base::CountHistogram;
+using base::FlagHistogram;
+using base::LinearHistogram;
+using mozilla::MakeUnique;
+using mozilla::StaticMutex;
+using mozilla::StaticMutexAutoLock;
+using mozilla::UniquePtr;
+using mozilla::Telemetry::HistogramAccumulation;
+using mozilla::Telemetry::HistogramCount;
+using mozilla::Telemetry::HistogramID;
+using mozilla::Telemetry::HistogramIDByNameLookup;
+using mozilla::Telemetry::KeyedHistogramAccumulation;
+using mozilla::Telemetry::ProcessID;
+using mozilla::Telemetry::Common::CanRecordDataset;
+using mozilla::Telemetry::Common::CanRecordProduct;
+using mozilla::Telemetry::Common::GetCurrentProduct;
+using mozilla::Telemetry::Common::GetIDForProcessName;
+using mozilla::Telemetry::Common::GetNameForProcessID;
+using mozilla::Telemetry::Common::IsExpiredVersion;
+using mozilla::Telemetry::Common::IsInDataset;
+using mozilla::Telemetry::Common::LogToBrowserConsole;
+using mozilla::Telemetry::Common::RecordedProcessType;
+using mozilla::Telemetry::Common::StringHashSet;
+using mozilla::Telemetry::Common::SupportedProduct;
+using mozilla::Telemetry::Common::ToJSString;
+
+namespace TelemetryIPCAccumulator = mozilla::TelemetryIPCAccumulator;
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// Naming: there are two kinds of functions in this file:
+//
+// * Functions named internal_*: these can only be reached via an
+// interface function (TelemetryHistogram::*). They mostly expect
+// the interface function to have acquired
+// |gTelemetryHistogramMutex|, so they do not have to be
+// thread-safe. However, those internal_* functions that are
+// reachable from internal_WrapAndReturnHistogram and
+// internal_WrapAndReturnKeyedHistogram can sometimes be called
+// without |gTelemetryHistogramMutex|, and so might be racey.
+//
+// * Functions named TelemetryHistogram::*. This is the external interface.
+// Entries and exits to these functions are serialised using
+// |gTelemetryHistogramMutex|, except for GetKeyedHistogramSnapshots and
+// CreateHistogramSnapshots.
+//
+// Avoiding races and deadlocks:
+//
+// All functions in the external interface (TelemetryHistogram::*) are
+// serialised using the mutex |gTelemetryHistogramMutex|. This means
+// that the external interface is thread-safe, and many of the
+// internal_* functions can ignore thread safety. But it also brings
+// a danger of deadlock if any function in the external interface can
+// get back to that interface. That is, we will deadlock on any call
+// chain like this
+//
+// TelemetryHistogram::* -> .. any functions .. -> TelemetryHistogram::*
+//
+// To reduce the danger of that happening, observe the following rules:
+//
+// * No function in TelemetryHistogram::* may directly call, nor take the
+// address of, any other function in TelemetryHistogram::*.
+//
+// * No internal function internal_* may call, nor take the address
+// of, any function in TelemetryHistogram::*.
+//
+// internal_WrapAndReturnHistogram and
+// internal_WrapAndReturnKeyedHistogram are not protected by
+// |gTelemetryHistogramMutex| because they make calls to the JS
+// engine, but that can in turn call back to Telemetry and hence back
+// to a TelemetryHistogram:: function, in order to report GC and other
+// statistics. This would lead to deadlock due to attempted double
+// acquisition of |gTelemetryHistogramMutex|, if the internal_* functions
+// were required to be protected by |gTelemetryHistogramMutex|. To
+// break that cycle, we relax that requirement. Unfortunately this
+// means that this file is not guaranteed race-free.
+
+// This is a StaticMutex rather than a plain Mutex (1) so that
+// it gets initialised in a thread-safe manner the first time
+// it is used, and (2) because it is never de-initialised, and
+// a normal Mutex would show up as a leak in BloatView. StaticMutex
+// also has the "OffTheBooks" property, so it won't show as a leak
+// in BloatView.
+static StaticMutex gTelemetryHistogramMutex MOZ_UNANNOTATED;
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE TYPES
+
+namespace {
+
+// Hardcoded probes
+//
+// The order of elements here is important to minimize the memory footprint of a
+// HistogramInfo instance.
+//
+// Any adjustements need to be reflected in gen_histogram_data.py
+struct HistogramInfo {
+ uint32_t min;
+ uint32_t max;
+ uint32_t bucketCount;
+ uint32_t name_offset;
+ uint32_t expiration_offset;
+ uint32_t label_count;
+ uint32_t key_count;
+ uint32_t store_count;
+ uint16_t label_index;
+ uint16_t key_index;
+ uint16_t store_index;
+ RecordedProcessType record_in_processes;
+ bool keyed;
+ uint8_t histogramType;
+ uint8_t dataset;
+ SupportedProduct products;
+
+ const char* name() const;
+ const char* expiration() const;
+ nsresult label_id(const char* label, uint32_t* labelId) const;
+ bool allows_key(const nsACString& key) const;
+ bool is_single_store() const;
+};
+
+// Structs used to keep information about the histograms for which a
+// snapshot should be created.
+struct HistogramSnapshotData {
+ CopyableTArray<base::Histogram::Sample> mBucketRanges;
+ CopyableTArray<base::Histogram::Count> mBucketCounts;
+ int64_t mSampleSum; // Same type as base::Histogram::SampleSet::sum_
+};
+
+struct HistogramSnapshotInfo {
+ HistogramSnapshotData data;
+ HistogramID histogramID;
+};
+
+typedef mozilla::Vector<HistogramSnapshotInfo> HistogramSnapshotsArray;
+typedef mozilla::Vector<HistogramSnapshotsArray> HistogramProcessSnapshotsArray;
+
+// The following is used to handle snapshot information for keyed histograms.
+typedef nsTHashMap<nsCStringHashKey, HistogramSnapshotData>
+ KeyedHistogramSnapshotData;
+
+struct KeyedHistogramSnapshotInfo {
+ KeyedHistogramSnapshotData data;
+ HistogramID histogramId;
+};
+
+typedef mozilla::Vector<KeyedHistogramSnapshotInfo>
+ KeyedHistogramSnapshotsArray;
+typedef mozilla::Vector<KeyedHistogramSnapshotsArray>
+ KeyedHistogramProcessSnapshotsArray;
+
+/**
+ * A Histogram storage engine.
+ *
+ * Takes care of recording data into multiple stores if necessary.
+ */
+class Histogram {
+ public:
+ /*
+ * Create a new histogram instance from the given info.
+ *
+ * If the histogram is already expired, this does not allocate.
+ */
+ Histogram(HistogramID histogramId, const HistogramInfo& info, bool expired);
+ ~Histogram();
+
+ /**
+ * Add a sample to this histogram in all registered stores.
+ */
+ void Add(uint32_t sample);
+
+ /**
+ * Clear the named store for this histogram.
+ */
+ void Clear(const nsACString& store);
+
+ /**
+ * Get the histogram instance from the named store.
+ */
+ bool GetHistogram(const nsACString& store, base::Histogram** h);
+
+ bool IsExpired() const { return mIsExpired; }
+
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
+
+ private:
+ // String -> Histogram*
+ typedef nsClassHashtable<nsCStringHashKey, base::Histogram> HistogramStore;
+ HistogramStore mStorage;
+
+ // A valid pointer if this histogram belongs to only the main store
+ base::Histogram* mSingleStore;
+
+ // We don't track stores for expired histograms.
+ // We just store a single flag and all other operations become a no-op.
+ bool mIsExpired;
+};
+
+class KeyedHistogram {
+ public:
+ KeyedHistogram(HistogramID id, const HistogramInfo& info, bool expired);
+ ~KeyedHistogram();
+ nsresult GetHistogram(const nsCString& aStore, const nsCString& key,
+ base::Histogram** histogram);
+ base::Histogram* GetHistogram(const nsCString& aStore, const nsCString& key);
+ uint32_t GetHistogramType() const { return mHistogramInfo.histogramType; }
+ nsresult GetKeys(const StaticMutexAutoLock& aLock, const nsCString& store,
+ nsTArray<nsCString>& aKeys);
+ // Note: unlike other methods, GetJSSnapshot is thread safe.
+ nsresult GetJSSnapshot(JSContext* cx, JS::Handle<JSObject*> obj,
+ const nsACString& aStore, bool clearSubsession);
+ nsresult GetSnapshot(const StaticMutexAutoLock& aLock,
+ const nsACString& aStore,
+ KeyedHistogramSnapshotData& aSnapshot,
+ bool aClearSubsession);
+
+ nsresult Add(const nsCString& key, uint32_t aSample, ProcessID aProcessType);
+ void Clear(const nsACString& aStore);
+
+ HistogramID GetHistogramID() const { return mId; }
+
+ bool IsEmpty(const nsACString& aStore) const;
+
+ bool IsExpired() const { return mIsExpired; }
+
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
+
+ private:
+ typedef nsClassHashtable<nsCStringHashKey, base::Histogram>
+ KeyedHistogramMapType;
+ typedef nsClassHashtable<nsCStringHashKey, KeyedHistogramMapType>
+ StoreMapType;
+
+ StoreMapType mStorage;
+ // A valid map if this histogram belongs to only the main store
+ KeyedHistogramMapType* mSingleStore;
+
+ const HistogramID mId;
+ const HistogramInfo& mHistogramInfo;
+ bool mIsExpired;
+};
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE STATE, SHARED BY ALL THREADS
+
+namespace {
+
+// Set to true once this global state has been initialized
+bool gInitDone = false;
+
+// Whether we are collecting the base, opt-out, Histogram data.
+bool gCanRecordBase = false;
+// Whether we are collecting the extended, opt-in, Histogram data.
+bool gCanRecordExtended = false;
+
+// The storage for actual Histogram instances.
+// We use separate ones for plain and keyed histograms.
+Histogram** gHistogramStorage;
+// Keyed histograms internally map string keys to individual Histogram
+// instances.
+KeyedHistogram** gKeyedHistogramStorage;
+
+// To simplify logic below we use a single histogram instance for all expired
+// histograms.
+Histogram* gExpiredHistogram = nullptr;
+
+// The single placeholder for expired keyed histograms.
+KeyedHistogram* gExpiredKeyedHistogram = nullptr;
+
+// This tracks whether recording is enabled for specific histograms.
+// To utilize C++ initialization rules, we invert the meaning to "disabled".
+bool gHistogramRecordingDisabled[HistogramCount] = {};
+
+// This is for gHistogramInfos, gHistogramStringTable
+#include "TelemetryHistogramData.inc"
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE CONSTANTS
+
+namespace {
+
+// List of histogram IDs which should have recording disabled initially.
+const HistogramID kRecordingInitiallyDisabledIDs[] = {
+ mozilla::Telemetry::FX_REFRESH_DRIVER_SYNC_SCROLL_FRAME_DELAY_MS,
+
+ // The array must not be empty. Leave these item here.
+ mozilla::Telemetry::TELEMETRY_TEST_COUNT_INIT_NO_RECORD,
+ mozilla::Telemetry::TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD};
+
+const char* TEST_HISTOGRAM_PREFIX = "TELEMETRY_TEST_";
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// The core storage access functions.
+// They wrap access to the histogram storage and lookup caches.
+
+namespace {
+
+size_t internal_KeyedHistogramStorageIndex(HistogramID aHistogramId,
+ ProcessID aProcessId) {
+ return aHistogramId * size_t(ProcessID::Count) + size_t(aProcessId);
+}
+
+size_t internal_HistogramStorageIndex(const StaticMutexAutoLock& aLock,
+ HistogramID aHistogramId,
+ ProcessID aProcessId) {
+ static_assert(HistogramCount < std::numeric_limits<size_t>::max() /
+ size_t(ProcessID::Count),
+ "Too many histograms and processes to store in a 1D array.");
+
+ return aHistogramId * size_t(ProcessID::Count) + size_t(aProcessId);
+}
+
+Histogram* internal_GetHistogramFromStorage(const StaticMutexAutoLock& aLock,
+ HistogramID aHistogramId,
+ ProcessID aProcessId) {
+ size_t index =
+ internal_HistogramStorageIndex(aLock, aHistogramId, aProcessId);
+ return gHistogramStorage[index];
+}
+
+void internal_SetHistogramInStorage(const StaticMutexAutoLock& aLock,
+ HistogramID aHistogramId,
+ ProcessID aProcessId,
+ Histogram* aHistogram) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Histograms are stored only in the parent process.");
+
+ size_t index =
+ internal_HistogramStorageIndex(aLock, aHistogramId, aProcessId);
+ MOZ_ASSERT(!gHistogramStorage[index],
+ "Mustn't overwrite storage without clearing it first.");
+ gHistogramStorage[index] = aHistogram;
+}
+
+KeyedHistogram* internal_GetKeyedHistogramFromStorage(HistogramID aHistogramId,
+ ProcessID aProcessId) {
+ size_t index = internal_KeyedHistogramStorageIndex(aHistogramId, aProcessId);
+ return gKeyedHistogramStorage[index];
+}
+
+void internal_SetKeyedHistogramInStorage(HistogramID aHistogramId,
+ ProcessID aProcessId,
+ KeyedHistogram* aKeyedHistogram) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Keyed Histograms are stored only in the parent process.");
+
+ size_t index = internal_KeyedHistogramStorageIndex(aHistogramId, aProcessId);
+ MOZ_ASSERT(!gKeyedHistogramStorage[index],
+ "Mustn't overwrite storage without clearing it first");
+ gKeyedHistogramStorage[index] = aKeyedHistogram;
+}
+
+// Factory function for base::Histogram instances.
+base::Histogram* internal_CreateBaseHistogramInstance(const HistogramInfo& info,
+ int bucketsOffset);
+
+// Factory function for histogram instances.
+Histogram* internal_CreateHistogramInstance(HistogramID histogramId);
+
+bool internal_IsHistogramEnumId(HistogramID aID) {
+ static_assert(((HistogramID)-1 > 0), "ID should be unsigned.");
+ return aID < HistogramCount;
+}
+
+// Look up a plain histogram by id.
+Histogram* internal_GetHistogramById(const StaticMutexAutoLock& aLock,
+ HistogramID histogramId,
+ ProcessID processId,
+ bool instantiate = true) {
+ MOZ_ASSERT(internal_IsHistogramEnumId(histogramId));
+ MOZ_ASSERT(!gHistogramInfos[histogramId].keyed);
+ MOZ_ASSERT(processId < ProcessID::Count);
+
+ Histogram* h =
+ internal_GetHistogramFromStorage(aLock, histogramId, processId);
+ if (h || !instantiate) {
+ return h;
+ }
+
+ h = internal_CreateHistogramInstance(histogramId);
+ MOZ_ASSERT(h);
+ internal_SetHistogramInStorage(aLock, histogramId, processId, h);
+
+ return h;
+}
+
+// Look up a keyed histogram by id.
+KeyedHistogram* internal_GetKeyedHistogramById(HistogramID histogramId,
+ ProcessID processId,
+ bool instantiate = true) {
+ MOZ_ASSERT(internal_IsHistogramEnumId(histogramId));
+ MOZ_ASSERT(gHistogramInfos[histogramId].keyed);
+ MOZ_ASSERT(processId < ProcessID::Count);
+
+ KeyedHistogram* kh =
+ internal_GetKeyedHistogramFromStorage(histogramId, processId);
+ if (kh || !instantiate) {
+ return kh;
+ }
+
+ const HistogramInfo& info = gHistogramInfos[histogramId];
+ const bool isExpired = IsExpiredVersion(info.expiration());
+
+ // If the keyed histogram is expired, set its storage to the expired
+ // keyed histogram.
+ if (isExpired) {
+ if (!gExpiredKeyedHistogram) {
+ // If we don't have an expired keyed histogram, create one.
+ gExpiredKeyedHistogram =
+ new KeyedHistogram(histogramId, info, true /* expired */);
+ MOZ_ASSERT(gExpiredKeyedHistogram);
+ }
+ kh = gExpiredKeyedHistogram;
+ } else {
+ kh = new KeyedHistogram(histogramId, info, false /* expired */);
+ }
+
+ internal_SetKeyedHistogramInStorage(histogramId, processId, kh);
+
+ return kh;
+}
+
+// Look up a histogram id from a histogram name.
+nsresult internal_GetHistogramIdByName(const StaticMutexAutoLock& aLock,
+ const nsACString& name,
+ HistogramID* id) {
+ const uint32_t idx = HistogramIDByNameLookup(name);
+ MOZ_ASSERT(idx < HistogramCount,
+ "Intermediate lookup should always give a valid index.");
+
+ // The lookup hashes the input and uses it as an index into the value array.
+ // Hash collisions can still happen for unknown values,
+ // therefore we check that the name matches.
+ if (name.Equals(gHistogramInfos[idx].name())) {
+ *id = HistogramID(idx);
+ return NS_OK;
+ }
+
+ return NS_ERROR_ILLEGAL_VALUE;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: Misc small helpers
+
+namespace {
+
+bool internal_CanRecordBase() { return gCanRecordBase; }
+
+bool internal_CanRecordExtended() { return gCanRecordExtended; }
+
+bool internal_AttemptedGPUProcess() {
+ // Check if it was tried to launch a process.
+ bool attemptedGPUProcess = false;
+ if (auto gpm = mozilla::gfx::GPUProcessManager::Get()) {
+ attemptedGPUProcess = gpm->AttemptedGPUProcess();
+ }
+ return attemptedGPUProcess;
+}
+
+// Note: this is completely unrelated to mozilla::IsEmpty.
+bool internal_IsEmpty(const StaticMutexAutoLock& aLock,
+ const base::Histogram* h) {
+ return h->is_empty();
+}
+
+void internal_SetHistogramRecordingEnabled(const StaticMutexAutoLock& aLock,
+ HistogramID id, bool aEnabled) {
+ MOZ_ASSERT(internal_IsHistogramEnumId(id));
+ gHistogramRecordingDisabled[id] = !aEnabled;
+}
+
+bool internal_IsRecordingEnabled(HistogramID id) {
+ MOZ_ASSERT(internal_IsHistogramEnumId(id));
+ return !gHistogramRecordingDisabled[id];
+}
+
+const char* HistogramInfo::name() const {
+ return &gHistogramStringTable[this->name_offset];
+}
+
+const char* HistogramInfo::expiration() const {
+ return &gHistogramStringTable[this->expiration_offset];
+}
+
+nsresult HistogramInfo::label_id(const char* label, uint32_t* labelId) const {
+ MOZ_ASSERT(label);
+ MOZ_ASSERT(this->histogramType == nsITelemetry::HISTOGRAM_CATEGORICAL);
+ if (this->histogramType != nsITelemetry::HISTOGRAM_CATEGORICAL) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (uint32_t i = 0; i < this->label_count; ++i) {
+ // gHistogramLabelTable contains the indices of the label strings in the
+ // gHistogramStringTable.
+ // They are stored in-order and consecutively, from the offset label_index
+ // to (label_index + label_count).
+ uint32_t string_offset = gHistogramLabelTable[this->label_index + i];
+ const char* const str = &gHistogramStringTable[string_offset];
+ if (::strcmp(label, str) == 0) {
+ *labelId = i;
+ return NS_OK;
+ }
+ }
+
+ return NS_ERROR_FAILURE;
+}
+
+bool HistogramInfo::allows_key(const nsACString& key) const {
+ MOZ_ASSERT(this->keyed);
+
+ // If we didn't specify a list of allowed keys, just return true.
+ if (this->key_count == 0) {
+ return true;
+ }
+
+ // Otherwise, check if |key| is in the list of allowed keys.
+ for (uint32_t i = 0; i < this->key_count; ++i) {
+ // gHistogramKeyTable contains the indices of the key strings in the
+ // gHistogramStringTable. They are stored in-order and consecutively,
+ // from the offset key_index to (key_index + key_count).
+ uint32_t string_offset = gHistogramKeyTable[this->key_index + i];
+ const char* const str = &gHistogramStringTable[string_offset];
+ if (key.EqualsASCII(str)) {
+ return true;
+ }
+ }
+
+ // |key| was not found.
+ return false;
+}
+
+bool HistogramInfo::is_single_store() const {
+ return store_count == 1 && store_index == UINT16_MAX;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: Histogram Get, Add, Clone, Clear functions
+
+namespace {
+
+nsresult internal_CheckHistogramArguments(const HistogramInfo& info) {
+ if (info.histogramType != nsITelemetry::HISTOGRAM_BOOLEAN &&
+ info.histogramType != nsITelemetry::HISTOGRAM_FLAG &&
+ info.histogramType != nsITelemetry::HISTOGRAM_COUNT) {
+ // Sanity checks for histogram parameters.
+ if (info.min >= info.max) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ if (info.bucketCount <= 2) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ if (info.min < 1) {
+ return NS_ERROR_ILLEGAL_VALUE;
+ }
+ }
+
+ return NS_OK;
+}
+
+Histogram* internal_CreateHistogramInstance(HistogramID histogramId) {
+ const HistogramInfo& info = gHistogramInfos[histogramId];
+
+ if (NS_FAILED(internal_CheckHistogramArguments(info))) {
+ MOZ_ASSERT(false, "Failed histogram argument checks.");
+ return nullptr;
+ }
+
+ const bool isExpired = IsExpiredVersion(info.expiration());
+
+ if (isExpired) {
+ if (!gExpiredHistogram) {
+ gExpiredHistogram = new Histogram(histogramId, info, /* expired */ true);
+ }
+
+ return gExpiredHistogram;
+ }
+
+ Histogram* wrapper = new Histogram(histogramId, info, /* expired */ false);
+
+ return wrapper;
+}
+
+base::Histogram* internal_CreateBaseHistogramInstance(
+ const HistogramInfo& passedInfo, int bucketsOffset) {
+ if (NS_FAILED(internal_CheckHistogramArguments(passedInfo))) {
+ MOZ_ASSERT(false, "Failed histogram argument checks.");
+ return nullptr;
+ }
+
+ // We don't actually store data for expired histograms at all.
+ MOZ_ASSERT(!IsExpiredVersion(passedInfo.expiration()));
+
+ HistogramInfo info = passedInfo;
+ const int* buckets = &gHistogramBucketLowerBounds[bucketsOffset];
+
+ base::Histogram::Flags flags = base::Histogram::kNoFlags;
+ base::Histogram* h = nullptr;
+ switch (info.histogramType) {
+ case nsITelemetry::HISTOGRAM_EXPONENTIAL:
+ h = base::Histogram::FactoryGet(info.min, info.max, info.bucketCount,
+ flags, buckets);
+ break;
+ case nsITelemetry::HISTOGRAM_LINEAR:
+ case nsITelemetry::HISTOGRAM_CATEGORICAL:
+ h = LinearHistogram::FactoryGet(info.min, info.max, info.bucketCount,
+ flags, buckets);
+ break;
+ case nsITelemetry::HISTOGRAM_BOOLEAN:
+ h = BooleanHistogram::FactoryGet(flags, buckets);
+ break;
+ case nsITelemetry::HISTOGRAM_FLAG:
+ h = FlagHistogram::FactoryGet(flags, buckets);
+ break;
+ case nsITelemetry::HISTOGRAM_COUNT:
+ h = CountHistogram::FactoryGet(flags, buckets);
+ break;
+ default:
+ MOZ_ASSERT(false, "Invalid histogram type");
+ return nullptr;
+ }
+
+ return h;
+}
+
+nsresult internal_HistogramAdd(const StaticMutexAutoLock& aLock,
+ Histogram& histogram, const HistogramID id,
+ uint32_t value, ProcessID aProcessType) {
+ // Check if we are allowed to record the data.
+ bool canRecordDataset =
+ CanRecordDataset(gHistogramInfos[id].dataset, internal_CanRecordBase(),
+ internal_CanRecordExtended());
+ // If `histogram` is a non-parent-process histogram, then recording-enabled
+ // has been checked in its owner process.
+ if (!canRecordDataset ||
+ (aProcessType == ProcessID::Parent && !internal_IsRecordingEnabled(id))) {
+ return NS_OK;
+ }
+
+ // Don't record if the current platform is not enabled
+ if (!CanRecordProduct(gHistogramInfos[id].products)) {
+ return NS_OK;
+ }
+
+ if (&histogram != gExpiredHistogram &&
+ GetCurrentProduct() == SupportedProduct::GeckoviewStreaming) {
+ const HistogramInfo& info = gHistogramInfos[id];
+ GeckoViewStreamingTelemetry::HistogramAccumulate(
+ nsDependentCString(info.name()),
+ info.histogramType == nsITelemetry::HISTOGRAM_CATEGORICAL, value);
+ return NS_OK;
+ }
+
+ // The internal representation of a base::Histogram's buckets uses `int`.
+ // Clamp large values of `value` to be INT_MAX so they continue to be treated
+ // as large values (instead of negative ones).
+ if (value > INT_MAX) {
+ TelemetryScalar::Add(
+ mozilla::Telemetry::ScalarID::TELEMETRY_ACCUMULATE_CLAMPED_VALUES,
+ NS_ConvertASCIItoUTF16(gHistogramInfos[id].name()), 1);
+ value = INT_MAX;
+ }
+
+ histogram.Add(value);
+
+ return NS_OK;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: Histogram reflection helpers
+
+namespace {
+
+/**
+ * Copy histograms and samples to Mozilla-friendly structures.
+ * Please note that this version does not make use of JS contexts.
+ *
+ * @param {StaticMutexAutoLock} the proof we hold the mutex.
+ * @param {Histogram} the histogram to reflect.
+ * @return {nsresult} NS_ERROR_FAILURE if we fail to allocate memory for the
+ * snapshot.
+ */
+nsresult internal_GetHistogramAndSamples(const StaticMutexAutoLock& aLock,
+ const base::Histogram* h,
+ HistogramSnapshotData& aSnapshot) {
+ MOZ_ASSERT(h);
+
+ // Convert the ranges of the buckets to a nsTArray.
+ const size_t bucketCount = h->bucket_count();
+ for (size_t i = 0; i < bucketCount; i++) {
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier, or change the return type to void.
+ aSnapshot.mBucketRanges.AppendElement(h->ranges(i));
+ }
+
+ // Get a snapshot of the samples.
+ base::Histogram::SampleSet ss = h->SnapshotSample();
+
+ // Get the number of samples in each bucket.
+ for (size_t i = 0; i < bucketCount; i++) {
+ // XXX(Bug 1631371) Check if this should use a fallible operation as it
+ // pretended earlier, or change the return type to void.
+ aSnapshot.mBucketCounts.AppendElement(ss.counts(i));
+ }
+
+ // Finally, save the |sum|. We don't need to reflect declared_min,
+ // declared_max and histogram_type as they are in gHistogramInfo.
+ aSnapshot.mSampleSum = ss.sum();
+ return NS_OK;
+}
+
+/**
+ * Reflect a histogram snapshot into a JavaScript object.
+ * The returned histogram object will have the following properties:
+ *
+ * bucket_count - Number of buckets of this histogram
+ * histogram_type - HISTOGRAM_EXPONENTIAL, HISTOGRAM_LINEAR,
+ * HISTOGRAM_BOOLEAN, HISTOGRAM_FLAG, HISTOGRAM_COUNT, or HISTOGRAM_CATEGORICAL
+ * sum - sum of the bucket contents
+ * range - A 2-item array of minimum and maximum bucket size
+ * values - Map from bucket start to the bucket's count
+ */
+nsresult internal_ReflectHistogramAndSamples(
+ JSContext* cx, JS::Handle<JSObject*> obj,
+ const HistogramInfo& aHistogramInfo,
+ const HistogramSnapshotData& aSnapshot) {
+ if (!(JS_DefineProperty(cx, obj, "bucket_count", aHistogramInfo.bucketCount,
+ JSPROP_ENUMERATE) &&
+ JS_DefineProperty(cx, obj, "histogram_type",
+ aHistogramInfo.histogramType, JSPROP_ENUMERATE) &&
+ JS_DefineProperty(cx, obj, "sum", double(aSnapshot.mSampleSum),
+ JSPROP_ENUMERATE))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Don't rely on the bucket counts from "aHistogramInfo": it may
+ // differ from the length of aSnapshot.mBucketCounts due to expired
+ // histograms.
+ const size_t count = aSnapshot.mBucketCounts.Length();
+ MOZ_ASSERT(count == aSnapshot.mBucketRanges.Length(),
+ "The number of buckets and the number of counts must match.");
+
+ // Create the "range" property and add it to the final object.
+ JS::Rooted<JSObject*> rarray(cx, JS::NewArrayObject(cx, 2));
+ if (rarray == nullptr ||
+ !JS_DefineProperty(cx, obj, "range", rarray, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ // Add [min, max] into the range array
+ if (!JS_DefineElement(cx, rarray, 0, aHistogramInfo.min, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!JS_DefineElement(cx, rarray, 1, aHistogramInfo.max, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ JS::Rooted<JSObject*> values(cx, JS_NewPlainObject(cx));
+ if (values == nullptr ||
+ !JS_DefineProperty(cx, obj, "values", values, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ bool first = true;
+ size_t last = 0;
+
+ for (size_t i = 0; i < count; i++) {
+ auto value = aSnapshot.mBucketCounts[i];
+ if (value == 0) {
+ continue;
+ }
+
+ if (i > 0 && first) {
+ auto range = aSnapshot.mBucketRanges[i - 1];
+ if (!JS_DefineProperty(cx, values, nsPrintfCString("%d", range).get(), 0,
+ JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ first = false;
+ last = i + 1;
+
+ auto range = aSnapshot.mBucketRanges[i];
+ if (!JS_DefineProperty(cx, values, nsPrintfCString("%d", range).get(),
+ value, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ if (last > 0 && last < count) {
+ auto range = aSnapshot.mBucketRanges[last];
+ if (!JS_DefineProperty(cx, values, nsPrintfCString("%d", range).get(), 0,
+ JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ return NS_OK;
+}
+
+bool internal_ShouldReflectHistogram(const StaticMutexAutoLock& aLock,
+ base::Histogram* h, HistogramID id) {
+ // Only flag histograms are serialized when they are empty.
+ // This has historical reasons, changing this will require downstream changes.
+ // The cheaper path here is to just deprecate flag histograms in favor
+ // of scalars.
+ uint32_t type = gHistogramInfos[id].histogramType;
+ if (internal_IsEmpty(aLock, h) && type != nsITelemetry::HISTOGRAM_FLAG) {
+ return false;
+ }
+
+ // Don't reflect the histogram if it's not allowed in this product.
+ if (!CanRecordProduct(gHistogramInfos[id].products)) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Helper function to get a snapshot of the histograms.
+ *
+ * @param {aLock} the lock proof.
+ * @param {aStore} the name of the store to snapshot.
+ * @param {aDataset} the dataset for which the snapshot is being requested.
+ * @param {aClearSubsession} whether or not to clear the data after
+ * taking the snapshot.
+ * @param {aIncludeGPU} whether or not to include data for the GPU.
+ * @param {aOutSnapshot} the container in which the snapshot data will be
+ * stored.
+ * @return {nsresult} NS_OK if the snapshot was successfully taken or
+ * NS_ERROR_OUT_OF_MEMORY if it failed to allocate memory.
+ */
+nsresult internal_GetHistogramsSnapshot(
+ const StaticMutexAutoLock& aLock, const nsACString& aStore,
+ unsigned int aDataset, bool aClearSubsession, bool aIncludeGPU,
+ bool aFilterTest, HistogramProcessSnapshotsArray& aOutSnapshot) {
+ if (!aOutSnapshot.resize(static_cast<uint32_t>(ProcessID::Count))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ for (uint32_t process = 0; process < static_cast<uint32_t>(ProcessID::Count);
+ ++process) {
+ HistogramSnapshotsArray& hArray = aOutSnapshot[process];
+
+ for (size_t i = 0; i < HistogramCount; ++i) {
+ const HistogramInfo& info = gHistogramInfos[i];
+ if (info.keyed) {
+ continue;
+ }
+
+ HistogramID id = HistogramID(i);
+
+ if (!CanRecordInProcess(info.record_in_processes, ProcessID(process)) ||
+ ((ProcessID(process) == ProcessID::Gpu) && !aIncludeGPU)) {
+ continue;
+ }
+
+ if (!IsInDataset(info.dataset, aDataset)) {
+ continue;
+ }
+
+ bool shouldInstantiate =
+ info.histogramType == nsITelemetry::HISTOGRAM_FLAG;
+ Histogram* w = internal_GetHistogramById(aLock, id, ProcessID(process),
+ shouldInstantiate);
+ if (!w || w->IsExpired()) {
+ continue;
+ }
+
+ base::Histogram* h = nullptr;
+ if (!w->GetHistogram(aStore, &h)) {
+ continue;
+ }
+
+ if (!internal_ShouldReflectHistogram(aLock, h, id)) {
+ continue;
+ }
+
+ const char* name = info.name();
+ if (aFilterTest && strncmp(TEST_HISTOGRAM_PREFIX, name,
+ strlen(TEST_HISTOGRAM_PREFIX)) == 0) {
+ if (aClearSubsession) {
+ h->Clear();
+ }
+ continue;
+ }
+
+ HistogramSnapshotData snapshotData;
+ if (NS_FAILED(internal_GetHistogramAndSamples(aLock, h, snapshotData))) {
+ continue;
+ }
+
+ if (!hArray.emplaceBack(HistogramSnapshotInfo{snapshotData, id})) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ if (aClearSubsession) {
+ h->Clear();
+ }
+ }
+ }
+ return NS_OK;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: class Histogram
+
+namespace {
+
+Histogram::Histogram(HistogramID histogramId, const HistogramInfo& info,
+ bool expired)
+ : mSingleStore(nullptr), mIsExpired(expired) {
+ if (IsExpired()) {
+ return;
+ }
+
+ const int bucketsOffset = gHistogramBucketLowerBoundIndex[histogramId];
+
+ if (info.is_single_store()) {
+ mSingleStore = internal_CreateBaseHistogramInstance(info, bucketsOffset);
+ } else {
+ for (uint32_t i = 0; i < info.store_count; i++) {
+ auto store = nsDependentCString(
+ &gHistogramStringTable[gHistogramStoresTable[info.store_index + i]]);
+ mStorage.InsertOrUpdate(store, UniquePtr<base::Histogram>(
+ internal_CreateBaseHistogramInstance(
+ info, bucketsOffset)));
+ }
+ }
+}
+
+Histogram::~Histogram() { delete mSingleStore; }
+
+void Histogram::Add(uint32_t sample) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Only add to histograms in the parent process");
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+
+ if (IsExpired()) {
+ return;
+ }
+
+ if (mSingleStore != nullptr) {
+ mSingleStore->Add(sample);
+ } else {
+ for (auto iter = mStorage.Iter(); !iter.Done(); iter.Next()) {
+ auto& h = iter.Data();
+ h->Add(sample);
+ }
+ }
+}
+
+void Histogram::Clear(const nsACString& store) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Only clear histograms in the parent process");
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+
+ if (mSingleStore != nullptr) {
+ if (store.EqualsASCII("main")) {
+ mSingleStore->Clear();
+ }
+ } else {
+ base::Histogram* h = nullptr;
+ bool found = GetHistogram(store, &h);
+ if (!found) {
+ return;
+ }
+ MOZ_ASSERT(h, "Should have found a valid histogram in the named store");
+
+ h->Clear();
+ }
+}
+
+bool Histogram::GetHistogram(const nsACString& store, base::Histogram** h) {
+ MOZ_ASSERT(!IsExpired());
+ if (IsExpired()) {
+ return false;
+ }
+
+ if (mSingleStore != nullptr) {
+ if (store.EqualsASCII("main")) {
+ *h = mSingleStore;
+ return true;
+ }
+
+ return false;
+ }
+
+ return mStorage.Get(store, h);
+}
+
+size_t Histogram::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) {
+ size_t n = 0;
+ n += aMallocSizeOf(this);
+ /*
+ * In theory mStorage.SizeOfExcludingThis should included the data part of the
+ * map, but the numbers seemed low, so we are only taking the shallow size and
+ * do the iteration here.
+ */
+ n += mStorage.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (const auto& h : mStorage.Values()) {
+ n += h->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ if (mSingleStore != nullptr) {
+ // base::Histogram doesn't have SizeOfExcludingThis, so we are overcounting
+ // the pointer here.
+ n += mSingleStore->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ return n;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: class KeyedHistogram and internal_ReflectKeyedHistogram
+
+namespace {
+
+nsresult internal_ReflectKeyedHistogram(
+ const KeyedHistogramSnapshotData& aSnapshot, const HistogramInfo& info,
+ JSContext* aCx, JS::Handle<JSObject*> aObj) {
+ for (const auto& entry : aSnapshot) {
+ const HistogramSnapshotData& keyData = entry.GetData();
+
+ JS::Rooted<JSObject*> histogramSnapshot(aCx, JS_NewPlainObject(aCx));
+ if (!histogramSnapshot) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (NS_FAILED(internal_ReflectHistogramAndSamples(aCx, histogramSnapshot,
+ info, keyData))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ const NS_ConvertUTF8toUTF16 key(entry.GetKey());
+ if (!JS_DefineUCProperty(aCx, aObj, key.Data(), key.Length(),
+ histogramSnapshot, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ return NS_OK;
+}
+
+KeyedHistogram::KeyedHistogram(HistogramID id, const HistogramInfo& info,
+ bool expired)
+ : mSingleStore(nullptr),
+ mId(id),
+ mHistogramInfo(info),
+ mIsExpired(expired) {
+ if (IsExpired()) {
+ return;
+ }
+
+ if (info.is_single_store()) {
+ mSingleStore = new KeyedHistogramMapType;
+ } else {
+ for (uint32_t i = 0; i < info.store_count; i++) {
+ auto store = nsDependentCString(
+ &gHistogramStringTable[gHistogramStoresTable[info.store_index + i]]);
+ mStorage.InsertOrUpdate(store, MakeUnique<KeyedHistogramMapType>());
+ }
+ }
+}
+
+KeyedHistogram::~KeyedHistogram() { delete mSingleStore; }
+
+nsresult KeyedHistogram::GetHistogram(const nsCString& aStore,
+ const nsCString& key,
+ base::Histogram** histogram) {
+ if (IsExpired()) {
+ MOZ_ASSERT(false,
+ "KeyedHistogram::GetHistogram called on an expired histogram.");
+ return NS_ERROR_FAILURE;
+ }
+
+ KeyedHistogramMapType* histogramMap;
+ bool found;
+
+ if (mSingleStore != nullptr) {
+ histogramMap = mSingleStore;
+ } else {
+ found = mStorage.Get(aStore, &histogramMap);
+ if (!found) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ found = histogramMap->Get(key, histogram);
+ if (found) {
+ return NS_OK;
+ }
+
+ int bucketsOffset = gHistogramBucketLowerBoundIndex[mId];
+ auto h = UniquePtr<base::Histogram>{
+ internal_CreateBaseHistogramInstance(mHistogramInfo, bucketsOffset)};
+ if (!h) {
+ return NS_ERROR_FAILURE;
+ }
+
+ h->ClearFlags(base::Histogram::kUmaTargetedHistogramFlag);
+ *histogram = h.get();
+
+ bool inserted =
+ histogramMap->InsertOrUpdate(key, std::move(h), mozilla::fallible);
+ if (MOZ_UNLIKELY(!inserted)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ return NS_OK;
+}
+
+base::Histogram* KeyedHistogram::GetHistogram(const nsCString& aStore,
+ const nsCString& key) {
+ base::Histogram* h = nullptr;
+ if (NS_FAILED(GetHistogram(aStore, key, &h))) {
+ return nullptr;
+ }
+ return h;
+}
+
+nsresult KeyedHistogram::Add(const nsCString& key, uint32_t sample,
+ ProcessID aProcessType) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Only add to keyed histograms in the parent process");
+ if (!XRE_IsParentProcess()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ bool canRecordDataset =
+ CanRecordDataset(mHistogramInfo.dataset, internal_CanRecordBase(),
+ internal_CanRecordExtended());
+ // If `histogram` is a non-parent-process histogram, then recording-enabled
+ // has been checked in its owner process.
+ if (!canRecordDataset || (aProcessType == ProcessID::Parent &&
+ !internal_IsRecordingEnabled(mId))) {
+ return NS_OK;
+ }
+
+ // Don't record if expired.
+ if (IsExpired()) {
+ return NS_OK;
+ }
+
+ // Don't record if the current platform is not enabled
+ if (!CanRecordProduct(gHistogramInfos[mId].products)) {
+ return NS_OK;
+ }
+
+ // The internal representation of a base::Histogram's buckets uses `int`.
+ // Clamp large values of `sample` to be INT_MAX so they continue to be treated
+ // as large values (instead of negative ones).
+ if (sample > INT_MAX) {
+ TelemetryScalar::Add(
+ mozilla::Telemetry::ScalarID::TELEMETRY_ACCUMULATE_CLAMPED_VALUES,
+ NS_ConvertASCIItoUTF16(mHistogramInfo.name()), 1);
+ sample = INT_MAX;
+ }
+
+ base::Histogram* histogram;
+ if (mSingleStore != nullptr) {
+ histogram = GetHistogram("main"_ns, key);
+ if (!histogram) {
+ MOZ_ASSERT(false, "Missing histogram in single store.");
+ return NS_ERROR_FAILURE;
+ }
+
+ histogram->Add(sample);
+ } else {
+ for (uint32_t i = 0; i < mHistogramInfo.store_count; i++) {
+ auto store = nsDependentCString(
+ &gHistogramStringTable
+ [gHistogramStoresTable[mHistogramInfo.store_index + i]]);
+ base::Histogram* histogram = GetHistogram(store, key);
+ MOZ_ASSERT(histogram);
+ if (histogram) {
+ histogram->Add(sample);
+ } else {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+void KeyedHistogram::Clear(const nsACString& aStore) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Only clear keyed histograms in the parent process");
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+
+ if (IsExpired()) {
+ return;
+ }
+
+ if (mSingleStore) {
+ if (aStore.EqualsASCII("main")) {
+ mSingleStore->Clear();
+ }
+ return;
+ }
+
+ KeyedHistogramMapType* histogramMap;
+ bool found = mStorage.Get(aStore, &histogramMap);
+ if (!found) {
+ return;
+ }
+
+ histogramMap->Clear();
+}
+
+bool KeyedHistogram::IsEmpty(const nsACString& aStore) const {
+ if (mSingleStore != nullptr) {
+ if (aStore.EqualsASCII("main")) {
+ return mSingleStore->IsEmpty();
+ }
+
+ return true;
+ }
+
+ KeyedHistogramMapType* histogramMap;
+ bool found = mStorage.Get(aStore, &histogramMap);
+ if (!found) {
+ return true;
+ }
+ return histogramMap->IsEmpty();
+}
+
+size_t KeyedHistogram::SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) {
+ size_t n = 0;
+ n += aMallocSizeOf(this);
+ /*
+ * In theory mStorage.SizeOfExcludingThis should included the data part of the
+ * map, but the numbers seemed low, so we are only taking the shallow size and
+ * do the iteration here.
+ */
+ n += mStorage.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (const auto& h : mStorage.Values()) {
+ n += h->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ if (mSingleStore != nullptr) {
+ // base::Histogram doesn't have SizeOfExcludingThis, so we are overcounting
+ // the pointer here.
+ n += mSingleStore->SizeOfExcludingThis(aMallocSizeOf);
+ }
+ return n;
+}
+
+nsresult KeyedHistogram::GetKeys(const StaticMutexAutoLock& aLock,
+ const nsCString& store,
+ nsTArray<nsCString>& aKeys) {
+ KeyedHistogramMapType* histogramMap;
+ if (mSingleStore != nullptr) {
+ histogramMap = mSingleStore;
+ } else {
+ bool found = mStorage.Get(store, &histogramMap);
+ if (!found) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ if (!aKeys.SetCapacity(histogramMap->Count(), mozilla::fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ for (const auto& key : histogramMap->Keys()) {
+ if (!aKeys.AppendElement(key, mozilla::fallible)) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult KeyedHistogram::GetJSSnapshot(JSContext* cx, JS::Handle<JSObject*> obj,
+ const nsACString& aStore,
+ bool clearSubsession) {
+ // Get a snapshot of the data.
+ KeyedHistogramSnapshotData dataSnapshot;
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ MOZ_ASSERT(internal_IsHistogramEnumId(mId));
+
+ // Take a snapshot of the data here, protected by the lock, and then,
+ // outside of the lock protection, mirror it to a JS structure.
+ nsresult rv = GetSnapshot(locker, aStore, dataSnapshot, clearSubsession);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ // Now that we have a copy of the data, mirror it to JS.
+ return internal_ReflectKeyedHistogram(dataSnapshot, gHistogramInfos[mId], cx,
+ obj);
+}
+
+/**
+ * Return a histogram snapshot for the named store.
+ *
+ * If getting the snapshot succeeds, NS_OK is returned and `aSnapshot` contains
+ * the snapshot data. If the histogram is not available in the named store,
+ * NS_ERROR_NO_CONTENT is returned. For other errors, NS_ERROR_FAILURE is
+ * returned.
+ */
+nsresult KeyedHistogram::GetSnapshot(const StaticMutexAutoLock& aLock,
+ const nsACString& aStore,
+ KeyedHistogramSnapshotData& aSnapshot,
+ bool aClearSubsession) {
+ KeyedHistogramMapType* histogramMap;
+ if (mSingleStore != nullptr) {
+ if (!aStore.EqualsASCII("main")) {
+ return NS_ERROR_NO_CONTENT;
+ }
+
+ histogramMap = mSingleStore;
+ } else {
+ bool found = mStorage.Get(aStore, &histogramMap);
+ if (!found) {
+ // Nothing in the main store is fine, it's just handled as empty
+ return NS_ERROR_NO_CONTENT;
+ }
+ }
+
+ // Snapshot every key.
+ for (const auto& entry : *histogramMap) {
+ base::Histogram* keyData = entry.GetWeak();
+ if (!keyData) {
+ return NS_ERROR_FAILURE;
+ }
+
+ HistogramSnapshotData keySnapshot;
+ if (NS_FAILED(
+ internal_GetHistogramAndSamples(aLock, keyData, keySnapshot))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Append to the final snapshot.
+ aSnapshot.InsertOrUpdate(entry.GetKey(), std::move(keySnapshot));
+ }
+
+ if (aClearSubsession) {
+ Clear(aStore);
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Helper function to get a snapshot of the keyed histograms.
+ *
+ * @param {aLock} the lock proof.
+ * @param {aDataset} the dataset for which the snapshot is being requested.
+ * @param {aClearSubsession} whether or not to clear the data after
+ * taking the snapshot.
+ * @param {aIncludeGPU} whether or not to include data for the GPU.
+ * @param {aOutSnapshot} the container in which the snapshot data will be
+ * stored.
+ * @return {nsresult} NS_OK if the snapshot was successfully taken or
+ * NS_ERROR_OUT_OF_MEMORY if it failed to allocate memory.
+ */
+nsresult internal_GetKeyedHistogramsSnapshot(
+ const StaticMutexAutoLock& aLock, const nsACString& aStore,
+ unsigned int aDataset, bool aClearSubsession, bool aIncludeGPU,
+ bool aFilterTest, KeyedHistogramProcessSnapshotsArray& aOutSnapshot) {
+ if (!aOutSnapshot.resize(static_cast<uint32_t>(ProcessID::Count))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ for (uint32_t process = 0; process < static_cast<uint32_t>(ProcessID::Count);
+ ++process) {
+ KeyedHistogramSnapshotsArray& hArray = aOutSnapshot[process];
+
+ for (size_t i = 0; i < HistogramCount; ++i) {
+ HistogramID id = HistogramID(i);
+ const HistogramInfo& info = gHistogramInfos[id];
+ if (!info.keyed) {
+ continue;
+ }
+
+ if (!CanRecordInProcess(info.record_in_processes, ProcessID(process)) ||
+ ((ProcessID(process) == ProcessID::Gpu) && !aIncludeGPU)) {
+ continue;
+ }
+
+ if (!IsInDataset(info.dataset, aDataset)) {
+ continue;
+ }
+
+ KeyedHistogram* keyed =
+ internal_GetKeyedHistogramById(id, ProcessID(process),
+ /* instantiate = */ false);
+ if (!keyed || keyed->IsEmpty(aStore) || keyed->IsExpired()) {
+ continue;
+ }
+
+ const char* name = info.name();
+ if (aFilterTest && strncmp(TEST_HISTOGRAM_PREFIX, name,
+ strlen(TEST_HISTOGRAM_PREFIX)) == 0) {
+ if (aClearSubsession) {
+ keyed->Clear(aStore);
+ }
+ continue;
+ }
+
+ // Take a snapshot of the keyed histogram data!
+ KeyedHistogramSnapshotData snapshot;
+ if (!NS_SUCCEEDED(
+ keyed->GetSnapshot(aLock, aStore, snapshot, aClearSubsession))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!hArray.emplaceBack(
+ KeyedHistogramSnapshotInfo{std::move(snapshot), id})) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: thread-unsafe helpers for the external interface
+
+namespace {
+
+bool internal_RemoteAccumulate(const StaticMutexAutoLock& aLock,
+ HistogramID aId, uint32_t aSample) {
+ if (XRE_IsParentProcess()) {
+ return false;
+ }
+
+ if (!internal_IsRecordingEnabled(aId)) {
+ return true;
+ }
+
+ TelemetryIPCAccumulator::AccumulateChildHistogram(aId, aSample);
+ return true;
+}
+
+bool internal_RemoteAccumulate(const StaticMutexAutoLock& aLock,
+ HistogramID aId, const nsCString& aKey,
+ uint32_t aSample) {
+ if (XRE_IsParentProcess()) {
+ return false;
+ }
+
+ if (!internal_IsRecordingEnabled(aId)) {
+ return true;
+ }
+
+ TelemetryIPCAccumulator::AccumulateChildKeyedHistogram(aId, aKey, aSample);
+ return true;
+}
+
+void internal_Accumulate(const StaticMutexAutoLock& aLock, HistogramID aId,
+ uint32_t aSample) {
+ if (!internal_CanRecordBase() ||
+ internal_RemoteAccumulate(aLock, aId, aSample)) {
+ return;
+ }
+
+ Histogram* w = internal_GetHistogramById(aLock, aId, ProcessID::Parent);
+ MOZ_ASSERT(w);
+ internal_HistogramAdd(aLock, *w, aId, aSample, ProcessID::Parent);
+}
+
+void internal_Accumulate(const StaticMutexAutoLock& aLock, HistogramID aId,
+ const nsCString& aKey, uint32_t aSample) {
+ if (!gInitDone || !internal_CanRecordBase() ||
+ internal_RemoteAccumulate(aLock, aId, aKey, aSample)) {
+ return;
+ }
+
+ KeyedHistogram* keyed =
+ internal_GetKeyedHistogramById(aId, ProcessID::Parent);
+ MOZ_ASSERT(keyed);
+ keyed->Add(aKey, aSample, ProcessID::Parent);
+}
+
+void internal_AccumulateChild(const StaticMutexAutoLock& aLock,
+ ProcessID aProcessType, HistogramID aId,
+ uint32_t aSample) {
+ if (!internal_CanRecordBase()) {
+ return;
+ }
+
+ Histogram* w = internal_GetHistogramById(aLock, aId, aProcessType);
+ if (w == nullptr) {
+ NS_WARNING("Failed GetHistogramById for CHILD");
+ } else {
+ internal_HistogramAdd(aLock, *w, aId, aSample, aProcessType);
+ }
+}
+
+void internal_AccumulateChildKeyed(const StaticMutexAutoLock& aLock,
+ ProcessID aProcessType, HistogramID aId,
+ const nsCString& aKey, uint32_t aSample) {
+ if (!gInitDone || !internal_CanRecordBase()) {
+ return;
+ }
+
+ KeyedHistogram* keyed = internal_GetKeyedHistogramById(aId, aProcessType);
+ MOZ_ASSERT(keyed);
+ keyed->Add(aKey, aSample, aProcessType);
+}
+
+void internal_ClearHistogram(const StaticMutexAutoLock& aLock, HistogramID id,
+ const nsACString& aStore) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+
+ // Handle keyed histograms.
+ if (gHistogramInfos[id].keyed) {
+ for (uint32_t process = 0;
+ process < static_cast<uint32_t>(ProcessID::Count); ++process) {
+ KeyedHistogram* kh = internal_GetKeyedHistogramById(
+ id, static_cast<ProcessID>(process), /* instantiate = */ false);
+ if (kh) {
+ kh->Clear(aStore);
+ }
+ }
+ } else {
+ // Reset the histograms instances for all processes.
+ for (uint32_t process = 0;
+ process < static_cast<uint32_t>(ProcessID::Count); ++process) {
+ Histogram* h =
+ internal_GetHistogramById(aLock, id, static_cast<ProcessID>(process),
+ /* instantiate = */ false);
+ if (h) {
+ h->Clear(aStore);
+ }
+ }
+ }
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: JSHistogram_* functions
+
+// NOTE: the functions in this section:
+//
+// internal_JSHistogram_Add
+// internal_JSHistogram_Name
+// internal_JSHistogram_Snapshot
+// internal_JSHistogram_Clear
+// internal_WrapAndReturnHistogram
+//
+// all run without protection from |gTelemetryHistogramMutex|. If they
+// held |gTelemetryHistogramMutex|, there would be the possibility of
+// deadlock because the JS_ calls that they make may call back into the
+// TelemetryHistogram interface, hence trying to re-acquire the mutex.
+//
+// This means that these functions potentially race against threads, but
+// that seems preferable to risking deadlock.
+
+namespace {
+
+static constexpr uint32_t HistogramObjectDataSlot = 0;
+static constexpr uint32_t HistogramObjectSlotCount =
+ HistogramObjectDataSlot + 1;
+
+void internal_JSHistogram_finalize(JS::GCContext*, JSObject*);
+
+static const JSClassOps sJSHistogramClassOps = {nullptr, /* addProperty */
+ nullptr, /* delProperty */
+ nullptr, /* enumerate */
+ nullptr, /* newEnumerate */
+ nullptr, /* resolve */
+ nullptr, /* mayResolve */
+ internal_JSHistogram_finalize};
+
+static const JSClass sJSHistogramClass = {
+ "JSHistogram", /* name */
+ JSCLASS_HAS_RESERVED_SLOTS(HistogramObjectSlotCount) |
+ JSCLASS_FOREGROUND_FINALIZE, /* flags */
+ &sJSHistogramClassOps};
+
+struct JSHistogramData {
+ HistogramID histogramId;
+};
+
+bool internal_JSHistogram_CoerceValue(JSContext* aCx,
+ JS::Handle<JS::Value> aElement,
+ HistogramID aId, uint32_t aHistogramType,
+ uint32_t& aValue) {
+ if (aElement.isString()) {
+ // Strings only allowed for categorical histograms
+ if (aHistogramType != nsITelemetry::HISTOGRAM_CATEGORICAL) {
+ LogToBrowserConsole(
+ nsIScriptError::errorFlag,
+ nsLiteralString(
+ u"String argument only allowed for categorical histogram"));
+ return false;
+ }
+
+ // Label is given by the string argument
+ nsAutoJSString label;
+ if (!label.init(aCx, aElement)) {
+ LogToBrowserConsole(nsIScriptError::errorFlag,
+ u"Invalid string parameter"_ns);
+ return false;
+ }
+
+ // Get the label id for accumulation
+ nsresult rv = gHistogramInfos[aId].label_id(
+ NS_ConvertUTF16toUTF8(label).get(), &aValue);
+ if (NS_FAILED(rv)) {
+ nsPrintfCString msg("'%s' is an invalid string label",
+ NS_ConvertUTF16toUTF8(label).get());
+ LogToBrowserConsole(nsIScriptError::errorFlag,
+ NS_ConvertUTF8toUTF16(msg));
+ return false;
+ }
+ } else if (!(aElement.isNumber() || aElement.isBoolean())) {
+ LogToBrowserConsole(nsIScriptError::errorFlag, u"Argument not a number"_ns);
+ return false;
+ } else if (aElement.isNumber() && aElement.toNumber() > UINT32_MAX) {
+ // Clamp large numerical arguments to aValue's acceptable values.
+ // JS::ToUint32 will take aElement modulo 2^32 before returning it, which
+ // may result in a smaller final value.
+ aValue = UINT32_MAX;
+#ifdef DEBUG
+ LogToBrowserConsole(nsIScriptError::errorFlag,
+ u"Clamped large numeric value"_ns);
+#endif
+ } else if (!JS::ToUint32(aCx, aElement, &aValue)) {
+ LogToBrowserConsole(nsIScriptError::errorFlag,
+ u"Failed to convert element to UInt32"_ns);
+ return false;
+ }
+
+ // If we're here then all type checks have passed and aValue contains the
+ // coerced integer
+ return true;
+}
+
+bool internal_JSHistogram_GetValueArray(JSContext* aCx, JS::CallArgs& args,
+ uint32_t aHistogramType,
+ HistogramID aId, bool isKeyed,
+ nsTArray<uint32_t>& aArray) {
+ // This function populates aArray with the values extracted from args. Handles
+ // keyed and non-keyed histograms, and single and array of values. Also
+ // performs sanity checks on the arguments. Returns true upon successful
+ // population, false otherwise.
+
+ uint32_t firstArgIndex = 0;
+ if (isKeyed) {
+ firstArgIndex = 1;
+ }
+
+ // Special case of no argument (or only key) and count histogram
+ if (args.length() == firstArgIndex) {
+ if (!(aHistogramType == nsITelemetry::HISTOGRAM_COUNT)) {
+ LogToBrowserConsole(
+ nsIScriptError::errorFlag,
+ nsLiteralString(
+ u"Need at least one argument for non count type histogram"));
+ return false;
+ }
+
+ aArray.AppendElement(1);
+ return true;
+ }
+
+ if (args[firstArgIndex].isObject() && !args[firstArgIndex].isString()) {
+ JS::Rooted<JSObject*> arrayObj(aCx, &args[firstArgIndex].toObject());
+
+ bool isArray = false;
+ JS::IsArrayObject(aCx, arrayObj, &isArray);
+
+ if (!isArray) {
+ LogToBrowserConsole(
+ nsIScriptError::errorFlag,
+ nsLiteralString(
+ u"The argument to accumulate can't be a non-array object"));
+ return false;
+ }
+
+ uint32_t arrayLength = 0;
+ if (!JS::GetArrayLength(aCx, arrayObj, &arrayLength)) {
+ LogToBrowserConsole(nsIScriptError::errorFlag,
+ u"Failed while trying to get array length"_ns);
+ return false;
+ }
+
+ for (uint32_t arrayIdx = 0; arrayIdx < arrayLength; arrayIdx++) {
+ JS::Rooted<JS::Value> element(aCx);
+
+ if (!JS_GetElement(aCx, arrayObj, arrayIdx, &element)) {
+ nsPrintfCString msg("Failed while trying to get element at index %d",
+ arrayIdx);
+ LogToBrowserConsole(nsIScriptError::errorFlag,
+ NS_ConvertUTF8toUTF16(msg));
+ return false;
+ }
+
+ uint32_t value = 0;
+ if (!internal_JSHistogram_CoerceValue(aCx, element, aId, aHistogramType,
+ value)) {
+ nsPrintfCString msg("Element at index %d failed type checks", arrayIdx);
+ LogToBrowserConsole(nsIScriptError::errorFlag,
+ NS_ConvertUTF8toUTF16(msg));
+ return false;
+ }
+ aArray.AppendElement(value);
+ }
+
+ return true;
+ }
+
+ uint32_t value = 0;
+ if (!internal_JSHistogram_CoerceValue(aCx, args[firstArgIndex], aId,
+ aHistogramType, value)) {
+ return false;
+ }
+ aArray.AppendElement(value);
+ return true;
+}
+
+static JSHistogramData* GetJSHistogramData(JSObject* obj) {
+ MOZ_ASSERT(JS::GetClass(obj) == &sJSHistogramClass);
+ return JS::GetMaybePtrFromReservedSlot<JSHistogramData>(
+ obj, HistogramObjectDataSlot);
+}
+
+bool internal_JSHistogram_Add(JSContext* cx, unsigned argc, JS::Value* vp) {
+ JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+ if (!args.thisv().isObject() ||
+ JS::GetClass(&args.thisv().toObject()) != &sJSHistogramClass) {
+ JS_ReportErrorASCII(cx, "Wrong JS class, expected JSHistogram class");
+ return false;
+ }
+
+ JSObject* obj = &args.thisv().toObject();
+ JSHistogramData* data = GetJSHistogramData(obj);
+ MOZ_ASSERT(data);
+ HistogramID id = data->histogramId;
+ MOZ_ASSERT(internal_IsHistogramEnumId(id));
+ uint32_t type = gHistogramInfos[id].histogramType;
+
+ // This function should always return |undefined| and never fail but
+ // rather report failures using the console.
+ args.rval().setUndefined();
+
+ nsTArray<uint32_t> values;
+ if (!internal_JSHistogram_GetValueArray(cx, args, type, id, false, values)) {
+ // Either GetValueArray or CoerceValue utility function will have printed a
+ // meaningful error message, so we simply return true
+ return true;
+ }
+
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ for (uint32_t aValue : values) {
+ internal_Accumulate(locker, id, aValue);
+ }
+ }
+ return true;
+}
+
+bool internal_JSHistogram_Name(JSContext* cx, unsigned argc, JS::Value* vp) {
+ JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+ if (!args.thisv().isObject() ||
+ JS::GetClass(&args.thisv().toObject()) != &sJSHistogramClass) {
+ JS_ReportErrorASCII(cx, "Wrong JS class, expected JSHistogram class");
+ return false;
+ }
+
+ JSObject* obj = &args.thisv().toObject();
+ JSHistogramData* data = GetJSHistogramData(obj);
+ MOZ_ASSERT(data);
+ HistogramID id = data->histogramId;
+ MOZ_ASSERT(internal_IsHistogramEnumId(id));
+ const char* name = gHistogramInfos[id].name();
+
+ auto cname = NS_ConvertASCIItoUTF16(name);
+ args.rval().setString(ToJSString(cx, cname));
+
+ return true;
+}
+
+/**
+ * Extract the store name from JavaScript function arguments.
+ * The first and only argument needs to be an object with a "store" property.
+ * If no arguments are given it defaults to "main".
+ */
+nsresult internal_JS_StoreFromObjectArgument(JSContext* cx,
+ const JS::CallArgs& args,
+ nsAutoString& aStoreName) {
+ if (args.length() == 0) {
+ aStoreName.AssignLiteral("main");
+ } else if (args.length() == 1) {
+ if (!args[0].isObject()) {
+ JS_ReportErrorASCII(cx, "Expected object argument.");
+ return NS_ERROR_FAILURE;
+ }
+
+ JS::Rooted<JS::Value> storeValue(cx);
+ JS::Rooted<JSObject*> argsObject(cx, &args[0].toObject());
+ if (!JS_GetProperty(cx, argsObject, "store", &storeValue)) {
+ JS_ReportErrorASCII(cx,
+ "Expected object argument to have property 'store'.");
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoJSString store;
+ if (!storeValue.isString() || !store.init(cx, storeValue)) {
+ JS_ReportErrorASCII(
+ cx, "Expected object argument's 'store' property to be a string.");
+ return NS_ERROR_FAILURE;
+ }
+
+ aStoreName.Assign(store);
+ } else {
+ JS_ReportErrorASCII(cx, "Expected at most one argument.");
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+bool internal_JSHistogram_Snapshot(JSContext* cx, unsigned argc,
+ JS::Value* vp) {
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+ if (!XRE_IsParentProcess()) {
+ JS_ReportErrorASCII(
+ cx, "Histograms can only be snapshotted in the parent process");
+ return false;
+ }
+
+ if (!args.thisv().isObject() ||
+ JS::GetClass(&args.thisv().toObject()) != &sJSHistogramClass) {
+ JS_ReportErrorASCII(cx, "Wrong JS class, expected JSHistogram class");
+ return false;
+ }
+
+ JSObject* obj = &args.thisv().toObject();
+ JSHistogramData* data = GetJSHistogramData(obj);
+ MOZ_ASSERT(data);
+ HistogramID id = data->histogramId;
+
+ nsAutoString storeName;
+ nsresult rv = internal_JS_StoreFromObjectArgument(cx, args, storeName);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ HistogramSnapshotData dataSnapshot;
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ MOZ_ASSERT(internal_IsHistogramEnumId(id));
+
+ // This is not good standard behavior given that we have histogram instances
+ // covering multiple processes.
+ // However, changing this requires some broader changes to callers.
+ Histogram* w = internal_GetHistogramById(locker, id, ProcessID::Parent);
+ base::Histogram* h = nullptr;
+ if (!w->GetHistogram(NS_ConvertUTF16toUTF8(storeName), &h)) {
+ // When it's not in the named store, let's skip the snapshot completely,
+ // but don't fail
+ args.rval().setUndefined();
+ return true;
+ }
+ // Take a snapshot of the data here, protected by the lock, and then,
+ // outside of the lock protection, mirror it to a JS structure
+ if (NS_FAILED(internal_GetHistogramAndSamples(locker, h, dataSnapshot))) {
+ return false;
+ }
+ }
+
+ JS::Rooted<JSObject*> snapshot(cx, JS_NewPlainObject(cx));
+ if (!snapshot) {
+ return false;
+ }
+
+ if (NS_FAILED(internal_ReflectHistogramAndSamples(
+ cx, snapshot, gHistogramInfos[id], dataSnapshot))) {
+ return false;
+ }
+
+ args.rval().setObject(*snapshot);
+ return true;
+}
+
+bool internal_JSHistogram_Clear(JSContext* cx, unsigned argc, JS::Value* vp) {
+ if (!XRE_IsParentProcess()) {
+ JS_ReportErrorASCII(cx,
+ "Histograms can only be cleared in the parent process");
+ return false;
+ }
+
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+ if (!args.thisv().isObject() ||
+ JS::GetClass(&args.thisv().toObject()) != &sJSHistogramClass) {
+ JS_ReportErrorASCII(cx, "Wrong JS class, expected JSHistogram class");
+ return false;
+ }
+
+ JSObject* obj = &args.thisv().toObject();
+ JSHistogramData* data = GetJSHistogramData(obj);
+ MOZ_ASSERT(data);
+
+ nsAutoString storeName;
+ nsresult rv = internal_JS_StoreFromObjectArgument(cx, args, storeName);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ // This function should always return |undefined| and never fail but
+ // rather report failures using the console.
+ args.rval().setUndefined();
+
+ HistogramID id = data->histogramId;
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+
+ MOZ_ASSERT(internal_IsHistogramEnumId(id));
+ internal_ClearHistogram(locker, id, NS_ConvertUTF16toUTF8(storeName));
+ }
+
+ return true;
+}
+
+// NOTE: Runs without protection from |gTelemetryHistogramMutex|.
+// See comment at the top of this section.
+nsresult internal_WrapAndReturnHistogram(HistogramID id, JSContext* cx,
+ JS::MutableHandle<JS::Value> ret) {
+ JS::Rooted<JSObject*> obj(cx, JS_NewObject(cx, &sJSHistogramClass));
+ if (!obj) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // The 3 functions that are wrapped up here are eventually called
+ // by the same thread that runs this function.
+ if (!(JS_DefineFunction(cx, obj, "add", internal_JSHistogram_Add, 1, 0) &&
+ JS_DefineFunction(cx, obj, "name", internal_JSHistogram_Name, 1, 0) &&
+ JS_DefineFunction(cx, obj, "snapshot", internal_JSHistogram_Snapshot, 1,
+ 0) &&
+ JS_DefineFunction(cx, obj, "clear", internal_JSHistogram_Clear, 1,
+ 0))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ JSHistogramData* data = new JSHistogramData{id};
+ JS::SetReservedSlot(obj, HistogramObjectDataSlot, JS::PrivateValue(data));
+ ret.setObject(*obj);
+
+ return NS_OK;
+}
+
+void internal_JSHistogram_finalize(JS::GCContext* gcx, JSObject* obj) {
+ if (!obj || JS::GetClass(obj) != &sJSHistogramClass) {
+ MOZ_ASSERT_UNREACHABLE("Should have the right JS class.");
+ return;
+ }
+
+ JSHistogramData* data = GetJSHistogramData(obj);
+ MOZ_ASSERT(data);
+ delete data;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: JSKeyedHistogram_* functions
+
+// NOTE: the functions in this section:
+//
+// internal_JSKeyedHistogram_Add
+// internal_JSKeyedHistogram_Name
+// internal_JSKeyedHistogram_Keys
+// internal_JSKeyedHistogram_Snapshot
+// internal_JSKeyedHistogram_Clear
+// internal_WrapAndReturnKeyedHistogram
+//
+// Same comments as above, at the JSHistogram_* section, regarding
+// deadlock avoidance, apply.
+
+namespace {
+
+void internal_JSKeyedHistogram_finalize(JS::GCContext*, JSObject*);
+
+static const JSClassOps sJSKeyedHistogramClassOps = {
+ nullptr, /* addProperty */
+ nullptr, /* delProperty */
+ nullptr, /* enumerate */
+ nullptr, /* newEnumerate */
+ nullptr, /* resolve */
+ nullptr, /* mayResolve */
+ internal_JSKeyedHistogram_finalize};
+
+static const JSClass sJSKeyedHistogramClass = {
+ "JSKeyedHistogram", /* name */
+ JSCLASS_HAS_RESERVED_SLOTS(HistogramObjectSlotCount) |
+ JSCLASS_FOREGROUND_FINALIZE, /* flags */
+ &sJSKeyedHistogramClassOps};
+
+static JSHistogramData* GetJSKeyedHistogramData(JSObject* obj) {
+ MOZ_ASSERT(JS::GetClass(obj) == &sJSKeyedHistogramClass);
+ return JS::GetMaybePtrFromReservedSlot<JSHistogramData>(
+ obj, HistogramObjectDataSlot);
+}
+
+bool internal_JSKeyedHistogram_Snapshot(JSContext* cx, unsigned argc,
+ JS::Value* vp) {
+ if (!XRE_IsParentProcess()) {
+ JS_ReportErrorASCII(
+ cx, "Keyed histograms can only be snapshotted in the parent process");
+ return false;
+ }
+
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+ if (!args.thisv().isObject() ||
+ JS::GetClass(&args.thisv().toObject()) != &sJSKeyedHistogramClass) {
+ JS_ReportErrorASCII(cx, "Wrong JS class, expected JSKeyedHistogram class");
+ return false;
+ }
+
+ JSObject* obj = &args.thisv().toObject();
+ JSHistogramData* data = GetJSKeyedHistogramData(obj);
+ MOZ_ASSERT(data);
+ HistogramID id = data->histogramId;
+ MOZ_ASSERT(internal_IsHistogramEnumId(id));
+
+ // This function should always return |undefined| and never fail but
+ // rather report failures using the console.
+ args.rval().setUndefined();
+
+ // This is not good standard behavior given that we have histogram instances
+ // covering multiple processes.
+ // However, changing this requires some broader changes to callers.
+ KeyedHistogram* keyed = internal_GetKeyedHistogramById(
+ id, ProcessID::Parent, /* instantiate = */ true);
+ if (!keyed) {
+ JS_ReportErrorASCII(cx, "Failed to look up keyed histogram");
+ return false;
+ }
+
+ nsAutoString storeName;
+ nsresult rv;
+ rv = internal_JS_StoreFromObjectArgument(cx, args, storeName);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ JS::Rooted<JSObject*> snapshot(cx, JS_NewPlainObject(cx));
+ if (!snapshot) {
+ JS_ReportErrorASCII(cx, "Failed to create object");
+ return false;
+ }
+
+ rv = keyed->GetJSSnapshot(cx, snapshot, NS_ConvertUTF16toUTF8(storeName),
+ false);
+
+ // If the store is not available, we return nothing and don't fail
+ if (rv == NS_ERROR_NO_CONTENT) {
+ args.rval().setUndefined();
+ return true;
+ }
+
+ if (!NS_SUCCEEDED(rv)) {
+ JS_ReportErrorASCII(cx, "Failed to reflect keyed histograms");
+ return false;
+ }
+
+ args.rval().setObject(*snapshot);
+ return true;
+}
+
+bool internal_JSKeyedHistogram_Add(JSContext* cx, unsigned argc,
+ JS::Value* vp) {
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+ if (!args.thisv().isObject() ||
+ JS::GetClass(&args.thisv().toObject()) != &sJSKeyedHistogramClass) {
+ JS_ReportErrorASCII(cx, "Wrong JS class, expected JSKeyedHistogram class");
+ return false;
+ }
+
+ JSObject* obj = &args.thisv().toObject();
+ JSHistogramData* data = GetJSKeyedHistogramData(obj);
+ MOZ_ASSERT(data);
+ HistogramID id = data->histogramId;
+ MOZ_ASSERT(internal_IsHistogramEnumId(id));
+
+ // This function should always return |undefined| and never fail but
+ // rather report failures using the console.
+ args.rval().setUndefined();
+ if (args.length() < 1) {
+ LogToBrowserConsole(nsIScriptError::errorFlag, u"Expected one argument"_ns);
+ return true;
+ }
+
+ nsAutoJSString key;
+ if (!args[0].isString() || !key.init(cx, args[0])) {
+ LogToBrowserConsole(nsIScriptError::errorFlag, u"Not a string"_ns);
+ return true;
+ }
+
+ // Check if we're allowed to record in the provided key, for this histogram.
+ if (!gHistogramInfos[id].allows_key(NS_ConvertUTF16toUTF8(key))) {
+ nsPrintfCString msg("%s - key '%s' not allowed for this keyed histogram",
+ gHistogramInfos[id].name(),
+ NS_ConvertUTF16toUTF8(key).get());
+ LogToBrowserConsole(nsIScriptError::errorFlag, NS_ConvertUTF8toUTF16(msg));
+ TelemetryScalar::Add(mozilla::Telemetry::ScalarID::
+ TELEMETRY_ACCUMULATE_UNKNOWN_HISTOGRAM_KEYS,
+ NS_ConvertASCIItoUTF16(gHistogramInfos[id].name()), 1);
+ return true;
+ }
+
+ const uint32_t type = gHistogramInfos[id].histogramType;
+
+ nsTArray<uint32_t> values;
+ if (!internal_JSHistogram_GetValueArray(cx, args, type, id, true, values)) {
+ // Either GetValueArray or CoerceValue utility function will have printed a
+ // meaningful error message so we simple return true
+ return true;
+ }
+
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ for (uint32_t aValue : values) {
+ internal_Accumulate(locker, id, NS_ConvertUTF16toUTF8(key), aValue);
+ }
+ }
+ return true;
+}
+
+bool internal_JSKeyedHistogram_Name(JSContext* cx, unsigned argc,
+ JS::Value* vp) {
+ JS::CallArgs args = CallArgsFromVp(argc, vp);
+
+ if (!args.thisv().isObject() ||
+ JS::GetClass(&args.thisv().toObject()) != &sJSKeyedHistogramClass) {
+ JS_ReportErrorASCII(cx, "Wrong JS class, expected JSKeyedHistogram class");
+ return false;
+ }
+
+ JSObject* obj = &args.thisv().toObject();
+ JSHistogramData* data = GetJSKeyedHistogramData(obj);
+ MOZ_ASSERT(data);
+ HistogramID id = data->histogramId;
+ MOZ_ASSERT(internal_IsHistogramEnumId(id));
+ const char* name = gHistogramInfos[id].name();
+
+ auto cname = NS_ConvertASCIItoUTF16(name);
+ args.rval().setString(ToJSString(cx, cname));
+
+ return true;
+}
+
+bool internal_JSKeyedHistogram_Keys(JSContext* cx, unsigned argc,
+ JS::Value* vp) {
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+ if (!args.thisv().isObject() ||
+ JS::GetClass(&args.thisv().toObject()) != &sJSKeyedHistogramClass) {
+ JS_ReportErrorASCII(cx, "Wrong JS class, expected JSKeyedHistogram class");
+ return false;
+ }
+
+ JSObject* obj = &args.thisv().toObject();
+ JSHistogramData* data = GetJSKeyedHistogramData(obj);
+ MOZ_ASSERT(data);
+ HistogramID id = data->histogramId;
+
+ nsAutoString storeName;
+ nsresult rv = internal_JS_StoreFromObjectArgument(cx, args, storeName);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ nsTArray<nsCString> keys;
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ MOZ_ASSERT(internal_IsHistogramEnumId(id));
+
+ // This is not good standard behavior given that we have histogram instances
+ // covering multiple processes.
+ // However, changing this requires some broader changes to callers.
+ KeyedHistogram* keyed =
+ internal_GetKeyedHistogramById(id, ProcessID::Parent);
+
+ MOZ_ASSERT(keyed);
+ if (!keyed) {
+ return false;
+ }
+
+ if (NS_FAILED(
+ keyed->GetKeys(locker, NS_ConvertUTF16toUTF8(storeName), keys))) {
+ return false;
+ }
+ }
+
+ // Convert keys from nsTArray<nsCString> to JS array.
+ JS::RootedVector<JS::Value> autoKeys(cx);
+ if (!autoKeys.reserve(keys.Length())) {
+ return false;
+ }
+
+ for (const auto& key : keys) {
+ JS::Rooted<JS::Value> jsKey(cx);
+ jsKey.setString(ToJSString(cx, key));
+ if (!autoKeys.append(jsKey)) {
+ return false;
+ }
+ }
+
+ JS::Rooted<JSObject*> jsKeys(cx, JS::NewArrayObject(cx, autoKeys));
+ if (!jsKeys) {
+ return false;
+ }
+
+ args.rval().setObject(*jsKeys);
+ return true;
+}
+
+bool internal_JSKeyedHistogram_Clear(JSContext* cx, unsigned argc,
+ JS::Value* vp) {
+ if (!XRE_IsParentProcess()) {
+ JS_ReportErrorASCII(
+ cx, "Keyed histograms can only be cleared in the parent process");
+ return false;
+ }
+
+ JS::CallArgs args = JS::CallArgsFromVp(argc, vp);
+
+ if (!args.thisv().isObject() ||
+ JS::GetClass(&args.thisv().toObject()) != &sJSKeyedHistogramClass) {
+ JS_ReportErrorASCII(cx, "Wrong JS class, expected JSKeyedHistogram class");
+ return false;
+ }
+
+ JSObject* obj = &args.thisv().toObject();
+ JSHistogramData* data = GetJSKeyedHistogramData(obj);
+ MOZ_ASSERT(data);
+ HistogramID id = data->histogramId;
+
+ // This function should always return |undefined| and never fail but
+ // rather report failures using the console.
+ args.rval().setUndefined();
+
+ nsAutoString storeName;
+ nsresult rv = internal_JS_StoreFromObjectArgument(cx, args, storeName);
+ if (NS_FAILED(rv)) {
+ return false;
+ }
+
+ KeyedHistogram* keyed = nullptr;
+ {
+ MOZ_ASSERT(internal_IsHistogramEnumId(id));
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+
+ // This is not good standard behavior given that we have histogram instances
+ // covering multiple processes.
+ // However, changing this requires some broader changes to callers.
+ keyed = internal_GetKeyedHistogramById(id, ProcessID::Parent,
+ /* instantiate = */ false);
+
+ if (!keyed) {
+ return true;
+ }
+
+ keyed->Clear(NS_ConvertUTF16toUTF8(storeName));
+ }
+
+ return true;
+}
+
+// NOTE: Runs without protection from |gTelemetryHistogramMutex|.
+// See comment at the top of this section.
+nsresult internal_WrapAndReturnKeyedHistogram(
+ HistogramID id, JSContext* cx, JS::MutableHandle<JS::Value> ret) {
+ JS::Rooted<JSObject*> obj(cx, JS_NewObject(cx, &sJSKeyedHistogramClass));
+ if (!obj) return NS_ERROR_FAILURE;
+ // The 6 functions that are wrapped up here are eventually called
+ // by the same thread that runs this function.
+ if (!(JS_DefineFunction(cx, obj, "add", internal_JSKeyedHistogram_Add, 2,
+ 0) &&
+ JS_DefineFunction(cx, obj, "name", internal_JSKeyedHistogram_Name, 1,
+ 0) &&
+ JS_DefineFunction(cx, obj, "snapshot",
+ internal_JSKeyedHistogram_Snapshot, 1, 0) &&
+ JS_DefineFunction(cx, obj, "keys", internal_JSKeyedHistogram_Keys, 1,
+ 0) &&
+ JS_DefineFunction(cx, obj, "clear", internal_JSKeyedHistogram_Clear, 1,
+ 0))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ JSHistogramData* data = new JSHistogramData{id};
+ JS::SetReservedSlot(obj, HistogramObjectDataSlot, JS::PrivateValue(data));
+ ret.setObject(*obj);
+
+ return NS_OK;
+}
+
+void internal_JSKeyedHistogram_finalize(JS::GCContext* gcx, JSObject* obj) {
+ if (!obj || JS::GetClass(obj) != &sJSKeyedHistogramClass) {
+ MOZ_ASSERT_UNREACHABLE("Should have the right JS class.");
+ return;
+ }
+
+ JSHistogramData* data = GetJSKeyedHistogramData(obj);
+ MOZ_ASSERT(data);
+ delete data;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// EXTERNALLY VISIBLE FUNCTIONS in namespace TelemetryHistogram::
+
+// All of these functions are actually in namespace TelemetryHistogram::,
+// but the ::TelemetryHistogram prefix is given explicitly. This is
+// because it is critical to see which calls from these functions are
+// to another function in this interface. Mis-identifying "inwards
+// calls" from "calls to another function in this interface" will lead
+// to deadlocking and/or races. See comments at the top of the file
+// for further (important!) details.
+
+void TelemetryHistogram::InitializeGlobalState(bool canRecordBase,
+ bool canRecordExtended) {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ MOZ_ASSERT(!gInitDone,
+ "TelemetryHistogram::InitializeGlobalState "
+ "may only be called once");
+
+ gCanRecordBase = canRecordBase;
+ gCanRecordExtended = canRecordExtended;
+
+ if (XRE_IsParentProcess()) {
+ gHistogramStorage =
+ new Histogram* [HistogramCount * size_t(ProcessID::Count)] {};
+ gKeyedHistogramStorage =
+ new KeyedHistogram* [HistogramCount * size_t(ProcessID::Count)] {};
+ }
+
+ // Some Telemetry histograms depend on the value of C++ constants and hardcode
+ // their values in Histograms.json.
+ // We add static asserts here for those values to match so that future changes
+ // don't go unnoticed.
+ // clang-format off
+ static_assert((uint32_t(JS::GCReason::NUM_TELEMETRY_REASONS) + 1) ==
+ gHistogramInfos[mozilla::Telemetry::GC_MINOR_REASON].bucketCount &&
+ (uint32_t(JS::GCReason::NUM_TELEMETRY_REASONS) + 1) ==
+ gHistogramInfos[mozilla::Telemetry::GC_MINOR_REASON_LONG].bucketCount &&
+ (uint32_t(JS::GCReason::NUM_TELEMETRY_REASONS) + 1) ==
+ gHistogramInfos[mozilla::Telemetry::GC_REASON_2].bucketCount,
+ "NUM_TELEMETRY_REASONS is assumed to be a fixed value in Histograms.json."
+ " If this was an intentional change, update the n_values for the "
+ "following in Histograms.json: GC_MINOR_REASON, GC_MINOR_REASON_LONG, "
+ "GC_REASON_2");
+
+ // clang-format on
+
+ gInitDone = true;
+}
+
+void TelemetryHistogram::DeInitializeGlobalState() {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ gCanRecordBase = false;
+ gCanRecordExtended = false;
+ gInitDone = false;
+
+ // FactoryGet `new`s Histograms for us, but requires us to manually delete.
+ if (XRE_IsParentProcess()) {
+ for (size_t i = 0; i < HistogramCount * size_t(ProcessID::Count); ++i) {
+ if (gKeyedHistogramStorage[i] != gExpiredKeyedHistogram) {
+ delete gKeyedHistogramStorage[i];
+ }
+ if (gHistogramStorage[i] != gExpiredHistogram) {
+ delete gHistogramStorage[i];
+ }
+ }
+ delete[] gHistogramStorage;
+ delete[] gKeyedHistogramStorage;
+ }
+ delete gExpiredHistogram;
+ gExpiredHistogram = nullptr;
+ delete gExpiredKeyedHistogram;
+ gExpiredKeyedHistogram = nullptr;
+}
+
+#ifdef DEBUG
+bool TelemetryHistogram::GlobalStateHasBeenInitialized() {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ return gInitDone;
+}
+#endif
+
+bool TelemetryHistogram::CanRecordBase() {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ return internal_CanRecordBase();
+}
+
+void TelemetryHistogram::SetCanRecordBase(bool b) {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ gCanRecordBase = b;
+}
+
+bool TelemetryHistogram::CanRecordExtended() {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ return internal_CanRecordExtended();
+}
+
+void TelemetryHistogram::SetCanRecordExtended(bool b) {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ gCanRecordExtended = b;
+}
+
+void TelemetryHistogram::InitHistogramRecordingEnabled() {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ auto processType = XRE_GetProcessType();
+ for (size_t i = 0; i < HistogramCount; ++i) {
+ const HistogramInfo& h = gHistogramInfos[i];
+ mozilla::Telemetry::HistogramID id = mozilla::Telemetry::HistogramID(i);
+ bool canRecordInProcess =
+ CanRecordInProcess(h.record_in_processes, processType);
+ internal_SetHistogramRecordingEnabled(locker, id, canRecordInProcess);
+ }
+
+ for (auto recordingInitiallyDisabledID : kRecordingInitiallyDisabledIDs) {
+ internal_SetHistogramRecordingEnabled(locker, recordingInitiallyDisabledID,
+ false);
+ }
+}
+
+void TelemetryHistogram::SetHistogramRecordingEnabled(HistogramID aID,
+ bool aEnabled) {
+ if (NS_WARN_IF(!internal_IsHistogramEnumId(aID))) {
+ MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids.");
+ return;
+ }
+
+ const HistogramInfo& h = gHistogramInfos[aID];
+ if (!CanRecordInProcess(h.record_in_processes, XRE_GetProcessType())) {
+ // Don't permit record_in_process-disabled recording to be re-enabled.
+ return;
+ }
+
+ if (!CanRecordProduct(h.products)) {
+ // Don't permit products-disabled recording to be re-enabled.
+ return;
+ }
+
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ internal_SetHistogramRecordingEnabled(locker, aID, aEnabled);
+}
+
+nsresult TelemetryHistogram::SetHistogramRecordingEnabled(
+ const nsACString& name, bool aEnabled) {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ HistogramID id;
+ if (NS_FAILED(internal_GetHistogramIdByName(locker, name, &id))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ const HistogramInfo& hi = gHistogramInfos[id];
+ if (CanRecordInProcess(hi.record_in_processes, XRE_GetProcessType())) {
+ internal_SetHistogramRecordingEnabled(locker, id, aEnabled);
+ }
+ return NS_OK;
+}
+
+void TelemetryHistogram::Accumulate(HistogramID aID, uint32_t aSample) {
+ if (NS_WARN_IF(!internal_IsHistogramEnumId(aID))) {
+ MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids.");
+ return;
+ }
+
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ internal_Accumulate(locker, aID, aSample);
+}
+
+void TelemetryHistogram::Accumulate(HistogramID aID,
+ const nsTArray<uint32_t>& aSamples) {
+ if (NS_WARN_IF(!internal_IsHistogramEnumId(aID))) {
+ MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids.");
+ return;
+ }
+
+ MOZ_ASSERT(!gHistogramInfos[aID].keyed,
+ "Cannot accumulate into a keyed histogram. No key given.");
+
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ for (uint32_t sample : aSamples) {
+ internal_Accumulate(locker, aID, sample);
+ }
+}
+
+void TelemetryHistogram::Accumulate(HistogramID aID, const nsCString& aKey,
+ uint32_t aSample) {
+ if (NS_WARN_IF(!internal_IsHistogramEnumId(aID))) {
+ MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids.");
+ return;
+ }
+
+ // Check if we're allowed to record in the provided key, for this histogram.
+ if (!gHistogramInfos[aID].allows_key(aKey)) {
+ nsPrintfCString msg("%s - key '%s' not allowed for this keyed histogram",
+ gHistogramInfos[aID].name(), aKey.get());
+ LogToBrowserConsole(nsIScriptError::errorFlag, NS_ConvertUTF8toUTF16(msg));
+ TelemetryScalar::Add(mozilla::Telemetry::ScalarID::
+ TELEMETRY_ACCUMULATE_UNKNOWN_HISTOGRAM_KEYS,
+ NS_ConvertASCIItoUTF16(gHistogramInfos[aID].name()),
+ 1);
+ return;
+ }
+
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ internal_Accumulate(locker, aID, aKey, aSample);
+}
+
+void TelemetryHistogram::Accumulate(HistogramID aID, const nsCString& aKey,
+ const nsTArray<uint32_t>& aSamples) {
+ if (NS_WARN_IF(!internal_IsHistogramEnumId(aID))) {
+ MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids");
+ return;
+ }
+
+ // Check that this histogram is keyed
+ MOZ_ASSERT(gHistogramInfos[aID].keyed,
+ "Cannot accumulate into a non-keyed histogram using a key.");
+
+ // Check if we're allowed to record in the provided key, for this histogram.
+ if (!gHistogramInfos[aID].allows_key(aKey)) {
+ nsPrintfCString msg("%s - key '%s' not allowed for this keyed histogram",
+ gHistogramInfos[aID].name(), aKey.get());
+ LogToBrowserConsole(nsIScriptError::errorFlag, NS_ConvertUTF8toUTF16(msg));
+ TelemetryScalar::Add(mozilla::Telemetry::ScalarID::
+ TELEMETRY_ACCUMULATE_UNKNOWN_HISTOGRAM_KEYS,
+ NS_ConvertASCIItoUTF16(gHistogramInfos[aID].name()),
+ 1);
+ return;
+ }
+
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ for (uint32_t sample : aSamples) {
+ internal_Accumulate(locker, aID, aKey, sample);
+ }
+}
+
+nsresult TelemetryHistogram::Accumulate(const char* name, uint32_t sample) {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ if (!internal_CanRecordBase()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ HistogramID id;
+ nsresult rv =
+ internal_GetHistogramIdByName(locker, nsDependentCString(name), &id);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ internal_Accumulate(locker, id, sample);
+ return NS_OK;
+}
+
+nsresult TelemetryHistogram::Accumulate(const char* name, const nsCString& key,
+ uint32_t sample) {
+ bool keyNotAllowed = false;
+
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ if (!internal_CanRecordBase()) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ HistogramID id;
+ nsresult rv =
+ internal_GetHistogramIdByName(locker, nsDependentCString(name), &id);
+ if (NS_SUCCEEDED(rv)) {
+ // Check if we're allowed to record in the provided key, for this
+ // histogram.
+ if (gHistogramInfos[id].allows_key(key)) {
+ internal_Accumulate(locker, id, key, sample);
+ return NS_OK;
+ }
+ // We're holding |gTelemetryHistogramMutex|, so we can't print a message
+ // here.
+ keyNotAllowed = true;
+ }
+ }
+
+ if (keyNotAllowed) {
+ LogToBrowserConsole(nsIScriptError::errorFlag,
+ u"Key not allowed for this keyed histogram"_ns);
+ TelemetryScalar::Add(mozilla::Telemetry::ScalarID::
+ TELEMETRY_ACCUMULATE_UNKNOWN_HISTOGRAM_KEYS,
+ NS_ConvertASCIItoUTF16(name), 1);
+ }
+ return NS_ERROR_FAILURE;
+}
+
+void TelemetryHistogram::AccumulateCategorical(HistogramID aId,
+ const nsCString& label) {
+ if (NS_WARN_IF(!internal_IsHistogramEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids.");
+ return;
+ }
+
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ if (!internal_CanRecordBase()) {
+ return;
+ }
+ uint32_t labelId = 0;
+ if (NS_FAILED(gHistogramInfos[aId].label_id(label.get(), &labelId))) {
+ return;
+ }
+ internal_Accumulate(locker, aId, labelId);
+}
+
+void TelemetryHistogram::AccumulateCategorical(
+ HistogramID aId, const nsTArray<nsCString>& aLabels) {
+ if (NS_WARN_IF(!internal_IsHistogramEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids.");
+ return;
+ }
+
+ if (!internal_CanRecordBase()) {
+ return;
+ }
+
+ // We use two loops, one for getting label_ids and another one for actually
+ // accumulating the values. This ensures that in the case of an invalid label
+ // in the array, no values are accumulated. In any call to this API, either
+ // all or (in case of error) none of the values will be accumulated.
+
+ nsTArray<uint32_t> intSamples(aLabels.Length());
+ for (const nsCString& label : aLabels) {
+ uint32_t labelId = 0;
+ if (NS_FAILED(gHistogramInfos[aId].label_id(label.get(), &labelId))) {
+ return;
+ }
+ intSamples.AppendElement(labelId);
+ }
+
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+
+ for (uint32_t sample : intSamples) {
+ internal_Accumulate(locker, aId, sample);
+ }
+}
+
+void TelemetryHistogram::AccumulateChild(
+ ProcessID aProcessType,
+ const nsTArray<HistogramAccumulation>& aAccumulations) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ if (!internal_CanRecordBase()) {
+ return;
+ }
+ for (uint32_t i = 0; i < aAccumulations.Length(); ++i) {
+ if (NS_WARN_IF(!internal_IsHistogramEnumId(aAccumulations[i].mId))) {
+ MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids.");
+ continue;
+ }
+ internal_AccumulateChild(locker, aProcessType, aAccumulations[i].mId,
+ aAccumulations[i].mSample);
+ }
+}
+
+void TelemetryHistogram::AccumulateChildKeyed(
+ ProcessID aProcessType,
+ const nsTArray<KeyedHistogramAccumulation>& aAccumulations) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ if (!internal_CanRecordBase()) {
+ return;
+ }
+ for (uint32_t i = 0; i < aAccumulations.Length(); ++i) {
+ if (NS_WARN_IF(!internal_IsHistogramEnumId(aAccumulations[i].mId))) {
+ MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids.");
+ continue;
+ }
+ internal_AccumulateChildKeyed(locker, aProcessType, aAccumulations[i].mId,
+ aAccumulations[i].mKey,
+ aAccumulations[i].mSample);
+ }
+}
+
+nsresult TelemetryHistogram::GetAllStores(StringHashSet& set) {
+ for (uint32_t storeIdx : gHistogramStoresTable) {
+ const char* name = &gHistogramStringTable[storeIdx];
+ nsAutoCString store;
+ store.AssignASCII(name);
+ if (!set.Insert(store, mozilla::fallible)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ return NS_OK;
+}
+
+nsresult TelemetryHistogram::GetCategoricalHistogramLabels(
+ JSContext* aCx, JS::MutableHandle<JS::Value> aResult) {
+ JS::Rooted<JSObject*> root_obj(aCx, JS_NewPlainObject(aCx));
+ if (!root_obj) {
+ return NS_ERROR_FAILURE;
+ }
+ aResult.setObject(*root_obj);
+
+ for (const HistogramInfo& info : gHistogramInfos) {
+ if (info.histogramType != nsITelemetry::HISTOGRAM_CATEGORICAL) {
+ continue;
+ }
+
+ const char* name = info.name();
+ JS::Rooted<JSObject*> labels(aCx,
+ JS::NewArrayObject(aCx, info.label_count));
+ if (!labels) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!JS_DefineProperty(aCx, root_obj, name, labels, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (uint32_t i = 0; i < info.label_count; ++i) {
+ uint32_t string_offset = gHistogramLabelTable[info.label_index + i];
+ const char* const label = &gHistogramStringTable[string_offset];
+ auto clabel = NS_ConvertASCIItoUTF16(label);
+ JS::Rooted<JS::Value> value(aCx);
+ value.setString(ToJSString(aCx, clabel));
+ if (!JS_DefineElement(aCx, labels, i, value, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult TelemetryHistogram::GetHistogramById(
+ const nsACString& name, JSContext* cx, JS::MutableHandle<JS::Value> ret) {
+ HistogramID id;
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ nsresult rv = internal_GetHistogramIdByName(locker, name, &id);
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (gHistogramInfos[id].keyed) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ // Runs without protection from |gTelemetryHistogramMutex|
+ return internal_WrapAndReturnHistogram(id, cx, ret);
+}
+
+nsresult TelemetryHistogram::GetKeyedHistogramById(
+ const nsACString& name, JSContext* cx, JS::MutableHandle<JS::Value> ret) {
+ HistogramID id;
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ nsresult rv = internal_GetHistogramIdByName(locker, name, &id);
+ if (NS_FAILED(rv)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!gHistogramInfos[id].keyed) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ // Runs without protection from |gTelemetryHistogramMutex|
+ return internal_WrapAndReturnKeyedHistogram(id, cx, ret);
+}
+
+const char* TelemetryHistogram::GetHistogramName(HistogramID id) {
+ if (NS_WARN_IF(!internal_IsHistogramEnumId(id))) {
+ MOZ_ASSERT_UNREACHABLE("Histogram usage requires valid ids.");
+ return nullptr;
+ }
+
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ const HistogramInfo& h = gHistogramInfos[id];
+ return h.name();
+}
+
+nsresult TelemetryHistogram::CreateHistogramSnapshots(
+ JSContext* aCx, JS::MutableHandle<JS::Value> aResult,
+ const nsACString& aStore, unsigned int aDataset, bool aClearSubsession,
+ bool aFilterTest) {
+ if (!XRE_IsParentProcess()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Runs without protection from |gTelemetryHistogramMutex|
+ JS::Rooted<JSObject*> root_obj(aCx, JS_NewPlainObject(aCx));
+ if (!root_obj) {
+ return NS_ERROR_FAILURE;
+ }
+ aResult.setObject(*root_obj);
+
+ // Include the GPU process in histogram snapshots only if we actually tried
+ // to launch a process for it.
+ bool includeGPUProcess = internal_AttemptedGPUProcess();
+
+ HistogramProcessSnapshotsArray processHistArray;
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ nsresult rv = internal_GetHistogramsSnapshot(
+ locker, aStore, aDataset, aClearSubsession, includeGPUProcess,
+ aFilterTest, processHistArray);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ // Make the JS calls on the stashed histograms for every process
+ for (uint32_t process = 0; process < processHistArray.length(); ++process) {
+ JS::Rooted<JSObject*> processObject(aCx, JS_NewPlainObject(aCx));
+ if (!processObject) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!JS_DefineProperty(aCx, root_obj,
+ GetNameForProcessID(ProcessID(process)),
+ processObject, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (const HistogramSnapshotInfo& hData : processHistArray[process]) {
+ HistogramID id = hData.histogramID;
+
+ JS::Rooted<JSObject*> hobj(aCx, JS_NewPlainObject(aCx));
+ if (!hobj) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (NS_FAILED(internal_ReflectHistogramAndSamples(
+ aCx, hobj, gHistogramInfos[id], hData.data))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!JS_DefineProperty(aCx, processObject, gHistogramInfos[id].name(),
+ hobj, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+nsresult TelemetryHistogram::GetKeyedHistogramSnapshots(
+ JSContext* aCx, JS::MutableHandle<JS::Value> aResult,
+ const nsACString& aStore, unsigned int aDataset, bool aClearSubsession,
+ bool aFilterTest) {
+ if (!XRE_IsParentProcess()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Runs without protection from |gTelemetryHistogramMutex|
+ JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx));
+ if (!obj) {
+ return NS_ERROR_FAILURE;
+ }
+ aResult.setObject(*obj);
+
+ // Include the GPU process in histogram snapshots only if we actually tried
+ // to launch a process for it.
+ bool includeGPUProcess = internal_AttemptedGPUProcess();
+
+ // Get a snapshot of all the data while holding the mutex.
+ KeyedHistogramProcessSnapshotsArray processHistArray;
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ nsresult rv = internal_GetKeyedHistogramsSnapshot(
+ locker, aStore, aDataset, aClearSubsession, includeGPUProcess,
+ aFilterTest, processHistArray);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ // Mirror the snapshot data to JS, now that we released the mutex.
+ for (uint32_t process = 0; process < processHistArray.length(); ++process) {
+ JS::Rooted<JSObject*> processObject(aCx, JS_NewPlainObject(aCx));
+ if (!processObject) {
+ return NS_ERROR_FAILURE;
+ }
+ if (!JS_DefineProperty(aCx, obj, GetNameForProcessID(ProcessID(process)),
+ processObject, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (const KeyedHistogramSnapshotInfo& hData : processHistArray[process]) {
+ const HistogramInfo& info = gHistogramInfos[hData.histogramId];
+
+ JS::Rooted<JSObject*> snapshot(aCx, JS_NewPlainObject(aCx));
+ if (!snapshot) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!NS_SUCCEEDED(internal_ReflectKeyedHistogram(hData.data, info, aCx,
+ snapshot))) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!JS_DefineProperty(aCx, processObject, info.name(), snapshot,
+ JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+ return NS_OK;
+}
+
+size_t TelemetryHistogram::GetHistogramSizesOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+
+ size_t n = 0;
+
+ // If we allocated the array, let's count the number of pointers in there and
+ // each entry's size.
+ if (gKeyedHistogramStorage) {
+ n += HistogramCount * size_t(ProcessID::Count) * sizeof(KeyedHistogram*);
+ for (size_t i = 0; i < HistogramCount * size_t(ProcessID::Count); ++i) {
+ if (gKeyedHistogramStorage[i] &&
+ gKeyedHistogramStorage[i] != gExpiredKeyedHistogram) {
+ n += gKeyedHistogramStorage[i]->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ }
+ }
+
+ // If we allocated the array, let's count the number of pointers in there.
+ if (gHistogramStorage) {
+ n += HistogramCount * size_t(ProcessID::Count) * sizeof(Histogram*);
+ for (size_t i = 0; i < HistogramCount * size_t(ProcessID::Count); ++i) {
+ if (gHistogramStorage[i] && gHistogramStorage[i] != gExpiredHistogram) {
+ n += gHistogramStorage[i]->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ }
+ }
+
+ // We only allocate the expired (keyed) histogram once.
+ if (gExpiredKeyedHistogram) {
+ n += gExpiredKeyedHistogram->SizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ if (gExpiredHistogram) {
+ n += gExpiredHistogram->SizeOfIncludingThis(aMallocSizeOf);
+ }
+
+ return n;
+}
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: GeckoView specific helpers
+
+namespace base {
+class PersistedSampleSet : public base::Histogram::SampleSet {
+ public:
+ explicit PersistedSampleSet(const nsTArray<base::Histogram::Count>& aCounts,
+ int64_t aSampleSum);
+};
+
+PersistedSampleSet::PersistedSampleSet(
+ const nsTArray<base::Histogram::Count>& aCounts, int64_t aSampleSum) {
+ // Initialize the data in the base class. See Histogram::SampleSet
+ // for the fields documentation.
+ const size_t numCounts = aCounts.Length();
+ counts_.SetLength(numCounts);
+
+ for (size_t i = 0; i < numCounts; i++) {
+ counts_[i] = aCounts[i];
+ redundant_count_ += aCounts[i];
+ }
+ sum_ = aSampleSum;
+};
+} // namespace base
+
+namespace {
+/**
+ * Helper function to write histogram properties to JSON.
+ * Please note that this needs to be called between
+ * StartObjectProperty/EndObject calls that mark the histogram's
+ * JSON creation.
+ */
+void internal_ReflectHistogramToJSON(const HistogramSnapshotData& aSnapshot,
+ mozilla::JSONWriter& aWriter) {
+ aWriter.IntProperty("sum", aSnapshot.mSampleSum);
+
+ // Fill the "counts" property.
+ aWriter.StartArrayProperty("counts");
+ for (size_t i = 0; i < aSnapshot.mBucketCounts.Length(); i++) {
+ aWriter.IntElement(aSnapshot.mBucketCounts[i]);
+ }
+ aWriter.EndArray();
+}
+
+bool internal_CanRecordHistogram(const HistogramID id, ProcessID aProcessType) {
+ // Check if we are allowed to record the data.
+ if (!CanRecordDataset(gHistogramInfos[id].dataset, internal_CanRecordBase(),
+ internal_CanRecordExtended())) {
+ return false;
+ }
+
+ // Check if we're allowed to record in the given process.
+ if (aProcessType == ProcessID::Parent && !internal_IsRecordingEnabled(id)) {
+ return false;
+ }
+
+ if (aProcessType != ProcessID::Parent &&
+ !CanRecordInProcess(gHistogramInfos[id].record_in_processes,
+ aProcessType)) {
+ return false;
+ }
+
+ // Don't record if the current platform is not enabled
+ if (!CanRecordProduct(gHistogramInfos[id].products)) {
+ return false;
+ }
+
+ return true;
+}
+
+nsresult internal_ParseHistogramData(
+ JSContext* aCx, JS::Handle<JS::PropertyKey> aEntryId,
+ JS::Handle<JSObject*> aContainerObj, nsACString& aOutName,
+ nsTArray<base::Histogram::Count>& aOutCountArray, int64_t& aOutSum) {
+ // Get the histogram name.
+ nsAutoJSString histogramName;
+ if (!histogramName.init(aCx, aEntryId)) {
+ JS_ClearPendingException(aCx);
+ return NS_ERROR_FAILURE;
+ }
+
+ CopyUTF16toUTF8(histogramName, aOutName);
+
+ // Get the data for this histogram.
+ JS::Rooted<JS::Value> histogramData(aCx);
+ if (!JS_GetPropertyById(aCx, aContainerObj, aEntryId, &histogramData)) {
+ JS_ClearPendingException(aCx);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!histogramData.isObject()) {
+ // base::Histogram data need to be an object. If that's not the case, skip
+ // it and try to load the rest of the data.
+ return NS_ERROR_FAILURE;
+ }
+
+ // Get the "sum" property.
+ JS::Rooted<JS::Value> sumValue(aCx);
+ JS::Rooted<JSObject*> histogramObj(aCx, &histogramData.toObject());
+ if (!JS_GetProperty(aCx, histogramObj, "sum", &sumValue)) {
+ JS_ClearPendingException(aCx);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!JS::ToInt64(aCx, sumValue, &aOutSum)) {
+ JS_ClearPendingException(aCx);
+ return NS_ERROR_FAILURE;
+ }
+
+ // Get the "counts" array.
+ JS::Rooted<JS::Value> countsArray(aCx);
+ bool countsIsArray = false;
+ if (!JS_GetProperty(aCx, histogramObj, "counts", &countsArray) ||
+ !JS::IsArrayObject(aCx, countsArray, &countsIsArray)) {
+ JS_ClearPendingException(aCx);
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!countsIsArray) {
+ // The "counts" property needs to be an array. If this is not the case,
+ // skip this histogram.
+ return NS_ERROR_FAILURE;
+ }
+
+ // Get the length of the array.
+ uint32_t countsLen = 0;
+ JS::Rooted<JSObject*> countsArrayObj(aCx, &countsArray.toObject());
+ if (!JS::GetArrayLength(aCx, countsArrayObj, &countsLen)) {
+ JS_ClearPendingException(aCx);
+ return NS_ERROR_FAILURE;
+ }
+
+ // Parse the "counts" in the array.
+ for (uint32_t arrayIdx = 0; arrayIdx < countsLen; arrayIdx++) {
+ JS::Rooted<JS::Value> elementValue(aCx);
+ int countAsInt = 0;
+ if (!JS_GetElement(aCx, countsArrayObj, arrayIdx, &elementValue) ||
+ !JS::ToInt32(aCx, elementValue, &countAsInt)) {
+ JS_ClearPendingException(aCx);
+ return NS_ERROR_FAILURE;
+ }
+ aOutCountArray.AppendElement(countAsInt);
+ }
+
+ return NS_OK;
+}
+
+} // Anonymous namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PUBLIC: GeckoView serialization/deserialization functions.
+
+nsresult TelemetryHistogram::SerializeHistograms(mozilla::JSONWriter& aWriter) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Only save histograms in the parent process");
+ if (!XRE_IsParentProcess()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Include the GPU process in histogram snapshots only if we actually tried
+ // to launch a process for it.
+ bool includeGPUProcess = internal_AttemptedGPUProcess();
+
+ // Take a snapshot of the histograms.
+ HistogramProcessSnapshotsArray processHistArray;
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ // We always request the "opt-in"/"prerelease" dataset: we internally
+ // record the right subset, so this will only return "prerelease" if
+ // it was recorded.
+ if (NS_FAILED(internal_GetHistogramsSnapshot(
+ locker, "main"_ns, nsITelemetry::DATASET_PRERELEASE_CHANNELS,
+ false /* aClearSubsession */, includeGPUProcess,
+ false /* aFilterTest */, processHistArray))) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Make the JSON calls on the stashed histograms for every process
+ for (uint32_t process = 0; process < processHistArray.length(); ++process) {
+ aWriter.StartObjectProperty(
+ mozilla::MakeStringSpan(GetNameForProcessID(ProcessID(process))));
+
+ for (const HistogramSnapshotInfo& hData : processHistArray[process]) {
+ HistogramID id = hData.histogramID;
+
+ aWriter.StartObjectProperty(
+ mozilla::MakeStringSpan(gHistogramInfos[id].name()));
+ internal_ReflectHistogramToJSON(hData.data, aWriter);
+ aWriter.EndObject();
+ }
+ aWriter.EndObject();
+ }
+
+ return NS_OK;
+}
+
+nsresult TelemetryHistogram::SerializeKeyedHistograms(
+ mozilla::JSONWriter& aWriter) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Only save keyed histograms in the parent process");
+ if (!XRE_IsParentProcess()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Include the GPU process in histogram snapshots only if we actually tried
+ // to launch a process for it.
+ bool includeGPUProcess = internal_AttemptedGPUProcess();
+
+ // Take a snapshot of the keyed histograms.
+ KeyedHistogramProcessSnapshotsArray processHistArray;
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+ // We always request the "opt-in"/"prerelease" dataset: we internally
+ // record the right subset, so this will only return "prerelease" if
+ // it was recorded.
+ if (NS_FAILED(internal_GetKeyedHistogramsSnapshot(
+ locker, "main"_ns, nsITelemetry::DATASET_PRERELEASE_CHANNELS,
+ false /* aClearSubsession */, includeGPUProcess,
+ false /* aFilterTest */, processHistArray))) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Serialize the keyed histograms for every process.
+ for (uint32_t process = 0; process < processHistArray.length(); ++process) {
+ aWriter.StartObjectProperty(
+ mozilla::MakeStringSpan(GetNameForProcessID(ProcessID(process))));
+
+ const KeyedHistogramSnapshotsArray& hArray = processHistArray[process];
+ for (size_t i = 0; i < hArray.length(); ++i) {
+ const KeyedHistogramSnapshotInfo& hData = hArray[i];
+ HistogramID id = hData.histogramId;
+ const HistogramInfo& info = gHistogramInfos[id];
+
+ aWriter.StartObjectProperty(mozilla::MakeStringSpan(info.name()));
+
+ // Each key is a new object with a "sum" and a "counts" property.
+ for (const auto& entry : hData.data) {
+ const HistogramSnapshotData& keyData = entry.GetData();
+ aWriter.StartObjectProperty(PromiseFlatCString(entry.GetKey()));
+ internal_ReflectHistogramToJSON(keyData, aWriter);
+ aWriter.EndObject();
+ }
+
+ aWriter.EndObject();
+ }
+ aWriter.EndObject();
+ }
+
+ return NS_OK;
+}
+
+nsresult TelemetryHistogram::DeserializeHistograms(
+ JSContext* aCx, JS::Handle<JS::Value> aData) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Only load histograms in the parent process");
+ if (!XRE_IsParentProcess()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Telemetry is disabled. This should never happen, but let's leave this check
+ // for consistency with other histogram updates routines.
+ if (!internal_CanRecordBase()) {
+ return NS_OK;
+ }
+
+ typedef std::tuple<nsCString, nsTArray<base::Histogram::Count>, int64_t>
+ PersistedHistogramTuple;
+ typedef mozilla::Vector<PersistedHistogramTuple> PersistedHistogramArray;
+ typedef mozilla::Vector<PersistedHistogramArray> PersistedHistogramStorage;
+
+ // Before updating the histograms, we need to get the data out of the JS
+ // wrappers. We can't hold the histogram mutex while handling JS stuff.
+ // Build a <histogram name, value> map.
+ JS::Rooted<JSObject*> histogramDataObj(aCx, &aData.toObject());
+ JS::Rooted<JS::IdVector> processes(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, histogramDataObj, &processes)) {
+ // We can't even enumerate the processes in the loaded data, so
+ // there is nothing we could recover from the persistence file. Bail out.
+ JS_ClearPendingException(aCx);
+ return NS_ERROR_FAILURE;
+ }
+
+ // Make sure we have enough storage for all the processes.
+ PersistedHistogramStorage histogramsToUpdate;
+ if (!histogramsToUpdate.resize(static_cast<uint32_t>(ProcessID::Count))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ // The following block of code attempts to extract as much data as possible
+ // from the serialized JSON, even in case of light data corruptions: if, for
+ // example, the data for a single process is corrupted or is in an unexpected
+ // form, we press on and attempt to load the data for the other processes.
+ JS::Rooted<JS::PropertyKey> process(aCx);
+ for (auto& processVal : processes) {
+ // This is required as JS API calls require an Handle<jsid> and not a
+ // plain jsid.
+ process = processVal;
+ // Get the process name.
+ nsAutoJSString processNameJS;
+ if (!processNameJS.init(aCx, process)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Make sure it's valid. Note that this is safe to call outside
+ // of a locked section.
+ NS_ConvertUTF16toUTF8 processName(processNameJS);
+ ProcessID processID = GetIDForProcessName(processName.get());
+ if (processID == ProcessID::Count) {
+ NS_WARNING(
+ nsPrintfCString("Failed to get process ID for %s", processName.get())
+ .get());
+ continue;
+ }
+
+ // And its probes.
+ JS::Rooted<JS::Value> processData(aCx);
+ if (!JS_GetPropertyById(aCx, histogramDataObj, process, &processData)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ if (!processData.isObject()) {
+ // |processData| should be an object containing histograms. If this is
+ // not the case, silently skip and try to load the data for the other
+ // processes.
+ continue;
+ }
+
+ // Iterate through each histogram.
+ JS::Rooted<JSObject*> processDataObj(aCx, &processData.toObject());
+ JS::Rooted<JS::IdVector> histograms(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, processDataObj, &histograms)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Get a reference to the deserialized data for this process.
+ PersistedHistogramArray& deserializedProcessData =
+ histogramsToUpdate[static_cast<uint32_t>(processID)];
+
+ JS::Rooted<JS::PropertyKey> histogram(aCx);
+ for (auto& histogramVal : histograms) {
+ histogram = histogramVal;
+
+ int64_t sum = 0;
+ nsTArray<base::Histogram::Count> deserializedCounts;
+ nsCString histogramName;
+ if (NS_FAILED(internal_ParseHistogramData(aCx, histogram, processDataObj,
+ histogramName,
+ deserializedCounts, sum))) {
+ continue;
+ }
+
+ // Finally append the deserialized data to the storage.
+ if (!deserializedProcessData.emplaceBack(std::make_tuple(
+ std::move(histogramName), std::move(deserializedCounts), sum))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+ }
+
+ // Update the histogram storage.
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+
+ for (uint32_t process = 0; process < histogramsToUpdate.length();
+ ++process) {
+ PersistedHistogramArray& processArray = histogramsToUpdate[process];
+
+ for (auto& histogramData : processArray) {
+ // Attempt to get the corresponding ID for the deserialized histogram
+ // name.
+ HistogramID id;
+ if (NS_FAILED(internal_GetHistogramIdByName(
+ locker, std::get<0>(histogramData), &id))) {
+ continue;
+ }
+
+ ProcessID procID = static_cast<ProcessID>(process);
+ if (!internal_CanRecordHistogram(id, procID)) {
+ // We're not allowed to record this, so don't try to restore it.
+ continue;
+ }
+
+ // Get the Histogram instance: this will instantiate it if it doesn't
+ // exist.
+ Histogram* w = internal_GetHistogramById(locker, id, procID);
+ MOZ_ASSERT(w);
+
+ if (!w || w->IsExpired()) {
+ continue;
+ }
+
+ base::Histogram* h = nullptr;
+ constexpr auto store = "main"_ns;
+ if (!w->GetHistogram(store, &h)) {
+ continue;
+ }
+ MOZ_ASSERT(h);
+
+ if (!h) {
+ // Don't restore expired histograms.
+ continue;
+ }
+
+ // Make sure that histogram counts have matching sizes. If not,
+ // |AddSampleSet| will fail and crash.
+ size_t numCounts = std::get<1>(histogramData).Length();
+ if (h->bucket_count() != numCounts) {
+ MOZ_ASSERT(false,
+ "The number of restored buckets does not match with the "
+ "on in the definition");
+ continue;
+ }
+
+ // Update the data for the histogram.
+ h->AddSampleSet(base::PersistedSampleSet(
+ std::move(std::get<1>(histogramData)), std::get<2>(histogramData)));
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult TelemetryHistogram::DeserializeKeyedHistograms(
+ JSContext* aCx, JS::Handle<JS::Value> aData) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Only load keyed histograms in the parent process");
+ if (!XRE_IsParentProcess()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Telemetry is disabled. This should never happen, but let's leave this check
+ // for consistency with other histogram updates routines.
+ if (!internal_CanRecordBase()) {
+ return NS_OK;
+ }
+
+ typedef std::tuple<nsCString, nsCString, nsTArray<base::Histogram::Count>,
+ int64_t>
+ PersistedKeyedHistogramTuple;
+ typedef mozilla::Vector<PersistedKeyedHistogramTuple>
+ PersistedKeyedHistogramArray;
+ typedef mozilla::Vector<PersistedKeyedHistogramArray>
+ PersistedKeyedHistogramStorage;
+
+ // Before updating the histograms, we need to get the data out of the JS
+ // wrappers. We can't hold the histogram mutex while handling JS stuff.
+ // Build a <histogram name, value> map.
+ JS::Rooted<JSObject*> histogramDataObj(aCx, &aData.toObject());
+ JS::Rooted<JS::IdVector> processes(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, histogramDataObj, &processes)) {
+ // We can't even enumerate the processes in the loaded data, so
+ // there is nothing we could recover from the persistence file. Bail out.
+ JS_ClearPendingException(aCx);
+ return NS_ERROR_FAILURE;
+ }
+
+ // Make sure we have enough storage for all the processes.
+ PersistedKeyedHistogramStorage histogramsToUpdate;
+ if (!histogramsToUpdate.resize(static_cast<uint32_t>(ProcessID::Count))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+
+ // The following block of code attempts to extract as much data as possible
+ // from the serialized JSON, even in case of light data corruptions: if, for
+ // example, the data for a single process is corrupted or is in an unexpected
+ // form, we press on and attempt to load the data for the other processes.
+ JS::Rooted<JS::PropertyKey> process(aCx);
+ for (auto& processVal : processes) {
+ // This is required as JS API calls require an Handle<jsid> and not a
+ // plain jsid.
+ process = processVal;
+ // Get the process name.
+ nsAutoJSString processNameJS;
+ if (!processNameJS.init(aCx, process)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Make sure it's valid. Note that this is safe to call outside
+ // of a locked section.
+ NS_ConvertUTF16toUTF8 processName(processNameJS);
+ ProcessID processID = GetIDForProcessName(processName.get());
+ if (processID == ProcessID::Count) {
+ NS_WARNING(
+ nsPrintfCString("Failed to get process ID for %s", processName.get())
+ .get());
+ continue;
+ }
+
+ // And its probes.
+ JS::Rooted<JS::Value> processData(aCx);
+ if (!JS_GetPropertyById(aCx, histogramDataObj, process, &processData)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ if (!processData.isObject()) {
+ // |processData| should be an object containing histograms. If this is
+ // not the case, silently skip and try to load the data for the other
+ // processes.
+ continue;
+ }
+
+ // Iterate through each keyed histogram.
+ JS::Rooted<JSObject*> processDataObj(aCx, &processData.toObject());
+ JS::Rooted<JS::IdVector> histograms(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, processDataObj, &histograms)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Get a reference to the deserialized data for this process.
+ PersistedKeyedHistogramArray& deserializedProcessData =
+ histogramsToUpdate[static_cast<uint32_t>(processID)];
+
+ JS::Rooted<JS::PropertyKey> histogram(aCx);
+ for (auto& histogramVal : histograms) {
+ histogram = histogramVal;
+ // Get the histogram name.
+ nsAutoJSString histogramName;
+ if (!histogramName.init(aCx, histogram)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Get the data for this histogram.
+ JS::Rooted<JS::Value> histogramData(aCx);
+ if (!JS_GetPropertyById(aCx, processDataObj, histogram, &histogramData)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Iterate through each key in the histogram.
+ JS::Rooted<JSObject*> keysDataObj(aCx, &histogramData.toObject());
+ JS::Rooted<JS::IdVector> keys(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, keysDataObj, &keys)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ JS::Rooted<JS::PropertyKey> key(aCx);
+ for (auto& keyVal : keys) {
+ key = keyVal;
+
+ int64_t sum = 0;
+ nsTArray<base::Histogram::Count> deserializedCounts;
+ nsCString keyName;
+ if (NS_FAILED(internal_ParseHistogramData(
+ aCx, key, keysDataObj, keyName, deserializedCounts, sum))) {
+ continue;
+ }
+
+ // Finally append the deserialized data to the storage.
+ if (!deserializedProcessData.emplaceBack(std::make_tuple(
+ nsCString(NS_ConvertUTF16toUTF8(histogramName)),
+ std::move(keyName), std::move(deserializedCounts), sum))) {
+ return NS_ERROR_OUT_OF_MEMORY;
+ }
+ }
+ }
+ }
+
+ // Update the keyed histogram storage.
+ {
+ StaticMutexAutoLock locker(gTelemetryHistogramMutex);
+
+ for (uint32_t process = 0; process < histogramsToUpdate.length();
+ ++process) {
+ PersistedKeyedHistogramArray& processArray = histogramsToUpdate[process];
+
+ for (auto& histogramData : processArray) {
+ // Attempt to get the corresponding ID for the deserialized histogram
+ // name.
+ HistogramID id;
+ if (NS_FAILED(internal_GetHistogramIdByName(
+ locker, std::get<0>(histogramData), &id))) {
+ continue;
+ }
+
+ ProcessID procID = static_cast<ProcessID>(process);
+ if (!internal_CanRecordHistogram(id, procID)) {
+ // We're not allowed to record this, so don't try to restore it.
+ continue;
+ }
+
+ KeyedHistogram* keyed = internal_GetKeyedHistogramById(id, procID);
+ MOZ_ASSERT(keyed);
+
+ if (!keyed || keyed->IsExpired()) {
+ // Don't restore if we don't have a destination storage or the
+ // histogram is expired.
+ continue;
+ }
+
+ // Get data for the key we're looking for.
+ base::Histogram* h = nullptr;
+ if (NS_FAILED(keyed->GetHistogram("main"_ns, std::get<1>(histogramData),
+ &h))) {
+ continue;
+ }
+ MOZ_ASSERT(h);
+
+ if (!h) {
+ // Don't restore if we don't have a destination storage.
+ continue;
+ }
+
+ // Make sure that histogram counts have matching sizes. If not,
+ // |AddSampleSet| will fail and crash.
+ size_t numCounts = std::get<2>(histogramData).Length();
+ if (h->bucket_count() != numCounts) {
+ MOZ_ASSERT(false,
+ "The number of restored buckets does not match with the "
+ "on in the definition");
+ continue;
+ }
+
+ // Update the data for the histogram.
+ h->AddSampleSet(base::PersistedSampleSet(
+ std::move(std::get<2>(histogramData)), std::get<3>(histogramData)));
+ }
+ }
+ }
+
+ return NS_OK;
+}
diff --git a/toolkit/components/telemetry/core/TelemetryHistogram.h b/toolkit/components/telemetry/core/TelemetryHistogram.h
new file mode 100644
index 0000000000..9f415f3637
--- /dev/null
+++ b/toolkit/components/telemetry/core/TelemetryHistogram.h
@@ -0,0 +1,124 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TelemetryHistogram_h__
+#define TelemetryHistogram_h__
+
+#include "mozilla/TelemetryComms.h"
+#include "mozilla/TelemetryHistogramEnums.h"
+#include "mozilla/TelemetryProcessEnums.h"
+#include "nsXULAppAPI.h"
+#include "TelemetryCommon.h"
+
+namespace mozilla {
+// This is only used for the GeckoView persistence.
+class JSONWriter;
+} // namespace mozilla
+
+// This module is internal to Telemetry. It encapsulates Telemetry's
+// histogram accumulation and storage logic. It should only be used by
+// Telemetry.cpp. These functions should not be used anywhere else.
+// For the public interface to Telemetry functionality, see Telemetry.h.
+
+namespace TelemetryHistogram {
+
+void InitializeGlobalState(bool canRecordBase, bool canRecordExtended);
+void DeInitializeGlobalState();
+#ifdef DEBUG
+bool GlobalStateHasBeenInitialized();
+#endif
+
+bool CanRecordBase();
+void SetCanRecordBase(bool b);
+bool CanRecordExtended();
+void SetCanRecordExtended(bool b);
+
+void InitHistogramRecordingEnabled();
+void SetHistogramRecordingEnabled(mozilla::Telemetry::HistogramID aID,
+ bool aEnabled);
+
+nsresult SetHistogramRecordingEnabled(const nsACString& id, bool aEnabled);
+
+void Accumulate(mozilla::Telemetry::HistogramID aHistogram, uint32_t aSample);
+void Accumulate(mozilla::Telemetry::HistogramID aHistogram,
+ const nsTArray<uint32_t>& aSamples);
+void Accumulate(mozilla::Telemetry::HistogramID aID, const nsCString& aKey,
+ uint32_t aSample);
+void Accumulate(mozilla::Telemetry::HistogramID aID, const nsCString& aKey,
+ const nsTArray<uint32_t>& aSamples);
+/*
+ * Accumulate a sample into the named histogram.
+ *
+ * Returns NS_OK on success.
+ * Returns NS_ERROR_NOT_AVAILABLE if recording Telemetry is disabled.
+ * Returns NS_ERROR_FAILURE on other errors.
+ */
+nsresult Accumulate(const char* name, uint32_t sample);
+
+/*
+ * Accumulate a sample into the named keyed histogram by key.
+ *
+ * Returns NS_OK on success.
+ * Returns NS_ERROR_NOT_AVAILABLE if recording Telemetry is disabled.
+ * Returns NS_ERROR_FAILURE on other errors.
+ */
+nsresult Accumulate(const char* name, const nsCString& key, uint32_t sample);
+
+void AccumulateCategorical(mozilla::Telemetry::HistogramID aId,
+ const nsCString& aLabel);
+void AccumulateCategorical(mozilla::Telemetry::HistogramID aId,
+ const nsTArray<nsCString>& aLabels);
+
+void AccumulateChild(
+ mozilla::Telemetry::ProcessID aProcessType,
+ const nsTArray<mozilla::Telemetry::HistogramAccumulation>& aAccumulations);
+void AccumulateChildKeyed(
+ mozilla::Telemetry::ProcessID aProcessType,
+ const nsTArray<mozilla::Telemetry::KeyedHistogramAccumulation>&
+ aAccumulations);
+
+/**
+ * Append the list of registered stores to the given set.
+ */
+nsresult GetAllStores(mozilla::Telemetry::Common::StringHashSet& set);
+
+nsresult GetCategoricalHistogramLabels(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult);
+
+nsresult GetHistogramById(const nsACString& name, JSContext* cx,
+ JS::MutableHandle<JS::Value> ret);
+
+nsresult GetKeyedHistogramById(const nsACString& name, JSContext* cx,
+ JS::MutableHandle<JS::Value> ret);
+
+const char* GetHistogramName(mozilla::Telemetry::HistogramID id);
+
+nsresult CreateHistogramSnapshots(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult,
+ const nsACString& aStore,
+ unsigned int aDataset, bool aClearSubsession,
+ bool aFilterTest = false);
+
+nsresult GetKeyedHistogramSnapshots(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult,
+ const nsACString& aStore,
+ unsigned int aDataset,
+ bool aClearSubsession,
+ bool aFilterTest = false);
+
+size_t GetHistogramSizesOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
+
+// These functions are only meant to be used for GeckoView persistence.
+// They are responsible for updating in-memory probes with the data persisted
+// on the disk and vice-versa.
+nsresult SerializeHistograms(mozilla::JSONWriter& aWriter);
+nsresult SerializeKeyedHistograms(mozilla::JSONWriter& aWriter);
+nsresult DeserializeHistograms(JSContext* aCx, JS::Handle<JS::Value> aData);
+nsresult DeserializeKeyedHistograms(JSContext* aCx,
+ JS::Handle<JS::Value> aData);
+
+} // namespace TelemetryHistogram
+
+#endif // TelemetryHistogram_h__
diff --git a/toolkit/components/telemetry/core/TelemetryScalar.cpp b/toolkit/components/telemetry/core/TelemetryScalar.cpp
new file mode 100644
index 0000000000..8a121e8f3f
--- /dev/null
+++ b/toolkit/components/telemetry/core/TelemetryScalar.cpp
@@ -0,0 +1,4190 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TelemetryScalar.h"
+
+#include "geckoview/streaming/GeckoViewStreamingTelemetry.h"
+#include "ipc/TelemetryIPCAccumulator.h"
+#include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject
+#include "js/PropertyAndElement.h" // JS_DefineProperty, JS_DefineUCProperty, JS_Enumerate, JS_GetElement, JS_GetProperty, JS_GetPropertyById, JS_HasProperty
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/JSONWriter.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/TelemetryComms.h"
+#include "mozilla/Unused.h"
+#include "nsBaseHashtable.h"
+#include "nsClassHashtable.h"
+#include "nsContentUtils.h"
+#include "nsHashKeys.h"
+#include "nsITelemetry.h"
+#include "nsIVariant.h"
+#include "nsIXPConnect.h"
+#include "nsJSUtils.h"
+#include "nsPrintfCString.h"
+#include "nsVariant.h"
+#include "TelemetryScalarData.h"
+
+using mozilla::MakeUnique;
+using mozilla::Nothing;
+using mozilla::Preferences;
+using mozilla::Some;
+using mozilla::StaticAutoPtr;
+using mozilla::StaticMutex;
+using mozilla::StaticMutexAutoLock;
+using mozilla::UniquePtr;
+using mozilla::Telemetry::DynamicScalarDefinition;
+using mozilla::Telemetry::KeyedScalarAction;
+using mozilla::Telemetry::ProcessID;
+using mozilla::Telemetry::ScalarAction;
+using mozilla::Telemetry::ScalarActionType;
+using mozilla::Telemetry::ScalarID;
+using mozilla::Telemetry::ScalarVariant;
+using mozilla::Telemetry::Common::AutoHashtable;
+using mozilla::Telemetry::Common::CanRecordDataset;
+using mozilla::Telemetry::Common::CanRecordProduct;
+using mozilla::Telemetry::Common::GetCurrentProduct;
+using mozilla::Telemetry::Common::GetIDForProcessName;
+using mozilla::Telemetry::Common::GetNameForProcessID;
+using mozilla::Telemetry::Common::IsExpiredVersion;
+using mozilla::Telemetry::Common::IsInDataset;
+using mozilla::Telemetry::Common::IsValidIdentifierString;
+using mozilla::Telemetry::Common::LogToBrowserConsole;
+using mozilla::Telemetry::Common::RecordedProcessType;
+using mozilla::Telemetry::Common::StringHashSet;
+using mozilla::Telemetry::Common::SupportedProduct;
+
+namespace TelemetryIPCAccumulator = mozilla::TelemetryIPCAccumulator;
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// Naming: there are two kinds of functions in this file:
+//
+// * Functions named internal_*: these can only be reached via an
+// interface function (TelemetryScalar::*). If they access shared
+// state, they require the interface function to have acquired
+// |gTelemetryScalarMutex| to ensure thread safety.
+//
+// * Functions named TelemetryScalar::*. This is the external interface.
+// Entries and exits to these functions are serialised using
+// |gTelemetryScalarsMutex|.
+//
+// Avoiding races and deadlocks:
+//
+// All functions in the external interface (TelemetryScalar::*) are
+// serialised using the mutex |gTelemetryScalarsMutex|. This means
+// that the external interface is thread-safe. But it also brings
+// a danger of deadlock if any function in the external interface can
+// get back to that interface. That is, we will deadlock on any call
+// chain like this
+//
+// TelemetryScalar::* -> .. any functions .. -> TelemetryScalar::*
+//
+// To reduce the danger of that happening, observe the following rules:
+//
+// * No function in TelemetryScalar::* may directly call, nor take the
+// address of, any other function in TelemetryScalar::*.
+//
+// * No internal function internal_* may call, nor take the address
+// of, any function in TelemetryScalar::*.
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE TYPES
+
+namespace {
+
+const uint32_t kMaximumNumberOfKeys = 100;
+const uint32_t kMaxEventSummaryKeys = 500;
+const uint32_t kMaximumKeyStringLength = 72;
+const uint32_t kMaximumStringValueLength = 50;
+// The category and scalar name maximum lengths are used by the dynamic
+// scalar registration function and must match the constants used by
+// the 'parse_scalars.py' script for static scalars.
+const uint32_t kMaximumCategoryNameLength = 40;
+const uint32_t kMaximumScalarNameLength = 40;
+const uint32_t kScalarCount =
+ static_cast<uint32_t>(mozilla::Telemetry::ScalarID::ScalarCount);
+
+// To stop growing unbounded in memory while waiting for scalar deserialization
+// to finish, we immediately apply pending operations if the array reaches
+// a certain high water mark of elements.
+const size_t kScalarActionsArrayHighWaterMark = 10000;
+
+const char* TEST_SCALAR_PREFIX = "telemetry.test.";
+
+// The max offset supported by gScalarStoresTable for static scalars' stores.
+// Also the sentinel value (with store_count == 0) for just the sole "main"
+// store.
+const uint32_t kMaxStaticStoreOffset = UINT16_MAX;
+
+enum class ScalarResult : uint8_t {
+ // Nothing went wrong.
+ Ok,
+ // General Scalar Errors
+ NotInitialized,
+ CannotUnpackVariant,
+ CannotRecordInProcess,
+ CannotRecordDataset,
+ KeyedTypeMismatch,
+ UnknownScalar,
+ OperationNotSupported,
+ InvalidType,
+ InvalidValue,
+ // Keyed Scalar Errors
+ KeyIsEmpty,
+ KeyTooLong,
+ TooManyKeys,
+ KeyNotAllowed,
+ // String Scalar Errors
+ StringTooLong,
+ // Unsigned Scalar Errors
+ UnsignedNegativeValue,
+ UnsignedTruncatedValue,
+};
+
+// A common identifier for both built-in and dynamic scalars.
+struct ScalarKey {
+ uint32_t id;
+ bool dynamic;
+};
+
+// Dynamic scalar store names.
+StaticAutoPtr<nsTArray<RefPtr<nsAtom>>> gDynamicStoreNames;
+
+/**
+ * Scalar information for dynamic definitions.
+ */
+struct DynamicScalarInfo : BaseScalarInfo {
+ nsCString mDynamicName;
+ bool mDynamicExpiration;
+ uint32_t store_count;
+ uint32_t store_offset;
+
+ DynamicScalarInfo(uint32_t aKind, bool aRecordOnRelease, bool aExpired,
+ const nsACString& aName, bool aKeyed, bool aBuiltin,
+ const nsTArray<nsCString>& aStores)
+ : BaseScalarInfo(aKind,
+ aRecordOnRelease
+ ? nsITelemetry::DATASET_ALL_CHANNELS
+ : nsITelemetry::DATASET_PRERELEASE_CHANNELS,
+ RecordedProcessType::All, aKeyed, 0, 0,
+ GetCurrentProduct(), aBuiltin),
+ mDynamicName(aName),
+ mDynamicExpiration(aExpired) {
+ store_count = aStores.Length();
+ if (store_count == 0) {
+ store_count = 1;
+ store_offset = kMaxStaticStoreOffset;
+ } else {
+ store_offset = kMaxStaticStoreOffset + 1 + gDynamicStoreNames->Length();
+ for (const auto& storeName : aStores) {
+ gDynamicStoreNames->AppendElement(NS_Atomize(storeName));
+ }
+ MOZ_ASSERT(
+ gDynamicStoreNames->Length() < UINT32_MAX - kMaxStaticStoreOffset - 1,
+ "Too many dynamic scalar store names. Overflow.");
+ }
+ };
+
+ // The following functions will read the stored text
+ // instead of looking it up in the statically generated
+ // tables.
+ const char* name() const override;
+ const char* expiration() const override;
+
+ uint32_t storeCount() const override;
+ uint32_t storeOffset() const override;
+};
+
+const char* DynamicScalarInfo::name() const { return mDynamicName.get(); }
+
+const char* DynamicScalarInfo::expiration() const {
+ // Dynamic scalars can either be expired or not (boolean flag).
+ // Return an appropriate version string to leverage the scalar expiration
+ // logic.
+ return mDynamicExpiration ? "1.0" : "never";
+}
+
+uint32_t DynamicScalarInfo::storeOffset() const { return store_offset; }
+uint32_t DynamicScalarInfo::storeCount() const { return store_count; }
+
+typedef nsBaseHashtableET<nsDepCharHashKey, ScalarKey> CharPtrEntryType;
+typedef AutoHashtable<CharPtrEntryType> ScalarMapType;
+
+// Dynamic scalar definitions.
+StaticAutoPtr<nsTArray<DynamicScalarInfo>> gDynamicScalarInfo;
+
+const BaseScalarInfo& internal_GetScalarInfo(const StaticMutexAutoLock& lock,
+ const ScalarKey& aId) {
+ if (!aId.dynamic) {
+ return gScalars[aId.id];
+ }
+
+ return (*gDynamicScalarInfo)[aId.id];
+}
+
+bool IsValidEnumId(mozilla::Telemetry::ScalarID aID) {
+ return aID < mozilla::Telemetry::ScalarID::ScalarCount;
+}
+
+bool internal_IsValidId(const StaticMutexAutoLock& lock, const ScalarKey& aId) {
+ // Please note that this function needs to be called with the scalar
+ // mutex being acquired: other functions might be messing with
+ // |gDynamicScalarInfo|.
+ return aId.dynamic
+ ? (aId.id < gDynamicScalarInfo->Length())
+ : IsValidEnumId(static_cast<mozilla::Telemetry::ScalarID>(aId.id));
+}
+
+/**
+ * Convert a nsIVariant to a mozilla::Variant, which is used for
+ * accumulating child process scalars.
+ */
+ScalarResult GetVariantFromIVariant(nsIVariant* aInput, uint32_t aScalarKind,
+ mozilla::Maybe<ScalarVariant>& aOutput) {
+ switch (aScalarKind) {
+ case nsITelemetry::SCALAR_TYPE_COUNT: {
+ uint32_t val = 0;
+ nsresult rv = aInput->GetAsUint32(&val);
+ if (NS_FAILED(rv)) {
+ return ScalarResult::CannotUnpackVariant;
+ }
+ aOutput = mozilla::Some(mozilla::AsVariant(val));
+ break;
+ }
+ case nsITelemetry::SCALAR_TYPE_STRING: {
+ nsString val;
+ nsresult rv = aInput->GetAsAString(val);
+ if (NS_FAILED(rv)) {
+ return ScalarResult::CannotUnpackVariant;
+ }
+ aOutput = mozilla::Some(mozilla::AsVariant(val));
+ break;
+ }
+ case nsITelemetry::SCALAR_TYPE_BOOLEAN: {
+ bool val = false;
+ nsresult rv = aInput->GetAsBool(&val);
+ if (NS_FAILED(rv)) {
+ return ScalarResult::CannotUnpackVariant;
+ }
+ aOutput = mozilla::Some(mozilla::AsVariant(val));
+ break;
+ }
+ default:
+ MOZ_ASSERT(false, "Unknown scalar kind.");
+ return ScalarResult::UnknownScalar;
+ }
+ return ScalarResult::Ok;
+}
+
+/**
+ * Write a nsIVariant with a JSONWriter, used for GeckoView persistence.
+ */
+nsresult WriteVariantToJSONWriter(
+ uint32_t aScalarType, nsIVariant* aInputValue,
+ const mozilla::Span<const char>& aPropertyName,
+ mozilla::JSONWriter& aWriter) {
+ MOZ_ASSERT(aInputValue);
+
+ switch (aScalarType) {
+ case nsITelemetry::SCALAR_TYPE_COUNT: {
+ uint32_t val = 0;
+ nsresult rv = aInputValue->GetAsUint32(&val);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aWriter.IntProperty(aPropertyName, val);
+ break;
+ }
+ case nsITelemetry::SCALAR_TYPE_STRING: {
+ nsCString val;
+ nsresult rv = aInputValue->GetAsACString(val);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aWriter.StringProperty(aPropertyName, val);
+ break;
+ }
+ case nsITelemetry::SCALAR_TYPE_BOOLEAN: {
+ bool val = false;
+ nsresult rv = aInputValue->GetAsBool(&val);
+ NS_ENSURE_SUCCESS(rv, rv);
+ aWriter.BoolProperty(aPropertyName, val);
+ break;
+ }
+ default:
+ MOZ_ASSERT(false, "Unknown scalar kind.");
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+// Implements the methods for ScalarInfo.
+const char* ScalarInfo::name() const {
+ return &gScalarsStringTable[this->name_offset];
+}
+
+const char* ScalarInfo::expiration() const {
+ return &gScalarsStringTable[this->expiration_offset];
+}
+
+/**
+ * The base scalar object, that serves as a common ancestor for storage
+ * purposes.
+ */
+class ScalarBase {
+ public:
+ explicit ScalarBase(const BaseScalarInfo& aInfo)
+ : mStoreCount(aInfo.storeCount()),
+ mStoreOffset(aInfo.storeOffset()),
+ mStoreHasValue(mStoreCount),
+ mName(aInfo.name()) {
+ mStoreHasValue.SetLength(mStoreCount);
+ for (auto& val : mStoreHasValue) {
+ val = false;
+ }
+ };
+ virtual ~ScalarBase() = default;
+
+ // Set, Add and SetMaximum functions as described in the Telemetry IDL.
+ virtual ScalarResult SetValue(nsIVariant* aValue) = 0;
+ virtual ScalarResult AddValue(nsIVariant* aValue) {
+ return ScalarResult::OperationNotSupported;
+ }
+ virtual ScalarResult SetMaximum(nsIVariant* aValue) {
+ return ScalarResult::OperationNotSupported;
+ }
+
+ // Convenience methods used by the C++ API.
+ virtual void SetValue(uint32_t aValue) {
+ mozilla::Unused << HandleUnsupported();
+ }
+ virtual ScalarResult SetValue(const nsAString& aValue) {
+ return HandleUnsupported();
+ }
+ virtual void SetValue(bool aValue) { mozilla::Unused << HandleUnsupported(); }
+ virtual void AddValue(uint32_t aValue) {
+ mozilla::Unused << HandleUnsupported();
+ }
+ virtual void SetMaximum(uint32_t aValue) {
+ mozilla::Unused << HandleUnsupported();
+ }
+
+ // GetValue is used to get the value of the scalar when persisting it to JS.
+ virtual nsresult GetValue(const nsACString& aStoreName, bool aClearStore,
+ nsCOMPtr<nsIVariant>& aResult) = 0;
+
+ // To measure the memory stats.
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+ virtual size_t SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const = 0;
+
+ protected:
+ bool HasValueInStore(size_t aStoreIndex) const;
+ void ClearValueInStore(size_t aStoreIndex);
+ void SetValueInStores();
+ nsresult StoreIndex(const nsACString& aStoreName, size_t* aStoreIndex) const;
+
+ private:
+ ScalarResult HandleUnsupported() const;
+
+ const uint32_t mStoreCount;
+ const uint32_t mStoreOffset;
+ nsTArray<bool> mStoreHasValue;
+
+ protected:
+ const nsCString mName;
+};
+
+ScalarResult ScalarBase::HandleUnsupported() const {
+ MOZ_ASSERT(false, "This operation is not support for this scalar type.");
+ return ScalarResult::OperationNotSupported;
+}
+
+bool ScalarBase::HasValueInStore(size_t aStoreIndex) const {
+ MOZ_ASSERT(aStoreIndex < mStoreHasValue.Length(),
+ "Invalid scalar store index.");
+ return mStoreHasValue[aStoreIndex];
+}
+
+void ScalarBase::ClearValueInStore(size_t aStoreIndex) {
+ MOZ_ASSERT(aStoreIndex < mStoreHasValue.Length(),
+ "Invalid scalar store index to clear.");
+ mStoreHasValue[aStoreIndex] = false;
+}
+
+void ScalarBase::SetValueInStores() {
+ for (auto& val : mStoreHasValue) {
+ val = true;
+ }
+}
+
+nsresult ScalarBase::StoreIndex(const nsACString& aStoreName,
+ size_t* aStoreIndex) const {
+ if (mStoreCount == 1 && mStoreOffset == kMaxStaticStoreOffset) {
+ // This Scalar is only in the "main" store.
+ if (aStoreName.EqualsLiteral("main")) {
+ *aStoreIndex = 0;
+ return NS_OK;
+ }
+ return NS_ERROR_NO_CONTENT;
+ }
+
+ // Multiple stores. Linear scan to find one that matches aStoreName.
+ // Dynamic Scalars start at kMaxStaticStoreOffset + 1
+ if (mStoreOffset > kMaxStaticStoreOffset) {
+ auto dynamicOffset = mStoreOffset - kMaxStaticStoreOffset - 1;
+ for (uint32_t i = 0; i < mStoreCount; ++i) {
+ auto scalarStore = (*gDynamicStoreNames)[dynamicOffset + i];
+ if (nsAtomCString(scalarStore).Equals(aStoreName)) {
+ *aStoreIndex = i;
+ return NS_OK;
+ }
+ }
+ return NS_ERROR_NO_CONTENT;
+ }
+
+ // Static Scalars are similar.
+ for (uint32_t i = 0; i < mStoreCount; ++i) {
+ uint32_t stringIndex = gScalarStoresTable[mStoreOffset + i];
+ if (aStoreName.EqualsASCII(&gScalarsStringTable[stringIndex])) {
+ *aStoreIndex = i;
+ return NS_OK;
+ }
+ }
+ return NS_ERROR_NO_CONTENT;
+}
+
+size_t ScalarBase::SizeOfExcludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ return mStoreHasValue.ShallowSizeOfExcludingThis(aMallocSizeOf);
+}
+
+/**
+ * The implementation for the unsigned int scalar type.
+ */
+class ScalarUnsigned : public ScalarBase {
+ public:
+ using ScalarBase::SetValue;
+
+ explicit ScalarUnsigned(const BaseScalarInfo& aInfo)
+ : ScalarBase(aInfo), mStorage(aInfo.storeCount()) {
+ mStorage.SetLength(aInfo.storeCount());
+ for (auto& val : mStorage) {
+ val = 0;
+ }
+ };
+
+ ~ScalarUnsigned() override = default;
+
+ ScalarResult SetValue(nsIVariant* aValue) final;
+ void SetValue(uint32_t aValue) final;
+ ScalarResult AddValue(nsIVariant* aValue) final;
+ void AddValue(uint32_t aValue) final;
+ ScalarResult SetMaximum(nsIVariant* aValue) final;
+ void SetMaximum(uint32_t aValue) final;
+ nsresult GetValue(const nsACString& aStoreName, bool aClearStore,
+ nsCOMPtr<nsIVariant>& aResult) final;
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const final;
+
+ private:
+ nsTArray<uint32_t> mStorage;
+
+ ScalarResult CheckInput(nsIVariant* aValue);
+
+ // Prevent copying.
+ ScalarUnsigned(const ScalarUnsigned& aOther) = delete;
+ void operator=(const ScalarUnsigned& aOther) = delete;
+};
+
+ScalarResult ScalarUnsigned::SetValue(nsIVariant* aValue) {
+ ScalarResult sr = CheckInput(aValue);
+ if (sr == ScalarResult::UnsignedNegativeValue) {
+ return sr;
+ }
+
+ uint32_t value = 0;
+ if (NS_FAILED(aValue->GetAsUint32(&value))) {
+ return ScalarResult::InvalidValue;
+ }
+
+ SetValue(value);
+ return sr;
+}
+
+void ScalarUnsigned::SetValue(uint32_t aValue) {
+ if (GetCurrentProduct() == SupportedProduct::GeckoviewStreaming) {
+ GeckoViewStreamingTelemetry::UintScalarSet(mName, aValue);
+ return;
+ }
+ for (auto& val : mStorage) {
+ val = aValue;
+ }
+ SetValueInStores();
+}
+
+ScalarResult ScalarUnsigned::AddValue(nsIVariant* aValue) {
+ ScalarResult sr = CheckInput(aValue);
+ if (sr == ScalarResult::UnsignedNegativeValue) {
+ return sr;
+ }
+
+ uint32_t newAddend = 0;
+ nsresult rv = aValue->GetAsUint32(&newAddend);
+ if (NS_FAILED(rv)) {
+ return ScalarResult::InvalidValue;
+ }
+
+ AddValue(newAddend);
+ return sr;
+}
+
+void ScalarUnsigned::AddValue(uint32_t aValue) {
+ MOZ_ASSERT(GetCurrentProduct() != SupportedProduct::GeckoviewStreaming);
+ for (auto& val : mStorage) {
+ val += aValue;
+ }
+ SetValueInStores();
+}
+
+ScalarResult ScalarUnsigned::SetMaximum(nsIVariant* aValue) {
+ MOZ_ASSERT(GetCurrentProduct() != SupportedProduct::GeckoviewStreaming);
+ ScalarResult sr = CheckInput(aValue);
+ if (sr == ScalarResult::UnsignedNegativeValue) {
+ return sr;
+ }
+
+ uint32_t newValue = 0;
+ nsresult rv = aValue->GetAsUint32(&newValue);
+ if (NS_FAILED(rv)) {
+ return ScalarResult::InvalidValue;
+ }
+
+ SetMaximum(newValue);
+ return sr;
+}
+
+void ScalarUnsigned::SetMaximum(uint32_t aValue) {
+ for (auto& val : mStorage) {
+ if (aValue > val) {
+ val = aValue;
+ }
+ }
+ SetValueInStores();
+}
+
+nsresult ScalarUnsigned::GetValue(const nsACString& aStoreName,
+ bool aClearStore,
+ nsCOMPtr<nsIVariant>& aResult) {
+ size_t storeIndex = 0;
+ nsresult rv = StoreIndex(aStoreName, &storeIndex);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (!HasValueInStore(storeIndex)) {
+ return NS_ERROR_NO_CONTENT;
+ }
+ nsCOMPtr<nsIWritableVariant> outVar(new nsVariant());
+ rv = outVar->SetAsUint32(mStorage[storeIndex]);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ aResult = std::move(outVar);
+ if (aClearStore) {
+ mStorage[storeIndex] = 0;
+ ClearValueInStore(storeIndex);
+ }
+ return NS_OK;
+}
+
+size_t ScalarUnsigned::SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t n = aMallocSizeOf(this);
+ n += ScalarBase::SizeOfExcludingThis(aMallocSizeOf);
+ n += mStorage.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ return n;
+}
+
+ScalarResult ScalarUnsigned::CheckInput(nsIVariant* aValue) {
+ // If this is a floating point value/double, we will probably get truncated.
+ uint16_t type = aValue->GetDataType();
+ if (type == nsIDataType::VTYPE_FLOAT || type == nsIDataType::VTYPE_DOUBLE) {
+ return ScalarResult::UnsignedTruncatedValue;
+ }
+
+ int32_t signedTest;
+ // If we're able to cast the number to an int, check its sign.
+ // Warn the user if he's trying to set the unsigned scalar to a negative
+ // number.
+ if (NS_SUCCEEDED(aValue->GetAsInt32(&signedTest)) && signedTest < 0) {
+ return ScalarResult::UnsignedNegativeValue;
+ }
+ return ScalarResult::Ok;
+}
+
+/**
+ * The implementation for the string scalar type.
+ */
+class ScalarString : public ScalarBase {
+ public:
+ using ScalarBase::SetValue;
+
+ explicit ScalarString(const BaseScalarInfo& aInfo)
+ : ScalarBase(aInfo), mStorage(aInfo.storeCount()) {
+ mStorage.SetLength(aInfo.storeCount());
+ };
+
+ ~ScalarString() override = default;
+
+ ScalarResult SetValue(nsIVariant* aValue) final;
+ ScalarResult SetValue(const nsAString& aValue) final;
+ nsresult GetValue(const nsACString& aStoreName, bool aClearStore,
+ nsCOMPtr<nsIVariant>& aResult) final;
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const final;
+
+ private:
+ nsTArray<nsString> mStorage;
+
+ // Prevent copying.
+ ScalarString(const ScalarString& aOther) = delete;
+ void operator=(const ScalarString& aOther) = delete;
+};
+
+ScalarResult ScalarString::SetValue(nsIVariant* aValue) {
+ // Check that we got the correct data type.
+ uint16_t type = aValue->GetDataType();
+ if (type != nsIDataType::VTYPE_CHAR && type != nsIDataType::VTYPE_WCHAR &&
+ type != nsIDataType::VTYPE_CHAR_STR &&
+ type != nsIDataType::VTYPE_WCHAR_STR &&
+ type != nsIDataType::VTYPE_STRING_SIZE_IS &&
+ type != nsIDataType::VTYPE_WSTRING_SIZE_IS &&
+ type != nsIDataType::VTYPE_UTF8STRING &&
+ type != nsIDataType::VTYPE_CSTRING &&
+ type != nsIDataType::VTYPE_ASTRING) {
+ return ScalarResult::InvalidType;
+ }
+
+ nsAutoString convertedString;
+ nsresult rv = aValue->GetAsAString(convertedString);
+ if (NS_FAILED(rv)) {
+ return ScalarResult::InvalidValue;
+ }
+ return SetValue(convertedString);
+};
+
+ScalarResult ScalarString::SetValue(const nsAString& aValue) {
+ auto str = Substring(aValue, 0, kMaximumStringValueLength);
+ if (GetCurrentProduct() == SupportedProduct::GeckoviewStreaming) {
+ GeckoViewStreamingTelemetry::StringScalarSet(mName,
+ NS_ConvertUTF16toUTF8(str));
+ return aValue.Length() > kMaximumStringValueLength
+ ? ScalarResult::StringTooLong
+ : ScalarResult::Ok;
+ }
+ for (auto& val : mStorage) {
+ val.Assign(str);
+ }
+ SetValueInStores();
+ if (aValue.Length() > kMaximumStringValueLength) {
+ return ScalarResult::StringTooLong;
+ }
+ return ScalarResult::Ok;
+}
+
+nsresult ScalarString::GetValue(const nsACString& aStoreName, bool aClearStore,
+ nsCOMPtr<nsIVariant>& aResult) {
+ nsCOMPtr<nsIWritableVariant> outVar(new nsVariant());
+ size_t storeIndex = 0;
+ nsresult rv = StoreIndex(aStoreName, &storeIndex);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (!HasValueInStore(storeIndex)) {
+ return NS_ERROR_NO_CONTENT;
+ }
+ rv = outVar->SetAsAString(mStorage[storeIndex]);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (aClearStore) {
+ ClearValueInStore(storeIndex);
+ }
+ aResult = std::move(outVar);
+ return NS_OK;
+}
+
+size_t ScalarString::SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t n = aMallocSizeOf(this);
+ n += ScalarBase::SizeOfExcludingThis(aMallocSizeOf);
+ n += mStorage.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (auto& val : mStorage) {
+ n += val.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+ return n;
+}
+
+/**
+ * The implementation for the boolean scalar type.
+ */
+class ScalarBoolean : public ScalarBase {
+ public:
+ using ScalarBase::SetValue;
+
+ explicit ScalarBoolean(const BaseScalarInfo& aInfo)
+ : ScalarBase(aInfo), mStorage(aInfo.storeCount()) {
+ mStorage.SetLength(aInfo.storeCount());
+ for (auto& val : mStorage) {
+ val = false;
+ }
+ };
+
+ ~ScalarBoolean() override = default;
+
+ ScalarResult SetValue(nsIVariant* aValue) final;
+ void SetValue(bool aValue) final;
+ nsresult GetValue(const nsACString& aStoreName, bool aClearStore,
+ nsCOMPtr<nsIVariant>& aResult) final;
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const final;
+
+ private:
+ nsTArray<bool> mStorage;
+
+ // Prevent copying.
+ ScalarBoolean(const ScalarBoolean& aOther) = delete;
+ void operator=(const ScalarBoolean& aOther) = delete;
+};
+
+ScalarResult ScalarBoolean::SetValue(nsIVariant* aValue) {
+ // Check that we got the correct data type.
+ uint16_t type = aValue->GetDataType();
+ if (type != nsIDataType::VTYPE_BOOL && type != nsIDataType::VTYPE_INT8 &&
+ type != nsIDataType::VTYPE_INT16 && type != nsIDataType::VTYPE_INT32 &&
+ type != nsIDataType::VTYPE_INT64 && type != nsIDataType::VTYPE_UINT8 &&
+ type != nsIDataType::VTYPE_UINT16 && type != nsIDataType::VTYPE_UINT32 &&
+ type != nsIDataType::VTYPE_UINT64) {
+ return ScalarResult::InvalidType;
+ }
+
+ bool value = false;
+ if (NS_FAILED(aValue->GetAsBool(&value))) {
+ return ScalarResult::InvalidValue;
+ }
+ SetValue(value);
+ return ScalarResult::Ok;
+};
+
+void ScalarBoolean::SetValue(bool aValue) {
+ if (GetCurrentProduct() == SupportedProduct::GeckoviewStreaming) {
+ GeckoViewStreamingTelemetry::BoolScalarSet(mName, aValue);
+ return;
+ }
+ for (auto& val : mStorage) {
+ val = aValue;
+ }
+ SetValueInStores();
+}
+
+nsresult ScalarBoolean::GetValue(const nsACString& aStoreName, bool aClearStore,
+ nsCOMPtr<nsIVariant>& aResult) {
+ nsCOMPtr<nsIWritableVariant> outVar(new nsVariant());
+ size_t storeIndex = 0;
+ nsresult rv = StoreIndex(aStoreName, &storeIndex);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (!HasValueInStore(storeIndex)) {
+ return NS_ERROR_NO_CONTENT;
+ }
+ if (aClearStore) {
+ ClearValueInStore(storeIndex);
+ }
+ rv = outVar->SetAsBool(mStorage[storeIndex]);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ aResult = std::move(outVar);
+ return NS_OK;
+}
+
+size_t ScalarBoolean::SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t n = aMallocSizeOf(this);
+ n += ScalarBase::SizeOfExcludingThis(aMallocSizeOf);
+ n += mStorage.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ return n;
+}
+
+/**
+ * Allocate a scalar class given the scalar info.
+ *
+ * @param aInfo The informations for the scalar coming from the definition file.
+ * @return nullptr if the scalar type is unknown, otherwise a valid pointer to
+ * the scalar type.
+ */
+ScalarBase* internal_ScalarAllocate(const BaseScalarInfo& aInfo) {
+ ScalarBase* scalar = nullptr;
+ switch (aInfo.kind) {
+ case nsITelemetry::SCALAR_TYPE_COUNT:
+ scalar = new ScalarUnsigned(aInfo);
+ break;
+ case nsITelemetry::SCALAR_TYPE_STRING:
+ scalar = new ScalarString(aInfo);
+ break;
+ case nsITelemetry::SCALAR_TYPE_BOOLEAN:
+ scalar = new ScalarBoolean(aInfo);
+ break;
+ default:
+ MOZ_ASSERT(false, "Invalid scalar type");
+ }
+ return scalar;
+}
+
+/**
+ * The implementation for the keyed scalar type.
+ */
+class KeyedScalar {
+ public:
+ typedef std::pair<nsCString, nsCOMPtr<nsIVariant>> KeyValuePair;
+
+ // We store the name instead of a reference to the BaseScalarInfo because
+ // the BaseScalarInfo can move if it's from a dynamic scalar.
+ explicit KeyedScalar(const BaseScalarInfo& info)
+ : mScalarName(info.name()),
+ mScalarKeyCount(info.key_count),
+ mScalarKeyOffset(info.key_offset),
+ mMaximumNumberOfKeys(kMaximumNumberOfKeys){};
+ ~KeyedScalar() = default;
+
+ // Set, Add and SetMaximum functions as described in the Telemetry IDL.
+ // These methods implicitly instantiate a Scalar[*] for each key.
+ ScalarResult SetValue(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, nsIVariant* aValue);
+ ScalarResult AddValue(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, nsIVariant* aValue);
+ ScalarResult SetMaximum(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, nsIVariant* aValue);
+
+ // Convenience methods used by the C++ API.
+ void SetValue(const StaticMutexAutoLock& locker, const nsAString& aKey,
+ uint32_t aValue);
+ void SetValue(const StaticMutexAutoLock& locker, const nsAString& aKey,
+ bool aValue);
+ void AddValue(const StaticMutexAutoLock& locker, const nsAString& aKey,
+ uint32_t aValue);
+ void SetMaximum(const StaticMutexAutoLock& locker, const nsAString& aKey,
+ uint32_t aValue);
+
+ // GetValue is used to get the key-value pairs stored in the keyed scalar
+ // when persisting it to JS.
+ nsresult GetValue(const nsACString& aStoreName, bool aClearStorage,
+ nsTArray<KeyValuePair>& aValues);
+
+ // To measure the memory stats.
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
+
+ // To permit more keys than normal.
+ void SetMaximumNumberOfKeys(uint32_t aMaximumNumberOfKeys) {
+ mMaximumNumberOfKeys = aMaximumNumberOfKeys;
+ };
+
+ private:
+ typedef nsClassHashtable<nsCStringHashKey, ScalarBase> ScalarKeysMapType;
+
+ const nsCString mScalarName;
+ ScalarKeysMapType mScalarKeys;
+ uint32_t mScalarKeyCount;
+ uint32_t mScalarKeyOffset;
+ uint32_t mMaximumNumberOfKeys;
+
+ ScalarResult GetScalarForKey(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, ScalarBase** aRet);
+
+ bool AllowsKey(const nsAString& aKey) const;
+};
+
+ScalarResult KeyedScalar::SetValue(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, nsIVariant* aValue) {
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(locker, aKey, &scalar);
+ if (sr != ScalarResult::Ok) {
+ return sr;
+ }
+
+ return scalar->SetValue(aValue);
+}
+
+ScalarResult KeyedScalar::AddValue(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, nsIVariant* aValue) {
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(locker, aKey, &scalar);
+ if (sr != ScalarResult::Ok) {
+ return sr;
+ }
+
+ return scalar->AddValue(aValue);
+}
+
+ScalarResult KeyedScalar::SetMaximum(const StaticMutexAutoLock& locker,
+ const nsAString& aKey,
+ nsIVariant* aValue) {
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(locker, aKey, &scalar);
+ if (sr != ScalarResult::Ok) {
+ return sr;
+ }
+
+ return scalar->SetMaximum(aValue);
+}
+
+void KeyedScalar::SetValue(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, uint32_t aValue) {
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(locker, aKey, &scalar);
+
+ if (sr != ScalarResult::Ok) {
+ // Bug 1451813 - We now report which scalars exceed the key limit in
+ // telemetry.keyed_scalars_exceed_limit.
+ if (sr == ScalarResult::KeyTooLong) {
+ MOZ_ASSERT(false, "Key too long to be recorded in the scalar.");
+ }
+ return;
+ }
+
+ return scalar->SetValue(aValue);
+}
+
+void KeyedScalar::SetValue(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, bool aValue) {
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(locker, aKey, &scalar);
+
+ if (sr != ScalarResult::Ok) {
+ // Bug 1451813 - We now report which scalars exceed the key limit in
+ // telemetry.keyed_scalars_exceed_limit.
+ if (sr == ScalarResult::KeyTooLong) {
+ MOZ_ASSERT(false, "Key too long to be recorded in the scalar.");
+ }
+ return;
+ }
+
+ return scalar->SetValue(aValue);
+}
+
+void KeyedScalar::AddValue(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, uint32_t aValue) {
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(locker, aKey, &scalar);
+
+ if (sr != ScalarResult::Ok) {
+ // Bug 1451813 - We now report which scalars exceed the key limit in
+ // telemetry.keyed_scalars_exceed_limit.
+ if (sr == ScalarResult::KeyTooLong) {
+ MOZ_ASSERT(false, "Key too long to be recorded in the scalar.");
+ }
+ return;
+ }
+
+ return scalar->AddValue(aValue);
+}
+
+void KeyedScalar::SetMaximum(const StaticMutexAutoLock& locker,
+ const nsAString& aKey, uint32_t aValue) {
+ ScalarBase* scalar = nullptr;
+ ScalarResult sr = GetScalarForKey(locker, aKey, &scalar);
+
+ if (sr != ScalarResult::Ok) {
+ // Bug 1451813 - We now report which scalars exceed the key limit in
+ // telemetry.keyed_scalars_exceed_limit.
+ if (sr == ScalarResult::KeyTooLong) {
+ MOZ_ASSERT(false, "Key too long to be recorded in the scalar.");
+ }
+
+ return;
+ }
+
+ return scalar->SetMaximum(aValue);
+}
+
+/**
+ * Get a key-value array with the values for the Keyed Scalar.
+ * @param aValue The array that will hold the key-value pairs.
+ * @return {nsresult} NS_OK or an error value as reported by the
+ * the specific scalar objects implementations (e.g.
+ * ScalarUnsigned).
+ */
+nsresult KeyedScalar::GetValue(const nsACString& aStoreName, bool aClearStorage,
+ nsTArray<KeyValuePair>& aValues) {
+ for (const auto& entry : mScalarKeys) {
+ ScalarBase* scalar = entry.GetWeak();
+
+ // Get the scalar value.
+ nsCOMPtr<nsIVariant> scalarValue;
+ nsresult rv = scalar->GetValue(aStoreName, aClearStorage, scalarValue);
+ if (rv == NS_ERROR_NO_CONTENT) {
+ // No value for this store.
+ continue;
+ }
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Append it to value list.
+ aValues.AppendElement(
+ std::make_pair(nsCString(entry.GetKey()), scalarValue));
+ }
+
+ return NS_OK;
+}
+
+// Forward declaration
+nsresult internal_GetKeyedScalarByEnum(const StaticMutexAutoLock& lock,
+ const ScalarKey& aId,
+ ProcessID aProcessStorage,
+ KeyedScalar** aRet);
+
+// Forward declaration
+nsresult internal_GetEnumByScalarName(const StaticMutexAutoLock& lock,
+ const nsACString& aName, ScalarKey* aId);
+
+/**
+ * Get the scalar for the referenced key.
+ * If there's no such key, instantiate a new Scalar object with the
+ * same type of the Keyed scalar and create the key.
+ */
+ScalarResult KeyedScalar::GetScalarForKey(const StaticMutexAutoLock& locker,
+ const nsAString& aKey,
+ ScalarBase** aRet) {
+ if (aKey.IsEmpty()) {
+ return ScalarResult::KeyIsEmpty;
+ }
+
+ if (!AllowsKey(aKey)) {
+ KeyedScalar* scalarUnknown = nullptr;
+ ScalarKey scalarUnknownUniqueId{
+ static_cast<uint32_t>(
+ mozilla::Telemetry::ScalarID::TELEMETRY_KEYED_SCALARS_UNKNOWN_KEYS),
+ false};
+ ProcessID process = ProcessID::Parent;
+ nsresult rv = internal_GetKeyedScalarByEnum(locker, scalarUnknownUniqueId,
+ process, &scalarUnknown);
+ if (NS_FAILED(rv)) {
+ return ScalarResult::TooManyKeys;
+ }
+ scalarUnknown->AddValue(locker, NS_ConvertUTF8toUTF16(mScalarName), 1);
+
+ return ScalarResult::KeyNotAllowed;
+ }
+
+ if (aKey.Length() > kMaximumKeyStringLength) {
+ return ScalarResult::KeyTooLong;
+ }
+
+ NS_ConvertUTF16toUTF8 utf8Key(aKey);
+
+ ScalarBase* scalar = nullptr;
+ if (mScalarKeys.Get(utf8Key, &scalar)) {
+ *aRet = scalar;
+ return ScalarResult::Ok;
+ }
+
+ ScalarKey uniqueId;
+ nsresult rv = internal_GetEnumByScalarName(locker, mScalarName, &uniqueId);
+ if (NS_FAILED(rv)) {
+ return (rv == NS_ERROR_FAILURE) ? ScalarResult::NotInitialized
+ : ScalarResult::UnknownScalar;
+ }
+
+ const BaseScalarInfo& info = internal_GetScalarInfo(locker, uniqueId);
+ if (mScalarKeys.Count() >= mMaximumNumberOfKeys) {
+ if (aKey.EqualsLiteral("telemetry.keyed_scalars_exceed_limit")) {
+ return ScalarResult::TooManyKeys;
+ }
+
+ KeyedScalar* scalarExceed = nullptr;
+
+ ScalarKey uniqueId{
+ static_cast<uint32_t>(
+ mozilla::Telemetry::ScalarID::TELEMETRY_KEYED_SCALARS_EXCEED_LIMIT),
+ false};
+
+ ProcessID process = ProcessID::Parent;
+ nsresult rv =
+ internal_GetKeyedScalarByEnum(locker, uniqueId, process, &scalarExceed);
+
+ if (NS_FAILED(rv)) {
+ return ScalarResult::TooManyKeys;
+ }
+
+ scalarExceed->AddValue(locker, NS_ConvertUTF8toUTF16(info.name()), 1);
+
+ return ScalarResult::TooManyKeys;
+ }
+
+ scalar = internal_ScalarAllocate(info);
+ if (!scalar) {
+ return ScalarResult::InvalidType;
+ }
+
+ mScalarKeys.InsertOrUpdate(utf8Key, UniquePtr<ScalarBase>(scalar));
+
+ *aRet = scalar;
+ return ScalarResult::Ok;
+}
+
+size_t KeyedScalar::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) {
+ size_t n = aMallocSizeOf(this);
+ for (const auto& scalar : mScalarKeys.Values()) {
+ n += scalar->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ return n;
+}
+
+bool KeyedScalar::AllowsKey(const nsAString& aKey) const {
+ // If we didn't specify a list of allowed keys, just return true.
+ if (mScalarKeyCount == 0) {
+ return true;
+ }
+
+ for (uint32_t i = 0; i < mScalarKeyCount; ++i) {
+ uint32_t stringIndex = gScalarKeysTable[mScalarKeyOffset + i];
+ if (aKey.EqualsASCII(&gScalarsStringTable[stringIndex])) {
+ return true;
+ }
+ }
+
+ return false;
+}
+
+typedef nsUint32HashKey ScalarIDHashKey;
+typedef nsUint32HashKey ProcessIDHashKey;
+typedef nsClassHashtable<ScalarIDHashKey, ScalarBase> ScalarStorageMapType;
+typedef nsClassHashtable<ScalarIDHashKey, KeyedScalar>
+ KeyedScalarStorageMapType;
+typedef nsClassHashtable<ProcessIDHashKey, ScalarStorageMapType>
+ ProcessesScalarsMapType;
+typedef nsClassHashtable<ProcessIDHashKey, KeyedScalarStorageMapType>
+ ProcessesKeyedScalarsMapType;
+
+typedef std::tuple<const char*, nsCOMPtr<nsIVariant>, uint32_t> ScalarDataTuple;
+typedef nsTArray<ScalarDataTuple> ScalarTupleArray;
+typedef nsTHashMap<ProcessIDHashKey, ScalarTupleArray> ScalarSnapshotTable;
+
+typedef std::tuple<const char*, nsTArray<KeyedScalar::KeyValuePair>, uint32_t>
+ KeyedScalarDataTuple;
+typedef nsTArray<KeyedScalarDataTuple> KeyedScalarTupleArray;
+typedef nsTHashMap<ProcessIDHashKey, KeyedScalarTupleArray>
+ KeyedScalarSnapshotTable;
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE STATE, SHARED BY ALL THREADS
+
+namespace {
+
+// Set to true once this global state has been initialized.
+bool gInitDone = false;
+
+bool gCanRecordBase;
+bool gCanRecordExtended;
+
+// The Name -> ID cache map.
+ScalarMapType gScalarNameIDMap(kScalarCount);
+
+// The (Process Id -> (Scalar ID -> Scalar Object)) map. This is a
+// nsClassHashtable, it owns the scalar instances and takes care of deallocating
+// them when they are removed from the map.
+ProcessesScalarsMapType gScalarStorageMap;
+// As above, for the keyed scalars.
+ProcessesKeyedScalarsMapType gKeyedScalarStorageMap;
+// Provide separate storage for "dynamic builtin" plain and keyed scalars,
+// needed to support "build faster" in local developer builds.
+ProcessesScalarsMapType gDynamicBuiltinScalarStorageMap;
+ProcessesKeyedScalarsMapType gDynamicBuiltinKeyedScalarStorageMap;
+
+// Whether or not the deserialization of persisted scalars is still in progress.
+// This is never the case on Desktop or Fennec.
+// Only GeckoView restores persisted scalars.
+bool gIsDeserializing = false;
+// This batches scalar accumulations that should be applied once loading
+// finished.
+StaticAutoPtr<nsTArray<ScalarAction>> gScalarsActions;
+StaticAutoPtr<nsTArray<KeyedScalarAction>> gKeyedScalarsActions;
+
+bool internal_IsScalarDeserializing(const StaticMutexAutoLock& lock) {
+ return gIsDeserializing;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: Function that may call JS code.
+
+// NOTE: the functions in this section all run without protection from
+// |gTelemetryScalarsMutex|. If they held the mutex, there would be the
+// possibility of deadlock because the JS_ calls that they make may call
+// back into the TelemetryScalar interface, hence trying to re-acquire the
+// mutex.
+//
+// This means that these functions potentially race against threads, but
+// that seems preferable to risking deadlock.
+
+namespace {
+
+/**
+ * Converts the error code to a human readable error message and prints it to
+ * the browser console.
+ *
+ * @param aScalarName The name of the scalar that raised the error.
+ * @param aSr The error code.
+ */
+void internal_LogScalarError(const nsACString& aScalarName, ScalarResult aSr) {
+ nsAutoString errorMessage;
+ AppendUTF8toUTF16(aScalarName, errorMessage);
+
+ switch (aSr) {
+ case ScalarResult::NotInitialized:
+ errorMessage.AppendLiteral(u" - Telemetry was not yet initialized.");
+ break;
+ case ScalarResult::CannotUnpackVariant:
+ errorMessage.AppendLiteral(
+ u" - Cannot convert the provided JS value to nsIVariant.");
+ break;
+ case ScalarResult::CannotRecordInProcess:
+ errorMessage.AppendLiteral(
+ u" - Cannot record the scalar in the current process.");
+ break;
+ case ScalarResult::KeyedTypeMismatch:
+ errorMessage.AppendLiteral(
+ u" - Attempting to manage a keyed scalar as a scalar (or "
+ u"vice-versa).");
+ break;
+ case ScalarResult::UnknownScalar:
+ errorMessage.AppendLiteral(u" - Unknown scalar.");
+ break;
+ case ScalarResult::OperationNotSupported:
+ errorMessage.AppendLiteral(
+ u" - The requested operation is not supported on this scalar.");
+ break;
+ case ScalarResult::InvalidType:
+ errorMessage.AppendLiteral(
+ u" - Attempted to set the scalar to an invalid data type.");
+ break;
+ case ScalarResult::InvalidValue:
+ errorMessage.AppendLiteral(
+ u" - Attempted to set the scalar to an incompatible value.");
+ break;
+ case ScalarResult::StringTooLong:
+ AppendUTF8toUTF16(
+ nsPrintfCString(" - Truncating scalar value to %d characters.",
+ kMaximumStringValueLength),
+ errorMessage);
+ break;
+ case ScalarResult::KeyIsEmpty:
+ errorMessage.AppendLiteral(u" - The key must not be empty.");
+ break;
+ case ScalarResult::KeyTooLong:
+ AppendUTF8toUTF16(
+ nsPrintfCString(" - The key length must be limited to %d characters.",
+ kMaximumKeyStringLength),
+ errorMessage);
+ break;
+ case ScalarResult::KeyNotAllowed:
+ AppendUTF8toUTF16(
+ nsPrintfCString(" - The key is not allowed for this scalar."),
+ errorMessage);
+ break;
+ case ScalarResult::TooManyKeys:
+ AppendUTF8toUTF16(
+ nsPrintfCString(" - Keyed scalars cannot have more than %d keys.",
+ kMaximumNumberOfKeys),
+ errorMessage);
+ break;
+ case ScalarResult::UnsignedNegativeValue:
+ errorMessage.AppendLiteral(
+ u" - Trying to set an unsigned scalar to a negative number.");
+ break;
+ case ScalarResult::UnsignedTruncatedValue:
+ errorMessage.AppendLiteral(u" - Truncating float/double number.");
+ break;
+ default:
+ // Nothing.
+ return;
+ }
+
+ LogToBrowserConsole(nsIScriptError::warningFlag, errorMessage);
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: helpers for the external interface
+
+namespace {
+
+bool internal_CanRecordBase(const StaticMutexAutoLock& lock) {
+ return gCanRecordBase;
+}
+
+bool internal_CanRecordExtended(const StaticMutexAutoLock& lock) {
+ return gCanRecordExtended;
+}
+
+/**
+ * Check if the given scalar is a keyed scalar.
+ *
+ * @param lock Instance of a lock locking gTelemetryHistogramMutex
+ * @param aId The scalar identifier.
+ * @return true if aId refers to a keyed scalar, false otherwise.
+ */
+bool internal_IsKeyedScalar(const StaticMutexAutoLock& lock,
+ const ScalarKey& aId) {
+ return internal_GetScalarInfo(lock, aId).keyed;
+}
+
+/**
+ * Check if we're allowed to record the given scalar in the current
+ * process.
+ *
+ * @param lock Instance of a lock locking gTelemetryHistogramMutex
+ * @param aId The scalar identifier.
+ * @return true if the scalar is allowed to be recorded in the current process,
+ * false otherwise.
+ */
+bool internal_CanRecordProcess(const StaticMutexAutoLock& lock,
+ const ScalarKey& aId) {
+ const BaseScalarInfo& info = internal_GetScalarInfo(lock, aId);
+ return CanRecordInProcess(info.record_in_processes, XRE_GetProcessType());
+}
+
+bool internal_CanRecordProduct(const StaticMutexAutoLock& lock,
+ const ScalarKey& aId) {
+ const BaseScalarInfo& info = internal_GetScalarInfo(lock, aId);
+ return CanRecordProduct(info.products);
+}
+
+bool internal_CanRecordForScalarID(const StaticMutexAutoLock& lock,
+ const ScalarKey& aId) {
+ // Get the scalar info from the id.
+ const BaseScalarInfo& info = internal_GetScalarInfo(lock, aId);
+
+ // Can we record at all?
+ bool canRecordBase = internal_CanRecordBase(lock);
+ if (!canRecordBase) {
+ return false;
+ }
+
+ bool canRecordDataset = CanRecordDataset(info.dataset, canRecordBase,
+ internal_CanRecordExtended(lock));
+ if (!canRecordDataset) {
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Check if we are allowed to record the provided scalar.
+ *
+ * @param lock Instance of a lock locking gTelemetryHistogramMutex
+ * @param aId The scalar identifier.
+ * @param aKeyed Are we attempting to write a keyed scalar?
+ * @param aForce Whether to allow recording even if the probe is not allowed on
+ * the current process.
+ * This must only be true for GeckoView persistence and recorded
+ * actions.
+ * @return ScalarResult::Ok if we can record, an error code otherwise.
+ */
+ScalarResult internal_CanRecordScalar(const StaticMutexAutoLock& lock,
+ const ScalarKey& aId, bool aKeyed,
+ bool aForce = false) {
+ // Make sure that we have a keyed scalar if we are trying to change one.
+ if (internal_IsKeyedScalar(lock, aId) != aKeyed) {
+ return ScalarResult::KeyedTypeMismatch;
+ }
+
+ // Are we allowed to record this scalar based on the current Telemetry
+ // settings?
+ if (!internal_CanRecordForScalarID(lock, aId)) {
+ return ScalarResult::CannotRecordDataset;
+ }
+
+ // Can we record in this process?
+ if (!aForce && !internal_CanRecordProcess(lock, aId)) {
+ return ScalarResult::CannotRecordInProcess;
+ }
+
+ // Can we record on this product?
+ if (!internal_CanRecordProduct(lock, aId)) {
+ return ScalarResult::CannotRecordDataset;
+ }
+
+ return ScalarResult::Ok;
+}
+
+/**
+ * Get the scalar enum id from the scalar name.
+ *
+ * @param lock Instance of a lock locking gTelemetryHistogramMutex
+ * @param aName The scalar name.
+ * @param aId The output variable to contain the enum.
+ * @return
+ * NS_ERROR_FAILURE if this was called before init is completed.
+ * NS_ERROR_INVALID_ARG if the name can't be found in the scalar definitions.
+ * NS_OK if the scalar was found and aId contains a valid enum id.
+ */
+nsresult internal_GetEnumByScalarName(const StaticMutexAutoLock& lock,
+ const nsACString& aName, ScalarKey* aId) {
+ if (!gInitDone) {
+ return NS_ERROR_FAILURE;
+ }
+
+ CharPtrEntryType* entry =
+ gScalarNameIDMap.GetEntry(PromiseFlatCString(aName).get());
+ if (!entry) {
+ return NS_ERROR_INVALID_ARG;
+ }
+ *aId = entry->GetData();
+ return NS_OK;
+}
+
+/**
+ * Get a scalar object by its enum id. This implicitly allocates the scalar
+ * object in the storage if it wasn't previously allocated.
+ *
+ * @param lock Instance of a lock locking gTelemetryHistogramMutex
+ * @param aId The scalar identifier.
+ * @param aProcessStorage This drives the selection of the map to use to store
+ * the scalar data coming from child processes. This is only meaningful
+ * when this function is called in parent process. If that's the case,
+ * if this is not |GeckoProcessType_Default|, the process id is used to
+ * allocate and store the scalars.
+ * @param aRes The output variable that stores scalar object.
+ * @return
+ * NS_ERROR_INVALID_ARG if the scalar id is unknown.
+ * NS_ERROR_NOT_AVAILABLE if the scalar is expired.
+ * NS_OK if the scalar was found. If that's the case, aResult contains a
+ * valid pointer to a scalar type.
+ */
+nsresult internal_GetScalarByEnum(const StaticMutexAutoLock& lock,
+ const ScalarKey& aId,
+ ProcessID aProcessStorage,
+ ScalarBase** aRet) {
+ if (!internal_IsValidId(lock, aId)) {
+ MOZ_ASSERT(false, "Requested a scalar with an invalid id.");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ const BaseScalarInfo& info = internal_GetScalarInfo(lock, aId);
+
+ // Dynamic scalars fixup: they are always stored in the "dynamic" process,
+ // unless they are part of the "builtin" Firefox probes. Please note that
+ // "dynamic builtin" probes are meant to support "artifact" and "build faster"
+ // builds.
+ if (aId.dynamic && !info.builtin) {
+ aProcessStorage = ProcessID::Dynamic;
+ }
+
+ ScalarBase* scalar = nullptr;
+ // Initialize the scalar storage to the parent storage. This will get
+ // set to the child storage if needed.
+ uint32_t storageId = static_cast<uint32_t>(aProcessStorage);
+
+ // Put dynamic-builtin scalars (used to support "build faster") in a
+ // separate storage.
+ ProcessesScalarsMapType& processStorage =
+ (aId.dynamic && info.builtin) ? gDynamicBuiltinScalarStorageMap
+ : gScalarStorageMap;
+
+ // Get the process-specific storage or create one if it's not
+ // available.
+ ScalarStorageMapType* const scalarStorage =
+ processStorage.GetOrInsertNew(storageId);
+
+ // Check if the scalar is already allocated in the parent or in the child
+ // storage.
+ if (scalarStorage->Get(aId.id, &scalar)) {
+ // Dynamic scalars can expire at any time during the session (e.g. an
+ // add-on was updated). Check if it expired.
+ if (aId.dynamic) {
+ const DynamicScalarInfo& dynInfo =
+ static_cast<const DynamicScalarInfo&>(info);
+ if (dynInfo.mDynamicExpiration) {
+ // The Dynamic scalar is expired.
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+ }
+ // This was not a dynamic scalar or was not expired.
+ *aRet = scalar;
+ return NS_OK;
+ }
+
+ // The scalar storage wasn't already allocated. Check if the scalar is expired
+ // and then allocate the storage, if needed.
+ if (IsExpiredVersion(info.expiration())) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ scalar = internal_ScalarAllocate(info);
+ if (!scalar) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ scalarStorage->InsertOrUpdate(aId.id, UniquePtr<ScalarBase>(scalar));
+ *aRet = scalar;
+ return NS_OK;
+}
+
+void internal_ApplyPendingOperations(const StaticMutexAutoLock& lock);
+
+/**
+ * Record the given action on a scalar into the pending actions list.
+ *
+ * If the pending actions list overflows the high water mark length
+ * all operations are immediately applied, including the passed action.
+ *
+ * @param aScalarAction The action to record.
+ */
+void internal_RecordScalarAction(const StaticMutexAutoLock& lock,
+ const ScalarAction& aScalarAction) {
+ // Make sure to have the storage.
+ if (!gScalarsActions) {
+ gScalarsActions = new nsTArray<ScalarAction>();
+ }
+
+ // Store the action.
+ gScalarsActions->AppendElement(aScalarAction);
+
+ // If this action overflows the pending actions array, we immediately apply
+ // pending operations and assume loading is over. If loading still happens
+ // afterwards, some scalar values might be overwritten and inconsistent, but
+ // we won't lose operations on otherwise untouched probes.
+ if (gScalarsActions->Length() > kScalarActionsArrayHighWaterMark) {
+ internal_ApplyPendingOperations(lock);
+ return;
+ }
+}
+
+/**
+ * Record the given action on a scalar on the main process into the pending
+ * actions list.
+ *
+ * If the pending actions list overflows the high water mark length
+ * all operations are immediately applied, including the passed action.
+ *
+ * @param aId The scalar's ID this action applies to
+ * @param aDynamic Determines if the scalar is dynamic
+ * @param aAction The action to record
+ * @param aValue The additional data for the recorded action
+ */
+void internal_RecordScalarAction(const StaticMutexAutoLock& lock, uint32_t aId,
+ bool aDynamic, ScalarActionType aAction,
+ const ScalarVariant& aValue) {
+ internal_RecordScalarAction(
+ lock,
+ ScalarAction{aId, aDynamic, aAction, Some(aValue), ProcessID::Parent});
+}
+
+/**
+ * Record the given action on a keyed scalar into the pending actions list.
+ *
+ * If the pending actions list overflows the high water mark length
+ * all operations are immediately applied, including the passed action.
+ *
+ * @param aScalarAction The action to record.
+ */
+void internal_RecordKeyedScalarAction(const StaticMutexAutoLock& lock,
+ const KeyedScalarAction& aScalarAction) {
+ // Make sure to have the storage.
+ if (!gKeyedScalarsActions) {
+ gKeyedScalarsActions = new nsTArray<KeyedScalarAction>();
+ }
+
+ // Store the action.
+ gKeyedScalarsActions->AppendElement(aScalarAction);
+
+ // If this action overflows the pending actions array, we immediately apply
+ // pending operations and assume loading is over. If loading still happens
+ // afterwards, some scalar values might be overwritten and inconsistent, but
+ // we won't lose operations on otherwise untouched probes.
+ if (gKeyedScalarsActions->Length() > kScalarActionsArrayHighWaterMark) {
+ internal_ApplyPendingOperations(lock);
+ return;
+ }
+}
+
+/**
+ * Record the given action on a keyed scalar on the main process into the
+ * pending actions list.
+ *
+ * If the pending actions list overflows the high water mark length
+ * all operations are immediately applied, including the passed action.
+ *
+ * @param aId The scalar's ID this action applies to
+ * @param aDynamic Determines if the scalar is dynamic
+ * @param aKey The scalar's key
+ * @param aAction The action to record
+ * @param aValue The additional data for the recorded action
+ */
+void internal_RecordKeyedScalarAction(const StaticMutexAutoLock& lock,
+ uint32_t aId, bool aDynamic,
+ const nsAString& aKey,
+ ScalarActionType aAction,
+ const ScalarVariant& aValue) {
+ internal_RecordKeyedScalarAction(
+ lock,
+ KeyedScalarAction{aId, aDynamic, aAction, NS_ConvertUTF16toUTF8(aKey),
+ Some(aValue), ProcessID::Parent});
+}
+
+/**
+ * Update the scalar with the provided value. This is used by the JS API.
+ *
+ * @param lock Instance of a lock locking gTelemetryHistogramMutex
+ * @param aName The scalar name.
+ * @param aType The action type for updating the scalar.
+ * @param aValue The value to use for updating the scalar.
+ * @param aProcessOverride The process for which the scalar must be updated.
+ * This must only be used for GeckoView persistence. It must be
+ * set to the ProcessID::Parent for all the other cases.
+ * @param aForce Whether to force updating even if load is in progress.
+ * @return a ScalarResult error value.
+ */
+ScalarResult internal_UpdateScalar(
+ const StaticMutexAutoLock& lock, const nsACString& aName,
+ ScalarActionType aType, nsIVariant* aValue,
+ ProcessID aProcessOverride = ProcessID::Parent, bool aForce = false) {
+ ScalarKey uniqueId;
+ nsresult rv = internal_GetEnumByScalarName(lock, aName, &uniqueId);
+ if (NS_FAILED(rv)) {
+ return (rv == NS_ERROR_FAILURE) ? ScalarResult::NotInitialized
+ : ScalarResult::UnknownScalar;
+ }
+
+ ScalarResult sr = internal_CanRecordScalar(lock, uniqueId, false, aForce);
+ if (sr != ScalarResult::Ok) {
+ if (sr == ScalarResult::CannotRecordDataset) {
+ return ScalarResult::Ok;
+ }
+ return sr;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ const BaseScalarInfo& info = internal_GetScalarInfo(lock, uniqueId);
+ // Convert the nsIVariant to a Variant.
+ mozilla::Maybe<ScalarVariant> variantValue;
+ sr = GetVariantFromIVariant(aValue, info.kind, variantValue);
+ if (sr != ScalarResult::Ok) {
+ MOZ_ASSERT(false, "Unable to convert nsIVariant to mozilla::Variant.");
+ return sr;
+ }
+ TelemetryIPCAccumulator::RecordChildScalarAction(
+ uniqueId.id, uniqueId.dynamic, aType, variantValue.ref());
+ return ScalarResult::Ok;
+ }
+
+ if (!aForce && internal_IsScalarDeserializing(lock)) {
+ const BaseScalarInfo& info = internal_GetScalarInfo(lock, uniqueId);
+ // Convert the nsIVariant to a Variant.
+ mozilla::Maybe<ScalarVariant> variantValue;
+ sr = GetVariantFromIVariant(aValue, info.kind, variantValue);
+ if (sr != ScalarResult::Ok) {
+ MOZ_ASSERT(false, "Unable to convert nsIVariant to mozilla::Variant.");
+ return sr;
+ }
+ internal_RecordScalarAction(lock, uniqueId.id, uniqueId.dynamic, aType,
+ variantValue.ref());
+ return ScalarResult::Ok;
+ }
+
+ // Finally get the scalar.
+ ScalarBase* scalar = nullptr;
+ rv = internal_GetScalarByEnum(lock, uniqueId, aProcessOverride, &scalar);
+ if (NS_FAILED(rv)) {
+ // Don't throw on expired scalars.
+ if (rv == NS_ERROR_NOT_AVAILABLE) {
+ return ScalarResult::Ok;
+ }
+ return ScalarResult::UnknownScalar;
+ }
+
+ if (aType == ScalarActionType::eAdd) {
+ return scalar->AddValue(aValue);
+ }
+ if (aType == ScalarActionType::eSet) {
+ return scalar->SetValue(aValue);
+ }
+
+ return scalar->SetMaximum(aValue);
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE: thread-unsafe helpers for the keyed scalars
+
+namespace {
+
+/**
+ * Get a keyed scalar object by its enum id. This implicitly allocates the keyed
+ * scalar object in the storage if it wasn't previously allocated.
+ *
+ * @param lock Instance of a lock locking gTelemetryHistogramMutex
+ * @param aId The scalar identifier.
+ * @param aProcessStorage This drives the selection of the map to use to store
+ * the scalar data coming from child processes. This is only meaningful
+ * when this function is called in parent process. If that's the case,
+ * if this is not |GeckoProcessType_Default|, the process id is used to
+ * allocate and store the scalars.
+ * @param aRet The output variable that stores scalar object.
+ * @return
+ * NS_ERROR_INVALID_ARG if the scalar id is unknown or a this is a keyed
+ * string scalar.
+ * NS_ERROR_NOT_AVAILABLE if the scalar is expired.
+ * NS_OK if the scalar was found. If that's the case, aResult contains a
+ * valid pointer to a scalar type.
+ */
+nsresult internal_GetKeyedScalarByEnum(const StaticMutexAutoLock& lock,
+ const ScalarKey& aId,
+ ProcessID aProcessStorage,
+ KeyedScalar** aRet) {
+ if (!internal_IsValidId(lock, aId)) {
+ MOZ_ASSERT(false, "Requested a keyed scalar with an invalid id.");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ const BaseScalarInfo& info = internal_GetScalarInfo(lock, aId);
+
+ // Dynamic scalars fixup: they are always stored in the "dynamic" process,
+ // unless they are part of the "builtin" Firefox probes. Please note that
+ // "dynamic builtin" probes are meant to support "artifact" and "build faster"
+ // builds.
+ if (aId.dynamic && !info.builtin) {
+ aProcessStorage = ProcessID::Dynamic;
+ }
+
+ KeyedScalar* scalar = nullptr;
+ // Initialize the scalar storage to the parent storage. This will get
+ // set to the child storage if needed.
+ uint32_t storageId = static_cast<uint32_t>(aProcessStorage);
+
+ // Put dynamic-builtin scalars (used to support "build faster") in a
+ // separate storage.
+ ProcessesKeyedScalarsMapType& processStorage =
+ (aId.dynamic && info.builtin) ? gDynamicBuiltinKeyedScalarStorageMap
+ : gKeyedScalarStorageMap;
+
+ // Get the process-specific storage or create one if it's not
+ // available.
+ KeyedScalarStorageMapType* const scalarStorage =
+ processStorage.GetOrInsertNew(storageId);
+
+ if (scalarStorage->Get(aId.id, &scalar)) {
+ *aRet = scalar;
+ return NS_OK;
+ }
+
+ if (IsExpiredVersion(info.expiration())) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ // We don't currently support keyed string scalars. Disable them.
+ if (info.kind == nsITelemetry::SCALAR_TYPE_STRING) {
+ MOZ_ASSERT(false, "Keyed string scalars are not currently supported.");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ scalar = new KeyedScalar(info);
+
+ scalarStorage->InsertOrUpdate(aId.id, UniquePtr<KeyedScalar>(scalar));
+ *aRet = scalar;
+ return NS_OK;
+}
+
+/**
+ * Update the keyed scalar with the provided value. This is used by the JS API.
+ *
+ * @param lock Instance of a lock locking gTelemetryHistogramMutex
+ * @param aName The scalar name.
+ * @param aKey The key name.
+ * @param aType The action type for updating the scalar.
+ * @param aValue The value to use for updating the scalar.
+ * @param aProcessOverride The process for which the scalar must be updated.
+ * This must only be used for GeckoView persistence. It must be
+ * set to the ProcessID::Parent for all the other cases.
+ * @return a ScalarResult error value.
+ */
+ScalarResult internal_UpdateKeyedScalar(
+ const StaticMutexAutoLock& lock, const nsACString& aName,
+ const nsAString& aKey, ScalarActionType aType, nsIVariant* aValue,
+ ProcessID aProcessOverride = ProcessID::Parent, bool aForce = false) {
+ ScalarKey uniqueId;
+ nsresult rv = internal_GetEnumByScalarName(lock, aName, &uniqueId);
+ if (NS_FAILED(rv)) {
+ return (rv == NS_ERROR_FAILURE) ? ScalarResult::NotInitialized
+ : ScalarResult::UnknownScalar;
+ }
+
+ ScalarResult sr = internal_CanRecordScalar(lock, uniqueId, true, aForce);
+ if (sr != ScalarResult::Ok) {
+ if (sr == ScalarResult::CannotRecordDataset) {
+ return ScalarResult::Ok;
+ }
+ return sr;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ const BaseScalarInfo& info = internal_GetScalarInfo(lock, uniqueId);
+ // Convert the nsIVariant to a Variant.
+ mozilla::Maybe<ScalarVariant> variantValue;
+ sr = GetVariantFromIVariant(aValue, info.kind, variantValue);
+ if (sr != ScalarResult::Ok) {
+ MOZ_ASSERT(false, "Unable to convert nsIVariant to mozilla::Variant.");
+ return sr;
+ }
+ TelemetryIPCAccumulator::RecordChildKeyedScalarAction(
+ uniqueId.id, uniqueId.dynamic, aKey, aType, variantValue.ref());
+ return ScalarResult::Ok;
+ }
+
+ if (!aForce && internal_IsScalarDeserializing(lock)) {
+ const BaseScalarInfo& info = internal_GetScalarInfo(lock, uniqueId);
+ // Convert the nsIVariant to a Variant.
+ mozilla::Maybe<ScalarVariant> variantValue;
+ sr = GetVariantFromIVariant(aValue, info.kind, variantValue);
+ if (sr != ScalarResult::Ok) {
+ MOZ_ASSERT(false, "Unable to convert nsIVariant to mozilla::Variant.");
+ return sr;
+ }
+ internal_RecordKeyedScalarAction(lock, uniqueId.id, uniqueId.dynamic, aKey,
+ aType, variantValue.ref());
+ return ScalarResult::Ok;
+ }
+
+ // Finally get the scalar.
+ KeyedScalar* scalar = nullptr;
+ rv = internal_GetKeyedScalarByEnum(lock, uniqueId, aProcessOverride, &scalar);
+ if (NS_FAILED(rv)) {
+ // Don't throw on expired scalars.
+ if (rv == NS_ERROR_NOT_AVAILABLE) {
+ return ScalarResult::Ok;
+ }
+ return ScalarResult::UnknownScalar;
+ }
+
+ if (aType == ScalarActionType::eAdd) {
+ return scalar->AddValue(lock, aKey, aValue);
+ }
+ if (aType == ScalarActionType::eSet) {
+ return scalar->SetValue(lock, aKey, aValue);
+ }
+
+ return scalar->SetMaximum(lock, aKey, aValue);
+}
+
+/**
+ * Helper function to convert an array of |DynamicScalarInfo|
+ * to |DynamicScalarDefinition| used by the IPC calls.
+ */
+void internal_DynamicScalarToIPC(
+ const StaticMutexAutoLock& lock,
+ const nsTArray<DynamicScalarInfo>& aDynamicScalarInfos,
+ nsTArray<DynamicScalarDefinition>& aIPCDefs) {
+ for (auto& info : aDynamicScalarInfos) {
+ DynamicScalarDefinition stubDefinition;
+ stubDefinition.type = info.kind;
+ stubDefinition.dataset = info.dataset;
+ stubDefinition.expired = info.mDynamicExpiration;
+ stubDefinition.keyed = info.keyed;
+ stubDefinition.name = info.mDynamicName;
+ stubDefinition.builtin = info.builtin;
+ aIPCDefs.AppendElement(stubDefinition);
+ }
+}
+
+/**
+ * Broadcasts the dynamic scalar definitions to all the other
+ * content processes.
+ */
+void internal_BroadcastDefinitions(
+ const nsTArray<DynamicScalarDefinition>& scalarDefs) {
+ nsTArray<mozilla::dom::ContentParent*> parents;
+ mozilla::dom::ContentParent::GetAll(parents);
+ if (!parents.Length()) {
+ return;
+ }
+
+ // Broadcast the definitions to the other content processes.
+ for (auto parent : parents) {
+ mozilla::Unused << parent->SendAddDynamicScalars(scalarDefs);
+ }
+}
+
+void internal_RegisterScalars(const StaticMutexAutoLock& lock,
+ const nsTArray<DynamicScalarInfo>& scalarInfos) {
+ // Register the new scalars.
+ if (!gDynamicScalarInfo) {
+ gDynamicScalarInfo = new nsTArray<DynamicScalarInfo>();
+ }
+ if (!gDynamicStoreNames) {
+ gDynamicStoreNames = new nsTArray<RefPtr<nsAtom>>();
+ }
+
+ for (auto& scalarInfo : scalarInfos) {
+ // Allow expiring scalars that were already registered.
+ CharPtrEntryType* existingKey =
+ gScalarNameIDMap.GetEntry(scalarInfo.name());
+ if (existingKey) {
+ // Change the scalar to expired if needed.
+ if (scalarInfo.mDynamicExpiration && !scalarInfo.builtin) {
+ DynamicScalarInfo& scalarData =
+ (*gDynamicScalarInfo)[existingKey->GetData().id];
+ scalarData.mDynamicExpiration = true;
+ }
+ continue;
+ }
+
+ gDynamicScalarInfo->AppendElement(scalarInfo);
+ uint32_t scalarId = gDynamicScalarInfo->Length() - 1;
+ CharPtrEntryType* entry = gScalarNameIDMap.PutEntry(scalarInfo.name());
+ entry->SetData(ScalarKey{scalarId, true});
+ }
+}
+
+/**
+ * Creates a snapshot of the desired scalar storage.
+ * @param {aLock} The proof of lock to access scalar data.
+ * @param {aScalarsToReflect} The table that will contain the snapshot.
+ * @param {aDataset} The dataset we're asking the snapshot for.
+ * @param {aProcessStorage} The scalar storage to take a snapshot of.
+ * @param {aIsBuiltinDynamic} Whether or not the storage is for dynamic builtin
+ * scalars.
+ * @return NS_OK or the error code describing the failure reason.
+ */
+nsresult internal_ScalarSnapshotter(const StaticMutexAutoLock& aLock,
+ ScalarSnapshotTable& aScalarsToReflect,
+ unsigned int aDataset,
+ ProcessesScalarsMapType& aProcessStorage,
+ bool aIsBuiltinDynamic, bool aClearScalars,
+ const nsACString& aStoreName) {
+ // Iterate the scalars in aProcessStorage. The storage may contain empty or
+ // yet to be initialized scalars from all the supported processes.
+ for (const auto& entry : aProcessStorage) {
+ ScalarStorageMapType* scalarStorage = entry.GetWeak();
+ ScalarTupleArray& processScalars =
+ aScalarsToReflect.LookupOrInsert(entry.GetKey());
+
+ // Are we in the "Dynamic" process?
+ bool isDynamicProcess =
+ ProcessID::Dynamic == static_cast<ProcessID>(entry.GetKey());
+
+ // Iterate each available child storage.
+ for (const auto& childEntry : *scalarStorage) {
+ ScalarBase* scalar = childEntry.GetWeak();
+
+ // Get the informations for this scalar.
+ const BaseScalarInfo& info = internal_GetScalarInfo(
+ aLock, ScalarKey{childEntry.GetKey(),
+ aIsBuiltinDynamic ? true : isDynamicProcess});
+
+ // Serialize the scalar if it's in the desired dataset.
+ if (IsInDataset(info.dataset, aDataset)) {
+ // Get the scalar value.
+ nsCOMPtr<nsIVariant> scalarValue;
+ nsresult rv = scalar->GetValue(aStoreName, aClearScalars, scalarValue);
+ if (rv == NS_ERROR_NO_CONTENT) {
+ // No value for this store. Proceed.
+ continue;
+ }
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ // Append it to our list.
+ processScalars.AppendElement(
+ std::make_tuple(info.name(), scalarValue, info.kind));
+ }
+ }
+ if (processScalars.Length() == 0) {
+ aScalarsToReflect.Remove(entry.GetKey());
+ }
+ }
+ return NS_OK;
+}
+
+/**
+ * Creates a snapshot of the desired keyed scalar storage.
+ * @param {aLock} The proof of lock to access scalar data.
+ * @param {aScalarsToReflect} The table that will contain the snapshot.
+ * @param {aDataset} The dataset we're asking the snapshot for.
+ * @param {aProcessStorage} The scalar storage to take a snapshot of.
+ * @param {aIsBuiltinDynamic} Whether or not the storage is for dynamic builtin
+ * scalars.
+ * @return NS_OK or the error code describing the failure reason.
+ */
+nsresult internal_KeyedScalarSnapshotter(
+ const StaticMutexAutoLock& aLock,
+ KeyedScalarSnapshotTable& aScalarsToReflect, unsigned int aDataset,
+ ProcessesKeyedScalarsMapType& aProcessStorage, bool aIsBuiltinDynamic,
+ bool aClearScalars, const nsACString& aStoreName) {
+ // Iterate the scalars in aProcessStorage. The storage may contain empty or
+ // yet to be initialized scalars from all the supported processes.
+ for (const auto& entry : aProcessStorage) {
+ KeyedScalarStorageMapType* scalarStorage = entry.GetWeak();
+ KeyedScalarTupleArray& processScalars =
+ aScalarsToReflect.LookupOrInsert(entry.GetKey());
+
+ // Are we in the "Dynamic" process?
+ bool isDynamicProcess =
+ ProcessID::Dynamic == static_cast<ProcessID>(entry.GetKey());
+
+ for (const auto& childEntry : *scalarStorage) {
+ KeyedScalar* scalar = childEntry.GetWeak();
+
+ // Get the informations for this scalar.
+ const BaseScalarInfo& info = internal_GetScalarInfo(
+ aLock, ScalarKey{childEntry.GetKey(),
+ aIsBuiltinDynamic ? true : isDynamicProcess});
+
+ // Serialize the scalar if it's in the desired dataset.
+ if (IsInDataset(info.dataset, aDataset)) {
+ // Get the keys for this scalar.
+ nsTArray<KeyedScalar::KeyValuePair> scalarKeyedData;
+ nsresult rv =
+ scalar->GetValue(aStoreName, aClearScalars, scalarKeyedData);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ if (scalarKeyedData.Length() == 0) {
+ // Don't bother with empty keyed scalars.
+ continue;
+ }
+ // Append it to our list.
+ processScalars.AppendElement(std::make_tuple(
+ info.name(), std::move(scalarKeyedData), info.kind));
+ }
+ }
+ if (processScalars.Length() == 0) {
+ aScalarsToReflect.Remove(entry.GetKey());
+ }
+ }
+ return NS_OK;
+}
+
+/**
+ * Helper function to get a snapshot of the scalars.
+ *
+ * @param {aLock} The proof of lock to access scalar data.
+ * @param {aScalarsToReflect} The table that will contain the snapshot.
+ * @param {aDataset} The dataset we're asking the snapshot for.
+ * @param {aClearScalars} Whether or not to clear the scalar storage.
+ * @param {aStoreName} The name of the store to snapshot.
+ * @return NS_OK or the error code describing the failure reason.
+ */
+nsresult internal_GetScalarSnapshot(const StaticMutexAutoLock& aLock,
+ ScalarSnapshotTable& aScalarsToReflect,
+ unsigned int aDataset, bool aClearScalars,
+ const nsACString& aStoreName) {
+ // Take a snapshot of the scalars.
+ nsresult rv =
+ internal_ScalarSnapshotter(aLock, aScalarsToReflect, aDataset,
+ gScalarStorageMap, false, /*aIsBuiltinDynamic*/
+ aClearScalars, aStoreName);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // And a snapshot of the dynamic builtin ones.
+ rv = internal_ScalarSnapshotter(aLock, aScalarsToReflect, aDataset,
+ gDynamicBuiltinScalarStorageMap,
+ true, /*aIsBuiltinDynamic*/
+ aClearScalars, aStoreName);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Helper function to get a snapshot of the keyed scalars.
+ *
+ * @param {aLock} The proof of lock to access scalar data.
+ * @param {aScalarsToReflect} The table that will contain the snapshot.
+ * @param {aDataset} The dataset we're asking the snapshot for.
+ * @param {aClearScalars} Whether or not to clear the scalar storage.
+ * @param {aStoreName} The name of the store to snapshot.
+ * @return NS_OK or the error code describing the failure reason.
+ */
+nsresult internal_GetKeyedScalarSnapshot(
+ const StaticMutexAutoLock& aLock,
+ KeyedScalarSnapshotTable& aScalarsToReflect, unsigned int aDataset,
+ bool aClearScalars, const nsACString& aStoreName) {
+ // Take a snapshot of the scalars.
+ nsresult rv = internal_KeyedScalarSnapshotter(
+ aLock, aScalarsToReflect, aDataset, gKeyedScalarStorageMap,
+ false, /*aIsBuiltinDynamic*/
+ aClearScalars, aStoreName);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // And a snapshot of the dynamic builtin ones.
+ rv = internal_KeyedScalarSnapshotter(aLock, aScalarsToReflect, aDataset,
+ gDynamicBuiltinKeyedScalarStorageMap,
+ true, /*aIsBuiltinDynamic*/
+ aClearScalars, aStoreName);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ return NS_OK;
+}
+
+} // namespace
+
+// helpers for recording/applying scalar operations
+namespace {
+
+void internal_ApplyScalarActions(
+ const StaticMutexAutoLock& lock,
+ const nsTArray<mozilla::Telemetry::ScalarAction>& aScalarActions,
+ const mozilla::Maybe<ProcessID>& aProcessType = Nothing()) {
+ if (!internal_CanRecordBase(lock)) {
+ return;
+ }
+
+ for (auto& upd : aScalarActions) {
+ ScalarKey uniqueId{upd.mId, upd.mDynamic};
+ if (NS_WARN_IF(!internal_IsValidId(lock, uniqueId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ continue;
+ }
+
+ if (internal_IsKeyedScalar(lock, uniqueId)) {
+ continue;
+ }
+
+ // Are we allowed to record this scalar? We don't need to check for
+ // allowed processes here, that's taken care of when recording
+ // in child processes.
+ if (!internal_CanRecordForScalarID(lock, uniqueId)) {
+ continue;
+ }
+
+ // Either we got passed a process type or it was explicitely set on the
+ // recorded action. It should never happen that it is set to an invalid
+ // value (such as ProcessID::Count)
+ ProcessID processType = aProcessType.valueOr(upd.mProcessType);
+ MOZ_ASSERT(processType != ProcessID::Count);
+
+ // Refresh the data in the parent process with the data coming from the
+ // child processes.
+ ScalarBase* scalar = nullptr;
+ nsresult rv =
+ internal_GetScalarByEnum(lock, uniqueId, processType, &scalar);
+ if (NS_FAILED(rv)) {
+ // Bug 1513496 - We no longer log a warning if the scalar is expired.
+ if (rv != NS_ERROR_NOT_AVAILABLE) {
+ NS_WARNING("NS_FAILED internal_GetScalarByEnum for CHILD");
+ }
+ continue;
+ }
+
+ if (upd.mData.isNothing()) {
+ MOZ_ASSERT(false, "There is no data in the ScalarActionType.");
+ continue;
+ }
+
+ // Get the type of this scalar from the scalar ID. We already checked
+ // for its validity a few lines above.
+ const uint32_t scalarType = internal_GetScalarInfo(lock, uniqueId).kind;
+
+ // Extract the data from the mozilla::Variant.
+ switch (upd.mActionType) {
+ case ScalarActionType::eSet: {
+ switch (scalarType) {
+ case nsITelemetry::SCALAR_TYPE_COUNT:
+ if (!upd.mData->is<uint32_t>()) {
+ NS_WARNING("Attempting to set a count scalar to a non-integer.");
+ continue;
+ }
+ scalar->SetValue(upd.mData->as<uint32_t>());
+ break;
+ case nsITelemetry::SCALAR_TYPE_BOOLEAN:
+ if (!upd.mData->is<bool>()) {
+ NS_WARNING(
+ "Attempting to set a boolean scalar to a non-boolean.");
+ continue;
+ }
+ scalar->SetValue(upd.mData->as<bool>());
+ break;
+ case nsITelemetry::SCALAR_TYPE_STRING:
+ if (!upd.mData->is<nsString>()) {
+ NS_WARNING("Attempting to set a string scalar to a non-string.");
+ continue;
+ }
+ scalar->SetValue(upd.mData->as<nsString>());
+ break;
+ }
+ break;
+ }
+ case ScalarActionType::eAdd: {
+ if (scalarType != nsITelemetry::SCALAR_TYPE_COUNT) {
+ NS_WARNING("Attempting to add on a non count scalar.");
+ continue;
+ }
+ // We only support adding uint32_t.
+ if (!upd.mData->is<uint32_t>()) {
+ NS_WARNING("Attempting to add to a count scalar with a non-integer.");
+ continue;
+ }
+ scalar->AddValue(upd.mData->as<uint32_t>());
+ break;
+ }
+ case ScalarActionType::eSetMaximum: {
+ if (scalarType != nsITelemetry::SCALAR_TYPE_COUNT) {
+ NS_WARNING("Attempting to setMaximum on a non count scalar.");
+ continue;
+ }
+ // We only support SetMaximum on uint32_t.
+ if (!upd.mData->is<uint32_t>()) {
+ NS_WARNING(
+ "Attempting to setMaximum a count scalar to a non-integer.");
+ continue;
+ }
+ scalar->SetMaximum(upd.mData->as<uint32_t>());
+ break;
+ }
+ default:
+ NS_WARNING("Unsupported action coming from scalar child updates.");
+ }
+ }
+}
+
+void internal_ApplyKeyedScalarActions(
+ const StaticMutexAutoLock& lock,
+ const nsTArray<mozilla::Telemetry::KeyedScalarAction>& aScalarActions,
+ const mozilla::Maybe<ProcessID>& aProcessType = Nothing()) {
+ if (!internal_CanRecordBase(lock)) {
+ return;
+ }
+
+ for (auto& upd : aScalarActions) {
+ ScalarKey uniqueId{upd.mId, upd.mDynamic};
+ if (NS_WARN_IF(!internal_IsValidId(lock, uniqueId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ continue;
+ }
+
+ if (!internal_IsKeyedScalar(lock, uniqueId)) {
+ continue;
+ }
+
+ // Are we allowed to record this scalar? We don't need to check for
+ // allowed processes here, that's taken care of when recording
+ // in child processes.
+ if (!internal_CanRecordForScalarID(lock, uniqueId)) {
+ continue;
+ }
+
+ // Either we got passed a process type or it was explicitely set on the
+ // recorded action. It should never happen that it is set to an invalid
+ // value (such as ProcessID::Count)
+ ProcessID processType = aProcessType.valueOr(upd.mProcessType);
+ MOZ_ASSERT(processType != ProcessID::Count);
+
+ // Refresh the data in the parent process with the data coming from the
+ // child processes.
+ KeyedScalar* scalar = nullptr;
+ nsresult rv =
+ internal_GetKeyedScalarByEnum(lock, uniqueId, processType, &scalar);
+ if (NS_FAILED(rv)) {
+ // Bug 1513496 - We no longer log a warning if the scalar is expired.
+ if (rv != NS_ERROR_NOT_AVAILABLE) {
+ NS_WARNING("NS_FAILED internal_GetKeyedScalarByEnum for CHILD");
+ }
+ continue;
+ }
+
+ if (upd.mData.isNothing()) {
+ MOZ_ASSERT(false, "There is no data in the KeyedScalarAction.");
+ continue;
+ }
+
+ // Get the type of this scalar from the scalar ID. We already checked
+ // for its validity a few lines above.
+ const uint32_t scalarType = internal_GetScalarInfo(lock, uniqueId).kind;
+
+ // Extract the data from the mozilla::Variant.
+ switch (upd.mActionType) {
+ case ScalarActionType::eSet: {
+ switch (scalarType) {
+ case nsITelemetry::SCALAR_TYPE_COUNT:
+ if (!upd.mData->is<uint32_t>()) {
+ NS_WARNING("Attempting to set a count scalar to a non-integer.");
+ continue;
+ }
+ scalar->SetValue(lock, NS_ConvertUTF8toUTF16(upd.mKey),
+ upd.mData->as<uint32_t>());
+ break;
+ case nsITelemetry::SCALAR_TYPE_BOOLEAN:
+ if (!upd.mData->is<bool>()) {
+ NS_WARNING(
+ "Attempting to set a boolean scalar to a non-boolean.");
+ continue;
+ }
+ scalar->SetValue(lock, NS_ConvertUTF8toUTF16(upd.mKey),
+ upd.mData->as<bool>());
+ break;
+ default:
+ NS_WARNING("Unsupported type coming from scalar child updates.");
+ }
+ break;
+ }
+ case ScalarActionType::eAdd: {
+ if (scalarType != nsITelemetry::SCALAR_TYPE_COUNT) {
+ NS_WARNING("Attempting to add on a non count scalar.");
+ continue;
+ }
+ // We only support adding on uint32_t.
+ if (!upd.mData->is<uint32_t>()) {
+ NS_WARNING("Attempting to add to a count scalar with a non-integer.");
+ continue;
+ }
+ scalar->AddValue(lock, NS_ConvertUTF8toUTF16(upd.mKey),
+ upd.mData->as<uint32_t>());
+ break;
+ }
+ case ScalarActionType::eSetMaximum: {
+ if (scalarType != nsITelemetry::SCALAR_TYPE_COUNT) {
+ NS_WARNING("Attempting to setMaximum on a non count scalar.");
+ continue;
+ }
+ // We only support SetMaximum on uint32_t.
+ if (!upd.mData->is<uint32_t>()) {
+ NS_WARNING(
+ "Attempting to setMaximum a count scalar to a non-integer.");
+ continue;
+ }
+ scalar->SetMaximum(lock, NS_ConvertUTF8toUTF16(upd.mKey),
+ upd.mData->as<uint32_t>());
+ break;
+ }
+ default:
+ NS_WARNING(
+ "Unsupported action coming from keyed scalar child updates.");
+ }
+ }
+}
+
+void internal_ApplyPendingOperations(const StaticMutexAutoLock& lock) {
+ if (gScalarsActions && gScalarsActions->Length() > 0) {
+ internal_ApplyScalarActions(lock, *gScalarsActions);
+ gScalarsActions->Clear();
+ }
+
+ if (gKeyedScalarsActions && gKeyedScalarsActions->Length() > 0) {
+ internal_ApplyKeyedScalarActions(lock, *gKeyedScalarsActions);
+ gKeyedScalarsActions->Clear();
+ }
+
+ // After all pending operations are applied deserialization is done
+ gIsDeserializing = false;
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// EXTERNALLY VISIBLE FUNCTIONS in namespace TelemetryScalars::
+
+// This is a StaticMutex rather than a plain Mutex (1) so that
+// it gets initialised in a thread-safe manner the first time
+// it is used, and (2) because it is never de-initialised, and
+// a normal Mutex would show up as a leak in BloatView. StaticMutex
+// also has the "OffTheBooks" property, so it won't show as a leak
+// in BloatView.
+// Another reason to use a StaticMutex instead of a plain Mutex is
+// that, due to the nature of Telemetry, we cannot rely on having a
+// mutex initialized in InitializeGlobalState. Unfortunately, we
+// cannot make sure that no other function is called before this point.
+static StaticMutex gTelemetryScalarsMutex MOZ_UNANNOTATED;
+
+void TelemetryScalar::InitializeGlobalState(bool aCanRecordBase,
+ bool aCanRecordExtended) {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ MOZ_ASSERT(!gInitDone,
+ "TelemetryScalar::InitializeGlobalState "
+ "may only be called once");
+
+ gCanRecordBase = aCanRecordBase;
+ gCanRecordExtended = aCanRecordExtended;
+
+ // Populate the static scalar name->id cache. Note that the scalar names are
+ // statically allocated and come from the automatically generated
+ // TelemetryScalarData.h.
+ uint32_t scalarCount =
+ static_cast<uint32_t>(mozilla::Telemetry::ScalarID::ScalarCount);
+ for (uint32_t i = 0; i < scalarCount; i++) {
+ CharPtrEntryType* entry = gScalarNameIDMap.PutEntry(gScalars[i].name());
+ entry->SetData(ScalarKey{i, false});
+ }
+
+ // To summarize dynamic events we need a dynamic scalar.
+ const nsTArray<DynamicScalarInfo> initialDynamicScalars({
+ DynamicScalarInfo{
+ nsITelemetry::SCALAR_TYPE_COUNT,
+ true /* recordOnRelease */,
+ false /* expired */,
+ nsAutoCString("telemetry.dynamic_event_counts"),
+ true /* keyed */,
+ false /* built-in */,
+ {} /* stores */,
+ },
+ });
+ internal_RegisterScalars(locker, initialDynamicScalars);
+
+ gInitDone = true;
+}
+
+void TelemetryScalar::DeInitializeGlobalState() {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ gCanRecordBase = false;
+ gCanRecordExtended = false;
+ gScalarNameIDMap.Clear();
+ gScalarStorageMap.Clear();
+ gKeyedScalarStorageMap.Clear();
+ gDynamicBuiltinScalarStorageMap.Clear();
+ gDynamicBuiltinKeyedScalarStorageMap.Clear();
+ gDynamicScalarInfo = nullptr;
+ gDynamicStoreNames = nullptr;
+ gInitDone = false;
+}
+
+void TelemetryScalar::DeserializationStarted() {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ gIsDeserializing = true;
+}
+
+void TelemetryScalar::ApplyPendingOperations() {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ internal_ApplyPendingOperations(locker);
+}
+
+void TelemetryScalar::SetCanRecordBase(bool b) {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ gCanRecordBase = b;
+}
+
+void TelemetryScalar::SetCanRecordExtended(bool b) {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ gCanRecordExtended = b;
+}
+
+/**
+ * Adds the value to the given scalar.
+ *
+ * @param aName The scalar name.
+ * @param aVal The numeric value to add to the scalar.
+ * @param aCx The JS context.
+ * @return NS_OK (always) so that the JS API call doesn't throw. In case of
+ * errors, a warning level message is printed in the browser console.
+ */
+nsresult TelemetryScalar::Add(const nsACString& aName,
+ JS::Handle<JS::Value> aVal, JSContext* aCx) {
+ // Unpack the aVal to nsIVariant. This uses the JS context.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv = nsContentUtils::XPConnect()->JSToVariant(
+ aCx, aVal, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ internal_LogScalarError(aName, ScalarResult::CannotUnpackVariant);
+ return NS_OK;
+ }
+
+ ScalarResult sr;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ sr = internal_UpdateScalar(locker, aName, ScalarActionType::eAdd,
+ unpackedVal);
+ }
+
+ // Warn the user about the error if we need to.
+ if (sr != ScalarResult::Ok) {
+ internal_LogScalarError(aName, sr);
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Adds the value to the given scalar.
+ *
+ * @param aName The scalar name.
+ * @param aKey The key name.
+ * @param aVal The numeric value to add to the scalar.
+ * @param aCx The JS context.
+ * @return NS_OK (always) so that the JS API call doesn't throw. In case of
+ * errors, a warning level message is printed in the browser console.
+ */
+nsresult TelemetryScalar::Add(const nsACString& aName, const nsAString& aKey,
+ JS::Handle<JS::Value> aVal, JSContext* aCx) {
+ // Unpack the aVal to nsIVariant. This uses the JS context.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv = nsContentUtils::XPConnect()->JSToVariant(
+ aCx, aVal, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ internal_LogScalarError(aName, ScalarResult::CannotUnpackVariant);
+ return NS_OK;
+ }
+
+ ScalarResult sr;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ sr = internal_UpdateKeyedScalar(locker, aName, aKey, ScalarActionType::eAdd,
+ unpackedVal);
+ }
+
+ // Warn the user about the error if we need to.
+ if (sr != ScalarResult::Ok) {
+ internal_LogScalarError(aName, sr);
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Adds the value to the given scalar.
+ *
+ * @param aId The scalar enum id.
+ * @param aVal The numeric value to add to the scalar.
+ */
+void TelemetryScalar::Add(mozilla::Telemetry::ScalarID aId, uint32_t aValue) {
+ if (NS_WARN_IF(!IsValidEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ return;
+ }
+
+ ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ if (internal_CanRecordScalar(locker, uniqueId, false) != ScalarResult::Ok) {
+ // We can't record this scalar. Bail out.
+ return;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ TelemetryIPCAccumulator::RecordChildScalarAction(
+ uniqueId.id, uniqueId.dynamic, ScalarActionType::eAdd,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ if (internal_IsScalarDeserializing(locker)) {
+ internal_RecordScalarAction(locker, uniqueId.id, uniqueId.dynamic,
+ ScalarActionType::eAdd, ScalarVariant(aValue));
+ return;
+ }
+
+ ScalarBase* scalar = nullptr;
+ nsresult rv =
+ internal_GetScalarByEnum(locker, uniqueId, ProcessID::Parent, &scalar);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ scalar->AddValue(aValue);
+}
+
+/**
+ * Adds the value to the given keyed scalar.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The key name.
+ * @param aVal The numeric value to add to the scalar.
+ */
+void TelemetryScalar::Add(mozilla::Telemetry::ScalarID aId,
+ const nsAString& aKey, uint32_t aValue) {
+ if (NS_WARN_IF(!IsValidEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ return;
+ }
+
+ ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ if (internal_CanRecordScalar(locker, uniqueId, true) != ScalarResult::Ok) {
+ // We can't record this scalar. Bail out.
+ return;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ TelemetryIPCAccumulator::RecordChildKeyedScalarAction(
+ uniqueId.id, uniqueId.dynamic, aKey, ScalarActionType::eAdd,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ if (internal_IsScalarDeserializing(locker)) {
+ internal_RecordKeyedScalarAction(locker, uniqueId.id, uniqueId.dynamic,
+ aKey, ScalarActionType::eAdd,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ KeyedScalar* scalar = nullptr;
+ nsresult rv = internal_GetKeyedScalarByEnum(locker, uniqueId,
+ ProcessID::Parent, &scalar);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ scalar->AddValue(locker, aKey, aValue);
+}
+
+/**
+ * Sets the scalar to the given value.
+ *
+ * @param aName The scalar name.
+ * @param aVal The value to set the scalar to.
+ * @param aCx The JS context.
+ * @return NS_OK (always) so that the JS API call doesn't throw. In case of
+ * errors, a warning level message is printed in the browser console.
+ */
+nsresult TelemetryScalar::Set(const nsACString& aName,
+ JS::Handle<JS::Value> aVal, JSContext* aCx) {
+ // Unpack the aVal to nsIVariant. This uses the JS context.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv = nsContentUtils::XPConnect()->JSToVariant(
+ aCx, aVal, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ internal_LogScalarError(aName, ScalarResult::CannotUnpackVariant);
+ return NS_OK;
+ }
+
+ ScalarResult sr;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ sr = internal_UpdateScalar(locker, aName, ScalarActionType::eSet,
+ unpackedVal);
+ }
+
+ // Warn the user about the error if we need to.
+ if (sr != ScalarResult::Ok) {
+ internal_LogScalarError(aName, sr);
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Sets the keyed scalar to the given value.
+ *
+ * @param aName The scalar name.
+ * @param aKey The key name.
+ * @param aVal The value to set the scalar to.
+ * @param aCx The JS context.
+ * @return NS_OK (always) so that the JS API call doesn't throw. In case of
+ * errors, a warning level message is printed in the browser console.
+ */
+nsresult TelemetryScalar::Set(const nsACString& aName, const nsAString& aKey,
+ JS::Handle<JS::Value> aVal, JSContext* aCx) {
+ // Unpack the aVal to nsIVariant. This uses the JS context.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv = nsContentUtils::XPConnect()->JSToVariant(
+ aCx, aVal, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ internal_LogScalarError(aName, ScalarResult::CannotUnpackVariant);
+ return NS_OK;
+ }
+
+ ScalarResult sr;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ sr = internal_UpdateKeyedScalar(locker, aName, aKey, ScalarActionType::eSet,
+ unpackedVal);
+ }
+
+ // Warn the user about the error if we need to.
+ if (sr != ScalarResult::Ok) {
+ internal_LogScalarError(aName, sr);
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Sets the scalar to the given numeric value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The numeric, unsigned value to set the scalar to.
+ */
+void TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId, uint32_t aValue) {
+ if (NS_WARN_IF(!IsValidEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ return;
+ }
+
+ ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ if (internal_CanRecordScalar(locker, uniqueId, false) != ScalarResult::Ok) {
+ // We can't record this scalar. Bail out.
+ return;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ TelemetryIPCAccumulator::RecordChildScalarAction(
+ uniqueId.id, uniqueId.dynamic, ScalarActionType::eSet,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ if (internal_IsScalarDeserializing(locker)) {
+ internal_RecordScalarAction(locker, uniqueId.id, uniqueId.dynamic,
+ ScalarActionType::eSet, ScalarVariant(aValue));
+ return;
+ }
+
+ ScalarBase* scalar = nullptr;
+ nsresult rv =
+ internal_GetScalarByEnum(locker, uniqueId, ProcessID::Parent, &scalar);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ scalar->SetValue(aValue);
+}
+
+/**
+ * Sets the scalar to the given string value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The string value to set the scalar to.
+ */
+void TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId,
+ const nsAString& aValue) {
+ if (NS_WARN_IF(!IsValidEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ return;
+ }
+
+ ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ if (internal_CanRecordScalar(locker, uniqueId, false) != ScalarResult::Ok) {
+ // We can't record this scalar. Bail out.
+ return;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ TelemetryIPCAccumulator::RecordChildScalarAction(
+ uniqueId.id, uniqueId.dynamic, ScalarActionType::eSet,
+ ScalarVariant(nsString(aValue)));
+ return;
+ }
+
+ if (internal_IsScalarDeserializing(locker)) {
+ internal_RecordScalarAction(locker, uniqueId.id, uniqueId.dynamic,
+ ScalarActionType::eSet,
+ ScalarVariant(nsString(aValue)));
+ return;
+ }
+
+ ScalarBase* scalar = nullptr;
+ nsresult rv =
+ internal_GetScalarByEnum(locker, uniqueId, ProcessID::Parent, &scalar);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ scalar->SetValue(aValue);
+}
+
+/**
+ * Sets the scalar to the given boolean value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The boolean value to set the scalar to.
+ */
+void TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId, bool aValue) {
+ if (NS_WARN_IF(!IsValidEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ return;
+ }
+
+ ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ if (internal_CanRecordScalar(locker, uniqueId, false) != ScalarResult::Ok) {
+ // We can't record this scalar. Bail out.
+ return;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ TelemetryIPCAccumulator::RecordChildScalarAction(
+ uniqueId.id, uniqueId.dynamic, ScalarActionType::eSet,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ if (internal_IsScalarDeserializing(locker)) {
+ internal_RecordScalarAction(locker, uniqueId.id, uniqueId.dynamic,
+ ScalarActionType::eSet, ScalarVariant(aValue));
+ return;
+ }
+
+ ScalarBase* scalar = nullptr;
+ nsresult rv =
+ internal_GetScalarByEnum(locker, uniqueId, ProcessID::Parent, &scalar);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ scalar->SetValue(aValue);
+}
+
+/**
+ * Sets the keyed scalar to the given numeric value.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The scalar key.
+ * @param aValue The numeric, unsigned value to set the scalar to.
+ */
+void TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId,
+ const nsAString& aKey, uint32_t aValue) {
+ if (NS_WARN_IF(!IsValidEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ return;
+ }
+
+ ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ if (internal_CanRecordScalar(locker, uniqueId, true) != ScalarResult::Ok) {
+ // We can't record this scalar. Bail out.
+ return;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ TelemetryIPCAccumulator::RecordChildKeyedScalarAction(
+ uniqueId.id, uniqueId.dynamic, aKey, ScalarActionType::eSet,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ if (internal_IsScalarDeserializing(locker)) {
+ internal_RecordKeyedScalarAction(locker, uniqueId.id, uniqueId.dynamic,
+ aKey, ScalarActionType::eSet,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ KeyedScalar* scalar = nullptr;
+ nsresult rv = internal_GetKeyedScalarByEnum(locker, uniqueId,
+ ProcessID::Parent, &scalar);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ scalar->SetValue(locker, aKey, aValue);
+}
+
+/**
+ * Sets the scalar to the given boolean value.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The scalar key.
+ * @param aValue The boolean value to set the scalar to.
+ */
+void TelemetryScalar::Set(mozilla::Telemetry::ScalarID aId,
+ const nsAString& aKey, bool aValue) {
+ if (NS_WARN_IF(!IsValidEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ return;
+ }
+
+ ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ if (internal_CanRecordScalar(locker, uniqueId, true) != ScalarResult::Ok) {
+ // We can't record this scalar. Bail out.
+ return;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ TelemetryIPCAccumulator::RecordChildKeyedScalarAction(
+ uniqueId.id, uniqueId.dynamic, aKey, ScalarActionType::eSet,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ if (internal_IsScalarDeserializing(locker)) {
+ internal_RecordKeyedScalarAction(locker, uniqueId.id, uniqueId.dynamic,
+ aKey, ScalarActionType::eSet,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ KeyedScalar* scalar = nullptr;
+ nsresult rv = internal_GetKeyedScalarByEnum(locker, uniqueId,
+ ProcessID::Parent, &scalar);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ scalar->SetValue(locker, aKey, aValue);
+}
+
+/**
+ * Sets the scalar to the maximum of the current and the passed value.
+ *
+ * @param aName The scalar name.
+ * @param aVal The numeric value to set the scalar to.
+ * @param aCx The JS context.
+ * @return NS_OK (always) so that the JS API call doesn't throw. In case of
+ * errors, a warning level message is printed in the browser console.
+ */
+nsresult TelemetryScalar::SetMaximum(const nsACString& aName,
+ JS::Handle<JS::Value> aVal,
+ JSContext* aCx) {
+ // Unpack the aVal to nsIVariant. This uses the JS context.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv = nsContentUtils::XPConnect()->JSToVariant(
+ aCx, aVal, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ internal_LogScalarError(aName, ScalarResult::CannotUnpackVariant);
+ return NS_OK;
+ }
+
+ ScalarResult sr;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ sr = internal_UpdateScalar(locker, aName, ScalarActionType::eSetMaximum,
+ unpackedVal);
+ }
+
+ // Warn the user about the error if we need to.
+ if (sr != ScalarResult::Ok) {
+ internal_LogScalarError(aName, sr);
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Sets the scalar to the maximum of the current and the passed value.
+ *
+ * @param aName The scalar name.
+ * @param aKey The key name.
+ * @param aVal The numeric value to set the scalar to.
+ * @param aCx The JS context.
+ * @return NS_OK (always) so that the JS API call doesn't throw. In case of
+ * errors, a warning level message is printed in the browser console.
+ */
+nsresult TelemetryScalar::SetMaximum(const nsACString& aName,
+ const nsAString& aKey,
+ JS::Handle<JS::Value> aVal,
+ JSContext* aCx) {
+ // Unpack the aVal to nsIVariant. This uses the JS context.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv = nsContentUtils::XPConnect()->JSToVariant(
+ aCx, aVal, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ internal_LogScalarError(aName, ScalarResult::CannotUnpackVariant);
+ return NS_OK;
+ }
+
+ ScalarResult sr;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ sr = internal_UpdateKeyedScalar(locker, aName, aKey,
+ ScalarActionType::eSetMaximum, unpackedVal);
+ }
+
+ // Warn the user about the error if we need to.
+ if (sr != ScalarResult::Ok) {
+ internal_LogScalarError(aName, sr);
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Sets the scalar to the maximum of the current and the passed value.
+ *
+ * @param aId The scalar enum id.
+ * @param aValue The numeric value to set the scalar to.
+ */
+void TelemetryScalar::SetMaximum(mozilla::Telemetry::ScalarID aId,
+ uint32_t aValue) {
+ if (NS_WARN_IF(!IsValidEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ return;
+ }
+
+ ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ if (internal_CanRecordScalar(locker, uniqueId, false) != ScalarResult::Ok) {
+ // We can't record this scalar. Bail out.
+ return;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ TelemetryIPCAccumulator::RecordChildScalarAction(
+ uniqueId.id, uniqueId.dynamic, ScalarActionType::eSetMaximum,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ if (internal_IsScalarDeserializing(locker)) {
+ internal_RecordScalarAction(locker, uniqueId.id, uniqueId.dynamic,
+ ScalarActionType::eSetMaximum,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ ScalarBase* scalar = nullptr;
+ nsresult rv =
+ internal_GetScalarByEnum(locker, uniqueId, ProcessID::Parent, &scalar);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ scalar->SetMaximum(aValue);
+}
+
+/**
+ * Sets the keyed scalar to the maximum of the current and the passed value.
+ *
+ * @param aId The scalar enum id.
+ * @param aKey The key name.
+ * @param aValue The numeric value to set the scalar to.
+ */
+void TelemetryScalar::SetMaximum(mozilla::Telemetry::ScalarID aId,
+ const nsAString& aKey, uint32_t aValue) {
+ if (NS_WARN_IF(!IsValidEnumId(aId))) {
+ MOZ_ASSERT_UNREACHABLE("Scalar usage requires valid ids.");
+ return;
+ }
+
+ ScalarKey uniqueId{static_cast<uint32_t>(aId), false};
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ if (internal_CanRecordScalar(locker, uniqueId, true) != ScalarResult::Ok) {
+ // We can't record this scalar. Bail out.
+ return;
+ }
+
+ // Accumulate in the child process if needed.
+ if (!XRE_IsParentProcess()) {
+ TelemetryIPCAccumulator::RecordChildKeyedScalarAction(
+ uniqueId.id, uniqueId.dynamic, aKey, ScalarActionType::eSetMaximum,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ if (internal_IsScalarDeserializing(locker)) {
+ internal_RecordKeyedScalarAction(locker, uniqueId.id, uniqueId.dynamic,
+ aKey, ScalarActionType::eSetMaximum,
+ ScalarVariant(aValue));
+ return;
+ }
+
+ KeyedScalar* scalar = nullptr;
+ nsresult rv = internal_GetKeyedScalarByEnum(locker, uniqueId,
+ ProcessID::Parent, &scalar);
+ if (NS_FAILED(rv)) {
+ return;
+ }
+
+ scalar->SetMaximum(locker, aKey, aValue);
+}
+
+nsresult TelemetryScalar::CreateSnapshots(unsigned int aDataset,
+ bool aClearScalars, JSContext* aCx,
+ uint8_t optional_argc,
+ JS::MutableHandle<JS::Value> aResult,
+ bool aFilterTest,
+ const nsACString& aStoreName) {
+ MOZ_ASSERT(
+ XRE_IsParentProcess(),
+ "Snapshotting scalars should only happen in the parent processes.");
+ // If no arguments were passed in, apply the default value.
+ if (!optional_argc) {
+ aClearScalars = false;
+ }
+
+ JS::Rooted<JSObject*> root_obj(aCx, JS_NewPlainObject(aCx));
+ if (!root_obj) {
+ return NS_ERROR_FAILURE;
+ }
+ aResult.setObject(*root_obj);
+
+ // Return `{}` in child processes.
+ if (!XRE_IsParentProcess()) {
+ return NS_OK;
+ }
+
+ // Only lock the mutex while accessing our data, without locking any JS
+ // related code.
+ ScalarSnapshotTable scalarsToReflect;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ nsresult rv = internal_GetScalarSnapshot(locker, scalarsToReflect, aDataset,
+ aClearScalars, aStoreName);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ // Reflect it to JS.
+ for (const auto& entry : scalarsToReflect) {
+ const ScalarTupleArray& processScalars = entry.GetData();
+ const char* processName = GetNameForProcessID(ProcessID(entry.GetKey()));
+
+ // Create the object that will hold the scalars for this process and add it
+ // to the returned root object.
+ JS::Rooted<JSObject*> processObj(aCx, JS_NewPlainObject(aCx));
+ if (!processObj || !JS_DefineProperty(aCx, root_obj, processName,
+ processObj, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (ScalarTupleArray::size_type i = 0; i < processScalars.Length(); i++) {
+ const ScalarDataTuple& scalar = processScalars[i];
+
+ const char* scalarName = std::get<0>(scalar);
+ if (aFilterTest && strncmp(TEST_SCALAR_PREFIX, scalarName,
+ strlen(TEST_SCALAR_PREFIX)) == 0) {
+ continue;
+ }
+
+ // Convert it to a JS Val.
+ JS::Rooted<JS::Value> scalarJsValue(aCx);
+ nsresult rv = nsContentUtils::XPConnect()->VariantToJS(
+ aCx, processObj, std::get<1>(scalar), &scalarJsValue);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Add it to the scalar object.
+ if (!JS_DefineProperty(aCx, processObj, scalarName, scalarJsValue,
+ JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult TelemetryScalar::CreateKeyedSnapshots(
+ unsigned int aDataset, bool aClearScalars, JSContext* aCx,
+ uint8_t optional_argc, JS::MutableHandle<JS::Value> aResult,
+ bool aFilterTest, const nsACString& aStoreName) {
+ MOZ_ASSERT(
+ XRE_IsParentProcess(),
+ "Snapshotting scalars should only happen in the parent processes.");
+ // If no arguments were passed in, apply the default value.
+ if (!optional_argc) {
+ aClearScalars = false;
+ }
+
+ JS::Rooted<JSObject*> root_obj(aCx, JS_NewPlainObject(aCx));
+ if (!root_obj) {
+ return NS_ERROR_FAILURE;
+ }
+ aResult.setObject(*root_obj);
+
+ // Return `{}` in child processes.
+ if (!XRE_IsParentProcess()) {
+ return NS_OK;
+ }
+
+ // Only lock the mutex while accessing our data, without locking any JS
+ // related code.
+ KeyedScalarSnapshotTable scalarsToReflect;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ nsresult rv = internal_GetKeyedScalarSnapshot(
+ locker, scalarsToReflect, aDataset, aClearScalars, aStoreName);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ // Reflect it to JS.
+ for (const auto& entry : scalarsToReflect) {
+ const KeyedScalarTupleArray& processScalars = entry.GetData();
+ const char* processName = GetNameForProcessID(ProcessID(entry.GetKey()));
+
+ // Create the object that will hold the scalars for this process and add it
+ // to the returned root object.
+ JS::Rooted<JSObject*> processObj(aCx, JS_NewPlainObject(aCx));
+ if (!processObj || !JS_DefineProperty(aCx, root_obj, processName,
+ processObj, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (KeyedScalarTupleArray::size_type i = 0; i < processScalars.Length();
+ i++) {
+ const KeyedScalarDataTuple& keyedScalarData = processScalars[i];
+
+ const char* scalarName = std::get<0>(keyedScalarData);
+ if (aFilterTest && strncmp(TEST_SCALAR_PREFIX, scalarName,
+ strlen(TEST_SCALAR_PREFIX)) == 0) {
+ continue;
+ }
+
+ // Go through each keyed scalar and create a keyed scalar object.
+ // This object will hold the values for all the keyed scalar keys.
+ JS::Rooted<JSObject*> keyedScalarObj(aCx, JS_NewPlainObject(aCx));
+
+ // Define a property for each scalar key, then add it to the keyed scalar
+ // object.
+ const nsTArray<KeyedScalar::KeyValuePair>& keyProps =
+ std::get<1>(keyedScalarData);
+ for (uint32_t i = 0; i < keyProps.Length(); i++) {
+ const KeyedScalar::KeyValuePair& keyData = keyProps[i];
+
+ // Convert the value for the key to a JSValue.
+ JS::Rooted<JS::Value> keyJsValue(aCx);
+ nsresult rv = nsContentUtils::XPConnect()->VariantToJS(
+ aCx, keyedScalarObj, keyData.second, &keyJsValue);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ // Add the key to the scalar representation.
+ const NS_ConvertUTF8toUTF16 key(keyData.first);
+ if (!JS_DefineUCProperty(aCx, keyedScalarObj, key.Data(), key.Length(),
+ keyJsValue, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Add the scalar to the root object.
+ if (!JS_DefineProperty(aCx, processObj, scalarName, keyedScalarObj,
+ JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult TelemetryScalar::RegisterScalars(const nsACString& aCategoryName,
+ JS::Handle<JS::Value> aScalarData,
+ bool aBuiltin, JSContext* cx) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Dynamic scalars should only be created in the parent process.");
+
+ if (!IsValidIdentifierString(aCategoryName, kMaximumCategoryNameLength, true,
+ false)) {
+ JS_ReportErrorASCII(cx, "Invalid category name %s.",
+ PromiseFlatCString(aCategoryName).get());
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ if (!aScalarData.isObject()) {
+ JS_ReportErrorASCII(cx, "Scalar data parameter should be an object");
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ JS::Rooted<JSObject*> obj(cx, &aScalarData.toObject());
+ JS::Rooted<JS::IdVector> scalarPropertyIds(cx, JS::IdVector(cx));
+ if (!JS_Enumerate(cx, obj, &scalarPropertyIds)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ // Collect the scalar data into local storage first.
+ // Only after successfully validating all contained scalars will we register
+ // them into global storage.
+ nsTArray<DynamicScalarInfo> newScalarInfos;
+
+ for (size_t i = 0, n = scalarPropertyIds.length(); i < n; i++) {
+ nsAutoJSString scalarName;
+ if (!scalarName.init(cx, scalarPropertyIds[i])) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!IsValidIdentifierString(NS_ConvertUTF16toUTF8(scalarName),
+ kMaximumScalarNameLength, false, true)) {
+ JS_ReportErrorASCII(
+ cx, "Invalid scalar name %s.",
+ PromiseFlatCString(NS_ConvertUTF16toUTF8(scalarName)).get());
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ // Join the category and the probe names.
+ nsPrintfCString fullName("%s.%s", PromiseFlatCString(aCategoryName).get(),
+ NS_ConvertUTF16toUTF8(scalarName).get());
+
+ JS::Rooted<JS::Value> value(cx);
+ if (!JS_GetPropertyById(cx, obj, scalarPropertyIds[i], &value) ||
+ !value.isObject()) {
+ return NS_ERROR_FAILURE;
+ }
+ JS::Rooted<JSObject*> scalarDef(cx, &value.toObject());
+
+ // Get the scalar's kind.
+ if (!JS_GetProperty(cx, scalarDef, "kind", &value) || !value.isInt32()) {
+ JS_ReportErrorASCII(cx, "Invalid or missing 'kind' for scalar %s.",
+ PromiseFlatCString(fullName).get());
+ return NS_ERROR_FAILURE;
+ }
+ uint32_t kind = static_cast<uint32_t>(value.toInt32());
+
+ // Get the optional scalar's recording policy (default to false).
+ bool hasProperty = false;
+ bool recordOnRelease = false;
+ if (JS_HasProperty(cx, scalarDef, "record_on_release", &hasProperty) &&
+ hasProperty) {
+ if (!JS_GetProperty(cx, scalarDef, "record_on_release", &value) ||
+ !value.isBoolean()) {
+ JS_ReportErrorASCII(cx, "Invalid 'record_on_release' for scalar %s.",
+ PromiseFlatCString(fullName).get());
+ return NS_ERROR_FAILURE;
+ }
+ recordOnRelease = static_cast<bool>(value.toBoolean());
+ }
+
+ // Get the optional scalar's keyed (default to false).
+ bool keyed = false;
+ if (JS_HasProperty(cx, scalarDef, "keyed", &hasProperty) && hasProperty) {
+ if (!JS_GetProperty(cx, scalarDef, "keyed", &value) ||
+ !value.isBoolean()) {
+ JS_ReportErrorASCII(cx, "Invalid 'keyed' for scalar %s.",
+ PromiseFlatCString(fullName).get());
+ return NS_ERROR_FAILURE;
+ }
+ keyed = static_cast<bool>(value.toBoolean());
+ }
+
+ // Get the optional scalar's expired state (default to false).
+ bool expired = false;
+ if (JS_HasProperty(cx, scalarDef, "expired", &hasProperty) && hasProperty) {
+ if (!JS_GetProperty(cx, scalarDef, "expired", &value) ||
+ !value.isBoolean()) {
+ JS_ReportErrorASCII(cx, "Invalid 'expired' for scalar %s.",
+ PromiseFlatCString(fullName).get());
+ return NS_ERROR_FAILURE;
+ }
+ expired = static_cast<bool>(value.toBoolean());
+ }
+
+ // Get the scalar's optional stores list (default to ["main"]).
+ nsTArray<nsCString> stores;
+ if (JS_HasProperty(cx, scalarDef, "stores", &hasProperty) && hasProperty) {
+ bool isArray = false;
+ if (!JS_GetProperty(cx, scalarDef, "stores", &value) ||
+ !JS::IsArrayObject(cx, value, &isArray) || !isArray) {
+ JS_ReportErrorASCII(cx, "Invalid 'stores' for scalar %s.",
+ PromiseFlatCString(fullName).get());
+ return NS_ERROR_FAILURE;
+ }
+
+ JS::Rooted<JSObject*> arrayObj(cx, &value.toObject());
+ uint32_t storesLength = 0;
+ if (!JS::GetArrayLength(cx, arrayObj, &storesLength)) {
+ JS_ReportErrorASCII(cx,
+ "Can't get 'stores' array length for scalar %s.",
+ PromiseFlatCString(fullName).get());
+ return NS_ERROR_FAILURE;
+ }
+
+ for (uint32_t i = 0; i < storesLength; ++i) {
+ JS::Rooted<JS::Value> elt(cx);
+ if (!JS_GetElement(cx, arrayObj, i, &elt)) {
+ JS_ReportErrorASCII(
+ cx, "Can't get element from scalar %s 'stores' array.",
+ PromiseFlatCString(fullName).get());
+ return NS_ERROR_FAILURE;
+ }
+ if (!elt.isString()) {
+ JS_ReportErrorASCII(cx,
+ "Element in scalar %s 'stores' array isn't a "
+ "string.",
+ PromiseFlatCString(fullName).get());
+ return NS_ERROR_FAILURE;
+ }
+
+ nsAutoJSString jsStr;
+ if (!jsStr.init(cx, elt)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ stores.AppendElement(NS_ConvertUTF16toUTF8(jsStr));
+ }
+ // In the event of the usual case (just "main"), save the storage.
+ if (stores.Length() == 1 && stores[0].EqualsLiteral("main")) {
+ stores.TruncateLength(0);
+ }
+ }
+
+ // We defer the actual registration here in case any other event description
+ // is invalid. In that case we don't need to roll back any partial
+ // registration.
+ newScalarInfos.AppendElement(
+ DynamicScalarInfo{kind, recordOnRelease, expired, fullName, keyed,
+ aBuiltin, std::move(stores)});
+ }
+
+ // Register the dynamic definition on the parent process.
+ nsTArray<DynamicScalarDefinition> ipcDefinitions;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ ::internal_RegisterScalars(locker, newScalarInfos);
+
+ // Convert the internal scalar representation to a stripped down IPC one.
+ ::internal_DynamicScalarToIPC(locker, newScalarInfos, ipcDefinitions);
+ }
+
+ // Propagate the registration to all the content-processes.
+ // Do not hold the mutex while calling IPC.
+ ::internal_BroadcastDefinitions(ipcDefinitions);
+
+ return NS_OK;
+}
+
+/**
+ * Count in Scalars how many of which events were recorded. See bug 1440673
+ *
+ * Event Telemetry unfortunately cannot use vanilla ScalarAdd because it needs
+ * to summarize events recorded in different processes to the
+ * telemetry.event_counts of the same process. Including "dynamic".
+ *
+ * @param aUniqueEventName - expected to be category#object#method
+ * @param aProcessType - the process of the event being summarized
+ * @param aDynamic - whether the event being summarized was dynamic
+ */
+void TelemetryScalar::SummarizeEvent(const nsCString& aUniqueEventName,
+ ProcessID aProcessType, bool aDynamic) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Only summarize events in the parent process");
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+
+ StaticMutexAutoLock lock(gTelemetryScalarsMutex);
+
+ ScalarKey scalarKey{static_cast<uint32_t>(ScalarID::TELEMETRY_EVENT_COUNTS),
+ aDynamic};
+ if (aDynamic) {
+ nsresult rv = internal_GetEnumByScalarName(
+ lock, nsAutoCString("telemetry.dynamic_event_counts"), &scalarKey);
+ if (NS_FAILED(rv)) {
+ NS_WARNING(
+ "NS_FAILED getting ScalarKey for telemetry.dynamic_event_counts");
+ return;
+ }
+ }
+
+ KeyedScalar* scalar = nullptr;
+ nsresult rv =
+ internal_GetKeyedScalarByEnum(lock, scalarKey, aProcessType, &scalar);
+
+ if (NS_FAILED(rv)) {
+ NS_WARNING("NS_FAILED getting keyed scalar for event summary. Wut.");
+ return;
+ }
+
+ // Set this each time as it may have been cleared and recreated between calls
+ scalar->SetMaximumNumberOfKeys(kMaxEventSummaryKeys);
+
+ scalar->AddValue(lock, NS_ConvertASCIItoUTF16(aUniqueEventName), 1);
+}
+
+/**
+ * Resets all the stored scalars. This is intended to be only used in tests.
+ */
+void TelemetryScalar::ClearScalars() {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Scalars should only be cleared in the parent process.");
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ gScalarStorageMap.Clear();
+ gKeyedScalarStorageMap.Clear();
+ gDynamicBuiltinScalarStorageMap.Clear();
+ gDynamicBuiltinKeyedScalarStorageMap.Clear();
+ gScalarsActions = nullptr;
+ gKeyedScalarsActions = nullptr;
+}
+
+size_t TelemetryScalar::GetMapShallowSizesOfExcludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ return gScalarNameIDMap.ShallowSizeOfExcludingThis(aMallocSizeOf);
+}
+
+size_t TelemetryScalar::GetScalarSizesOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ size_t n = 0;
+
+ auto getSizeOf = [aMallocSizeOf](auto& storageMap) {
+ size_t partial = 0;
+ for (const auto& scalarStorage : storageMap.Values()) {
+ for (const auto& scalar : scalarStorage->Values()) {
+ partial += scalar->SizeOfIncludingThis(aMallocSizeOf);
+ }
+ }
+ return partial;
+ };
+
+ // Account for all the storage used for the different scalar types.
+ n += getSizeOf(gScalarStorageMap);
+ n += getSizeOf(gKeyedScalarStorageMap);
+ n += getSizeOf(gDynamicBuiltinScalarStorageMap);
+ n += getSizeOf(gDynamicBuiltinKeyedScalarStorageMap);
+
+ return n;
+}
+
+void TelemetryScalar::UpdateChildData(
+ ProcessID aProcessType,
+ const nsTArray<mozilla::Telemetry::ScalarAction>& aScalarActions) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "The stored child processes scalar data must be updated from the "
+ "parent process.");
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ // If scalars are still being deserialized, we need to record the incoming
+ // operations as well.
+ if (internal_IsScalarDeserializing(locker)) {
+ for (const ScalarAction& action : aScalarActions) {
+ // We're only getting immutable access, so let's copy it
+ ScalarAction copy = action;
+ // Fix up the process type
+ copy.mProcessType = aProcessType;
+ internal_RecordScalarAction(locker, copy);
+ }
+
+ return;
+ }
+
+ internal_ApplyScalarActions(locker, aScalarActions, Some(aProcessType));
+}
+
+void TelemetryScalar::UpdateChildKeyedData(
+ ProcessID aProcessType,
+ const nsTArray<mozilla::Telemetry::KeyedScalarAction>& aScalarActions) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "The stored child processes keyed scalar data must be updated "
+ "from the parent process.");
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+
+ // If scalars are still being deserialized, we need to record the incoming
+ // operations as well.
+ if (internal_IsScalarDeserializing(locker)) {
+ for (const KeyedScalarAction& action : aScalarActions) {
+ // We're only getting immutable access, so let's copy it
+ KeyedScalarAction copy = action;
+ // Fix up the process type
+ copy.mProcessType = aProcessType;
+ internal_RecordKeyedScalarAction(locker, copy);
+ }
+
+ return;
+ }
+
+ internal_ApplyKeyedScalarActions(locker, aScalarActions, Some(aProcessType));
+}
+
+void TelemetryScalar::RecordDiscardedData(
+ ProcessID aProcessType,
+ const mozilla::Telemetry::DiscardedData& aDiscardedData) {
+ MOZ_ASSERT(XRE_IsParentProcess(),
+ "Discarded Data must be updated from the parent process.");
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ if (!internal_CanRecordBase(locker)) {
+ return;
+ }
+
+ if (GetCurrentProduct() == SupportedProduct::GeckoviewStreaming) {
+ return;
+ }
+
+ ScalarBase* scalar = nullptr;
+ mozilla::DebugOnly<nsresult> rv;
+
+ rv = internal_GetScalarByEnum(
+ locker,
+ ScalarKey{
+ static_cast<uint32_t>(ScalarID::TELEMETRY_DISCARDED_ACCUMULATIONS),
+ false},
+ aProcessType, &scalar);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ scalar->AddValue(aDiscardedData.mDiscardedHistogramAccumulations);
+
+ rv = internal_GetScalarByEnum(
+ locker,
+ ScalarKey{static_cast<uint32_t>(
+ ScalarID::TELEMETRY_DISCARDED_KEYED_ACCUMULATIONS),
+ false},
+ aProcessType, &scalar);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ scalar->AddValue(aDiscardedData.mDiscardedKeyedHistogramAccumulations);
+
+ rv = internal_GetScalarByEnum(
+ locker,
+ ScalarKey{
+ static_cast<uint32_t>(ScalarID::TELEMETRY_DISCARDED_SCALAR_ACTIONS),
+ false},
+ aProcessType, &scalar);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ scalar->AddValue(aDiscardedData.mDiscardedScalarActions);
+
+ rv = internal_GetScalarByEnum(
+ locker,
+ ScalarKey{static_cast<uint32_t>(
+ ScalarID::TELEMETRY_DISCARDED_KEYED_SCALAR_ACTIONS),
+ false},
+ aProcessType, &scalar);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ scalar->AddValue(aDiscardedData.mDiscardedKeyedScalarActions);
+
+ rv = internal_GetScalarByEnum(
+ locker,
+ ScalarKey{
+ static_cast<uint32_t>(ScalarID::TELEMETRY_DISCARDED_CHILD_EVENTS),
+ false},
+ aProcessType, &scalar);
+ MOZ_ASSERT(NS_SUCCEEDED(rv));
+ scalar->AddValue(aDiscardedData.mDiscardedChildEvents);
+}
+
+/**
+ * Get the dynamic scalar definitions in an IPC-friendly
+ * structure.
+ */
+void TelemetryScalar::GetDynamicScalarDefinitions(
+ nsTArray<DynamicScalarDefinition>& aDefArray) {
+ MOZ_ASSERT(XRE_IsParentProcess());
+ if (!gDynamicScalarInfo) {
+ // Don't have dynamic scalar definitions. Bail out!
+ return;
+ }
+
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ internal_DynamicScalarToIPC(locker, *gDynamicScalarInfo, aDefArray);
+}
+
+/**
+ * This adds the dynamic scalar definitions coming from
+ * the parent process to this child process. If a dynamic
+ * scalar definition is already defined, check if the new definition
+ * makes the scalar expired and eventually update the expiration
+ * state.
+ */
+void TelemetryScalar::AddDynamicScalarDefinitions(
+ const nsTArray<DynamicScalarDefinition>& aDefs) {
+ MOZ_ASSERT(!XRE_IsParentProcess());
+
+ nsTArray<DynamicScalarInfo> dynamicStubs;
+
+ // Populate the definitions array before acquiring the lock.
+ for (auto& def : aDefs) {
+ bool recordOnRelease = def.dataset == nsITelemetry::DATASET_ALL_CHANNELS;
+ dynamicStubs.AppendElement(DynamicScalarInfo{def.type,
+ recordOnRelease,
+ def.expired,
+ def.name,
+ def.keyed,
+ def.builtin,
+ {} /* stores */});
+ }
+
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ internal_RegisterScalars(locker, dynamicStubs);
+ }
+}
+
+nsresult TelemetryScalar::GetAllStores(StringHashSet& set) {
+ // Static stores
+ for (uint32_t storeIdx : gScalarStoresTable) {
+ const char* name = &gScalarsStringTable[storeIdx];
+ nsAutoCString store;
+ store.AssignASCII(name);
+ if (!set.Insert(store, mozilla::fallible)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ // Dynamic stores
+ for (auto& ptr : *gDynamicStoreNames) {
+ nsAutoCString store;
+ ptr->ToUTF8String(store);
+ if (!set.Insert(store, mozilla::fallible)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ return NS_OK;
+}
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PUBLIC: GeckoView serialization/deserialization functions.
+
+/**
+ * Write the scalar data to the provided Json object, for
+ * GeckoView measurement persistence. The output format is the same one used
+ * for snapshotting the scalars.
+ *
+ * @param {aWriter} The JSON object to write to.
+ * @returns NS_OK or a failure value explaining why persistence failed.
+ */
+nsresult TelemetryScalar::SerializeScalars(mozilla::JSONWriter& aWriter) {
+ // Get a copy of the data, without clearing.
+ ScalarSnapshotTable scalarsToReflect;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ // For persistence, we care about all the datasets. Worst case, they
+ // will be empty.
+ nsresult rv = internal_GetScalarSnapshot(
+ locker, scalarsToReflect, nsITelemetry::DATASET_PRERELEASE_CHANNELS,
+ false, /*aClearScalars*/
+ "main"_ns);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ // Persist the scalars to the JSON object.
+ for (const auto& entry : scalarsToReflect) {
+ const ScalarTupleArray& processScalars = entry.GetData();
+ const char* processName = GetNameForProcessID(ProcessID(entry.GetKey()));
+
+ aWriter.StartObjectProperty(mozilla::MakeStringSpan(processName));
+
+ for (const ScalarDataTuple& scalar : processScalars) {
+ nsresult rv = WriteVariantToJSONWriter(
+ std::get<2>(scalar) /*aScalarType*/,
+ std::get<1>(scalar) /*aInputValue*/,
+ mozilla::MakeStringSpan(std::get<0>(scalar)) /*aPropertyName*/,
+ aWriter /*aWriter*/);
+ if (NS_FAILED(rv)) {
+ // Skip this scalar if we failed to write it. We don't bail out just
+ // yet as we may salvage other scalars. We eventually need to call
+ // EndObject.
+ continue;
+ }
+ }
+
+ aWriter.EndObject();
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Write the keyed scalar data to the provided Json object, for
+ * GeckoView measurement persistence. The output format is the same
+ * one used for snapshotting the keyed scalars.
+ *
+ * @param {aWriter} The JSON object to write to.
+ * @returns NS_OK or a failure value explaining why persistence failed.
+ */
+nsresult TelemetryScalar::SerializeKeyedScalars(mozilla::JSONWriter& aWriter) {
+ // Get a copy of the data, without clearing.
+ KeyedScalarSnapshotTable keyedScalarsToReflect;
+ {
+ StaticMutexAutoLock locker(gTelemetryScalarsMutex);
+ // For persistence, we care about all the datasets. Worst case, they
+ // will be empty.
+ nsresult rv = internal_GetKeyedScalarSnapshot(
+ locker, keyedScalarsToReflect,
+ nsITelemetry::DATASET_PRERELEASE_CHANNELS, false, /*aClearScalars*/
+ "main"_ns);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ // Persist the scalars to the JSON object.
+ for (const auto& entry : keyedScalarsToReflect) {
+ const KeyedScalarTupleArray& processScalars = entry.GetData();
+ const char* processName = GetNameForProcessID(ProcessID(entry.GetKey()));
+
+ aWriter.StartObjectProperty(mozilla::MakeStringSpan(processName));
+
+ for (const KeyedScalarDataTuple& keyedScalarData : processScalars) {
+ aWriter.StartObjectProperty(
+ mozilla::MakeStringSpan(std::get<0>(keyedScalarData)));
+
+ // Define a property for each scalar key, then add it to the keyed scalar
+ // object.
+ const nsTArray<KeyedScalar::KeyValuePair>& keyProps =
+ std::get<1>(keyedScalarData);
+ for (const KeyedScalar::KeyValuePair& keyData : keyProps) {
+ nsresult rv = WriteVariantToJSONWriter(
+ std::get<2>(keyedScalarData) /*aScalarType*/,
+ keyData.second /*aInputValue*/,
+ PromiseFlatCString(keyData.first) /*aOutKey*/, aWriter /*aWriter*/);
+ if (NS_FAILED(rv)) {
+ // Skip this scalar if we failed to write it. We don't bail out just
+ // yet as we may salvage other scalars. We eventually need to call
+ // EndObject.
+ continue;
+ }
+ }
+ aWriter.EndObject();
+ }
+ aWriter.EndObject();
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Load the persisted measurements from a Json object and inject them
+ * in the relevant process storage.
+ *
+ * @param {aData} The input Json object.
+ * @returns NS_OK if loading was performed, an error code explaining the
+ * failure reason otherwise.
+ */
+nsresult TelemetryScalar::DeserializePersistedScalars(
+ JSContext* aCx, JS::Handle<JS::Value> aData) {
+ MOZ_ASSERT(XRE_IsParentProcess(), "Only load scalars in the parent process");
+ if (!XRE_IsParentProcess()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ typedef std::pair<nsCString, nsCOMPtr<nsIVariant>> PersistedScalarPair;
+ typedef nsTArray<PersistedScalarPair> PersistedScalarArray;
+ typedef nsTHashMap<ProcessIDHashKey, PersistedScalarArray>
+ PeristedScalarStorage;
+
+ PeristedScalarStorage scalarsToUpdate;
+
+ // Before updating the scalars, we need to get the data out of the JS
+ // wrappers. We can't hold the scalars mutex while handling JS stuff.
+ // Build a <scalar name, value> map.
+ JS::Rooted<JSObject*> scalarDataObj(aCx, &aData.toObject());
+ JS::Rooted<JS::IdVector> processes(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, scalarDataObj, &processes)) {
+ // We can't even enumerate the processes in the loaded data, so
+ // there is nothing we could recover from the persistence file. Bail out.
+ JS_ClearPendingException(aCx);
+ return NS_ERROR_FAILURE;
+ }
+
+ // The following block of code attempts to extract as much data as possible
+ // from the serialized JSON, even in case of light data corruptions: if, for
+ // example, the data for a single process is corrupted or is in an unexpected
+ // form, we press on and attempt to load the data for the other processes.
+ JS::Rooted<JS::PropertyKey> process(aCx);
+ for (auto& processVal : processes) {
+ // This is required as JS API calls require an Handle<jsid> and not a
+ // plain jsid.
+ process = processVal;
+ // Get the process name.
+ nsAutoJSString processNameJS;
+ if (!processNameJS.init(aCx, process)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Make sure it's valid. Note that this is safe to call outside
+ // of a locked section.
+ NS_ConvertUTF16toUTF8 processName(processNameJS);
+ ProcessID processID = GetIDForProcessName(processName.get());
+ if (processID == ProcessID::Count) {
+ NS_WARNING(
+ nsPrintfCString("Failed to get process ID for %s", processName.get())
+ .get());
+ continue;
+ }
+
+ // And its probes.
+ JS::Rooted<JS::Value> processData(aCx);
+ if (!JS_GetPropertyById(aCx, scalarDataObj, process, &processData)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ if (!processData.isObject()) {
+ // |processData| should be an object containing scalars. If this is
+ // not the case, silently skip and try to load the data for the other
+ // processes.
+ continue;
+ }
+
+ // Iterate through each scalar.
+ JS::Rooted<JSObject*> processDataObj(aCx, &processData.toObject());
+ JS::Rooted<JS::IdVector> scalars(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, processDataObj, &scalars)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ JS::Rooted<JS::PropertyKey> scalar(aCx);
+ for (auto& scalarVal : scalars) {
+ scalar = scalarVal;
+ // Get the scalar name.
+ nsAutoJSString scalarName;
+ if (!scalarName.init(aCx, scalar)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Get the scalar value as a JS value.
+ JS::Rooted<JS::Value> scalarValue(aCx);
+ if (!JS_GetPropertyById(aCx, processDataObj, scalar, &scalarValue)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ if (scalarValue.isNullOrUndefined()) {
+ // We can't set scalars to null or undefined values, skip this
+ // and try to load other scalars.
+ continue;
+ }
+
+ // Unpack the aVal to nsIVariant.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv = nsContentUtils::XPConnect()->JSToVariant(
+ aCx, scalarValue, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Add the scalar to the map.
+ PersistedScalarArray& processScalars =
+ scalarsToUpdate.LookupOrInsert(static_cast<uint32_t>(processID));
+ processScalars.AppendElement(std::make_pair(
+ nsCString(NS_ConvertUTF16toUTF8(scalarName)), unpackedVal));
+ }
+ }
+
+ // Now that all the JS specific operations are finished, update the scalars.
+ {
+ StaticMutexAutoLock lock(gTelemetryScalarsMutex);
+
+ for (const auto& entry : scalarsToUpdate) {
+ const PersistedScalarArray& processScalars = entry.GetData();
+ for (PersistedScalarArray::size_type i = 0; i < processScalars.Length();
+ i++) {
+ mozilla::Unused << internal_UpdateScalar(
+ lock, processScalars[i].first, ScalarActionType::eSet,
+ processScalars[i].second, ProcessID(entry.GetKey()),
+ true /* aForce */);
+ }
+ }
+ }
+
+ return NS_OK;
+}
+
+/**
+ * Load the persisted measurements from a Json object and injects them
+ * in the relevant process storage.
+ *
+ * @param {aData} The input Json object.
+ * @returns NS_OK if loading was performed, an error code explaining the
+ * failure reason otherwise.
+ */
+nsresult TelemetryScalar::DeserializePersistedKeyedScalars(
+ JSContext* aCx, JS::Handle<JS::Value> aData) {
+ MOZ_ASSERT(XRE_IsParentProcess(), "Only load scalars in the parent process");
+ if (!XRE_IsParentProcess()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ typedef std::tuple<nsCString, nsString, nsCOMPtr<nsIVariant>>
+ PersistedKeyedScalarTuple;
+ typedef nsTArray<PersistedKeyedScalarTuple> PersistedKeyedScalarArray;
+ typedef nsTHashMap<ProcessIDHashKey, PersistedKeyedScalarArray>
+ PeristedKeyedScalarStorage;
+
+ PeristedKeyedScalarStorage scalarsToUpdate;
+
+ // Before updating the keyed scalars, we need to get the data out of the JS
+ // wrappers. We can't hold the scalars mutex while handling JS stuff.
+ // Build a <scalar name, value> map.
+ JS::Rooted<JSObject*> scalarDataObj(aCx, &aData.toObject());
+ JS::Rooted<JS::IdVector> processes(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, scalarDataObj, &processes)) {
+ // We can't even enumerate the processes in the loaded data, so
+ // there is nothing we could recover from the persistence file. Bail out.
+ JS_ClearPendingException(aCx);
+ return NS_ERROR_FAILURE;
+ }
+
+ // The following block of code attempts to extract as much data as possible
+ // from the serialized JSON, even in case of light data corruptions: if, for
+ // example, the data for a single process is corrupted or is in an unexpected
+ // form, we press on and attempt to load the data for the other processes.
+ JS::Rooted<JS::PropertyKey> process(aCx);
+ for (auto& processVal : processes) {
+ process = processVal;
+ // Get the process name.
+ nsAutoJSString processNameJS;
+ if (!processNameJS.init(aCx, process)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Make sure it's valid. Note that this is safe to call outside
+ // of a locked section.
+ NS_ConvertUTF16toUTF8 processName(processNameJS);
+ ProcessID processID = GetIDForProcessName(processName.get());
+ if (processID == ProcessID::Count) {
+ NS_WARNING(
+ nsPrintfCString("Failed to get process ID for %s", processName.get())
+ .get());
+ continue;
+ }
+
+ // And its probes.
+ JS::Rooted<JS::Value> processData(aCx);
+ if (!JS_GetPropertyById(aCx, scalarDataObj, process, &processData)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ if (!processData.isObject()) {
+ // |processData| should be an object containing scalars. If this is
+ // not the case, silently skip and try to load the data for the other
+ // processes.
+ continue;
+ }
+
+ // Iterate through each keyed scalar.
+ JS::Rooted<JSObject*> processDataObj(aCx, &processData.toObject());
+ JS::Rooted<JS::IdVector> keyedScalars(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, processDataObj, &keyedScalars)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ JS::Rooted<JS::PropertyKey> keyedScalar(aCx);
+ for (auto& keyedScalarVal : keyedScalars) {
+ keyedScalar = keyedScalarVal;
+ // Get the scalar name.
+ nsAutoJSString scalarName;
+ if (!scalarName.init(aCx, keyedScalar)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Get the data for this keyed scalar.
+ JS::Rooted<JS::Value> keyedScalarData(aCx);
+ if (!JS_GetPropertyById(aCx, processDataObj, keyedScalar,
+ &keyedScalarData)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ if (!keyedScalarData.isObject()) {
+ // Keyed scalar data need to be an object. If that's not the case, skip
+ // it and try to load the rest of the data.
+ continue;
+ }
+
+ // Get the keys in the keyed scalar.
+ JS::Rooted<JSObject*> keyedScalarDataObj(aCx,
+ &keyedScalarData.toObject());
+ JS::Rooted<JS::IdVector> keys(aCx, JS::IdVector(aCx));
+ if (!JS_Enumerate(aCx, keyedScalarDataObj, &keys)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ JS::Rooted<JS::PropertyKey> key(aCx);
+ for (auto keyVal : keys) {
+ key = keyVal;
+ // Get the process name.
+ nsAutoJSString keyName;
+ if (!keyName.init(aCx, key)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Get the scalar value as a JS value.
+ JS::Rooted<JS::Value> scalarValue(aCx);
+ if (!JS_GetPropertyById(aCx, keyedScalarDataObj, key, &scalarValue)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ if (scalarValue.isNullOrUndefined()) {
+ // We can't set scalars to null or undefined values, skip this
+ // and try to load other scalars.
+ continue;
+ }
+
+ // Unpack the aVal to nsIVariant.
+ nsCOMPtr<nsIVariant> unpackedVal;
+ nsresult rv = nsContentUtils::XPConnect()->JSToVariant(
+ aCx, scalarValue, getter_AddRefs(unpackedVal));
+ if (NS_FAILED(rv)) {
+ JS_ClearPendingException(aCx);
+ continue;
+ }
+
+ // Add the scalar to the map.
+ PersistedKeyedScalarArray& processScalars =
+ scalarsToUpdate.LookupOrInsert(static_cast<uint32_t>(processID));
+ processScalars.AppendElement(
+ std::make_tuple(nsCString(NS_ConvertUTF16toUTF8(scalarName)),
+ nsString(keyName), unpackedVal));
+ }
+ }
+ }
+
+ // Now that all the JS specific operations are finished, update the scalars.
+ {
+ StaticMutexAutoLock lock(gTelemetryScalarsMutex);
+
+ for (const auto& entry : scalarsToUpdate) {
+ const PersistedKeyedScalarArray& processScalars = entry.GetData();
+ for (PersistedKeyedScalarArray::size_type i = 0;
+ i < processScalars.Length(); i++) {
+ mozilla::Unused << internal_UpdateKeyedScalar(
+ lock, std::get<0>(processScalars[i]),
+ std::get<1>(processScalars[i]), ScalarActionType::eSet,
+ std::get<2>(processScalars[i]), ProcessID(entry.GetKey()),
+ true /* aForce */);
+ }
+ }
+ }
+
+ return NS_OK;
+}
diff --git a/toolkit/components/telemetry/core/TelemetryScalar.h b/toolkit/components/telemetry/core/TelemetryScalar.h
new file mode 100644
index 0000000000..c7e5352860
--- /dev/null
+++ b/toolkit/components/telemetry/core/TelemetryScalar.h
@@ -0,0 +1,133 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TelemetryScalar_h__
+#define TelemetryScalar_h__
+
+#include <stdint.h>
+#include "mozilla/TelemetryProcessEnums.h"
+#include "mozilla/TelemetryScalarEnums.h"
+#include "nsTArray.h"
+#include "TelemetryCommon.h"
+
+// This module is internal to Telemetry. It encapsulates Telemetry's
+// scalar accumulation and storage logic. It should only be used by
+// Telemetry.cpp. These functions should not be used anywhere else.
+// For the public interface to Telemetry functionality, see Telemetry.h.
+
+namespace mozilla {
+// This is only used for the GeckoView persistence.
+class JSONWriter;
+namespace Telemetry {
+struct ScalarAction;
+struct KeyedScalarAction;
+struct DiscardedData;
+struct DynamicScalarDefinition;
+} // namespace Telemetry
+} // namespace mozilla
+
+namespace TelemetryScalar {
+
+void InitializeGlobalState(bool canRecordBase, bool canRecordExtended);
+void DeInitializeGlobalState();
+
+void SetCanRecordBase(bool b);
+void SetCanRecordExtended(bool b);
+
+// JS API Endpoints.
+nsresult Add(const nsACString& aName, JS::Handle<JS::Value> aVal,
+ JSContext* aCx);
+nsresult Set(const nsACString& aName, JS::Handle<JS::Value> aVal,
+ JSContext* aCx);
+nsresult SetMaximum(const nsACString& aName, JS::Handle<JS::Value> aVal,
+ JSContext* aCx);
+nsresult CreateSnapshots(unsigned int aDataset, bool aClearScalars,
+ JSContext* aCx, uint8_t optional_argc,
+ JS::MutableHandle<JS::Value> aResult, bool aFilterTest,
+ const nsACString& aStoreName);
+
+// Keyed JS API Endpoints.
+nsresult Add(const nsACString& aName, const nsAString& aKey,
+ JS::Handle<JS::Value> aVal, JSContext* aCx);
+nsresult Set(const nsACString& aName, const nsAString& aKey,
+ JS::Handle<JS::Value> aVal, JSContext* aCx);
+nsresult SetMaximum(const nsACString& aName, const nsAString& aKey,
+ JS::Handle<JS::Value> aVal, JSContext* aCx);
+nsresult CreateKeyedSnapshots(unsigned int aDataset, bool aClearScalars,
+ JSContext* aCx, uint8_t optional_argc,
+ JS::MutableHandle<JS::Value> aResult,
+ bool aFilterTest, const nsACString& aStoreName);
+
+// C++ API Endpoints.
+void Add(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
+void Set(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
+void Set(mozilla::Telemetry::ScalarID aId, const nsAString& aValue);
+void Set(mozilla::Telemetry::ScalarID aId, bool aValue);
+void SetMaximum(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
+
+// Keyed C++ API Endpoints.
+void Add(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+ uint32_t aValue);
+void Set(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+ uint32_t aValue);
+void Set(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, bool aValue);
+void SetMaximum(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
+ uint32_t aValue);
+
+nsresult RegisterScalars(const nsACString& aCategoryName,
+ JS::Handle<JS::Value> aScalarData, bool aBuiltin,
+ JSContext* cx);
+
+// Event Summary
+void SummarizeEvent(const nsCString& aUniqueEventName,
+ mozilla::Telemetry::ProcessID aProcessType, bool aDynamic);
+
+// Only to be used for testing.
+void ClearScalars();
+
+size_t GetMapShallowSizesOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf);
+size_t GetScalarSizesOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
+
+void UpdateChildData(
+ mozilla::Telemetry::ProcessID aProcessType,
+ const nsTArray<mozilla::Telemetry::ScalarAction>& aScalarActions);
+
+void UpdateChildKeyedData(
+ mozilla::Telemetry::ProcessID aProcessType,
+ const nsTArray<mozilla::Telemetry::KeyedScalarAction>& aScalarActions);
+
+void RecordDiscardedData(
+ mozilla::Telemetry::ProcessID aProcessType,
+ const mozilla::Telemetry::DiscardedData& aDiscardedData);
+
+void GetDynamicScalarDefinitions(
+ nsTArray<mozilla::Telemetry::DynamicScalarDefinition>&);
+void AddDynamicScalarDefinitions(
+ const nsTArray<mozilla::Telemetry::DynamicScalarDefinition>&);
+
+/**
+ * Append the list of registered stores to the given set.
+ * This includes dynamic stores.
+ */
+nsresult GetAllStores(mozilla::Telemetry::Common::StringHashSet& set);
+
+// They are responsible for updating in-memory probes with the data persisted
+// on the disk and vice-versa.
+nsresult SerializeScalars(mozilla::JSONWriter& aWriter);
+nsresult SerializeKeyedScalars(mozilla::JSONWriter& aWriter);
+nsresult DeserializePersistedScalars(JSContext* aCx,
+ JS::Handle<JS::Value> aData);
+nsresult DeserializePersistedKeyedScalars(JSContext* aCx,
+ JS::Handle<JS::Value> aData);
+// Mark deserialization as in progress.
+// After this, all scalar operations are recorded into the pending operations
+// list.
+void DeserializationStarted();
+// Apply all operations from the pending operations list and mark
+// deserialization finished afterwards.
+void ApplyPendingOperations();
+} // namespace TelemetryScalar
+
+#endif // TelemetryScalar_h__
diff --git a/toolkit/components/telemetry/core/TelemetryUserInteraction.cpp b/toolkit/components/telemetry/core/TelemetryUserInteraction.cpp
new file mode 100644
index 0000000000..ad6601c22c
--- /dev/null
+++ b/toolkit/components/telemetry/core/TelemetryUserInteraction.cpp
@@ -0,0 +1,101 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsString.h"
+#include "MainThreadUtils.h"
+#include "TelemetryUserInteraction.h"
+#include "TelemetryUserInteractionData.h"
+#include "TelemetryUserInteractionNameMap.h"
+#include "UserInteractionInfo.h"
+
+using mozilla::Telemetry::UserInteractionIDByNameLookup;
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE TYPES
+namespace {
+
+struct UserInteractionKey {
+ uint32_t id;
+};
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// PRIVATE STATE, SHARED BY ALL THREADS
+
+namespace {
+// Set to true once this global state has been initialized.
+bool gInitDone = false;
+
+bool gCanRecordBase;
+bool gCanRecordExtended;
+} // namespace
+
+namespace {
+// Implements the methods for UserInteractionInfo.
+const char* UserInteractionInfo::name() const {
+ return &gUserInteractionsStringTable[this->name_offset];
+}
+
+} // namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// EXTERNALLY VISIBLE FUNCTIONS in namespace TelemetryUserInteraction::
+
+void TelemetryUserInteraction::InitializeGlobalState(bool aCanRecordBase,
+ bool aCanRecordExtended) {
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(!gInitDone,
+ "TelemetryUserInteraction::InitializeGlobalState "
+ "may only be called once");
+
+ gCanRecordBase = aCanRecordBase;
+ gCanRecordExtended = aCanRecordExtended;
+ gInitDone = true;
+}
+
+void TelemetryUserInteraction::DeInitializeGlobalState() {
+ if (!XRE_IsParentProcess()) {
+ return;
+ }
+
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(gInitDone);
+ if (!gInitDone) {
+ return;
+ }
+
+ gInitDone = false;
+}
+
+bool TelemetryUserInteraction::CanRecord(const nsAString& aName) {
+ if (!gCanRecordBase) {
+ return false;
+ }
+
+ nsCString name = NS_ConvertUTF16toUTF8(aName);
+ const uint32_t idx = UserInteractionIDByNameLookup(name);
+
+ MOZ_DIAGNOSTIC_ASSERT(
+ idx < mozilla::Telemetry::UserInteractionID::UserInteractionCount,
+ "Intermediate lookup should always give a valid index.");
+
+ if (name.Equals(gUserInteractions[idx].name())) {
+ return true;
+ }
+
+ return false;
+}
diff --git a/toolkit/components/telemetry/core/TelemetryUserInteraction.h b/toolkit/components/telemetry/core/TelemetryUserInteraction.h
new file mode 100644
index 0000000000..2b12a5f275
--- /dev/null
+++ b/toolkit/components/telemetry/core/TelemetryUserInteraction.h
@@ -0,0 +1,20 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TelemetryUserInteraction_h__
+#define TelemetryUserInteraction_h__
+
+#include "nsStringFwd.h"
+
+namespace TelemetryUserInteraction {
+
+void InitializeGlobalState(bool canRecordBase, bool canRecordExtended);
+void DeInitializeGlobalState();
+
+bool CanRecord(const nsAString& aName);
+
+} // namespace TelemetryUserInteraction
+
+#endif // TelemetryUserInteraction_h__
diff --git a/toolkit/components/telemetry/core/UserInteractionInfo.h b/toolkit/components/telemetry/core/UserInteractionInfo.h
new file mode 100644
index 0000000000..e20017b790
--- /dev/null
+++ b/toolkit/components/telemetry/core/UserInteractionInfo.h
@@ -0,0 +1,30 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TelemetryUserInteractionInfo_h__
+#define TelemetryUserInteractionInfo_h__
+
+#include "TelemetryCommon.h"
+
+// This module is internal to Telemetry. It defines a structure that holds the
+// UserInteraction info. It should only be used by
+// TelemetryUserInteractionData.h automatically generated file and
+// TelemetryUserInteraction.cpp. This should not be used anywhere else. For the
+// public interface to Telemetry functionality, see Telemetry.h.
+
+namespace {
+
+struct UserInteractionInfo {
+ const uint32_t name_offset;
+
+ explicit constexpr UserInteractionInfo(const uint32_t aNameOffset)
+ : name_offset(aNameOffset) {}
+
+ const char* name() const;
+};
+
+} // namespace
+
+#endif // TelemetryUserInteractionInfo_h__
diff --git a/toolkit/components/telemetry/core/components.conf b/toolkit/components/telemetry/core/components.conf
new file mode 100644
index 0000000000..d003b414a1
--- /dev/null
+++ b/toolkit/components/telemetry/core/components.conf
@@ -0,0 +1,21 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Headers = ['mozilla/Telemetry.h']
+
+UnloadFunc = 'mozilla::Telemetry::ShutdownTelemetry'
+
+Classes = [
+ {
+ 'js_name': 'telemetry',
+ 'cid': '{aea477f2-b3a2-469c-aa29-0a82d132b829}',
+ 'contract_ids': ['@mozilla.org/base/telemetry;1'],
+ 'interfaces': ['nsITelemetry'],
+ 'singleton': True,
+ 'type': 'nsITelemetry',
+ 'processes': ProcessSelector.ALLOW_IN_GPU_RDD_VR_SOCKET_UTILITY_AND_GMPLUGIN_PROCESS,
+ },
+]
diff --git a/toolkit/components/telemetry/core/ipc/TelemetryComms.h b/toolkit/components/telemetry/core/ipc/TelemetryComms.h
new file mode 100644
index 0000000000..75f59209b0
--- /dev/null
+++ b/toolkit/components/telemetry/core/ipc/TelemetryComms.h
@@ -0,0 +1,400 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef Telemetry_Comms_h__
+#define Telemetry_Comms_h__
+
+#include "ipc/IPCMessageUtils.h"
+#include "ipc/IPCMessageUtilsSpecializations.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/TelemetryProcessEnums.h"
+#include "mozilla/TimeStamp.h"
+#include "mozilla/Variant.h"
+#include "nsITelemetry.h"
+
+namespace mozilla {
+namespace Telemetry {
+
+// Histogram accumulation types.
+enum HistogramID : uint32_t;
+
+struct HistogramAccumulation {
+ mozilla::Telemetry::HistogramID mId;
+ uint32_t mSample;
+};
+
+struct KeyedHistogramAccumulation {
+ mozilla::Telemetry::HistogramID mId;
+ uint32_t mSample;
+ nsCString mKey;
+};
+
+// Scalar accumulation types.
+enum class ScalarID : uint32_t;
+
+enum class ScalarActionType : uint32_t { eSet = 0, eAdd = 1, eSetMaximum = 2 };
+
+typedef mozilla::Variant<uint32_t, bool, nsString> ScalarVariant;
+
+struct ScalarAction {
+ uint32_t mId;
+ bool mDynamic;
+ ScalarActionType mActionType;
+ // We need to wrap mData in a Maybe otherwise the IPC system
+ // is unable to instantiate a ScalarAction.
+ Maybe<ScalarVariant> mData;
+ // The process type this scalar should be recorded for.
+ // The IPC system will determine the process this action was coming from
+ // later.
+ mozilla::Telemetry::ProcessID mProcessType;
+};
+
+struct KeyedScalarAction {
+ uint32_t mId;
+ bool mDynamic;
+ ScalarActionType mActionType;
+ nsCString mKey;
+ // We need to wrap mData in a Maybe otherwise the IPC system
+ // is unable to instantiate a ScalarAction.
+ Maybe<ScalarVariant> mData;
+ // The process type this scalar should be recorded for.
+ // The IPC system will determine the process this action was coming from
+ // later.
+ mozilla::Telemetry::ProcessID mProcessType;
+};
+
+// Dynamic scalars support.
+struct DynamicScalarDefinition {
+ uint32_t type;
+ uint32_t dataset;
+ bool expired;
+ bool keyed;
+ bool builtin;
+ nsCString name;
+
+ bool operator==(const DynamicScalarDefinition& rhs) const {
+ return type == rhs.type && dataset == rhs.dataset &&
+ expired == rhs.expired && keyed == rhs.keyed &&
+ builtin == rhs.builtin && name.Equals(rhs.name);
+ }
+};
+
+struct ChildEventData {
+ mozilla::TimeStamp timestamp;
+ nsCString category;
+ nsCString method;
+ nsCString object;
+ mozilla::Maybe<nsCString> value;
+ CopyableTArray<EventExtraEntry> extra;
+};
+
+struct DiscardedData {
+ uint32_t mDiscardedHistogramAccumulations;
+ uint32_t mDiscardedKeyedHistogramAccumulations;
+ uint32_t mDiscardedScalarActions;
+ uint32_t mDiscardedKeyedScalarActions;
+ uint32_t mDiscardedChildEvents;
+};
+
+} // namespace Telemetry
+} // namespace mozilla
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::Telemetry::HistogramAccumulation> {
+ typedef mozilla::Telemetry::HistogramAccumulation paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ aWriter->WriteUInt32(aParam.mId);
+ WriteParam(aWriter, aParam.mSample);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ if (!aReader->ReadUInt32(reinterpret_cast<uint32_t*>(&(aResult->mId))) ||
+ !ReadParam(aReader, &(aResult->mSample))) {
+ return false;
+ }
+
+ return true;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::Telemetry::KeyedHistogramAccumulation> {
+ typedef mozilla::Telemetry::KeyedHistogramAccumulation paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ aWriter->WriteUInt32(aParam.mId);
+ WriteParam(aWriter, aParam.mSample);
+ WriteParam(aWriter, aParam.mKey);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ if (!aReader->ReadUInt32(reinterpret_cast<uint32_t*>(&(aResult->mId))) ||
+ !ReadParam(aReader, &(aResult->mSample)) ||
+ !ReadParam(aReader, &(aResult->mKey))) {
+ return false;
+ }
+
+ return true;
+ }
+};
+
+/**
+ * IPC scalar data message serialization and de-serialization.
+ */
+template <>
+struct ParamTraits<mozilla::Telemetry::ScalarAction> {
+ typedef mozilla::Telemetry::ScalarAction paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ // Write the message type
+ aWriter->WriteUInt32(aParam.mId);
+ WriteParam(aWriter, aParam.mDynamic);
+ WriteParam(aWriter, static_cast<uint32_t>(aParam.mActionType));
+
+ if (aParam.mData.isNothing()) {
+ MOZ_CRASH("There is no data in the ScalarAction.");
+ return;
+ }
+
+ if (aParam.mData->is<uint32_t>()) {
+ // That's a nsITelemetry::SCALAR_TYPE_COUNT.
+ WriteParam(aWriter,
+ static_cast<uint32_t>(nsITelemetry::SCALAR_TYPE_COUNT));
+ WriteParam(aWriter, aParam.mData->as<uint32_t>());
+ } else if (aParam.mData->is<nsString>()) {
+ // That's a nsITelemetry::SCALAR_TYPE_STRING.
+ WriteParam(aWriter,
+ static_cast<uint32_t>(nsITelemetry::SCALAR_TYPE_STRING));
+ WriteParam(aWriter, aParam.mData->as<nsString>());
+ } else if (aParam.mData->is<bool>()) {
+ // That's a nsITelemetry::SCALAR_TYPE_BOOLEAN.
+ WriteParam(aWriter,
+ static_cast<uint32_t>(nsITelemetry::SCALAR_TYPE_BOOLEAN));
+ WriteParam(aWriter, aParam.mData->as<bool>());
+ } else {
+ MOZ_CRASH("Unknown scalar type.");
+ }
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ // Read the scalar ID and the scalar type.
+ uint32_t scalarType = 0;
+ if (!aReader->ReadUInt32(reinterpret_cast<uint32_t*>(&(aResult->mId))) ||
+ !ReadParam(aReader, reinterpret_cast<bool*>(&(aResult->mDynamic))) ||
+ !ReadParam(aReader,
+ reinterpret_cast<uint32_t*>(&(aResult->mActionType))) ||
+ !ReadParam(aReader, &scalarType)) {
+ return false;
+ }
+
+ // De-serialize the data based on the scalar type.
+ switch (scalarType) {
+ case nsITelemetry::SCALAR_TYPE_COUNT: {
+ uint32_t data = 0;
+ // De-serialize the data.
+ if (!ReadParam(aReader, &data)) {
+ return false;
+ }
+ aResult->mData = mozilla::Some(mozilla::AsVariant(data));
+ break;
+ }
+ case nsITelemetry::SCALAR_TYPE_STRING: {
+ nsString data;
+ // De-serialize the data.
+ if (!ReadParam(aReader, &data)) {
+ return false;
+ }
+ aResult->mData = mozilla::Some(mozilla::AsVariant(data));
+ break;
+ }
+ case nsITelemetry::SCALAR_TYPE_BOOLEAN: {
+ bool data = false;
+ // De-serialize the data.
+ if (!ReadParam(aReader, &data)) {
+ return false;
+ }
+ aResult->mData = mozilla::Some(mozilla::AsVariant(data));
+ break;
+ }
+ default:
+ MOZ_ASSERT(false, "Unknown scalar type.");
+ return false;
+ }
+
+ return true;
+ }
+};
+
+/**
+ * IPC keyed scalar data message serialization and de-serialization.
+ */
+template <>
+struct ParamTraits<mozilla::Telemetry::KeyedScalarAction> {
+ typedef mozilla::Telemetry::KeyedScalarAction paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ // Write the message type
+ aWriter->WriteUInt32(static_cast<uint32_t>(aParam.mId));
+ WriteParam(aWriter, aParam.mDynamic);
+ WriteParam(aWriter, static_cast<uint32_t>(aParam.mActionType));
+ WriteParam(aWriter, aParam.mKey);
+
+ if (aParam.mData.isNothing()) {
+ MOZ_CRASH("There is no data in the KeyedScalarAction.");
+ return;
+ }
+
+ if (aParam.mData->is<uint32_t>()) {
+ // That's a nsITelemetry::SCALAR_TYPE_COUNT.
+ WriteParam(aWriter,
+ static_cast<uint32_t>(nsITelemetry::SCALAR_TYPE_COUNT));
+ WriteParam(aWriter, aParam.mData->as<uint32_t>());
+ } else if (aParam.mData->is<nsString>()) {
+ // That's a nsITelemetry::SCALAR_TYPE_STRING.
+ // Keyed string scalars are not supported.
+ MOZ_ASSERT(false,
+ "Keyed String Scalar unable to be write from child process. "
+ "Not supported.");
+ } else if (aParam.mData->is<bool>()) {
+ // That's a nsITelemetry::SCALAR_TYPE_BOOLEAN.
+ WriteParam(aWriter,
+ static_cast<uint32_t>(nsITelemetry::SCALAR_TYPE_BOOLEAN));
+ WriteParam(aWriter, aParam.mData->as<bool>());
+ } else {
+ MOZ_CRASH("Unknown keyed scalar type.");
+ }
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ // Read the scalar ID and the scalar type.
+ uint32_t scalarType = 0;
+ if (!aReader->ReadUInt32(reinterpret_cast<uint32_t*>(&(aResult->mId))) ||
+ !ReadParam(aReader, reinterpret_cast<bool*>(&(aResult->mDynamic))) ||
+ !ReadParam(aReader,
+ reinterpret_cast<uint32_t*>(&(aResult->mActionType))) ||
+ !ReadParam(aReader, &(aResult->mKey)) ||
+ !ReadParam(aReader, &scalarType)) {
+ return false;
+ }
+
+ // De-serialize the data based on the scalar type.
+ switch (scalarType) {
+ case nsITelemetry::SCALAR_TYPE_COUNT: {
+ uint32_t data = 0;
+ // De-serialize the data.
+ if (!ReadParam(aReader, &data)) {
+ return false;
+ }
+ aResult->mData = mozilla::Some(mozilla::AsVariant(data));
+ break;
+ }
+ case nsITelemetry::SCALAR_TYPE_STRING: {
+ // Keyed string scalars are not supported.
+ MOZ_ASSERT(false,
+ "Keyed String Scalar unable to be read from child process. "
+ "Not supported.");
+ return false;
+ }
+ case nsITelemetry::SCALAR_TYPE_BOOLEAN: {
+ bool data = false;
+ // De-serialize the data.
+ if (!ReadParam(aReader, &data)) {
+ return false;
+ }
+ aResult->mData = mozilla::Some(mozilla::AsVariant(data));
+ break;
+ }
+ default:
+ MOZ_ASSERT(false, "Unknown keyed scalar type.");
+ return false;
+ }
+
+ return true;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::Telemetry::DynamicScalarDefinition> {
+ typedef mozilla::Telemetry::DynamicScalarDefinition paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ nsCString name;
+ WriteParam(aWriter, aParam.type);
+ WriteParam(aWriter, aParam.dataset);
+ WriteParam(aWriter, aParam.expired);
+ WriteParam(aWriter, aParam.keyed);
+ WriteParam(aWriter, aParam.builtin);
+ WriteParam(aWriter, aParam.name);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ if (!ReadParam(aReader, reinterpret_cast<uint32_t*>(&(aResult->type))) ||
+ !ReadParam(aReader, reinterpret_cast<uint32_t*>(&(aResult->dataset))) ||
+ !ReadParam(aReader, reinterpret_cast<bool*>(&(aResult->expired))) ||
+ !ReadParam(aReader, reinterpret_cast<bool*>(&(aResult->keyed))) ||
+ !ReadParam(aReader, reinterpret_cast<bool*>(&(aResult->builtin))) ||
+ !ReadParam(aReader, &(aResult->name))) {
+ return false;
+ }
+ return true;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::Telemetry::ChildEventData> {
+ typedef mozilla::Telemetry::ChildEventData paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.timestamp);
+ WriteParam(aWriter, aParam.category);
+ WriteParam(aWriter, aParam.method);
+ WriteParam(aWriter, aParam.object);
+ WriteParam(aWriter, aParam.value);
+ WriteParam(aWriter, aParam.extra);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ if (!ReadParam(aReader, &(aResult->timestamp)) ||
+ !ReadParam(aReader, &(aResult->category)) ||
+ !ReadParam(aReader, &(aResult->method)) ||
+ !ReadParam(aReader, &(aResult->object)) ||
+ !ReadParam(aReader, &(aResult->value)) ||
+ !ReadParam(aReader, &(aResult->extra))) {
+ return false;
+ }
+
+ return true;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::Telemetry::EventExtraEntry> {
+ typedef mozilla::Telemetry::EventExtraEntry paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.key);
+ WriteParam(aWriter, aParam.value);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ if (!ReadParam(aReader, &(aResult->key)) ||
+ !ReadParam(aReader, &(aResult->value))) {
+ return false;
+ }
+
+ return true;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::Telemetry::DiscardedData>
+ : public PlainOldDataSerializer<mozilla::Telemetry::DiscardedData> {};
+
+} // namespace IPC
+
+#endif // Telemetry_Comms_h__
diff --git a/toolkit/components/telemetry/core/ipc/TelemetryIPC.cpp b/toolkit/components/telemetry/core/ipc/TelemetryIPC.cpp
new file mode 100644
index 0000000000..daeaeece65
--- /dev/null
+++ b/toolkit/components/telemetry/core/ipc/TelemetryIPC.cpp
@@ -0,0 +1,59 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TelemetryIPC.h"
+#include "../TelemetryScalar.h"
+#include "../TelemetryHistogram.h"
+#include "../TelemetryEvent.h"
+
+namespace mozilla {
+
+void TelemetryIPC::AccumulateChildHistograms(
+ Telemetry::ProcessID aProcessType,
+ const nsTArray<Telemetry::HistogramAccumulation>& aAccumulations) {
+ TelemetryHistogram::AccumulateChild(aProcessType, aAccumulations);
+}
+
+void TelemetryIPC::AccumulateChildKeyedHistograms(
+ Telemetry::ProcessID aProcessType,
+ const nsTArray<Telemetry::KeyedHistogramAccumulation>& aAccumulations) {
+ TelemetryHistogram::AccumulateChildKeyed(aProcessType, aAccumulations);
+}
+
+void TelemetryIPC::UpdateChildScalars(
+ Telemetry::ProcessID aProcessType,
+ const nsTArray<Telemetry::ScalarAction>& aScalarActions) {
+ TelemetryScalar::UpdateChildData(aProcessType, aScalarActions);
+}
+
+void TelemetryIPC::UpdateChildKeyedScalars(
+ Telemetry::ProcessID aProcessType,
+ const nsTArray<Telemetry::KeyedScalarAction>& aScalarActions) {
+ TelemetryScalar::UpdateChildKeyedData(aProcessType, aScalarActions);
+}
+
+void TelemetryIPC::GetDynamicScalarDefinitions(
+ nsTArray<mozilla::Telemetry::DynamicScalarDefinition>& aDefs) {
+ TelemetryScalar::GetDynamicScalarDefinitions(aDefs);
+}
+
+void TelemetryIPC::AddDynamicScalarDefinitions(
+ const nsTArray<mozilla::Telemetry::DynamicScalarDefinition>& aDefs) {
+ TelemetryScalar::AddDynamicScalarDefinitions(aDefs);
+}
+
+void TelemetryIPC::RecordChildEvents(
+ Telemetry::ProcessID aProcessType,
+ const nsTArray<Telemetry::ChildEventData>& aEvents) {
+ TelemetryEvent::RecordChildEvents(aProcessType, aEvents);
+}
+
+void TelemetryIPC::RecordDiscardedData(
+ Telemetry::ProcessID aProcessType,
+ const Telemetry::DiscardedData& aDiscardedData) {
+ TelemetryScalar::RecordDiscardedData(aProcessType, aDiscardedData);
+}
+} // namespace mozilla
diff --git a/toolkit/components/telemetry/core/ipc/TelemetryIPC.h b/toolkit/components/telemetry/core/ipc/TelemetryIPC.h
new file mode 100644
index 0000000000..bf45280059
--- /dev/null
+++ b/toolkit/components/telemetry/core/ipc/TelemetryIPC.h
@@ -0,0 +1,114 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TelemetryIPC_h__
+#define TelemetryIPC_h__
+
+#include "mozilla/TelemetryProcessEnums.h"
+#include "nsTArray.h"
+
+// This module provides the interface to accumulate Telemetry from child
+// processes. Top-level actors for different child processes types
+// (ContentParent, GPUChild) will call this for messages from their respective
+// processes.
+
+namespace mozilla {
+
+namespace Telemetry {
+
+struct HistogramAccumulation;
+struct KeyedHistogramAccumulation;
+struct ScalarAction;
+struct KeyedScalarAction;
+struct DynamicScalarDefinition;
+struct ChildEventData;
+struct DiscardedData;
+
+} // namespace Telemetry
+
+namespace TelemetryIPC {
+
+/**
+ * Accumulate child process data into histograms for the given process type.
+ *
+ * @param aProcessType - the process type to accumulate the histograms for
+ * @param aAccumulations - accumulation actions to perform
+ */
+void AccumulateChildHistograms(
+ Telemetry::ProcessID aProcessType,
+ const nsTArray<Telemetry::HistogramAccumulation>& aAccumulations);
+
+/**
+ * Accumulate child process data into keyed histograms for the given process
+ * type.
+ *
+ * @param aProcessType - the process type to accumulate the keyed histograms for
+ * @param aAccumulations - accumulation actions to perform
+ */
+void AccumulateChildKeyedHistograms(
+ Telemetry::ProcessID aProcessType,
+ const nsTArray<Telemetry::KeyedHistogramAccumulation>& aAccumulations);
+
+/**
+ * Update scalars for the given process type with the data coming from child
+ * process.
+ *
+ * @param aProcessType - the process type to process the scalar actions for
+ * @param aScalarActions - actions to update the scalar data
+ */
+void UpdateChildScalars(
+ Telemetry::ProcessID aProcessType,
+ const nsTArray<Telemetry::ScalarAction>& aScalarActions);
+
+/**
+ * Update keyed scalars for the given process type with the data coming from
+ * child process.
+ *
+ * @param aProcessType - the process type to process the keyed scalar actions
+ * for
+ * @param aScalarActions - actions to update the keyed scalar data
+ */
+void UpdateChildKeyedScalars(
+ Telemetry::ProcessID aProcessType,
+ const nsTArray<Telemetry::KeyedScalarAction>& aScalarActions);
+
+/**
+ * Record events for the given process type with the data coming from child
+ * process.
+ *
+ * @param aProcessType - the process type to record the events for
+ * @param aEvents - events to record
+ */
+void RecordChildEvents(Telemetry::ProcessID aProcessType,
+ const nsTArray<Telemetry::ChildEventData>& aEvents);
+
+/**
+ * Record the counts of data the child process had to discard
+ *
+ * @param aProcessType - the process reporting the discarded data
+ * @param aDiscardedData - stats about the discarded data
+ */
+void RecordDiscardedData(Telemetry::ProcessID aProcessType,
+ const Telemetry::DiscardedData& aDiscardedData);
+
+/**
+ * Get the dynamic scalar definitions from the parent process.
+ * @param aDefs - The array that will contain the scalar definitions.
+ */
+void GetDynamicScalarDefinitions(
+ nsTArray<mozilla::Telemetry::DynamicScalarDefinition>& aDefs);
+
+/**
+ * Add the dynamic scalar definitions coming from the parent process
+ * to the current child process.
+ * @param aDefs - The array that contains the scalar definitions.
+ */
+void AddDynamicScalarDefinitions(
+ const nsTArray<mozilla::Telemetry::DynamicScalarDefinition>& aDefs);
+
+} // namespace TelemetryIPC
+} // namespace mozilla
+
+#endif // TelemetryIPC_h__
diff --git a/toolkit/components/telemetry/core/ipc/TelemetryIPCAccumulator.cpp b/toolkit/components/telemetry/core/ipc/TelemetryIPCAccumulator.cpp
new file mode 100644
index 0000000000..fd0f945daf
--- /dev/null
+++ b/toolkit/components/telemetry/core/ipc/TelemetryIPCAccumulator.cpp
@@ -0,0 +1,351 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TelemetryIPCAccumulator.h"
+
+#include "core/TelemetryScalar.h"
+#include "mozilla/TelemetryProcessEnums.h"
+#include "mozilla/dom/ContentChild.h"
+#include "mozilla/gfx/GPUParent.h"
+#include "mozilla/RDDParent.h"
+#include "mozilla/net/SocketProcessChild.h"
+#include "mozilla/ipc/UtilityProcessChild.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPrefs_toolkit.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/Unused.h"
+#include "nsITimer.h"
+#include "nsThreadUtils.h"
+
+using mozilla::StaticAutoPtr;
+using mozilla::StaticMutex;
+using mozilla::StaticMutexAutoLock;
+using mozilla::Telemetry::ChildEventData;
+using mozilla::Telemetry::DiscardedData;
+using mozilla::Telemetry::HistogramAccumulation;
+using mozilla::Telemetry::KeyedHistogramAccumulation;
+using mozilla::Telemetry::KeyedScalarAction;
+using mozilla::Telemetry::ScalarAction;
+using mozilla::Telemetry::ScalarActionType;
+using mozilla::Telemetry::ScalarVariant;
+
+namespace TelemetryIPCAccumulator = mozilla::TelemetryIPCAccumulator;
+
+// To stop growing unbounded in memory while waiting for
+// StaticPrefs::toolkit_telemetry_ipcBatchTimeout() milliseconds to drain the
+// probe accumulation arrays, we request an immediate flush if the arrays
+// manage to reach certain high water mark of elements.
+const size_t kHistogramAccumulationsArrayHighWaterMark = 5 * 1024;
+const size_t kScalarActionsArrayHighWaterMark = 10000;
+// With the current limits, events cost us about 1100 bytes each.
+// This limits memory use to about 10MB.
+const size_t kEventsArrayHighWaterMark = 10000;
+// If we are starved we can overshoot the watermark.
+// This is the multiplier over which we will discard data.
+const size_t kWaterMarkDiscardFactor = 5;
+
+// Counts of how many pieces of data we have discarded.
+DiscardedData gDiscardedData = {0};
+
+// This timer is used for batching and sending child process accumulations to
+// the parent.
+nsITimer* gIPCTimer = nullptr;
+mozilla::Atomic<bool, mozilla::Relaxed> gIPCTimerArmed(false);
+mozilla::Atomic<bool, mozilla::Relaxed> gIPCTimerArming(false);
+
+// This batches child process accumulations that should be sent to the parent.
+StaticAutoPtr<nsTArray<HistogramAccumulation>> gHistogramAccumulations;
+StaticAutoPtr<nsTArray<KeyedHistogramAccumulation>>
+ gKeyedHistogramAccumulations;
+StaticAutoPtr<nsTArray<ScalarAction>> gChildScalarsActions;
+StaticAutoPtr<nsTArray<KeyedScalarAction>> gChildKeyedScalarsActions;
+StaticAutoPtr<nsTArray<ChildEventData>> gChildEvents;
+
+// This is a StaticMutex rather than a plain Mutex so that (1)
+// it gets initialised in a thread-safe manner the first time
+// it is used, and (2) because it is never de-initialised, and
+// a normal Mutex would show up as a leak in BloatView. StaticMutex
+// also has the "OffTheBooks" property, so it won't show as a leak
+// in BloatView.
+static StaticMutex gTelemetryIPCAccumulatorMutex MOZ_UNANNOTATED;
+
+namespace {
+
+void DoArmIPCTimerMainThread(const StaticMutexAutoLock& lock) {
+ MOZ_ASSERT(NS_IsMainThread());
+ gIPCTimerArming = false;
+ if (gIPCTimerArmed) {
+ return;
+ }
+ if (!gIPCTimer) {
+ gIPCTimer = NS_NewTimer().take();
+ }
+ if (gIPCTimer) {
+ gIPCTimer->InitWithNamedFuncCallback(
+ TelemetryIPCAccumulator::IPCTimerFired, nullptr,
+ mozilla::StaticPrefs::toolkit_telemetry_ipcBatchTimeout(),
+ nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY,
+ "TelemetryIPCAccumulator::IPCTimerFired");
+ gIPCTimerArmed = true;
+ }
+}
+
+void ArmIPCTimer(const StaticMutexAutoLock& lock) {
+ if (gIPCTimerArmed || gIPCTimerArming) {
+ return;
+ }
+ gIPCTimerArming = true;
+ if (NS_IsMainThread()) {
+ DoArmIPCTimerMainThread(lock);
+ } else {
+ TelemetryIPCAccumulator::DispatchToMainThread(NS_NewRunnableFunction(
+ "TelemetryIPCAccumulator::ArmIPCTimer", []() -> void {
+ StaticMutexAutoLock locker(gTelemetryIPCAccumulatorMutex);
+ DoArmIPCTimerMainThread(locker);
+ }));
+ }
+}
+
+void DispatchIPCTimerFired() {
+ TelemetryIPCAccumulator::DispatchToMainThread(NS_NewRunnableFunction(
+ "TelemetryIPCAccumulator::IPCTimerFired", []() -> void {
+ TelemetryIPCAccumulator::IPCTimerFired(nullptr, nullptr);
+ }));
+}
+
+} // anonymous namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
+// EXTERNALLY VISIBLE FUNCTIONS in namespace TelemetryIPCAccumulator::
+
+void TelemetryIPCAccumulator::AccumulateChildHistogram(
+ mozilla::Telemetry::HistogramID aId, uint32_t aSample) {
+ StaticMutexAutoLock locker(gTelemetryIPCAccumulatorMutex);
+ if (!gHistogramAccumulations) {
+ gHistogramAccumulations = new nsTArray<HistogramAccumulation>();
+ }
+ if (gHistogramAccumulations->Length() >=
+ kWaterMarkDiscardFactor * kHistogramAccumulationsArrayHighWaterMark) {
+ gDiscardedData.mDiscardedHistogramAccumulations++;
+ return;
+ }
+ if (gHistogramAccumulations->Length() ==
+ kHistogramAccumulationsArrayHighWaterMark) {
+ DispatchIPCTimerFired();
+ }
+ gHistogramAccumulations->AppendElement(HistogramAccumulation{aId, aSample});
+ ArmIPCTimer(locker);
+}
+
+void TelemetryIPCAccumulator::AccumulateChildKeyedHistogram(
+ mozilla::Telemetry::HistogramID aId, const nsCString& aKey,
+ uint32_t aSample) {
+ StaticMutexAutoLock locker(gTelemetryIPCAccumulatorMutex);
+ if (!gKeyedHistogramAccumulations) {
+ gKeyedHistogramAccumulations = new nsTArray<KeyedHistogramAccumulation>();
+ }
+ if (gKeyedHistogramAccumulations->Length() >=
+ kWaterMarkDiscardFactor * kHistogramAccumulationsArrayHighWaterMark) {
+ gDiscardedData.mDiscardedKeyedHistogramAccumulations++;
+ return;
+ }
+ if (gKeyedHistogramAccumulations->Length() ==
+ kHistogramAccumulationsArrayHighWaterMark) {
+ DispatchIPCTimerFired();
+ }
+ gKeyedHistogramAccumulations->AppendElement(
+ KeyedHistogramAccumulation{aId, aSample, aKey});
+ ArmIPCTimer(locker);
+}
+
+void TelemetryIPCAccumulator::RecordChildScalarAction(
+ uint32_t aId, bool aDynamic, ScalarActionType aAction,
+ const ScalarVariant& aValue) {
+ StaticMutexAutoLock locker(gTelemetryIPCAccumulatorMutex);
+ // Make sure to have the storage.
+ if (!gChildScalarsActions) {
+ gChildScalarsActions = new nsTArray<ScalarAction>();
+ }
+ if (gChildScalarsActions->Length() >=
+ kWaterMarkDiscardFactor * kScalarActionsArrayHighWaterMark) {
+ gDiscardedData.mDiscardedScalarActions++;
+ return;
+ }
+ if (gChildScalarsActions->Length() == kScalarActionsArrayHighWaterMark) {
+ DispatchIPCTimerFired();
+ }
+ // Store the action. The ProcessID will be determined by the receiver.
+ gChildScalarsActions->AppendElement(ScalarAction{
+ aId, aDynamic, aAction, Some(aValue), Telemetry::ProcessID::Count});
+ ArmIPCTimer(locker);
+}
+
+void TelemetryIPCAccumulator::RecordChildKeyedScalarAction(
+ uint32_t aId, bool aDynamic, const nsAString& aKey,
+ ScalarActionType aAction, const ScalarVariant& aValue) {
+ StaticMutexAutoLock locker(gTelemetryIPCAccumulatorMutex);
+ // Make sure to have the storage.
+ if (!gChildKeyedScalarsActions) {
+ gChildKeyedScalarsActions = new nsTArray<KeyedScalarAction>();
+ }
+ if (gChildKeyedScalarsActions->Length() >=
+ kWaterMarkDiscardFactor * kScalarActionsArrayHighWaterMark) {
+ gDiscardedData.mDiscardedKeyedScalarActions++;
+ return;
+ }
+ if (gChildKeyedScalarsActions->Length() == kScalarActionsArrayHighWaterMark) {
+ DispatchIPCTimerFired();
+ }
+ // Store the action. The ProcessID will be determined by the receiver.
+ gChildKeyedScalarsActions->AppendElement(
+ KeyedScalarAction{aId, aDynamic, aAction, NS_ConvertUTF16toUTF8(aKey),
+ Some(aValue), Telemetry::ProcessID::Count});
+ ArmIPCTimer(locker);
+}
+
+void TelemetryIPCAccumulator::RecordChildEvent(
+ const mozilla::TimeStamp& timestamp, const nsACString& category,
+ const nsACString& method, const nsACString& object,
+ const mozilla::Maybe<nsCString>& value,
+ const nsTArray<mozilla::Telemetry::EventExtraEntry>& extra) {
+ StaticMutexAutoLock locker(gTelemetryIPCAccumulatorMutex);
+
+ if (!gChildEvents) {
+ gChildEvents = new nsTArray<ChildEventData>();
+ }
+
+ if (gChildEvents->Length() >=
+ kWaterMarkDiscardFactor * kEventsArrayHighWaterMark) {
+ gDiscardedData.mDiscardedChildEvents++;
+ return;
+ }
+
+ if (gChildEvents->Length() == kEventsArrayHighWaterMark) {
+ DispatchIPCTimerFired();
+ }
+
+ // Store the event.
+ gChildEvents->AppendElement(
+ ChildEventData{timestamp, nsCString(category), nsCString(method),
+ nsCString(object), value, extra.Clone()});
+ ArmIPCTimer(locker);
+}
+
+// This method takes the lock only to double-buffer the batched telemetry.
+// It releases the lock before calling out to IPC code which can (and does)
+// Accumulate (which would deadlock)
+template <class TActor>
+static void SendAccumulatedData(TActor* ipcActor) {
+ // Get the accumulated data and free the storage buffers.
+ nsTArray<HistogramAccumulation> histogramsToSend;
+ nsTArray<KeyedHistogramAccumulation> keyedHistogramsToSend;
+ nsTArray<ScalarAction> scalarsToSend;
+ nsTArray<KeyedScalarAction> keyedScalarsToSend;
+ nsTArray<ChildEventData> eventsToSend;
+ DiscardedData discardedData;
+
+ {
+ StaticMutexAutoLock locker(gTelemetryIPCAccumulatorMutex);
+ if (gHistogramAccumulations) {
+ histogramsToSend = std::move(*gHistogramAccumulations);
+ }
+ if (gKeyedHistogramAccumulations) {
+ keyedHistogramsToSend = std::move(*gKeyedHistogramAccumulations);
+ }
+ if (gChildScalarsActions) {
+ scalarsToSend = std::move(*gChildScalarsActions);
+ }
+ if (gChildKeyedScalarsActions) {
+ keyedScalarsToSend = std::move(*gChildKeyedScalarsActions);
+ }
+ if (gChildEvents) {
+ eventsToSend = std::move(*gChildEvents);
+ }
+ discardedData = gDiscardedData;
+ gDiscardedData = {0};
+ }
+
+ // Send the accumulated data to the parent process.
+ MOZ_ASSERT(ipcActor);
+ if (histogramsToSend.Length()) {
+ mozilla::Unused << NS_WARN_IF(
+ !ipcActor->SendAccumulateChildHistograms(histogramsToSend));
+ }
+ if (keyedHistogramsToSend.Length()) {
+ mozilla::Unused << NS_WARN_IF(
+ !ipcActor->SendAccumulateChildKeyedHistograms(keyedHistogramsToSend));
+ }
+ if (scalarsToSend.Length()) {
+ mozilla::Unused << NS_WARN_IF(
+ !ipcActor->SendUpdateChildScalars(scalarsToSend));
+ }
+ if (keyedScalarsToSend.Length()) {
+ mozilla::Unused << NS_WARN_IF(
+ !ipcActor->SendUpdateChildKeyedScalars(keyedScalarsToSend));
+ }
+ if (eventsToSend.Length()) {
+ mozilla::Unused << NS_WARN_IF(
+ !ipcActor->SendRecordChildEvents(eventsToSend));
+ }
+ mozilla::Unused << NS_WARN_IF(
+ !ipcActor->SendRecordDiscardedData(discardedData));
+}
+
+// To ensure we don't loop IPCTimerFired->AccumulateChild->arm timer, we don't
+// unset gIPCTimerArmed until the IPC completes
+//
+// This function must be called on the main thread, otherwise IPC will fail.
+void TelemetryIPCAccumulator::IPCTimerFired(nsITimer* aTimer, void* aClosure) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ // Send accumulated data to the correct parent process.
+ switch (XRE_GetProcessType()) {
+ case GeckoProcessType_Content:
+ SendAccumulatedData(mozilla::dom::ContentChild::GetSingleton());
+ break;
+ case GeckoProcessType_GPU:
+ SendAccumulatedData(mozilla::gfx::GPUParent::GetSingleton());
+ break;
+ case GeckoProcessType_RDD:
+ SendAccumulatedData(mozilla::RDDParent::GetSingleton());
+ break;
+ case GeckoProcessType_Socket:
+ SendAccumulatedData(mozilla::net::SocketProcessChild::GetSingleton());
+ break;
+ case GeckoProcessType_Utility:
+ SendAccumulatedData(
+ mozilla::ipc::UtilityProcessChild::GetSingleton().get());
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unsupported process type");
+ break;
+ }
+
+ gIPCTimerArmed = false;
+}
+
+void TelemetryIPCAccumulator::DeInitializeGlobalState() {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ StaticMutexAutoLock locker(gTelemetryIPCAccumulatorMutex);
+ if (gIPCTimer) {
+ NS_RELEASE(gIPCTimer);
+ }
+
+ gHistogramAccumulations = nullptr;
+ gKeyedHistogramAccumulations = nullptr;
+ gChildScalarsActions = nullptr;
+ gChildKeyedScalarsActions = nullptr;
+ gChildEvents = nullptr;
+}
+
+void TelemetryIPCAccumulator::DispatchToMainThread(
+ already_AddRefed<nsIRunnable>&& aEvent) {
+ SchedulerGroup::Dispatch(std::move(aEvent));
+}
diff --git a/toolkit/components/telemetry/core/ipc/TelemetryIPCAccumulator.h b/toolkit/components/telemetry/core/ipc/TelemetryIPCAccumulator.h
new file mode 100644
index 0000000000..e77ca95391
--- /dev/null
+++ b/toolkit/components/telemetry/core/ipc/TelemetryIPCAccumulator.h
@@ -0,0 +1,56 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef TelemetryIPCAccumulator_h__
+#define TelemetryIPCAccumulator_h__
+
+#include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/Maybe.h"
+#include "nsStringFwd.h"
+#include "mozilla/Telemetry.h" // for EventExtraEntry
+#include "mozilla/TelemetryComms.h" // for ScalarActionType, Scala...
+#include "mozilla/TelemetryHistogramEnums.h" // for HistogramID
+
+class nsIRunnable;
+class nsITimer;
+
+namespace mozilla {
+
+class TimeStamp;
+
+namespace TelemetryIPCAccumulator {
+
+// Histogram accumulation functions.
+void AccumulateChildHistogram(mozilla::Telemetry::HistogramID aId,
+ uint32_t aSample);
+void AccumulateChildKeyedHistogram(mozilla::Telemetry::HistogramID aId,
+ const nsCString& aKey, uint32_t aSample);
+
+// Scalar accumulation functions.
+void RecordChildScalarAction(uint32_t aId, bool aDynamic,
+ mozilla::Telemetry::ScalarActionType aAction,
+ const mozilla::Telemetry::ScalarVariant& aValue);
+
+void RecordChildKeyedScalarAction(
+ uint32_t aId, bool aDynamic, const nsAString& aKey,
+ mozilla::Telemetry::ScalarActionType aAction,
+ const mozilla::Telemetry::ScalarVariant& aValue);
+
+void RecordChildEvent(
+ const mozilla::TimeStamp& timestamp, const nsACString& category,
+ const nsACString& method, const nsACString& object,
+ const mozilla::Maybe<nsCString>& value,
+ const nsTArray<mozilla::Telemetry::EventExtraEntry>& extra);
+
+void IPCTimerFired(nsITimer* aTimer, void* aClosure);
+
+void DeInitializeGlobalState();
+
+void DispatchToMainThread(already_AddRefed<nsIRunnable>&& aEvent);
+
+} // namespace TelemetryIPCAccumulator
+} // namespace mozilla
+
+#endif // TelemetryIPCAccumulator_h__
diff --git a/toolkit/components/telemetry/core/nsITelemetry.idl b/toolkit/components/telemetry/core/nsITelemetry.idl
new file mode 100644
index 0000000000..ddad0ea7d4
--- /dev/null
+++ b/toolkit/components/telemetry/core/nsITelemetry.idl
@@ -0,0 +1,680 @@
+/* -*- Mode: C++; c-basic-offset: 2; indent-tabs-mode: nil; tab-width: 8 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+#include "nsIFile.idl"
+
+[scriptable,function, uuid(3d3b9075-5549-4244-9c08-b64fefa1dd60)]
+interface nsIFetchTelemetryDataCallback : nsISupports
+{
+ void complete();
+};
+
+[scriptable, uuid(273d2dd0-6c63-475a-b864-cb65160a1909)]
+interface nsITelemetry : nsISupports
+{
+ /**
+ * Histogram types:
+ * HISTOGRAM_EXPONENTIAL - buckets increase exponentially
+ * HISTOGRAM_LINEAR - buckets increase linearly
+ * HISTOGRAM_BOOLEAN - For storing 0/1 values
+ * HISTOGRAM_FLAG - For storing a single value; its count is always == 1.
+ * HISTOGRAM_COUNT - For storing counter values without bucketing.
+ * HISTOGRAM_CATEGORICAL - For storing enumerated values by label.
+ */
+ const unsigned long HISTOGRAM_EXPONENTIAL = 0;
+ const unsigned long HISTOGRAM_LINEAR = 1;
+ const unsigned long HISTOGRAM_BOOLEAN = 2;
+ const unsigned long HISTOGRAM_FLAG = 3;
+ const unsigned long HISTOGRAM_COUNT = 4;
+ const unsigned long HISTOGRAM_CATEGORICAL = 5;
+
+ /**
+ * Scalar types:
+ * SCALAR_TYPE_COUNT - for storing a numeric value
+ * SCALAR_TYPE_STRING - for storing a string value
+ * SCALAR_TYPE_BOOLEAN - for storing a boolean value
+ */
+ const unsigned long SCALAR_TYPE_COUNT = 0;
+ const unsigned long SCALAR_TYPE_STRING = 1;
+ const unsigned long SCALAR_TYPE_BOOLEAN = 2;
+
+ /**
+ * Dataset types:
+ * DATASET_ALL_CHANNELS - the basic dataset that is on-by-default on all channels
+ * DATASET_PRERELEASE_CHANNELS - the extended dataset that is opt-in on release,
+ * opt-out on pre-release channels.
+ */
+ const unsigned long DATASET_ALL_CHANNELS = 0;
+ const unsigned long DATASET_PRERELEASE_CHANNELS = 1;
+
+ /**
+ * Serializes the histogram labels for categorical hitograms.
+ * The returned structure looks like:
+ * { "histogram1": [ "histogram1_label1", "histogram1_label2", ...],
+ * "histogram2": [ "histogram2_label1", "histogram2_label2", ...]
+ * ...
+ * }
+ *
+ * Note that this function should only be used in tests and about:telemetry.
+ */
+ [implicit_jscontext]
+ jsval getCategoricalLabels();
+
+ /**
+ * Serializes the histograms from the given store to a JSON-style object.
+ * The returned structure looks like:
+ * { "process": { "name1": histogramData1, "name2": histogramData2 }, ... }
+ *
+ * Each histogram is represented in a packed format and has the following properties:
+ * bucket_count - Number of buckets of this histogram
+ * histogram_type - HISTOGRAM_EXPONENTIAL, HISTOGRAM_LINEAR, HISTOGRAM_BOOLEAN,
+ * HISTOGRAM_FLAG, HISTOGRAM_COUNT, or HISTOGRAM_CATEGORICAL
+ * sum - sum of the bucket contents
+ * range - A 2-item array of minimum and maximum bucket size
+ * values - Map from bucket to the bucket's count
+ *
+ * @param aStoreName The name of the store to snapshot.
+ * Defaults to "main".
+ * Custom stores are available when probes have them defined.
+ * See the `record_into_store` attribute on histograms.
+ * @see https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/collection/histograms.html#record-into-store
+ * @param aClearStore Whether to clear out the histograms in the named store after snapshotting.
+ * Defaults to false.
+ * @param aFilterTest If true, `TELEMETRY_TEST_` histograms will be filtered out.
+ Filtered histograms are still cleared if `aClearStore` is true.
+ * Defaults to false.
+ */
+ [implicit_jscontext]
+ jsval getSnapshotForHistograms([optional] in ACString aStoreName, [optional] in boolean aClearStore, [optional] in boolean aFilterTest);
+
+ /**
+ * Serializes the keyed histograms from the given store to a JSON-style object.
+ * The returned structure looks like:
+ * { "process": { "name1": { "key_1": histogramData1, "key_2": histogramData2 }, ...}, ... }
+ *
+ * @param aStoreName The name of the store to snapshot.
+ * Defaults to "main".
+ * Custom stores are available when probes have them defined.
+ * See the `record_into_store` attribute on histograms.
+ * @see https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/collection/histograms.html#record-into-store
+ * @param aClearStore Whether to clear out the keyed histograms in the named store after snapshotting.
+ * Defaults to false.
+ * @param aFilterTest If true, `TELEMETRY_TEST_` histograms will be filtered out.
+ Filtered histograms are still cleared if `aClearStore` is true.
+ * Defaults to false.
+ */
+ [implicit_jscontext]
+ jsval getSnapshotForKeyedHistograms([optional] in ACString aStoreName, [optional] in boolean aClearStore, [optional] in boolean aFilterTest);
+
+ /**
+ * Serializes the scalars from the given store to a JSON-style object.
+ * The returned structure looks like:
+ * { "process": { "category1.probe": 1,"category1.other_probe": false, ... }, ... }.
+ *
+ * @param aStoreName The name of the store to snapshot.
+ * Defaults to "main".
+ * Custom stores are available when probes have them defined.
+ * See the `record_into_store` attribute on scalars.
+ * @see https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/collection/scalars.html#optional-fields
+ * @param aClearStore Whether to clear out the scalars in the named store after snapshotting.
+ * Defaults to false.
+ * @param aFilterTest If true, `telemetry.test` scalars will be filtered out.
+ Filtered scalars are still cleared if `aClearStore` is true.
+ * Defaults to false.
+ */
+ [implicit_jscontext]
+ jsval getSnapshotForScalars([optional] in ACString aStoreName, [optional] in boolean aClearStore, [optional] in boolean aFilterTest);
+
+ /**
+ * Serializes the keyed scalars from the given store to a JSON-style object.
+ * The returned structure looks like:
+ * { "process": { "category1.probe": { "key_1": 2, "key_2": 1, ... }, ... }, ... }
+ *
+ * @param aStoreName The name of the store to snapshot.
+ * Defaults to "main".
+ * Custom stores are available when probes have them defined.
+ * See the `record_into_store` attribute on scalars.
+ * @see https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/collection/scalars.html#optional-fields
+ * @param aClearStore Whether to clear out the keyed scalars in the named store after snapshotting.
+ * Defaults to false.
+ * @param aFilterTest If true, `telemetry.test` scalars will be filtered out.
+ Filtered scalars are still cleared if `aClearStore` is true.
+ * Defaults to false.
+ */
+ [implicit_jscontext]
+ jsval getSnapshotForKeyedScalars([optional] in ACString aStoreName, [optional] in boolean aClearStore, [optional] in boolean aFilterTest);
+
+ /**
+ * The amount of time, in milliseconds, that the last session took
+ * to shutdown. Reads as 0 to indicate failure.
+ */
+ readonly attribute uint32_t lastShutdownDuration;
+
+ /**
+ * The number of failed profile lock attempts that have occurred prior to
+ * successfully locking the profile
+ */
+ readonly attribute uint32_t failedProfileLockCount;
+
+ /*
+ * An object containing information about slow SQL statements.
+ *
+ * {
+ * mainThread: { "sqlString1": [<hit count>, <total time>], "sqlString2": [...], ... },
+ * otherThreads: { "sqlString3": [<hit count>, <total time>], "sqlString4": [...], ... }
+ * }
+ *
+ * where:
+ * mainThread: Slow statements that executed on the main thread
+ * otherThreads: Slow statements that executed on a non-main thread
+ * sqlString - String of the offending statement (see note)
+ * hit count - The number of times this statement required longer than the threshold time to execute
+ * total time - The sum of all execution times above the threshold time for this statement
+ *
+ * Note that dynamic SQL strings and SQL strings executed against addon DBs could contain private information.
+ * This property represents such SQL as aggregate database-level stats and the sqlString contains the database
+ * filename instead.
+ */
+ [implicit_jscontext]
+ readonly attribute jsval slowSQL;
+
+ /*
+ * See slowSQL above.
+ *
+ * An object containing full strings of every slow SQL statement if toolkit.telemetry.debugSlowSql = true
+ * The returned SQL strings may contain private information and should not be reported to Telemetry.
+ */
+ [implicit_jscontext]
+ readonly attribute jsval debugSlowSQL;
+
+ /**
+ * Flags for getUntrustedModuleLoadEvents.
+ */
+
+ /**
+ * This flag is set to retrieve all data including instances which have been
+ * retrieved before. If not set, only new instances since the last call
+ * will be returned.
+ * If this flag is set, KEEP_LOADEVENTS_NEW must not be set unless
+ * EXCLUDE_STACKINFO_FROM_LOADEVENTS is set.
+ * (See also MultiGetUntrustedModulesData::Serialize.)
+ */
+ const unsigned long INCLUDE_OLD_LOADEVENTS = 1 << 0;
+
+ /**
+ * This flag is set to keep the returned instances as if they were not
+ * retrieved, meaning those instances will be returned by a next method
+ * call without INCLUDE_OLD_LOADEVENTS. If not set, the returned instances
+ * can be re-retrieved only when INCLUDE_OLD_LOADEVENTS is specified.
+ * If this flag is set, INCLUDE_OLD_LOADEVENTS must not be set unless
+ * EXCLUDE_STACKINFO_FROM_LOADEVENTS is set.
+ * (See also MultiGetUntrustedModulesData::Serialize.)
+ */
+ const unsigned long KEEP_LOADEVENTS_NEW = 1 << 1;
+
+ /**
+ * This flag is set to include private fields.
+ * Do not specify this flag to retrieve data to be submitted.
+ */
+ const unsigned long INCLUDE_PRIVATE_FIELDS_IN_LOADEVENTS = 1 << 2;
+
+ /**
+ * This flag is set to exclude the "combinedStacks" field.
+ * Without this flag, the flags INCLUDE_OLD_LOADEVENTS and KEEP_LOADEVENTS_NEW
+ * cannot be set at the same time.
+ */
+ const unsigned long EXCLUDE_STACKINFO_FROM_LOADEVENTS = 1 << 3;
+
+ /*
+ * An array of untrusted module load events. Each element describes one or
+ * more modules that were loaded, contextual information at the time of the
+ * event (including stack trace), and flags describing the module's
+ * trustworthiness.
+ *
+ * @param aFlags Combination (bitwise OR) of the flags specified above.
+ * Defaults to 0.
+ */
+ [implicit_jscontext]
+ Promise getUntrustedModuleLoadEvents([optional] in unsigned long aFlags);
+
+ /*
+ * Whether the untrusted module load events are ready for processing.
+ * Calling getUntrustedModuleLoadEvents() before this attribute is true
+ * will result in an empty array. */
+ readonly attribute boolean areUntrustedModuleLoadEventsReady;
+
+ /*
+ * Asynchronously get an array of the modules loaded in the process.
+ *
+ * The data has the following structure:
+ *
+ * [
+ * {
+ * "name": <string>, // Name of the module file (e.g. xul.dll)
+ * "version": <string>, // Version of the module
+ * "debugName": <string>, // ID of the debug information file
+ * "debugID": <string>, // Name of the debug information file
+ * "certSubject": <string>, // Name of the organization that signed the binary (Optional, only defined when present)
+ * },
+ * ...
+ * ]
+ *
+ * @return A promise that resolves to an array of modules or rejects with
+ NS_ERROR_FAILURE on failure.
+ * @throws NS_ERROR_NOT_IMPLEMENTED if the Gecko profiler is not enabled.
+ */
+ [implicit_jscontext]
+ Promise getLoadedModules();
+
+ /*
+ * An object with two fields: memoryMap and stacks.
+ * * memoryMap is a list of loaded libraries.
+ * * stacks is a list of stacks. Each stack is a list of pairs of the form
+ * [moduleIndex, offset]. The moduleIndex is an index into the memoryMap and
+ * offset is an offset in the library at memoryMap[moduleIndex].
+ * This format is used to make it easier to send the stacks to the
+ * symbolication server.
+ */
+ [implicit_jscontext]
+ readonly attribute jsval lateWrites;
+
+ /**
+ * Create and return a histogram registered in TelemetryHistograms.h.
+ *
+ * @param id - unique identifier from TelemetryHistograms.h
+ * The returned object has the following functions:
+ * add(value) - Adds a sample of `value` to the histogram.
+ `value` may be a categorical histogram's label as a string,
+ a boolean histogram's value as a boolean,
+ or a number that fits inside a uint32_t.
+ * snapshot([optional] {store}) - Returns a snapshot of the histogram with the same data fields
+ as in getSnapshotForHistograms().
+ Defaults to the "main" store.
+ * clear([optional] {store}) - Zeros out the histogram's buckets and sum.
+ Defaults to the "main" store.
+ Note: This is intended to be only used in tests.
+ */
+ [implicit_jscontext]
+ jsval getHistogramById(in ACString id);
+
+ /**
+ * Create and return a histogram registered in TelemetryHistograms.h.
+ *
+ * @param id - unique identifier from TelemetryHistograms.h
+ * The returned object has the following functions:
+ * add(string key, [optional] value) - Adds a sample of `value` to the histogram for that key.
+ If no histogram for that key exists yet, it is created.
+ `value` may be a categorical histogram's label as a string,
+ a boolean histogram's value as a boolean,
+ or a number that fits inside a uint32_t.
+ * snapshot([optional] {store}) - Returns the snapshots of all the registered keys in the form
+ {key1: snapshot1, key2: snapshot2, ...} in the specified store.
+ * Defaults to the "main" store.
+ * keys([optional] {store}) - Returns an array with the string keys of the currently registered
+ histograms in the given store.
+ Defaults to "main".
+ * clear([optional] {store}) - Clears the registered histograms from this.
+ * Defaults to the "main" store.
+ * Note: This is intended to be only used in tests.
+ */
+ [implicit_jscontext]
+ jsval getKeyedHistogramById(in ACString id);
+
+ /**
+ * A flag indicating if Telemetry can record base data (FHR data). This is true if the
+ * FHR data reporting service or self-support are enabled.
+ *
+ * In the unlikely event that adding a new base probe is needed, please check the data
+ * collection wiki at https://wiki.mozilla.org/Firefox/Data_Collection and talk to the
+ * Telemetry team.
+ */
+ attribute boolean canRecordBase;
+
+ /**
+ * A flag indicating if Telemetry is allowed to record extended data. Returns false if
+ * the user hasn't opted into "extended Telemetry" on the Release channel, when the
+ * user has explicitly opted out of Telemetry on Nightly/Aurora/Beta or if manually
+ * set to false during tests.
+ *
+ * Set this to false in tests to disable gathering of extended telemetry statistics.
+ */
+ attribute boolean canRecordExtended;
+
+ /**
+ * A flag indicating whether Telemetry is recording release data, which is a
+ * smallish subset of our usage data that we're prepared to handle from our
+ * largish release population.
+ *
+ * This is true most of the time.
+ *
+ * This will always return true in the case of a non-content child process.
+ * Only values returned on the parent process are valid.
+ *
+ * This does not indicate whether Telemetry will send any data. That is
+ * governed by user preference and other mechanisms.
+ *
+ * You may use this to determine if it's okay to record your data.
+ */
+ readonly attribute boolean canRecordReleaseData;
+
+ /**
+ * A flag indicating whether Telemetry is recording prerelease data, which is
+ * a largish amount of usage data that we're prepared to handle from our
+ * smallish pre-release population.
+ *
+ * This is true on pre-release branches of Firefox.
+ *
+ * This does not indicate whether Telemetry will send any data. That is
+ * governed by user preference and other mechanisms.
+ *
+ * You may use this to determine if it's okay to record your data.
+ */
+ readonly attribute boolean canRecordPrereleaseData;
+
+ /**
+ * A flag indicating whether Telemetry can submit official results (for base or extended
+ * data). This is true on official, non-debug builds with built in support for Mozilla
+ * Telemetry reporting.
+ *
+ * This will always return true in the case of a non-content child process.
+ * Only values returned on the parent process are valid.
+ */
+ readonly attribute boolean isOfficialTelemetry;
+
+ /**
+ * Enable/disable recording for this histogram at runtime.
+ * Recording is enabled by default, unless listed at kRecordingInitiallyDisabledIDs[].
+ * Name must be a valid Histogram identifier, otherwise an assertion will be triggered.
+ *
+ * @param id - unique identifier from histograms.json
+ * @param enabled - whether or not to enable recording from now on.
+ */
+ void setHistogramRecordingEnabled(in ACString id, in boolean enabled);
+
+ /**
+ * Read data from the previous run. After the callback is called, the last
+ * shutdown time is available in lastShutdownDuration and any late
+ * writes in lateWrites.
+ */
+ void asyncFetchTelemetryData(in nsIFetchTelemetryDataCallback aCallback);
+
+ /**
+ * Get statistics of file IO reports, null, if not recorded.
+ *
+ * The statistics are returned as an object whose propoerties are the names
+ * of the files that have been accessed and whose corresponding values are
+ * arrays of size three, representing startup, normal, and shutdown stages.
+ * Each stage's entry is either null or an array with the layout
+ * [total_time, #creates, #reads, #writes, #fsyncs, #stats]
+ */
+ [implicit_jscontext]
+ readonly attribute jsval fileIOReports;
+
+ /**
+ * Return the number of milliseconds since process start using monotonic
+ * timestamps (unaffected by system clock changes). On Windows, this includes
+ * the period of time the device was suspended. On Linux and macOS, this does
+ * not include the period of time the device was suspneded.
+ */
+ double msSinceProcessStart();
+
+ /**
+ * Return the number of milliseconds since process start using monotonic
+ * timestamps (unaffected by system clock changes), including the periods of
+ * time the device was suspended.
+ * @throws NS_ERROR_NOT_AVAILABLE if unavailable.
+ */
+ double msSinceProcessStartIncludingSuspend();
+
+ /**
+ * Return the number of milliseconds since process start using monotonic
+ * timestamps (unaffected by system clock changes), excluding the periods of
+ * time the device was suspended.
+ * @throws NS_ERROR_NOT_AVAILABLE if unavailable.
+ */
+ double msSinceProcessStartExcludingSuspend();
+
+ /**
+ * Time since the system wide epoch. This is not a monotonic timer but
+ * can be used across process boundaries.
+ */
+ double msSystemNow();
+
+ /**
+ * Adds the value to the given scalar.
+ *
+ * @param aName The scalar name.
+ * @param aValue The numeric value to add to the scalar. Only unsigned integers supported.
+ */
+ [implicit_jscontext]
+ void scalarAdd(in ACString aName, in jsval aValue);
+
+ /**
+ * Sets the scalar to the given value.
+ *
+ * @param aName The scalar name.
+ * @param aValue The value to set the scalar to. If the type of aValue doesn't match the
+ * type of the scalar, the function will fail. For scalar string types, the this
+ * is truncated to 50 characters.
+ */
+ [implicit_jscontext]
+ void scalarSet(in ACString aName, in jsval aValue);
+
+ /**
+ * Sets the scalar to the maximum of the current and the passed value.
+ *
+ * @param aName The scalar name.
+ * @param aValue The numeric value to set the scalar to. Only unsigned integers supported.
+ */
+ [implicit_jscontext]
+ void scalarSetMaximum(in ACString aName, in jsval aValue);
+
+ /**
+ * Adds the value to the given keyed scalar.
+ *
+ * @param aName The scalar name.
+ * @param aKey The key name.
+ * @param aValue The numeric value to add to the scalar. Only unsigned integers supported.
+ */
+ [implicit_jscontext]
+ void keyedScalarAdd(in ACString aName, in AString aKey, in jsval aValue);
+
+ /**
+ * Sets the keyed scalar to the given value.
+ *
+ * @param aName The scalar name.
+ * @param aKey The key name.
+ * @param aValue The value to set the scalar to. If the type of aValue doesn't match the
+ * type of the scalar, the function will fail.
+ */
+ [implicit_jscontext]
+ void keyedScalarSet(in ACString aName, in AString aKey, in jsval aValue);
+
+ /**
+ * Sets the keyed scalar to the maximum of the current and the passed value.
+ *
+ * @param aName The scalar name.
+ * @param aKey The key name.
+ * @param aValue The numeric value to set the scalar to. Only unsigned integers supported.
+ */
+ [implicit_jscontext]
+ void keyedScalarSetMaximum(in ACString aName, in AString aKey, in jsval aValue);
+
+ /**
+ * Resets all the stored scalars. This is intended to be only used in tests.
+ */
+ void clearScalars();
+
+ /**
+ * Immediately sends any Telemetry batched on this process to the parent
+ * process. This is intended only to be used on process shutdown.
+ */
+ void flushBatchedChildTelemetry();
+
+ /**
+ * Record an event in Telemetry.
+ *
+ * @param aCategory The category name.
+ * @param aMethod The method name.
+ * @param aObject The object name.
+ * @param aValue An optional string value to record.
+ * @param aExtra An optional object of the form (string -> string).
+ * It should only contain registered extra keys.
+ *
+ * @throws NS_ERROR_INVALID_ARG When trying to record an unknown event.
+ */
+ [implicit_jscontext, optional_argc]
+ void recordEvent(in ACString aCategory, in ACString aMethod, in ACString aObject, [optional] in jsval aValue, [optional] in jsval extra);
+
+ /**
+ * Enable recording of events in a category.
+ * Events default to recording disabled. This allows to toggle recording for all events
+ * in the specified category.
+ *
+ * @param aCategory The category name.
+ * @param aEnabled Whether recording is enabled for events in that category.
+ */
+ void setEventRecordingEnabled(in ACString aCategory, in boolean aEnabled);
+
+ /**
+ * Serializes the recorded events to a JSON-appropriate array and optionally resets them.
+ * The returned structure looks like this:
+ * [
+ * // [timestamp, category, method, object, stringValue, extraValues]
+ * [43245, "category1", "method1", "object1", "string value", null],
+ * [43258, "category1", "method2", "object1", null, {"key1": "string value"}],
+ * ...
+ * ]
+ *
+ * @param aDataset DATASET_ALL_CHANNELS or DATASET_PRERELEASE_CHANNELS.
+ * @param [aClear=false] Whether to clear out the flushed events after snapshotting.
+ * @param aEventLimit How many events per process to limit the snapshot to contain, all if unspecified.
+ * Even if aClear, the leftover event records are not cleared.
+ */
+ [implicit_jscontext, optional_argc]
+ jsval snapshotEvents(in uint32_t aDataset, [optional] in boolean aClear, [optional] in uint32_t aEventLimit);
+
+ /**
+ * Register new events to record them from addons. This allows registering multiple
+ * events for a category. They will be valid only for the current Firefox session.
+ * Note that events shipping in Firefox should be registered in Events.yaml.
+ *
+ * @param aCategory The unique category the events are registered in.
+ * @param aEventData An object that contains registration data for 1-N events of the form:
+ * {
+ * "categoryName": {
+ * "methods": ["test1"],
+ * "objects": ["object1"],
+ * "record_on_release": false,
+ * "extra_keys": ["key1", "key2"], // optional
+ * "expired": false // optional, defaults to false.
+ * },
+ * ...
+ * }
+ * @param aEventData.<name>.methods List of methods for this event entry.
+ * @param aEventData.<name>.objects List of objects for this event entry.
+ * @param aEventData.<name>.extra_keys Optional, list of allowed extra keys for this event entry.
+ * @param aEventData.<name>.record_on_release Optional, whether to record this data on release.
+ * Defaults to false.
+ * @param aEventData.<name>.expired Optional, whether this event entry is expired. This allows
+ * recording it without error, but it will be discarded. Defaults to false.
+ */
+ [implicit_jscontext]
+ void registerEvents(in ACString aCategory, in jsval aEventData);
+
+ /**
+ * Parent process only. Register dynamic builtin events. The parameters
+ * have the same meaning as the usual |registerEvents| function.
+ *
+ * This function is only meant to be used to support the "artifact build"/
+ * "build faster" developers by allowing to add new events without rebuilding
+ * the C++ components including the headers files.
+ */
+ [implicit_jscontext]
+ void registerBuiltinEvents(in ACString aCategory, in jsval aEventData);
+
+ /**
+ * Parent process only. Register new scalars to record them from addons. This
+ * allows registering multiple scalars for a category. They will be valid only for
+ * the current Firefox session.
+ * Note that scalars shipping in Firefox should be registered in Scalars.yaml.
+ *
+ * @param aCategoryName The unique category the scalars are registered in.
+ * @param aScalarData An object that contains registration data for multiple scalars in the form:
+ * {
+ * "sample_scalar": {
+ * "kind": Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ * "keyed": true, //optional, defaults to false
+ * "record_on_release: true, // optional, defaults to false
+ * "expired": false // optional, defaults to false.
+ * },
+ * ...
+ * }
+ * @param aScalarData.<name>.kind One of the scalar types defined in this file (SCALAR_TYPE_*)
+ * @param aScalarData.<name>.keyed Optional, whether this is a keyed scalar or not. Defaults to false.
+ * @param aScalarData.<name>.record_on_release Optional, whether to record this data on release.
+ * Defaults to false.
+ * @param aScalarData.<name>.expired Optional, whether this scalar entry is expired. This allows
+ * recording it without error, but it will be discarded. Defaults to false.
+ */
+ [implicit_jscontext]
+ void registerScalars(in ACString aCategoryName, in jsval aScalarData);
+
+ /**
+ * Parent process only. Register dynamic builtin scalars. The parameters
+ * have the same meaning as the usual |registerScalars| function.
+ *
+ * This function is only meant to be used to support the "artifact build"/
+ * "build faster" developers by allowing to add new scalars without rebuilding
+ * the C++ components including the headers files.
+ */
+ [implicit_jscontext]
+ void registerBuiltinScalars(in ACString aCategoryName, in jsval aScalarData);
+
+ /**
+ * Resets all the stored events. This is intended to be only used in tests.
+ * Events recorded but not yet flushed to the parent process storage won't be cleared.
+ * Override the pref. `toolkit.telemetry.ipcBatchTimeout` to reduce the time to flush events.
+ */
+ void clearEvents();
+
+ /**
+ * Get a list of all registered stores.
+ *
+ * The list is deduplicated, but unordered.
+ */
+ [implicit_jscontext]
+ jsval getAllStores();
+
+ /**
+ * Does early, cheap initialization for native telemetry data providers.
+ * Currently, this includes only MemoryTelemetry.
+ */
+ void earlyInit();
+
+ /**
+ * Does late, expensive initialization for native telemetry data providers.
+ * Currently, this includes only MemoryTelemetry.
+ *
+ * This should only be called after startup has completed and the event loop
+ * is idle.
+ */
+ void delayedInit();
+
+ /**
+ * Shuts down native telemetry providers. Currently, this includes only
+ * MemoryTelemetry.
+ */
+ void shutdown();
+
+ /**
+ * Gathers telemetry data for memory usage and records it to the data store.
+ * Returns a promise which resolves when asynchronous data collection has
+ * completed and all data has been recorded.
+ */
+ [implicit_jscontext]
+ Promise gatherMemory();
+};
diff --git a/toolkit/components/telemetry/dap/DAPTelemetry.cpp b/toolkit/components/telemetry/dap/DAPTelemetry.cpp
new file mode 100644
index 0000000000..31d634e47b
--- /dev/null
+++ b/toolkit/components/telemetry/dap/DAPTelemetry.cpp
@@ -0,0 +1,223 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DAPTelemetryBindings.h"
+
+#include "mozilla/Logging.h"
+#include "nsPromiseFlatString.h"
+
+#include "nss.h"
+#include "nsNSSComponent.h"
+#include "secmodt.h"
+#include "pk11pub.h"
+#include "ScopedNSSTypes.h"
+
+static mozilla::LazyLogModule sLogger("DAPTelemetry");
+#undef LOG
+#define LOG(...) MOZ_LOG(sLogger, mozilla::LogLevel::Debug, (__VA_ARGS__))
+
+namespace mozilla {
+
+NS_IMPL_ISUPPORTS(DAPTelemetry, nsIDAPTelemetry)
+
+// This function was copied from pk11_hpke_unittest.cc
+// And modified to take a Span.
+static std::vector<uint8_t> Pkcs8(Span<const uint8_t> sk,
+ Span<const uint8_t> pk) {
+ // Only X25519 format.
+ std::vector<uint8_t> v(105);
+ v.assign({
+ 0x30, 0x67, 0x02, 0x01, 0x00, 0x30, 0x14, 0x06, 0x07, 0x2a, 0x86, 0x48,
+ 0xce, 0x3d, 0x02, 0x01, 0x06, 0x09, 0x2b, 0x06, 0x01, 0x04, 0x01, 0xda,
+ 0x47, 0x0f, 0x01, 0x04, 0x4c, 0x30, 0x4a, 0x02, 0x01, 0x01, 0x04, 0x20,
+ });
+ v.insert(v.end(), sk.begin(), sk.end());
+ v.insert(v.end(), {
+ 0xa1,
+ 0x23,
+ 0x03,
+ 0x21,
+ 0x00,
+ });
+ v.insert(v.end(), pk.begin(), pk.end());
+ return v;
+}
+
+// This function was copied from cpputil.h
+static unsigned char* toUcharPtr(const uint8_t* v) {
+ return const_cast<unsigned char*>(static_cast<const unsigned char*>(v));
+}
+
+/// If successful this returns a pointer to a HpkeContext which must be
+/// released using dapDestroyHpkeContext or PK11_HPKE_DestroyContext.
+HpkeContext* dapSetupHpkeContextInternal(
+ const uint8_t* aKey, uint32_t aKeyLength, const uint8_t* aInfo,
+ uint32_t aInfoLength, SECKEYPublicKey* aPkE, SECKEYPrivateKey* aSkE,
+ nsTArray<uint8_t>* aOutputEncapsulatedKey) {
+ SECStatus status = PK11_HPKE_ValidateParameters(
+ HpkeDhKemX25519Sha256, HpkeKdfHkdfSha256, HpkeAeadAes128Gcm);
+ if (status != SECSuccess) {
+ MOZ_LOG(sLogger, mozilla::LogLevel::Error,
+ ("Invalid HKPE parameters found."));
+ return nullptr;
+ }
+
+ UniqueHpkeContext context(
+ PK11_HPKE_NewContext(HpkeDhKemX25519Sha256, HpkeKdfHkdfSha256,
+ HpkeAeadAes128Gcm, nullptr, nullptr));
+
+ SECKEYPublicKey* pkR_raw = nullptr;
+ status = PK11_HPKE_Deserialize(context.get(), aKey, aKeyLength, &pkR_raw);
+ UniqueSECKEYPublicKey pkR(pkR_raw);
+ pkR_raw = nullptr;
+ if (status != SECSuccess) {
+ MOZ_LOG(sLogger, mozilla::LogLevel::Error,
+ ("Failed to deserialize HPKE encryption key."));
+ return nullptr;
+ }
+
+ const SECItem hpkeInfo = {siBuffer, toUcharPtr(aInfo), aInfoLength};
+
+ status = PK11_HPKE_SetupS(context.get(), aPkE, aSkE, pkR.get(), &hpkeInfo);
+ if (status != SECSuccess) {
+ MOZ_LOG(sLogger, mozilla::LogLevel::Error, ("HPKE setup failed."));
+ return nullptr;
+ }
+
+ const SECItem* hpkeEncapKey = PK11_HPKE_GetEncapPubKey(context.get());
+ if (!hpkeEncapKey) {
+ MOZ_LOG(sLogger, mozilla::LogLevel::Error,
+ ("Failed to get HPKE encapsulated public key."));
+ return nullptr;
+ }
+
+ aOutputEncapsulatedKey->AppendElements(hpkeEncapKey->data, hpkeEncapKey->len);
+
+ return context.release();
+}
+
+extern "C" {
+/// Takes additional ephemeral keys to make everything deterministic for test
+/// vectors.
+/// If successful this returns a pointer to a HpkeContext which must be
+/// released using dapDestroyHpkeContext or PK11_HPKE_DestroyContext.
+HpkeContext* dapSetupHpkeContextForTesting(
+ const uint8_t* aKey, uint32_t aKeyLength, const uint8_t* aInfo,
+ uint32_t aInfoLength, const uint8_t* aPkEm, uint32_t aPkEmLength,
+ const uint8_t* aSkEm, uint32_t aSkEmLength,
+ nsTArray<uint8_t>* aOutputEncapsulatedKey) {
+ Span<const uint8_t> sk_e(aSkEm, aSkEm + aSkEmLength);
+ Span<const uint8_t> pk_e(aPkEm, aPkEm + aPkEmLength);
+ std::vector<uint8_t> pkcs8_e = Pkcs8(sk_e, pk_e);
+
+ MOZ_RELEASE_ASSERT(EnsureNSSInitializedChromeOrContent(),
+ "Could not initialize NSS.");
+
+ UniquePK11SlotInfo slot(PK11_GetInternalSlot());
+ MOZ_RELEASE_ASSERT(slot, "Failed to get slot.");
+
+ SECItem keys_e = {siBuffer, toUcharPtr(pkcs8_e.data()),
+ static_cast<unsigned int>(pkcs8_e.size())};
+ SECKEYPrivateKey* internal_skE_raw = nullptr;
+ SECStatus rv = PK11_ImportDERPrivateKeyInfoAndReturnKey(
+ slot.get(), &keys_e, nullptr, nullptr, false, false, KU_ALL,
+ &internal_skE_raw, nullptr);
+ UniqueSECKEYPrivateKey internal_skE(internal_skE_raw);
+ internal_skE_raw = nullptr;
+ MOZ_RELEASE_ASSERT(rv == SECSuccess, "Failed to import skE/pkE.");
+
+ UniqueSECKEYPublicKey internal_pkE(
+ SECKEY_ConvertToPublicKey(internal_skE.get()));
+
+ UniqueHpkeContext result(dapSetupHpkeContextInternal(
+ aKey, aKeyLength, aInfo, aInfoLength, internal_pkE.get(),
+ internal_skE.get(), aOutputEncapsulatedKey));
+
+ return result.release();
+}
+
+void dapDestroyHpkeContext(HpkeContext* aContext) {
+ PK11_HPKE_DestroyContext(aContext, true);
+}
+
+bool dapHpkeEncrypt(HpkeContext* aContext, const uint8_t* aAad,
+ uint32_t aAadLength, const uint8_t* aPlaintext,
+ uint32_t aPlaintextLength,
+ nsTArray<uint8_t>* aOutputShare) {
+ SECItem aad_si = {siBuffer, toUcharPtr(aAad), aAadLength};
+ SECItem plaintext_si = {siBuffer, toUcharPtr(aPlaintext), aPlaintextLength};
+ SECItem* chCt = nullptr;
+ SECStatus rv = PK11_HPKE_Seal(aContext, &aad_si, &plaintext_si, &chCt);
+ if (rv != SECSuccess) {
+ return false;
+ }
+ UniqueSECItem ct(chCt);
+
+ aOutputShare->AppendElements(ct->data, ct->len);
+ return true;
+}
+
+bool dapHpkeEncryptOneshot(const uint8_t* aKey, uint32_t aKeyLength,
+ const uint8_t* aInfo, uint32_t aInfoLength,
+ const uint8_t* aAad, uint32_t aAadLength,
+ const uint8_t* aPlaintext, uint32_t aPlaintextLength,
+ nsTArray<uint8_t>* aOutputEncapsulatedKey,
+ nsTArray<uint8_t>* aOutputShare) {
+ MOZ_RELEASE_ASSERT(EnsureNSSInitializedChromeOrContent(),
+ "Could not initialize NSS.");
+ UniqueHpkeContext context(
+ dapSetupHpkeContextInternal(aKey, aKeyLength, aInfo, aInfoLength, nullptr,
+ nullptr, aOutputEncapsulatedKey));
+ if (!context) {
+ return false;
+ }
+
+ return dapHpkeEncrypt(context.get(), aAad, aAadLength, aPlaintext,
+ aPlaintextLength, aOutputShare);
+}
+}
+
+NS_IMETHODIMP DAPTelemetry::GetReportU8(
+ const nsTArray<uint8_t>& aLeaderHpkeConfig,
+ const nsTArray<uint8_t>& aHelperHpkeConfig, uint8_t aMeasurement,
+ const nsTArray<uint8_t>& aTaskID, const uint64_t aTimePrecision,
+ nsTArray<uint8_t>& aOutReport) {
+ MOZ_RELEASE_ASSERT(aTaskID.Length() == 32, "TaskID must have 32 bytes.");
+ if (!dapGetReportU8(&aLeaderHpkeConfig, &aHelperHpkeConfig, aMeasurement,
+ &aTaskID, aTimePrecision, &aOutReport)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP DAPTelemetry::GetReportVecU8(
+ const nsTArray<uint8_t>& aLeaderHpkeConfig,
+ const nsTArray<uint8_t>& aHelperHpkeConfig,
+ const nsTArray<uint8_t>& aMeasurement, const nsTArray<uint8_t>& aTaskID,
+ const uint64_t aTimePrecision, nsTArray<uint8_t>& aOutReport) {
+ MOZ_RELEASE_ASSERT(aTaskID.Length() == 32, "TaskID must have 32 bytes.");
+ if (!dapGetReportVecU8(&aLeaderHpkeConfig, &aHelperHpkeConfig, &aMeasurement,
+ &aTaskID, aTimePrecision, &aOutReport)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+NS_IMETHODIMP DAPTelemetry::GetReportVecU16(
+ const nsTArray<uint8_t>& aLeaderHpkeConfig,
+ const nsTArray<uint8_t>& aHelperHpkeConfig,
+ const nsTArray<uint16_t>& aMeasurement, const nsTArray<uint8_t>& aTaskID,
+ const uint64_t aTimePrecision, nsTArray<uint8_t>& aOutReport) {
+ MOZ_RELEASE_ASSERT(aTaskID.Length() == 32, "TaskID must have 32 bytes.");
+ if (!dapGetReportVecU16(&aLeaderHpkeConfig, &aHelperHpkeConfig, &aMeasurement,
+ &aTaskID, aTimePrecision, &aOutReport)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/telemetry/dap/DAPTelemetry.h b/toolkit/components/telemetry/dap/DAPTelemetry.h
new file mode 100644
index 0000000000..b6a0c8c17f
--- /dev/null
+++ b/toolkit/components/telemetry/dap/DAPTelemetry.h
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+#ifndef mozilla_nsIDAPTelemetry_h__
+#define mozilla_nsIDAPTelemetry_h__
+
+#include "nsIDAPTelemetry.h"
+
+namespace mozilla {
+
+class DAPTelemetry final : public nsIDAPTelemetry {
+ NS_DECL_ISUPPORTS
+
+ NS_DECL_NSIDAPTELEMETRY
+
+ public:
+ DAPTelemetry() = default;
+
+ private:
+ ~DAPTelemetry() = default;
+};
+
+} // namespace mozilla
+
+#endif
diff --git a/toolkit/components/telemetry/dap/DAPTelemetryBindings.h b/toolkit/components/telemetry/dap/DAPTelemetryBindings.h
new file mode 100644
index 0000000000..bf62d7c0bc
--- /dev/null
+++ b/toolkit/components/telemetry/dap/DAPTelemetryBindings.h
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef DAPTelemetryBindings_h
+#define DAPTelemetryBindings_h
+
+#include "DAPTelemetry.h"
+#include "mozilla/dap_ffi_generated.h"
+
+#endif // DAPTelemetryBindings_h
diff --git a/toolkit/components/telemetry/dap/DAPTelemetrySender.sys.mjs b/toolkit/components/telemetry/dap/DAPTelemetrySender.sys.mjs
new file mode 100644
index 0000000000..0a2516ad1f
--- /dev/null
+++ b/toolkit/components/telemetry/dap/DAPTelemetrySender.sys.mjs
@@ -0,0 +1,335 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+import { HPKEConfigManager } from "resource://gre/modules/HPKEConfigManager.sys.mjs";
+
+let lazy = {};
+
+ChromeUtils.defineLazyGetter(lazy, "logConsole", function () {
+ return console.createInstance({
+ prefix: "DAPTelemetrySender",
+ maxLogLevelPref: "toolkit.telemetry.dap.logLevel",
+ });
+});
+ChromeUtils.defineESModuleGetters(lazy, {
+ AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+ DAPVisitCounter: "resource://gre/modules/DAPVisitCounter.sys.mjs",
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+const PREF_LEADER = "toolkit.telemetry.dap_leader";
+const PREF_HELPER = "toolkit.telemetry.dap_helper";
+
+XPCOMUtils.defineLazyPreferenceGetter(lazy, "LEADER", PREF_LEADER, undefined);
+XPCOMUtils.defineLazyPreferenceGetter(lazy, "HELPER", PREF_HELPER, undefined);
+
+/**
+ * The purpose of this singleton is to handle sending of DAP telemetry data.
+ * The current DAP draft standard is available here:
+ * https://github.com/ietf-wg-ppm/draft-ietf-ppm-dap
+ *
+ * The specific purpose of this singleton is to make the necessary calls to fetch to do networking.
+ */
+
+export const DAPTelemetrySender = new (class {
+ startup() {
+ lazy.logConsole.debug("Performing DAP startup");
+
+ if (lazy.NimbusFeatures.dapTelemetry.getVariable("visitCountingEnabled")) {
+ lazy.DAPVisitCounter.startup();
+ }
+
+ if (lazy.NimbusFeatures.dapTelemetry.getVariable("task1Enabled")) {
+ let tasks = [];
+ lazy.logConsole.debug("Task 1 is enabled.");
+ let task1_id =
+ lazy.NimbusFeatures.dapTelemetry.getVariable("task1TaskId");
+ if (task1_id !== undefined && task1_id != "") {
+ /** @typedef { 'u8' | 'vecu8' | 'vecu16' } measurementtype */
+
+ /**
+ * @typedef {object} Task
+ * @property {string} id - The task ID, base 64 encoded.
+ * @property {string} leader_endpoint - Base URL for the leader.
+ * @property {string} helper_endpoint - Base URL for the helper.
+ * @property {number} time_precision - Timestamps (in s) are rounded to the nearest multiple of this.
+ * @property {measurementtype} measurement_type - Defines measurements and aggregations used by this task. Effectively specifying the VDAF.
+ */
+ let task = {
+ // this is testing task 1
+ id: task1_id,
+ leader_endpoint: null,
+ helper_endpoint: null,
+ time_precision: 300,
+ measurement_type: "vecu8",
+ };
+ tasks.push(task);
+
+ lazy.setTimeout(
+ () => this.timedSendTestReports(tasks),
+ this.timeout_value()
+ );
+
+ lazy.NimbusFeatures.dapTelemetry.onUpdate(async (event, reason) => {
+ if (typeof this.counters !== "undefined") {
+ await this.sendTestReports(tasks, 30 * 1000, "nimbus-update");
+ }
+ });
+ }
+
+ this._asyncShutdownBlocker = async () => {
+ lazy.logConsole.debug(`Sending on shutdown.`);
+ // Shorter timeout to prevent crashing due to blocking shutdown
+ await this.sendTestReports(tasks, 2 * 1000, "shutdown");
+ };
+
+ lazy.AsyncShutdown.quitApplicationGranted.addBlocker(
+ "DAPTelemetrySender: sending data",
+ this._asyncShutdownBlocker
+ );
+ }
+ }
+
+ async sendTestReports(tasks, timeout, reason) {
+ for (let task of tasks) {
+ let measurement;
+ if (task.measurement_type == "u8") {
+ measurement = 3;
+ } else if (task.measurement_type == "vecu8") {
+ measurement = new Uint8Array(20);
+ let r = Math.floor(Math.random() * 10);
+ measurement[r] += 1;
+ measurement[19] += 1;
+ }
+
+ await this.sendDAPMeasurement(task, measurement, timeout, reason);
+ }
+ }
+
+ async timedSendTestReports(tasks) {
+ lazy.logConsole.debug("Sending on timer.");
+ await this.sendTestReports(tasks, 30 * 1000, "periodic");
+ lazy.setTimeout(
+ () => this.timedSendTestReports(tasks),
+ this.timeout_value()
+ );
+ }
+
+ timeout_value() {
+ const MINUTE = 60 * 1000;
+ return MINUTE * (9 + Math.random() * 2); // 9 - 11 minutes
+ }
+
+ /**
+ * Creates a DAP report for a specific task from a measurement and sends it.
+ *
+ * @param {Task} task
+ * Definition of the task for which the measurement was taken.
+ * @param {number} measurement
+ * The measured value for which a report is generated.
+ */
+ async sendDAPMeasurement(task, measurement, timeout, reason) {
+ task.leader_endpoint = lazy.LEADER;
+ if (!task.leader_endpoint) {
+ lazy.logConsole.error('Preference "' + PREF_LEADER + '" not set');
+ return;
+ }
+
+ task.helper_endpoint = lazy.HELPER;
+ if (!task.helper_endpoint) {
+ lazy.logConsole.error('Preference "' + PREF_HELPER + '" not set');
+ return;
+ }
+
+ try {
+ const controller = new AbortController();
+ lazy.setTimeout(() => controller.abort(), timeout);
+ let report = await this.generateReport(
+ task,
+ measurement,
+ controller.signal
+ );
+ Glean.dap.reportGenerationStatus.success.add(1);
+ await this.sendReport(
+ task.leader_endpoint,
+ task.id,
+ report,
+ controller.signal,
+ reason
+ );
+ } catch (e) {
+ if (e.name === "AbortError") {
+ Glean.dap.reportGenerationStatus.abort.add(1);
+ lazy.logConsole.error("Aborted DAP report generation: ", e);
+ } else {
+ Glean.dap.reportGenerationStatus.failure.add(1);
+ lazy.logConsole.error("DAP report generation failed: " + e);
+ }
+ }
+ }
+
+ /**
+ * Downloads HPKE configs for endpoints and generates report.
+ *
+ * @param {Task} task
+ * Definition of the task for which the measurement was taken.
+ * @param {number} measurement
+ * The measured value for which a report is generated.
+ * @returns Promise
+ * @resolves {Uint8Array} The generated binary report data.
+ * @rejects {Error} If an exception is thrown while generating the report.
+ */
+ async generateReport(task, measurement, abortSignal) {
+ let [leader_config_bytes, helper_config_bytes] = await Promise.all([
+ this.getHpkeConfig(
+ task.leader_endpoint + "/hpke_config?task_id=" + task.id,
+ abortSignal
+ ),
+ this.getHpkeConfig(
+ task.helper_endpoint + "/hpke_config?task_id=" + task.id,
+ abortSignal
+ ),
+ ]);
+ if (leader_config_bytes == null) {
+ lazy.logConsole.error("HPKE config download failed for leader.");
+ Glean.dap.reportGenerationStatus.hpke_leader_fail.add(1);
+ }
+ if (helper_config_bytes == null) {
+ lazy.logConsole.error("HPKE config download failed for helper.");
+ Glean.dap.reportGenerationStatus.hpke_helper_fail.add(1);
+ }
+ if (abortSignal.aborted) {
+ throw new DOMException("HPKE config download was aborted", "AbortError");
+ }
+ if (leader_config_bytes === null || helper_config_bytes === null) {
+ throw new Error(`HPKE config download failed.`);
+ }
+
+ let task_id = new Uint8Array(
+ ChromeUtils.base64URLDecode(task.id, { padding: "ignore" })
+ );
+ let report = {};
+ if (task.measurement_type == "u8") {
+ Services.DAPTelemetry.GetReportU8(
+ leader_config_bytes,
+ helper_config_bytes,
+ measurement,
+ task_id,
+ task.time_precision,
+ report
+ );
+ } else if (task.measurement_type == "vecu8") {
+ Services.DAPTelemetry.GetReportVecU8(
+ leader_config_bytes,
+ helper_config_bytes,
+ measurement,
+ task_id,
+ task.time_precision,
+ report
+ );
+ } else if (task.measurement_type == "vecu16") {
+ Services.DAPTelemetry.GetReportVecU16(
+ leader_config_bytes,
+ helper_config_bytes,
+ measurement,
+ task_id,
+ task.time_precision,
+ report
+ );
+ } else {
+ throw new Error(
+ `Unknown measurement type for task ${task.id}: ${task.measurement_type}`
+ );
+ }
+ let reportData = new Uint8Array(report.value);
+ return reportData;
+ }
+
+ /**
+ * Fetches TLS encoded HPKE config from a URL.
+ *
+ * @param {string} endpoint
+ * The URL from where to get the data.
+ * @returns Promise
+ * @resolves {Uint8Array} The binary representation of the endpoint configuration.
+ * @rejects {Error} If an exception is thrown while fetching the configuration.
+ */
+ async getHpkeConfig(endpoint, abortSignal) {
+ // Use HPKEConfigManager to cache config for up to 24 hr. This reduces
+ // unecessary requests while limiting how long a stale config can be stuck
+ // if a server change is made ungracefully.
+ let buffer = await HPKEConfigManager.get(endpoint, {
+ maxAge: 24 * 60 * 60 * 1000,
+ abortSignal,
+ });
+ if (buffer === null) {
+ return null;
+ }
+ let hpke_config_bytes = new Uint8Array(buffer);
+ return hpke_config_bytes;
+ }
+
+ /**
+ * Sends a report to the leader.
+ *
+ * @param {string} leader_endpoint
+ * The URL for the leader.
+ * @param {Uint8Array} report
+ * Raw bytes of the TLS encoded report.
+ * @returns Promise
+ * @resolves {undefined} Once the attempt to send the report completes, whether or not it was successful.
+ */
+ async sendReport(leader_endpoint, task_id, report, abortSignal, reason) {
+ const upload_path = leader_endpoint + "/tasks/" + task_id + "/reports";
+ try {
+ let response = await fetch(upload_path, {
+ method: "PUT",
+ headers: { "Content-Type": "application/dap-report" },
+ body: report,
+ signal: abortSignal,
+ });
+
+ if (response.status != 200) {
+ if (response.status == 502) {
+ Glean.dap.uploadStatus.http_502.add(1);
+ } else {
+ Glean.dap.uploadStatus.http_error.add(1);
+ }
+ const content_type = response.headers.get("content-type");
+ if (content_type && content_type === "application/json") {
+ // A JSON error from the DAP server.
+ let error = await response.json();
+ lazy.logConsole.error(
+ `Sending failed. HTTP response: ${response.status} ${response.statusText}. Error: ${error.type} ${error.title}`
+ );
+ } else {
+ // A different error, e.g. from a load-balancer.
+ let error = await response.text();
+ lazy.logConsole.error(
+ `Sending failed. HTTP response: ${response.status} ${response.statusText}. Error: ${error}`
+ );
+ }
+ } else {
+ lazy.logConsole.debug("DAP report sent");
+ Glean.dap.uploadStatus.success.add(1);
+ }
+ } catch (err) {
+ if (err.name === "AbortError") {
+ lazy.logConsole.error("Aborted DAP report sending: ", err);
+ if (reason == "periodic") {
+ Glean.dap.uploadStatus.abort_timed.add(1);
+ } else if (reason == "shutdown") {
+ Glean.dap.uploadStatus.abort_shutdown.add(1);
+ } else {
+ Glean.dap.uploadStatus.abort.add(1);
+ }
+ } else {
+ lazy.logConsole.error("Failed to send report: ", err);
+ Glean.dap.uploadStatus.failure.add(1);
+ }
+ }
+ }
+})();
diff --git a/toolkit/components/telemetry/dap/DAPVisitCounter.sys.mjs b/toolkit/components/telemetry/dap/DAPVisitCounter.sys.mjs
new file mode 100644
index 0000000000..dfee5d0ff6
--- /dev/null
+++ b/toolkit/components/telemetry/dap/DAPVisitCounter.sys.mjs
@@ -0,0 +1,160 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { DAPTelemetrySender } from "./DAPTelemetrySender.sys.mjs";
+
+let lazy = {};
+
+ChromeUtils.defineLazyGetter(lazy, "logConsole", function () {
+ return console.createInstance({
+ prefix: "DAPVisitCounter",
+ maxLogLevelPref: "toolkit.telemetry.dap.logLevel",
+ });
+});
+ChromeUtils.defineESModuleGetters(lazy, {
+ AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
+ NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
+ PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+export const DAPVisitCounter = new (class {
+ startup() {
+ this._asyncShutdownBlocker = async () => {
+ lazy.logConsole.debug(`Sending on shutdown.`);
+ await this.send(2 * 1000, "shutdown");
+ };
+
+ lazy.AsyncShutdown.quitApplicationGranted.addBlocker(
+ "DAPVisitCounter: sending data",
+ this._asyncShutdownBlocker
+ );
+
+ const listener = events => {
+ // Even using the event.hidden flag there mayb be some double counting
+ // here. It would have to be fixed in the Places API.
+ for (const event of events) {
+ lazy.logConsole.debug(`Visited: ${event.url}`);
+ if (event.hidden) {
+ continue;
+ }
+ for (const counter of this.counters) {
+ for (const pattern of counter.patterns) {
+ if (pattern.matches(event.url)) {
+ lazy.logConsole.debug(`${pattern.pattern} matched!`);
+ counter.count += 1;
+ }
+ }
+ }
+ }
+ };
+
+ lazy.NimbusFeatures.dapTelemetry.onUpdate(async (event, reason) => {
+ if (typeof this.counters !== "undefined") {
+ await this.send(30 * 1000, "nimbus-update");
+ }
+ this.initialize_counters();
+ });
+
+ if (typeof this.counters === "undefined") {
+ this.initialize_counters();
+ }
+
+ lazy.PlacesUtils.observers.addListener(["page-visited"], listener);
+
+ lazy.setTimeout(() => this.timed_send(), this.timeout_value());
+ }
+
+ initialize_counters() {
+ let experiments = lazy.NimbusFeatures.dapTelemetry.getVariable(
+ "visitCountingExperimentList"
+ );
+
+ this.counters = [];
+ // This allows two different formats for distributing the URLs for the
+ // experiment. The experiments get quite large and over 4096 bytes they
+ // result in a warning (when mirrored in a pref as in this case).
+ if (Array.isArray(experiments)) {
+ for (const experiment of experiments) {
+ let counter = { experiment, count: 0, patterns: [] };
+ this.counters.push(counter);
+ for (const url of experiment.urls) {
+ let mpattern = new MatchPattern(url);
+ counter.patterns.push(mpattern);
+ }
+ }
+ } else {
+ for (const [task, urls] of Object.entries(experiments)) {
+ for (const [idx, url] of urls.entries()) {
+ const fullUrl = `*://${url}/*`;
+
+ this.counters.push({
+ experiment: {
+ task_id: task,
+ task_veclen: 20,
+ bucket: idx,
+ },
+ count: 0,
+ patterns: [new MatchPattern(fullUrl)],
+ });
+ }
+ }
+ }
+ }
+
+ async timed_send() {
+ lazy.logConsole.debug("Sending on timer.");
+ await this.send(30 * 1000, "periodic");
+ lazy.setTimeout(() => this.timed_send(), this.timeout_value());
+ }
+
+ timeout_value() {
+ const MINUTE = 60 * 1000;
+ return MINUTE * (9 + Math.random() * 2); // 9 - 11 minutes
+ }
+
+ async send(timeout, reason) {
+ let collected_measurements = new Map();
+ for (const counter of this.counters) {
+ if (!collected_measurements.has(counter.experiment.task_id)) {
+ collected_measurements.set(
+ counter.experiment.task_id,
+ new Uint8Array(counter.experiment.task_veclen)
+ );
+ }
+ collected_measurements.get(counter.experiment.task_id)[
+ counter.experiment.bucket
+ ] = counter.count;
+ counter.count = 0;
+ }
+
+ let send_promises = [];
+ for (const [task_id, measurement] of collected_measurements) {
+ let task = {
+ id: task_id,
+ time_precision: 60,
+ measurement_type: "vecu8",
+ };
+
+ send_promises.push(
+ DAPTelemetrySender.sendDAPMeasurement(
+ task,
+ measurement,
+ timeout,
+ reason
+ )
+ );
+ }
+ await Promise.all(send_promises);
+ }
+
+ show() {
+ for (const counter of this.counters) {
+ lazy.logConsole.info(
+ `Experiment: ${counter.experiment.url} -> ${counter.count}`
+ );
+ }
+ return this.counters;
+ }
+})();
diff --git a/toolkit/components/telemetry/dap/components.conf b/toolkit/components/telemetry/dap/components.conf
new file mode 100644
index 0000000000..237868bcee
--- /dev/null
+++ b/toolkit/components/telemetry/dap/components.conf
@@ -0,0 +1,10 @@
+Classes = [
+ {
+ 'cid': '{58a4c579-d2dd-46b7-9c3b-6881a1c36c6a}',
+ 'interfaces': ['nsIDAPTelemetry'],
+ 'contract_ids': ['@mozilla.org/base/daptelemetry;1'],
+ 'type': 'mozilla::DAPTelemetry',
+ 'headers': ['mozilla/DAPTelemetry.h'],
+ 'js_name': 'DAPTelemetry',
+ },
+]
diff --git a/toolkit/components/telemetry/dap/ffi-gtest/Cargo.toml b/toolkit/components/telemetry/dap/ffi-gtest/Cargo.toml
new file mode 100644
index 0000000000..abad140b7d
--- /dev/null
+++ b/toolkit/components/telemetry/dap/ffi-gtest/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "dap_ffi-gtest"
+version = "0.1.0"
+authors = [
+ "Simon Friedberger <simon@mozilla.com>",
+]
+license = "MPL-2.0"
+description = "Tests for Rust code for DAP; mainly encoding and NSS bindings."
+edition = "2021"
+
+[dependencies]
+dap_ffi = { path = "../ffi" }
+hex = { version = "0.4.3", features = ["serde"] }
+prio = {version = "0.15.3", default-features = false }
+serde = { version = "1.0", features = ["derive"] }
+serde_json = { version = "1.0" }
+thin-vec = { version = "0.2.1", features = ["gecko-ffi"] }
+
+[lib]
+path = "test.rs"
diff --git a/toolkit/components/telemetry/dap/ffi-gtest/TestDAPTelemetry.cpp b/toolkit/components/telemetry/dap/ffi-gtest/TestDAPTelemetry.cpp
new file mode 100644
index 0000000000..80821cd118
--- /dev/null
+++ b/toolkit/components/telemetry/dap/ffi-gtest/TestDAPTelemetry.cpp
@@ -0,0 +1,19 @@
+
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "gtest/gtest.h"
+#include "mozilla/DAPTelemetryBindings.h"
+
+using namespace mozilla;
+
+extern "C" void dap_test_hpke_encrypt();
+TEST(DAPTelemetryTests, TestHpkeEnc)
+{ dap_test_hpke_encrypt(); }
+
+extern "C" void dap_test_encoding();
+TEST(DAPTelemetryTests, TestReportSerialization)
+{ dap_test_encoding(); }
diff --git a/toolkit/components/telemetry/dap/ffi-gtest/moz.build b/toolkit/components/telemetry/dap/ffi-gtest/moz.build
new file mode 100644
index 0000000000..d809ebc418
--- /dev/null
+++ b/toolkit/components/telemetry/dap/ffi-gtest/moz.build
@@ -0,0 +1,6 @@
+UNIFIED_SOURCES = ["TestDAPTelemetry.cpp"]
+FINAL_LIBRARY = "xul-gtest"
+
+TEST_HARNESS_FILES.gtest += [
+ "../../../../../security/nss/gtests/pk11_gtest/hpke-vectors.json",
+]
diff --git a/toolkit/components/telemetry/dap/ffi-gtest/test.rs b/toolkit/components/telemetry/dap/ffi-gtest/test.rs
new file mode 100644
index 0000000000..3871965107
--- /dev/null
+++ b/toolkit/components/telemetry/dap/ffi-gtest/test.rs
@@ -0,0 +1,204 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+use serde::Deserialize;
+use std::ffi::c_void;
+use std::fs::File;
+use std::io::Cursor;
+
+use thin_vec::ThinVec;
+
+use dap_ffi::types::Report;
+
+use prio::codec::{Decode, Encode};
+
+#[no_mangle]
+pub extern "C" fn dap_test_encoding() {
+ let r = Report::new_dummy();
+ let mut encoded = Vec::<u8>::new();
+ Report::encode(&r, &mut encoded);
+ let decoded = Report::decode(&mut Cursor::new(&encoded)).expect("Report decoding failed!");
+ if r != decoded {
+ println!("Report:");
+ println!("{:?}", r);
+ println!("Encoded Report:");
+ println!("{:?}", encoded);
+ println!("Decoded Report:");
+ println!("{:?}", decoded);
+ panic!("Report changed after encoding & decoding.");
+ }
+}
+
+extern "C" {
+ pub fn dapHpkeEncrypt(
+ aContext: *mut c_void,
+ aAad: *mut u8,
+ aAadLength: u32,
+ aPlaintext: *mut u8,
+ aPlaintextLength: u32,
+ aOutputShare: &mut ThinVec<u8>,
+ ) -> bool;
+ pub fn dapSetupHpkeContextForTesting(
+ aKey: *const u8,
+ aKeyLength: u32,
+ aInfo: *mut u8,
+ aInfoLength: u32,
+ aPkEm: *const u8,
+ aPkEmLength: u32,
+ aSkEm: *const u8,
+ aSkEmLength: u32,
+ aOutputEncapsulatedKey: &mut ThinVec<u8>,
+ ) -> *mut c_void;
+ pub fn dapDestroyHpkeContext(aContext: *mut c_void);
+}
+
+struct HpkeContext(*mut c_void);
+
+impl Drop for HpkeContext {
+ fn drop(&mut self) {
+ unsafe {
+ dapDestroyHpkeContext(self.0);
+ }
+ }
+}
+
+type Testsuites = Vec<CiphersuiteTest>;
+
+#[derive(Debug, Deserialize)]
+pub struct HexString(#[serde(with = "hex")] Vec<u8>);
+impl AsRef<[u8]> for HexString {
+ fn as_ref(&self) -> &[u8] {
+ &self.0
+ }
+}
+#[allow(dead_code)]
+#[derive(Debug, Deserialize)]
+struct CiphersuiteTest {
+ mode: i64,
+ kem_id: i64,
+ kdf_id: i64,
+ aead_id: i64,
+ info: HexString,
+ #[serde(rename = "ikmR")]
+ ikm_r: HexString,
+ #[serde(rename = "ikmE")]
+ ikm_e: HexString,
+ #[serde(rename = "skRm")]
+ sk_r_m: HexString,
+ #[serde(rename = "skEm")]
+ sk_e_m: HexString,
+ #[serde(rename = "pkRm")]
+ pk_r_m: HexString,
+ #[serde(rename = "pkEm")]
+ pk_e_m: HexString,
+ enc: HexString,
+ shared_secret: HexString,
+ key_schedule_context: HexString,
+ secret: HexString,
+ key: HexString,
+ base_nonce: HexString,
+ exporter_secret: HexString,
+ encryptions: Vec<Encryption>,
+ exports: Vec<Export>,
+ psk: Option<HexString>,
+ psk_id: Option<HexString>,
+ ikm_s: Option<HexString>,
+ sk_sm: Option<HexString>,
+ pk_sm: Option<HexString>,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct Encryption {
+ pub aad: HexString,
+ pub ciphertext: HexString,
+ pub nonce: HexString,
+ pub plaintext: HexString,
+}
+
+#[derive(Debug, Deserialize)]
+pub struct Export {
+ pub exporter_context: HexString,
+ #[serde(rename = "L")]
+ pub length: i64,
+ pub exported_value: HexString,
+}
+
+#[no_mangle]
+pub extern "C" fn dap_test_hpke_encrypt() {
+ let file = File::open("hpke-vectors.json").unwrap();
+ let tests: Testsuites = serde_json::from_reader(file).unwrap();
+
+ let mut have_tested = false;
+
+ for (test_idx, test) in tests.into_iter().enumerate() {
+ // Mode must be "Base"
+ if test.mode != 0
+ // KEM must be DHKEM(X25519, HKDF-SHA256)
+ || test.kem_id != 32
+ // KDF must be HKDF-SHA256
+ || test.kdf_id != 1
+ // AEAD must be AES-128-GCM
+ || test.aead_id != 1
+ {
+ continue;
+ }
+
+ have_tested = true;
+
+ let mut pk_r_serialized = test.pk_r_m.0;
+ let mut info = test.info.0;
+ let mut pk_e_serialized = test.pk_e_m.0;
+ let mut sk_e_serialized = test.sk_e_m.0;
+
+ let mut encapsulated_key = ThinVec::<u8>::new();
+
+ let ctx = HpkeContext(unsafe {
+ dapSetupHpkeContextForTesting(
+ pk_r_serialized.as_mut_ptr(),
+ pk_r_serialized.len().try_into().unwrap(),
+ info.as_mut_ptr(),
+ info.len().try_into().unwrap(),
+ pk_e_serialized.as_mut_ptr(),
+ pk_e_serialized.len().try_into().unwrap(),
+ sk_e_serialized.as_mut_ptr(),
+ sk_e_serialized.len().try_into().unwrap(),
+ &mut encapsulated_key,
+ )
+ });
+ if ctx.0.is_null() {
+ panic!("Failed to set up HPKE context.");
+ }
+ if encapsulated_key != test.enc.0 {
+ panic!("Encapsulated key is wrong!");
+ }
+
+ for (encryption_idx, encryption) in test.encryptions.into_iter().enumerate() {
+ let mut encrypted_share = ThinVec::<u8>::new();
+
+ let mut aad = encryption.aad.0.clone();
+ let mut pt = encryption.plaintext.0.clone();
+ unsafe {
+ dapHpkeEncrypt(
+ ctx.0,
+ aad.as_mut_ptr(),
+ aad.len().try_into().unwrap(),
+ pt.as_mut_ptr(),
+ pt.len().try_into().unwrap(),
+ &mut encrypted_share,
+ );
+ }
+
+ if encrypted_share != encryption.ciphertext.0 {
+ println!("Test: {}, Encryption: {}", test_idx, encryption_idx);
+ println!("Expected:");
+ println!("{:?}", encryption.ciphertext.0);
+ println!("Actual:");
+ println!("{:?}", encrypted_share);
+ panic!("Encryption outputs did not match!");
+ }
+ }
+ }
+
+ assert!(have_tested);
+}
diff --git a/toolkit/components/telemetry/dap/ffi/Cargo.toml b/toolkit/components/telemetry/dap/ffi/Cargo.toml
new file mode 100644
index 0000000000..dfb69e4821
--- /dev/null
+++ b/toolkit/components/telemetry/dap/ffi/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "dap_ffi"
+version = "0.1.0"
+edition = "2021"
+authors = [
+ "Simon Friedberger <simon@mozilla.com>",
+]
+license = "MPL-2.0"
+
+[dependencies]
+prio = {version = "0.15.3", default-features = false }
+thin-vec = { version = "0.2.1", features = ["gecko-ffi"] }
+rand = "0.8"
diff --git a/toolkit/components/telemetry/dap/ffi/cbindgen.toml b/toolkit/components/telemetry/dap/ffi/cbindgen.toml
new file mode 100644
index 0000000000..e2c032133a
--- /dev/null
+++ b/toolkit/components/telemetry/dap/ffi/cbindgen.toml
@@ -0,0 +1,11 @@
+header = """/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */"""
+autogen_warning = """/* DO NOT MODIFY THIS MANUALLY! This file was generated using cbindgen. See RunCbindgen.py */
+#ifndef DAPTelemetryBindings_h
+#error "Don't include this file directly, instead include DAPTelemetryBindings.h"
+#endif
+"""
+
+[export.rename]
+"ThinVec" = "nsTArray"
diff --git a/toolkit/components/telemetry/dap/ffi/src/lib.rs b/toolkit/components/telemetry/dap/ffi/src/lib.rs
new file mode 100644
index 0000000000..998c8af204
--- /dev/null
+++ b/toolkit/components/telemetry/dap/ffi/src/lib.rs
@@ -0,0 +1,335 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+use std::error::Error;
+use std::io::Cursor;
+
+use prio::vdaf::prio3::Prio3Sum;
+use prio::vdaf::prio3::Prio3SumVec;
+use thin_vec::ThinVec;
+
+pub mod types;
+use types::HpkeConfig;
+use types::PlaintextInputShare;
+use types::Report;
+use types::ReportID;
+use types::ReportMetadata;
+use types::Time;
+
+use prio::codec::Encode;
+use prio::codec::{decode_u16_items, encode_u32_items};
+use prio::flp::types::{Sum, SumVec};
+use prio::vdaf::prio3::Prio3;
+use prio::vdaf::Client;
+use prio::vdaf::VdafError;
+
+use crate::types::HpkeCiphertext;
+
+extern "C" {
+ pub fn dapHpkeEncryptOneshot(
+ aKey: *const u8,
+ aKeyLength: u32,
+ aInfo: *const u8,
+ aInfoLength: u32,
+ aAad: *const u8,
+ aAadLength: u32,
+ aPlaintext: *const u8,
+ aPlaintextLength: u32,
+ aOutputEncapsulatedKey: &mut ThinVec<u8>,
+ aOutputShare: &mut ThinVec<u8>,
+ ) -> bool;
+}
+
+pub fn new_prio_u8(num_aggregators: u8, bits: u32) -> Result<Prio3Sum, VdafError> {
+ if bits > 64 {
+ return Err(VdafError::Uncategorized(format!(
+ "bit length ({}) exceeds limit for aggregate type (64)",
+ bits
+ )));
+ }
+
+ Prio3::new(num_aggregators, Sum::new(bits as usize)?)
+}
+
+pub fn new_prio_vecu8(num_aggregators: u8, len: usize) -> Result<Prio3SumVec, VdafError> {
+ let chunk_length = prio::vdaf::prio3::optimal_chunk_length(8 * len);
+ Prio3::new(num_aggregators, SumVec::new(8, len, chunk_length)?)
+}
+
+pub fn new_prio_vecu16(num_aggregators: u8, len: usize) -> Result<Prio3SumVec, VdafError> {
+ let chunk_length = prio::vdaf::prio3::optimal_chunk_length(16 * len);
+ Prio3::new(num_aggregators, SumVec::new(16, len, chunk_length)?)
+}
+
+enum Role {
+ Leader = 2,
+ Helper = 3,
+}
+
+/// A minimal wrapper around the FFI function which mostly just converts datatypes.
+fn hpke_encrypt_wrapper(
+ plain_share: &Vec<u8>,
+ aad: &Vec<u8>,
+ info: &Vec<u8>,
+ hpke_config: &HpkeConfig,
+) -> Result<HpkeCiphertext, Box<dyn std::error::Error>> {
+ let mut encrypted_share = ThinVec::<u8>::new();
+ let mut encapsulated_key = ThinVec::<u8>::new();
+ unsafe {
+ if !dapHpkeEncryptOneshot(
+ hpke_config.public_key.as_ptr(),
+ hpke_config.public_key.len() as u32,
+ info.as_ptr(),
+ info.len() as u32,
+ aad.as_ptr(),
+ aad.len() as u32,
+ plain_share.as_ptr(),
+ plain_share.len() as u32,
+ &mut encapsulated_key,
+ &mut encrypted_share,
+ ) {
+ return Err(Box::from("Encryption failed."));
+ }
+ }
+
+ Ok(HpkeCiphertext {
+ config_id: hpke_config.id,
+ enc: encapsulated_key.to_vec(),
+ payload: encrypted_share.to_vec(),
+ })
+}
+
+trait Shardable {
+ fn shard(
+ &self,
+ nonce: &[u8; 16],
+ ) -> Result<(Vec<u8>, Vec<Vec<u8>>), Box<dyn std::error::Error>>;
+}
+
+impl Shardable for u8 {
+ fn shard(
+ &self,
+ nonce: &[u8; 16],
+ ) -> Result<(Vec<u8>, Vec<Vec<u8>>), Box<dyn std::error::Error>> {
+ let prio = new_prio_u8(2, 2)?;
+
+ let (public_share, input_shares) = prio.shard(&(*self as u128), nonce)?;
+
+ debug_assert_eq!(input_shares.len(), 2);
+
+ let encoded_input_shares = input_shares.iter().map(|s| s.get_encoded()).collect();
+ let encoded_public_share = public_share.get_encoded();
+ Ok((encoded_public_share, encoded_input_shares))
+ }
+}
+
+impl Shardable for ThinVec<u8> {
+ fn shard(
+ &self,
+ nonce: &[u8; 16],
+ ) -> Result<(Vec<u8>, Vec<Vec<u8>>), Box<dyn std::error::Error>> {
+ let prio = new_prio_vecu8(2, self.len())?;
+
+ let measurement: Vec<u128> = self.iter().map(|e| (*e as u128)).collect();
+ let (public_share, input_shares) = prio.shard(&measurement, nonce)?;
+
+ debug_assert_eq!(input_shares.len(), 2);
+
+ let encoded_input_shares = input_shares.iter().map(|s| s.get_encoded()).collect();
+ let encoded_public_share = public_share.get_encoded();
+ Ok((encoded_public_share, encoded_input_shares))
+ }
+}
+
+impl Shardable for ThinVec<u16> {
+ fn shard(
+ &self,
+ nonce: &[u8; 16],
+ ) -> Result<(Vec<u8>, Vec<Vec<u8>>), Box<dyn std::error::Error>> {
+ let prio = new_prio_vecu16(2, self.len())?;
+
+ let measurement: Vec<u128> = self.iter().map(|e| (*e as u128)).collect();
+ let (public_share, input_shares) = prio.shard(&measurement, nonce)?;
+
+ debug_assert_eq!(input_shares.len(), 2);
+
+ let encoded_input_shares = input_shares.iter().map(|s| s.get_encoded()).collect();
+ let encoded_public_share = public_share.get_encoded();
+ Ok((encoded_public_share, encoded_input_shares))
+ }
+}
+
+/// Pre-fill the info part of the HPKE sealing with the constants from the standard.
+fn make_base_info() -> Vec<u8> {
+ let mut info = Vec::<u8>::new();
+ const START: &[u8] = "dap-07 input share".as_bytes();
+ info.extend(START);
+ const FIXED: u8 = 1;
+ info.push(FIXED);
+
+ info
+}
+
+fn select_hpke_config(configs: Vec<HpkeConfig>) -> Result<HpkeConfig, Box<dyn Error>> {
+ for config in configs {
+ if config.kem_id == 0x20 /* DHKEM(X25519, HKDF-SHA256) */ &&
+ config.kdf_id == 0x01 /* HKDF-SHA256 */ &&
+ config.aead_id == 0x01
+ /* AES-128-GCM */
+ {
+ return Ok(config);
+ }
+ }
+
+ Err("No suitable HPKE config found.".into())
+}
+
+/// This function creates a full report - ready to send - for a measurement.
+///
+/// To do that it also needs the HPKE configurations for the endpoints and some
+/// additional data which is part of the authentication.
+fn get_dap_report_internal<T: Shardable>(
+ leader_hpke_config_encoded: &ThinVec<u8>,
+ helper_hpke_config_encoded: &ThinVec<u8>,
+ measurement: &T,
+ task_id: &[u8; 32],
+ time_precision: u64,
+) -> Result<Report, Box<dyn std::error::Error>> {
+ let leader_hpke_configs: Vec<HpkeConfig> =
+ decode_u16_items(&(), &mut Cursor::new(leader_hpke_config_encoded))?;
+ let leader_hpke_config = select_hpke_config(leader_hpke_configs)?;
+ let helper_hpke_configs: Vec<HpkeConfig> =
+ decode_u16_items(&(), &mut Cursor::new(helper_hpke_config_encoded))?;
+ let helper_hpke_config = select_hpke_config(helper_hpke_configs)?;
+
+ let report_id = ReportID::generate();
+ let (encoded_public_share, encoded_input_shares) = measurement.shard(report_id.as_ref())?;
+
+ let plaintext_input_shares: Vec<Vec<u8>> = encoded_input_shares
+ .into_iter()
+ .map(|encoded_input_share| {
+ PlaintextInputShare {
+ extensions: Vec::new(),
+ payload: encoded_input_share,
+ }
+ .get_encoded()
+ })
+ .collect();
+
+ let metadata = ReportMetadata {
+ report_id,
+ time: Time::generate(time_precision),
+ };
+
+ // This quote from the standard describes which info and aad to use for the encryption:
+ // enc, payload = SealBase(pk,
+ // "dap-02 input share" || 0x01 || server_role,
+ // task_id || metadata || public_share, input_share)
+ // https://www.ietf.org/archive/id/draft-ietf-ppm-dap-02.html#name-upload-request
+ let mut info = make_base_info();
+
+ let mut aad = Vec::from(*task_id);
+ metadata.encode(&mut aad);
+ encode_u32_items(&mut aad, &(), &encoded_public_share);
+
+ info.push(Role::Leader as u8);
+
+ let leader_payload =
+ hpke_encrypt_wrapper(&plaintext_input_shares[0], &aad, &info, &leader_hpke_config)?;
+
+ *info.last_mut().unwrap() = Role::Helper as u8;
+
+ let helper_payload =
+ hpke_encrypt_wrapper(&plaintext_input_shares[1], &aad, &info, &helper_hpke_config)?;
+
+ Ok(Report {
+ metadata,
+ public_share: encoded_public_share,
+ leader_encrypted_input_share: leader_payload,
+ helper_encrypted_input_share: helper_payload,
+ })
+}
+
+/// Wraps the function above with minor C interop.
+/// Mostly it turns any error result into a return value of false.
+#[no_mangle]
+pub extern "C" fn dapGetReportU8(
+ leader_hpke_config_encoded: &ThinVec<u8>,
+ helper_hpke_config_encoded: &ThinVec<u8>,
+ measurement: u8,
+ task_id: &ThinVec<u8>,
+ time_precision: u64,
+ out_report: &mut ThinVec<u8>,
+) -> bool {
+ assert_eq!(task_id.len(), 32);
+
+ if let Ok(report) = get_dap_report_internal::<u8>(
+ leader_hpke_config_encoded,
+ helper_hpke_config_encoded,
+ &measurement,
+ &task_id.as_slice().try_into().unwrap(),
+ time_precision,
+ ) {
+ let encoded_report = report.get_encoded();
+ out_report.extend(encoded_report);
+
+ true
+ } else {
+ false
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn dapGetReportVecU8(
+ leader_hpke_config_encoded: &ThinVec<u8>,
+ helper_hpke_config_encoded: &ThinVec<u8>,
+ measurement: &ThinVec<u8>,
+ task_id: &ThinVec<u8>,
+ time_precision: u64,
+ out_report: &mut ThinVec<u8>,
+) -> bool {
+ assert_eq!(task_id.len(), 32);
+
+ if let Ok(report) = get_dap_report_internal::<ThinVec<u8>>(
+ leader_hpke_config_encoded,
+ helper_hpke_config_encoded,
+ measurement,
+ &task_id.as_slice().try_into().unwrap(),
+ time_precision,
+ ) {
+ let encoded_report = report.get_encoded();
+ out_report.extend(encoded_report);
+
+ true
+ } else {
+ false
+ }
+}
+
+#[no_mangle]
+pub extern "C" fn dapGetReportVecU16(
+ leader_hpke_config_encoded: &ThinVec<u8>,
+ helper_hpke_config_encoded: &ThinVec<u8>,
+ measurement: &ThinVec<u16>,
+ task_id: &ThinVec<u8>,
+ time_precision: u64,
+ out_report: &mut ThinVec<u8>,
+) -> bool {
+ assert_eq!(task_id.len(), 32);
+
+ if let Ok(report) = get_dap_report_internal::<ThinVec<u16>>(
+ leader_hpke_config_encoded,
+ helper_hpke_config_encoded,
+ measurement,
+ &task_id.as_slice().try_into().unwrap(),
+ time_precision,
+ ) {
+ let encoded_report = report.get_encoded();
+ out_report.extend(encoded_report);
+
+ true
+ } else {
+ false
+ }
+}
diff --git a/toolkit/components/telemetry/dap/ffi/src/types.rs b/toolkit/components/telemetry/dap/ffi/src/types.rs
new file mode 100644
index 0000000000..e8f6385dcd
--- /dev/null
+++ b/toolkit/components/telemetry/dap/ffi/src/types.rs
@@ -0,0 +1,358 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+//! This file contains structs for use in the DAP protocol and implements TLS compatible
+//! serialization/deserialization as required for the wire protocol.
+//!
+//! The current draft standard with the definition of these structs is available here:
+//! https://github.com/ietf-wg-ppm/draft-ietf-ppm-dap
+//! This code is based on version 07 of the standard available here:
+//! https://www.ietf.org/archive/id/draft-ietf-ppm-dap-07.html
+
+use prio::codec::{
+ decode_u16_items, decode_u32_items, encode_u16_items, encode_u32_items, CodecError, Decode,
+ Encode,
+};
+use std::io::{Cursor, Read};
+use std::time::{SystemTime, UNIX_EPOCH};
+
+use rand::{Rng, RngCore};
+
+/// opaque TaskId[32];
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-07.html#name-task-configuration
+#[derive(Debug, PartialEq, Eq)]
+pub struct TaskID(pub [u8; 32]);
+
+impl Decode for TaskID {
+ fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> {
+ // this should probably be available in codec...?
+ let mut data: [u8; 32] = [0; 32];
+ bytes.read_exact(&mut data)?;
+ Ok(TaskID(data))
+ }
+}
+
+impl Encode for TaskID {
+ fn encode(&self, bytes: &mut Vec<u8>) {
+ bytes.extend_from_slice(&self.0);
+ }
+}
+
+/// uint64 Time;
+/// seconds elapsed since start of UNIX epoch
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-07.html#name-protocol-definition
+#[derive(Debug, PartialEq, Eq)]
+pub struct Time(pub u64);
+
+impl Decode for Time {
+ fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> {
+ Ok(Time(u64::decode(bytes)?))
+ }
+}
+
+impl Encode for Time {
+ fn encode(&self, bytes: &mut Vec<u8>) {
+ u64::encode(&self.0, bytes);
+ }
+}
+
+impl Time {
+ /// Generates a Time for the current system time rounded to the desired precision.
+ pub fn generate(time_precision: u64) -> Time {
+ let now_secs = SystemTime::now()
+ .duration_since(UNIX_EPOCH)
+ .expect("Failed to get time.")
+ .as_secs();
+ let timestamp = (now_secs / time_precision) * time_precision;
+ Time(timestamp)
+ }
+}
+
+/// struct {
+/// ExtensionType extension_type;
+/// opaque extension_data<0..2^16-1>;
+/// } Extension;
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-07.html#name-upload-extensions
+#[derive(Debug, PartialEq)]
+pub struct Extension {
+ extension_type: ExtensionType,
+ extension_data: Vec<u8>,
+}
+
+impl Decode for Extension {
+ fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> {
+ let extension_type = ExtensionType::from_u16(u16::decode(bytes)?);
+ let extension_data: Vec<u8> = decode_u16_items(&(), bytes)?;
+
+ Ok(Extension {
+ extension_type,
+ extension_data,
+ })
+ }
+}
+
+impl Encode for Extension {
+ fn encode(&self, bytes: &mut Vec<u8>) {
+ (self.extension_type as u16).encode(bytes);
+ encode_u16_items(bytes, &(), &self.extension_data);
+ }
+}
+
+/// enum {
+/// TBD(0),
+/// (65535)
+/// } ExtensionType;
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-07.html#name-upload-extensions
+#[derive(Debug, PartialEq, Clone, Copy)]
+#[repr(u16)]
+enum ExtensionType {
+ Tbd = 0,
+}
+
+impl ExtensionType {
+ fn from_u16(value: u16) -> ExtensionType {
+ match value {
+ 0 => ExtensionType::Tbd,
+ _ => panic!("Unknown value for Extension Type: {}", value),
+ }
+ }
+}
+
+/// struct {
+/// Extension extensions<0..2^16-1>;
+/// opaque payload<0..2^32-1>;
+/// } PlaintextInputShare;
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-07.html#section-4.4.2-9
+#[derive(Debug)]
+pub struct PlaintextInputShare {
+ pub extensions: Vec<Extension>,
+ pub payload: Vec<u8>,
+}
+
+impl Encode for PlaintextInputShare {
+ fn encode(&self, bytes: &mut Vec<u8>) {
+ encode_u16_items(bytes, &(), &self.extensions);
+ encode_u32_items(bytes, &(), &self.payload);
+ }
+}
+
+/// Identifier for a server's HPKE configuration
+/// uint8 HpkeConfigId;
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-07.html#name-protocol-definition
+#[derive(Debug, PartialEq, Eq, Copy, Clone)]
+pub struct HpkeConfigId(u8);
+
+impl Decode for HpkeConfigId {
+ fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> {
+ Ok(HpkeConfigId(u8::decode(bytes)?))
+ }
+}
+
+impl Encode for HpkeConfigId {
+ fn encode(&self, bytes: &mut Vec<u8>) {
+ self.0.encode(bytes);
+ }
+}
+
+/// struct {
+/// HpkeConfigId id;
+/// HpkeKemId kem_id;
+/// HpkeKdfId kdf_id;
+/// HpkeAeadId aead_id;
+/// HpkePublicKey public_key;
+/// } HpkeConfig;
+/// opaque HpkePublicKey<1..2^16-1>;
+/// uint16 HpkeAeadId; /* Defined in [HPKE] */
+/// uint16 HpkeKemId; /* Defined in [HPKE] */
+/// uint16 HpkeKdfId; /* Defined in [HPKE] */
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-07.html#name-hpke-configuration-request
+#[derive(Debug, Clone)]
+pub struct HpkeConfig {
+ pub id: HpkeConfigId,
+ pub kem_id: u16,
+ pub kdf_id: u16,
+ pub aead_id: u16,
+ pub public_key: Vec<u8>,
+}
+
+impl Decode for HpkeConfig {
+ fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> {
+ Ok(HpkeConfig {
+ id: HpkeConfigId::decode(bytes)?,
+ kem_id: u16::decode(bytes)?,
+ kdf_id: u16::decode(bytes)?,
+ aead_id: u16::decode(bytes)?,
+ public_key: decode_u16_items(&(), bytes)?,
+ })
+ }
+}
+
+impl Encode for HpkeConfig {
+ fn encode(&self, bytes: &mut Vec<u8>) {
+ self.id.encode(bytes);
+ self.kem_id.encode(bytes);
+ self.kdf_id.encode(bytes);
+ self.aead_id.encode(bytes);
+ encode_u16_items(bytes, &(), &self.public_key);
+ }
+}
+
+/// An HPKE ciphertext.
+/// struct {
+/// HpkeConfigId config_id; /* config ID */
+/// opaque enc<1..2^16-1>; /* encapsulated HPKE key */
+/// opaque payload<1..2^32-1>; /* ciphertext */
+/// } HpkeCiphertext;
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-07.html#name-protocol-definition
+#[derive(Debug, PartialEq, Eq)]
+pub struct HpkeCiphertext {
+ pub config_id: HpkeConfigId,
+ pub enc: Vec<u8>,
+ pub payload: Vec<u8>,
+}
+
+impl Decode for HpkeCiphertext {
+ fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> {
+ let config_id = HpkeConfigId::decode(bytes)?;
+ let enc: Vec<u8> = decode_u16_items(&(), bytes)?;
+ let payload: Vec<u8> = decode_u32_items(&(), bytes)?;
+
+ Ok(HpkeCiphertext {
+ config_id,
+ enc,
+ payload,
+ })
+ }
+}
+
+impl Encode for HpkeCiphertext {
+ fn encode(&self, bytes: &mut Vec<u8>) {
+ self.config_id.encode(bytes);
+ encode_u16_items(bytes, &(), &self.enc);
+ encode_u32_items(bytes, &(), &self.payload);
+ }
+}
+
+/// opaque ReportID[16];
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-07.html#name-protocol-definition
+#[derive(Debug, PartialEq, Eq)]
+pub struct ReportID(pub [u8; 16]);
+
+impl Decode for ReportID {
+ fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> {
+ let mut data: [u8; 16] = [0; 16];
+ bytes.read_exact(&mut data)?;
+ Ok(ReportID(data))
+ }
+}
+
+impl Encode for ReportID {
+ fn encode(&self, bytes: &mut Vec<u8>) {
+ bytes.extend_from_slice(&self.0);
+ }
+}
+
+impl ReportID {
+ pub fn generate() -> ReportID {
+ ReportID(rand::thread_rng().gen())
+ }
+}
+
+impl AsRef<[u8; 16]> for ReportID {
+ fn as_ref(&self) -> &[u8; 16] {
+ &self.0
+ }
+}
+
+/// struct {
+/// ReportID report_id;
+/// Time time;
+/// } ReportMetadata;
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-07.html#name-upload-request
+#[derive(Debug, PartialEq)]
+pub struct ReportMetadata {
+ pub report_id: ReportID,
+ pub time: Time,
+}
+
+impl Decode for ReportMetadata {
+ fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> {
+ let report_id = ReportID::decode(bytes)?;
+ let time = Time::decode(bytes)?;
+
+ Ok(ReportMetadata { report_id, time })
+ }
+}
+
+impl Encode for ReportMetadata {
+ fn encode(&self, bytes: &mut Vec<u8>) {
+ self.report_id.encode(bytes);
+ self.time.encode(bytes);
+ }
+}
+
+/// struct {
+/// ReportMetadata metadata;
+/// opaque public_share<0..2^32-1>;
+/// HpkeCiphertext leader_encrypted_input_share;
+/// HpkeCiphertext helper_encrypted_input_share;
+/// } Report;
+/// https://www.ietf.org/archive/id/draft-ietf-ppm-dap-07.html#name-upload-request
+#[derive(Debug, PartialEq)]
+pub struct Report {
+ pub metadata: ReportMetadata,
+ pub public_share: Vec<u8>,
+ pub leader_encrypted_input_share: HpkeCiphertext,
+ pub helper_encrypted_input_share: HpkeCiphertext,
+}
+
+
+impl Report {
+ /// Creates a minimal report for use in tests.
+ pub fn new_dummy() -> Self {
+ let mut enc = [0u8; 32];
+ rand::thread_rng().fill_bytes(&mut enc);
+ let mut payload = [0u8; 200];
+ rand::thread_rng().fill_bytes(&mut payload);
+ Report {
+ metadata: ReportMetadata {
+ report_id: ReportID::generate(),
+ time: Time::generate(1),
+ },
+ public_share: vec![],
+ leader_encrypted_input_share: HpkeCiphertext { config_id: HpkeConfigId(5), enc: vec![1, 2, 3, 4, 5], payload: vec![6, 7, 8, 9, 10] },
+ helper_encrypted_input_share: HpkeCiphertext { config_id: HpkeConfigId(100), enc: enc.into(), payload: payload.into() },
+ }
+ }
+}
+
+impl Decode for Report {
+ fn decode(bytes: &mut Cursor<&[u8]>) -> Result<Self, CodecError> {
+ let metadata = ReportMetadata::decode(bytes)?;
+ let public_share: Vec<u8> = decode_u32_items(&(), bytes)?;
+ let leader_encrypted_input_share: HpkeCiphertext = HpkeCiphertext::decode(bytes)?;
+ let helper_encrypted_input_share: HpkeCiphertext = HpkeCiphertext::decode(bytes)?;
+
+ let remaining_bytes = bytes.get_ref().len() - (bytes.position() as usize);
+ if remaining_bytes == 0 {
+ Ok(Report {
+ metadata,
+ public_share,
+ leader_encrypted_input_share,
+ helper_encrypted_input_share,
+ })
+ } else {
+ Err(CodecError::BytesLeftOver(remaining_bytes))
+ }
+ }
+}
+
+impl Encode for Report {
+ fn encode(&self, bytes: &mut Vec<u8>) {
+ self.metadata.encode(bytes);
+ encode_u32_items(bytes, &(), &self.public_share);
+ self.leader_encrypted_input_share.encode(bytes);
+ self.helper_encrypted_input_share.encode(bytes);
+ }
+}
diff --git a/toolkit/components/telemetry/dap/metrics.yaml b/toolkit/components/telemetry/dap/metrics.yaml
new file mode 100644
index 0000000000..6ab2d4f9f3
--- /dev/null
+++ b/toolkit/components/telemetry/dap/metrics.yaml
@@ -0,0 +1,54 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Adding a new metric? We have docs for that!
+# https://firefox-source-docs.mozilla.org/toolkit/components/glean/user/new_definitions_file.html
+
+---
+$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
+$tags:
+ - 'Toolkit :: Telemetry'
+
+dap:
+ upload_status:
+ type: labeled_counter
+ labels:
+ - success
+ - failure
+ - http_error
+ - http_502
+ - abort
+ - abort_timed
+ - abort_shutdown
+ description: >
+ The result of trying to upload a report to the DAP server.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1775035
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1775035
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - simon@mozilla.com
+ expires: never
+
+ report_generation_status:
+ type: labeled_counter
+ labels:
+ - success
+ - failure
+ - abort
+ - hpke_leader_fail
+ - hpke_helper_fail
+ description: >
+ The result of trying to generate a DAP report.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1775035
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1775035
+ data_sensitivity:
+ - technical
+ notification_emails:
+ - simon@mozilla.com
+ expires: never
diff --git a/toolkit/components/telemetry/dap/nsIDAPTelemetry.idl b/toolkit/components/telemetry/dap/nsIDAPTelemetry.idl
new file mode 100644
index 0000000000..909d5ab3f3
--- /dev/null
+++ b/toolkit/components/telemetry/dap/nsIDAPTelemetry.idl
@@ -0,0 +1,37 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+[scriptable, builtinclass, uuid(58a4c579-d2dd-46b7-9c3b-6881a1c36c6a)]
+interface nsIDAPTelemetry : nsISupports {
+ /**
+ * Split measurement into shares and create a report with encrypted shares.
+ *
+ * @param leaderHpkeConfig The leader share will be encrypted with this
+ * config.
+ * @param helperHpkeConfig Same for the helper.
+ * @param measurement The data which will be encoded and shared.
+ * @param task_id Identifies which task this measurement is for
+ * which influences both encoding and encryption.
+ * @param time_precision Determines the report timestamp.
+ *
+ * @return The raw bytes of a report, ready for sending.
+ *
+ * @note This can potentially run for a long time. Take care not to block
+ * the main thread for too long.
+ */
+ void GetReportU8(in Array<uint8_t> leaderHpkeConfig,
+ in Array<uint8_t> helperHpkeConfig, in uint8_t measurement,
+ in Array<uint8_t> task_id, in uint64_t time_precision,
+ out Array<uint8_t> report);
+ void GetReportVecU8(in Array<uint8_t> leaderHpkeConfig,
+ in Array<uint8_t> helperHpkeConfig,
+ in Array<uint8_t> measurement, in Array<uint8_t> task_id,
+ in uint64_t time_precision, out Array<uint8_t> report);
+ void GetReportVecU16(in Array<uint8_t> leaderHpkeConfig,
+ in Array<uint8_t> helperHpkeConfig,
+ in Array<uint16_t> measurement, in Array<uint8_t> task_id,
+ in uint64_t time_precision, out Array<uint8_t> report);
+};
diff --git a/toolkit/components/telemetry/dap/tests/xpcshell/test_dap.js b/toolkit/components/telemetry/dap/tests/xpcshell/test_dap.js
new file mode 100644
index 0000000000..deb68bafef
--- /dev/null
+++ b/toolkit/components/telemetry/dap/tests/xpcshell/test_dap.js
@@ -0,0 +1,149 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ DAPTelemetrySender: "resource://gre/modules/DAPTelemetrySender.sys.mjs",
+});
+
+const BinaryOutputStream = Components.Constructor(
+ "@mozilla.org/binaryoutputstream;1",
+ "nsIBinaryOutputStream",
+ "setOutputStream"
+);
+
+const BinaryInputStream = Components.Constructor(
+ "@mozilla.org/binaryinputstream;1",
+ "nsIBinaryInputStream",
+ "setInputStream"
+);
+
+const PREF_LEADER = "toolkit.telemetry.dap_leader";
+const PREF_HELPER = "toolkit.telemetry.dap_helper";
+
+let received = false;
+let server;
+let server_addr;
+
+const tasks = [
+ {
+ // this is testing task 1
+ id: "QjMD4n8l_MHBoLrbCfLTFi8hC264fC59SKHPviPF0q8",
+ leader_endpoint: null,
+ helper_endpoint: null,
+ time_precision: 300,
+ measurement_type: "u8",
+ },
+ {
+ // this is testing task 2
+ id: "DSZGMFh26hBYXNaKvhL_N4AHA3P5lDn19on1vFPBxJM",
+ leader_endpoint: null,
+ helper_endpoint: null,
+ time_precision: 300,
+ measurement_type: "vecu8",
+ },
+];
+
+function hpkeConfigHandler(request, response) {
+ if (
+ request.queryString ==
+ "task_id=QjMD4n8l_MHBoLrbCfLTFi8hC264fC59SKHPviPF0q8" ||
+ request.queryString == "task_id=DSZGMFh26hBYXNaKvhL_N4AHA3P5lDn19on1vFPBxJM"
+ ) {
+ let config_bytes;
+ if (request.path.startsWith("/leader")) {
+ config_bytes = new Uint8Array([
+ 0, 41, 47, 0, 32, 0, 1, 0, 1, 0, 32, 11, 33, 206, 33, 131, 56, 220, 82,
+ 153, 110, 228, 200, 53, 98, 210, 38, 177, 197, 252, 198, 36, 201, 86,
+ 121, 169, 238, 220, 34, 143, 112, 177, 10,
+ ]);
+ } else {
+ config_bytes = new Uint8Array([
+ 0, 41, 42, 0, 32, 0, 1, 0, 1, 0, 32, 28, 62, 242, 195, 117, 7, 173, 149,
+ 250, 15, 139, 178, 86, 241, 117, 143, 75, 26, 57, 60, 88, 130, 199, 175,
+ 195, 9, 241, 130, 61, 47, 215, 101,
+ ]);
+ }
+ response.setHeader("Content-Type", "application/dap-hpke-config");
+ let bos = new BinaryOutputStream(response.bodyOutputStream);
+ bos.writeByteArray(config_bytes);
+ } else {
+ Assert.ok(false, `Unknown query string: ${request.queryString}`);
+ }
+}
+
+function uploadHandler(request, response) {
+ Assert.equal(
+ request.getHeader("Content-Type"),
+ "application/dap-report",
+ "Wrong Content-Type header."
+ );
+
+ let body = new BinaryInputStream(request.bodyInputStream);
+ console.log(body.available());
+ Assert.equal(
+ true,
+ body.available() == 406 || body.available() == 3654,
+ "Wrong request body size."
+ );
+ received = true;
+ response.setStatusLine(request.httpVersion, 200);
+}
+
+add_setup(async function () {
+ do_get_profile();
+ Services.fog.initializeFOG();
+
+ // Set up a mock server to represent the DAP endpoints.
+ server = new HttpServer();
+ server.registerPathHandler("/leader_endpoint/hpke_config", hpkeConfigHandler);
+ server.registerPathHandler("/helper_endpoint/hpke_config", hpkeConfigHandler);
+ server.registerPrefixHandler("/leader_endpoint/tasks/", uploadHandler);
+ server.start(-1);
+
+ const orig_leader = Services.prefs.getStringPref(PREF_LEADER);
+ const orig_helper = Services.prefs.getStringPref(PREF_HELPER);
+ const i = server.identity;
+ server_addr = i.primaryScheme + "://" + i.primaryHost + ":" + i.primaryPort;
+ Services.prefs.setStringPref(PREF_LEADER, server_addr + "/leader_endpoint");
+ Services.prefs.setStringPref(PREF_HELPER, server_addr + "/helper_endpoint");
+ registerCleanupFunction(() => {
+ Services.prefs.setStringPref(PREF_LEADER, orig_leader);
+ Services.prefs.setStringPref(PREF_HELPER, orig_helper);
+
+ return new Promise(resolve => {
+ server.stop(resolve);
+ });
+ });
+});
+
+add_task(async function testVerificationTask() {
+ Services.fog.testResetFOG();
+ let before = Glean.dap.uploadStatus.success.testGetValue() ?? 0;
+ await lazy.DAPTelemetrySender.sendTestReports(tasks, 5000);
+ let after = Glean.dap.uploadStatus.success.testGetValue() ?? 0;
+
+ Assert.equal(before + 2, after, "Successful submissions should be counted.");
+ Assert.ok(received, "Report upload successful.");
+});
+
+add_task(async function testNetworkError() {
+ Services.fog.testResetFOG();
+ let before = Glean.dap.reportGenerationStatus.failure.testGetValue() ?? 0;
+ Services.prefs.setStringPref(PREF_LEADER, server_addr + "/invalid-endpoint");
+ await lazy.DAPTelemetrySender.sendTestReports(tasks, 5000);
+ let after = Glean.dap.reportGenerationStatus.failure.testGetValue() ?? 0;
+ Assert.equal(
+ before + 2,
+ after,
+ "Failed report generation should be counted."
+ );
+});
diff --git a/toolkit/components/telemetry/dap/tests/xpcshell/xpcshell.toml b/toolkit/components/telemetry/dap/tests/xpcshell/xpcshell.toml
new file mode 100644
index 0000000000..6bdebc17f2
--- /dev/null
+++ b/toolkit/components/telemetry/dap/tests/xpcshell/xpcshell.toml
@@ -0,0 +1,4 @@
+[DEFAULT]
+
+["test_dap.js"]
+skip-if = ["os == 'android'"] # DAP is not supported on Android
diff --git a/toolkit/components/telemetry/docs/collection/custom-pings.rst b/toolkit/components/telemetry/docs/collection/custom-pings.rst
new file mode 100644
index 0000000000..fc1fd5a5ac
--- /dev/null
+++ b/toolkit/components/telemetry/docs/collection/custom-pings.rst
@@ -0,0 +1,80 @@
+.. _submitting-customping:
+
+=======================
+Submitting custom pings
+=======================
+
+Custom pings can be submitted from JavaScript using:
+
+.. code-block:: js
+
+ TelemetryController.submitExternalPing(type, payload, options)
+
+- ``type`` - a ``string`` that is the type of the ping, limited to ``/^[a-z0-9][a-z0-9-]+[a-z0-9]$/i``.
+- ``payload`` - the actual payload data for the ping, has to be a JSON style object.
+- ``options`` - optional, an object containing additional options:
+ - ``addClientId``- whether to add the client id to the ping, defaults to ``false``
+ - ``addEnvironment`` - whether to add the environment data to the ping, defaults to ``false``
+ - ``overrideEnvironment`` - a JSON style object that overrides the environment data
+
+``TelemetryController`` will assemble a ping with the passed payload and the specified options.
+That ping will be archived locally for use with Shield and inspection in ``about:telemetry``.
+If preferences allow the upload of Telemetry pings, the ping will be uploaded at the next opportunity (this is subject to throttling, retry-on-failure, etc.).
+
+.. important::
+
+ Every new or changed data collection in Firefox needs a `data collection review <https://wiki.mozilla.org/Firefox/Data_Collection>`__ from a Data Steward.
+
+Submission constraints
+----------------------
+
+When submitting pings on shutdown, they should not be submitted after Telemetry shutdown.
+Pings should be submitted at the latest within:
+
+- the `observer notification <https://developer.mozilla.org/docs/Observer_Notifications#Application_shutdown>`_ ``"profile-before-change"``
+- the :ref:`AsyncShutdown phase <AsyncShutdown_phases>` ``sendTelemetry``
+
+There are other constraints that can lead to a ping submission getting dropped:
+
+- invalid ping type strings.
+- invalid payload types: E.g. strings instead of objects.
+- oversized payloads: We currently only drop pings >1MB, but targeting sizes of <=10KB is recommended.
+
+Tools
+=====
+
+Helpful tools for designing new pings include:
+
+- `gzipServer <https://github.com/mozilla/gzipServer>`_ - a Python script that can run locally and receives and saves Telemetry pings. Making Firefox send to it allows inspecting outgoing pings easily.
+- ``about:telemetry`` - allows inspecting submitted pings from the local archive, including all custom ones.
+
+Designing custom pings
+======================
+
+In general, creating a new custom ping means you don't benefit automatically from the existing tooling. Further work is needed to make data show up in re:dash or other analysis tools.
+
+In addition to the `data collection review <https://wiki.mozilla.org/Firefox/Data_Collection>`__, questions to guide a new ping design are:
+
+- Submission interval & triggers:
+ - What events trigger ping submission?
+ - What interval is the ping submitted in?
+ - Is there a throttling mechanism?
+ - What is the desired latency? (submitting "at least daily" still leads to certain latency tails)
+ - Are pings submitted on a clock schedule? Or based on "time since session start", "time since last ping" etc.? (I.e. will we get sharp spikes in submission volume?)
+- Size and volume:
+ - What’s the size of the submitted payload?
+ - What's the full ping size including metadata in the pipeline?
+ - What’s the target population?
+ - What's the overall estimated volume?
+- Dataset:
+ - Is it opt-out?
+ - Does it need to be opt-out?
+ - Does it need to be in a separate ping? (why can’t the data live in probes?)
+- Privacy:
+ - Is there risk to leak PII?
+ - How is that risk mitigated?
+- Data contents:
+ - Does the submitted data answer the posed product questions?
+ - Does the shape of the data allow to answer the questions efficiently?
+ - Is the data limited to what's needed to answer the questions?
+ - Does the data use common formats? (i.e. can we reuse tooling or analysis know-how)
diff --git a/toolkit/components/telemetry/docs/collection/events.rst b/toolkit/components/telemetry/docs/collection/events.rst
new file mode 100644
index 0000000000..831c40a8bc
--- /dev/null
+++ b/toolkit/components/telemetry/docs/collection/events.rst
@@ -0,0 +1,349 @@
+.. _eventtelemetry:
+
+======
+Events
+======
+
+Across the different Firefox initiatives, there is a common need for a mechanism for recording, storing, sending & analysing application usage in an event-oriented format.
+*Event Telemetry* specifies a common events data format, which allows for broader, shared usage of data processing tools.
+Adding events is supported in artifact builds and build faster workflows.
+
+For events recorded into Firefox Telemetry we also provide an API that opaquely handles storage and submission to our servers.
+
+.. important::
+
+ Every new or changed data collection in Firefox needs a `data collection review <https://wiki.mozilla.org/Firefox/Data_Collection>`__ from a Data Steward.
+
+.. _events.serializationformat:
+
+Serialization format
+====================
+
+Events are submitted in an :doc:`../data/event-ping` as an array, e.g.:
+
+.. code-block:: js
+
+ [
+ [2147, "ui", "click", "back_button"],
+ [2213, "ui", "search", "search_bar", "google"],
+ [2892, "ui", "completion", "search_bar", "yahoo",
+ {"querylen": "7", "results": "23"}],
+ [5434, "dom", "load", "frame", null,
+ {"prot": "https", "src": "script"}],
+ // ...
+ ]
+
+Each event is of the form:
+
+.. code-block:: js
+
+ [timestamp, category, method, object, value, extra]
+
+Where the individual fields are:
+
+- ``timestamp``: ``Number``, positive integer. This is the time in ms when the event was recorded, relative to the main process start time.
+- ``category``: ``String``, identifier. The category is a group name for events and helps to avoid name conflicts.
+- ``method``: ``String``, identifier. This describes the type of event that occurred, e.g. ``click``, ``keydown`` or ``focus``.
+- ``object``: ``String``, identifier. This is the object the event occurred on, e.g. ``reload_button`` or ``urlbar``.
+- ``value``: ``String``, optional, may be ``null``. This is a user defined value, providing context for the event.
+- ``extra``: ``Object``, optional, may be ``null``. This is an object of the form ``{"key": "value", ...}``, both keys and values need to be strings, keys are identifiers. This is used for events where additional richer context is needed.
+
+.. _eventlimits:
+
+Limits
+------
+
+Each ``String`` marked as an identifier (the event ``name``, ``category``, ``method``,
+``object``, and the keys of ``extra``) is restricted to be composed of alphanumeric ASCII
+characters ([a-zA-Z0-9]) plus infix underscores ('_' characters that aren't the first or last).
+``category`` is also permitted infix periods ('.' characters, so long as they aren't the
+first or last character).
+
+For the Firefox Telemetry implementation, several fields are subject to length limits:
+
+- ``category``: Max. byte length is ``30``.
+- ``method``: Max. byte length is ``20``.
+- ``object``: Max. byte length is ``20``.
+- ``value``: Max. byte length is ``80``.
+- ``extra``: Max. number of keys is ``10``.
+
+ - Each extra key name: Max. string length is ``15``.
+ - Each extra value: Max. byte length is ``80``.
+
+Only ``value`` and the values of ``extra`` will be truncated if over the specified length.
+Any other ``String`` going over its limit will be reported as an error and the operation
+aborted.
+
+.. _eventdefinition:
+
+The YAML definition file
+========================
+
+Any event recorded into Firefox Telemetry must be registered before it can be recorded.
+For any code that ships as part of Firefox that happens in `Events.yaml <https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/Events.yaml>`_.
+
+The probes in the definition file are represented in a fixed-depth, three-level structure. The first level contains *category* names (grouping multiple events together), the second level contains *event* names, under which the events properties are listed. E.g.:
+
+.. code-block:: yaml
+
+ # The following is a category of events named "browser.ui".
+ browser.ui:
+ click: # This is the event named "click".
+ objects: ["reload-btn"] # List the objects for this event.
+ description: >
+ Describes this event in detail, potentially over
+ multiple lines.
+ # ... and more event properties.
+ # ... and more events.
+ # This is the "dom" category.
+ search:
+ # And the "completion" event.
+ completion:
+ # ...
+ description: Recorded when a search completion suggestion was clicked.
+ extra_keys:
+ distance: The edit distance to the current search query input.
+ loadtime: How long it took to load this completion entry.
+ # ...
+
+Category and event names are subject to the limits :ref:`specified above <eventlimits>`.
+
+The following event properties are valid:
+
+- ``methods`` *(optional, list of strings)*: The valid event methods. If not set this defaults to ``[eventName]``.
+- ``objects`` *(required, list of strings)*: The valid event objects.
+- ``description`` *(required, string)*: Description of the event and its semantics.
+- ``release_channel_collection`` *(optional, string)*: This can be set to ``opt-in`` (default) or ``opt-out``.
+- ``record_in_processes`` *(required, list of strings)*: A list of processes the event can be recorded in. Currently supported values are:
+
+ - ``main``
+ - ``content``
+ - ``gpu``
+ - ``all_children`` (record in all the child processes)
+ - ``all`` (record in all the processes).
+
+- ``bug_numbers`` *(required, list of numbers)*: A list of Bugzilla bug numbers that are relevant to this event.
+- ``notification_emails`` *(required, list of strings)*: A list of emails of owners for this event. This is used for contact for data reviews and potentially to email alerts.
+- expiry: There are two properties that can specify expiry, at least one needs to be set:
+
+ - ``expiry_version`` *(required, string)*: The version number in which the event expires, e.g. ``"50"``, or ``"never"``. A version number of type "N" is automatically converted to "N.0a1" in order to expire the event also in the development channels. For events that never expire the value ``never`` can be used.
+
+- ``extra_keys`` *(optional, object)*: An object that specifies valid keys for the ``extra`` argument and a description - see the example above.
+- ``products`` *(required, list of strings)*: A list of products the event can be recorded on. Currently supported values are:
+
+ - ``firefox`` - Collected in Firefox Desktop for submission via Firefox Telemetry.
+ - ``thunderbird`` - Collected in Thunderbird for submission via Thunderbird Telemetry.
+
+- ``operating_systems`` *(optional, list of strings)*: This field restricts recording to certain operating systems only. It defaults to ``all``. Currently supported values are:
+
+ - ``mac``
+ - ``linux``
+ - ``windows``
+ - ``android``
+ - ``unix``
+ - ``all`` (record on all operating systems)
+
+.. note::
+
+ Combinations of ``category``, ``method``, and ``object`` defined in the file must be unique.
+
+The API
+=======
+
+Public JS API
+-------------
+
+``recordEvent()``
+~~~~~~~~~~~~~~~~~
+
+.. code-block:: js
+
+ Services.telemetry.recordEvent(category, method, object, value, extra);
+
+Record a registered event.
+
+* ``value``: Optional, may be ``null``. A string value, limited to 80 bytes.
+* ``extra``: Optional. An object with string keys & values. Key strings are limited to what was registered. Value strings are limited to 80 bytes.
+
+Throws if the combination of ``category``, ``method`` and ``object`` is unknown.
+Recording an expired event will not throw, but print a warning into the browser console.
+
+.. note::
+
+ Each ``recordEvent`` of a known non-expired combination of ``category``, ``method``, and
+ ``object``, will be :ref:`summarized <events.event-summary>`.
+
+.. warning::
+
+ Event Telemetry recording is designed to be cheap, not free. If you wish to record events in a performance-sensitive piece of code, store the events locally and record them only after the performance-sensitive piece ("hot path") has completed.
+
+Example:
+
+.. code-block:: js
+
+ Services.telemetry.recordEvent("ui", "click", "reload-btn");
+ // event: [543345, "ui", "click", "reload-btn"]
+ Services.telemetry.recordEvent("ui", "search", "search-bar", "google");
+ // event: [89438, "ui", "search", "search-bar", "google"]
+ Services.telemetry.recordEvent("ui", "completion", "search-bar", "yahoo",
+ {"querylen": "7", "results": "23"});
+ // event: [982134, "ui", "completion", "search-bar", "yahoo",
+ // {"qerylen": "7", "results": "23"}]
+
+``setEventRecordingEnabled()``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: js
+
+ Services.telemetry.setEventRecordingEnabled(category, enabled);
+
+Event recording is currently disabled by default for events registered in Events.yaml.
+Dynamically-registered events (those registered using ``registerEvents()``) are enabled by default, and cannot be disabled.
+Privileged add-ons and Firefox code can enable & disable recording events for specific categories using this function.
+
+Example:
+
+.. code-block:: js
+
+ Services.telemetry.setEventRecordingEnabled("ui", true);
+ // ... now events in the "ui" category will be recorded.
+ Services.telemetry.setEventRecordingEnabled("ui", false);
+ // ... now "ui" events will not be recorded anymore.
+
+.. note::
+
+ Even if your event category isn't enabled, counts of events that attempted to be recorded will
+ be :ref:`summarized <events.event-summary>`.
+
+.. _registerevents:
+
+``registerEvents()``
+~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: js
+
+ Services.telemetry.registerEvents(category, eventData);
+
+Register new events from add-ons.
+
+* ``category`` - *(required, string)* The category the events are in.
+* ``eventData`` - *(required, object)* An object of the form ``{eventName1: event1Data, ...}``, where each events data is an object with the entries:
+
+ * ``methods`` - *(required, list of strings)* The valid event methods.
+ * ``objects`` - *(required, list of strings)* The valid event objects.
+ * ``extra_keys`` - *(optional, list of strings)* The valid extra keys for the event.
+ * ``record_on_release`` - *(optional, bool)*
+ * ``expired`` - *(optional, bool)* Whether this event entry is expired. This allows recording it without error, but it will be discarded. Defaults to false.
+
+For events recorded from add-ons, registration happens at runtime. Any new events must first be registered through this function before they can be recorded.
+The registered categories will automatically be enabled for recording, and cannot be disabled.
+If a dynamic event uses the same category as a static event, the category will also be enabled upon registration.
+
+After registration, the events can be recorded through the ``recordEvent()`` function. They will be submitted in event pings like static events are, under the ``dynamic`` process.
+
+New events registered here are subject to the same limitations as the ones registered through ``Events.yaml``, although the naming was in parts updated to recent policy changes.
+
+When add-ons are updated, they may re-register all of their events. In that case, any changes to events that are already registered are ignored. The only exception is expiry; an event that is re-registered with ``expired: true`` will not be recorded anymore.
+
+Example:
+
+.. code-block:: js
+
+ Services.telemetry.registerEvents("myAddon.interaction", {
+ "click": {
+ methods: ["click"],
+ objects: ["red_button", "blue_button"],
+ }
+ });
+ // Now events can be recorded.
+ Services.telemetry.recordEvent("myAddon.interaction", "click", "red_button");
+
+Internal API
+------------
+
+.. code-block:: js
+
+ Services.telemetry.snapshotEvents(dataset, clear, eventLimit);
+ Services.telemetry.clearEvents();
+
+These functions are only supposed to be used by Telemetry internally or in tests.
+
+Also, the ``event-telemetry-storage-limit-reached`` topic is notified when the event ping event
+limit is reached (1000 event records).
+This is intended only for use internally or in tests.
+
+.. _events.event-summary:
+
+Event Summary
+=============
+
+Calling ``recordEvent`` on any non-expired registered event will accumulate to a
+:doc:`Scalar <scalars>` for ease of analysing uptake and usage patterns. Even if the event category
+isn't enabled.
+
+The scalar is ``telemetry.event_counts`` for statically-registered events (the ones in
+``Events.yaml``) and ``telemetry.dynamic_event_counts`` for dynamically-registered events (the ones
+registered via ``registerEvents``). These are :ref:`keyed scalars <scalars.keyed-scalars>` where
+the keys are of the form ``category#method#object`` and the values are counts of the number of
+times ``recordEvent`` was called with that combination of ``category``, ``method``, and ``object``.
+
+These two scalars have a default maximum key limit of 500 per process.
+
+Example:
+
+.. code-block:: js
+
+ // telemetry.event_counts summarizes in the same process the events were recorded
+
+ // Let us suppose in the parent process this happens:
+ Services.telemetry.recordEvent("interaction", "click", "document", "xuldoc");
+ Services.telemetry.recordEvent("interaction", "click", "document", "xuldoc-neighbour");
+
+ // And in each of child processes 1 through 4, this happens:
+ Services.telemetry.recordEvent("interaction", "click", "document", "htmldoc");
+
+In the case that ``interaction.click.document`` is statically-registered, this will result in the
+parent-process scalar ``telemetry.event_counts`` having a key ``interaction#click#document`` with
+value ``2`` and the content-process scalar ``telemetry.event_counts`` having a key
+``interaction#click#document`` with the value ``4``.
+
+All dynamically-registered events end up in the dynamic-process ``telemetry.dynamic_event_counts``
+(notice the different name) regardless of in which process the events were recorded. From the
+example above, if ``interaction.click.document`` was registered with ``registerEvents`` then
+the dynamic-process scalar ``telemetry.dynamic_event_counts`` would have a key
+``interaction#click#document`` with the value ``6``.
+
+Testing
+=======
+
+Tests involving Event Telemetry often follow this four-step form:
+
+1. ``Services.telemetry.clearEvents();`` To minimize the effects of prior code and tests.
+2. ``Services.telemetry.setEventRecordingEnabled(myCategory, true);`` To enable the collection of
+ your events. (May or may not be relevant in your case)
+3. ``runTheCode();`` This is part of the test where you call the code that's supposed to collect
+ Event Telemetry.
+4. ``TelemetryTestUtils.assertEvents(expected, filter, options);`` This will check the
+ events recorded by Event Telemetry against your provided list of expected events.
+ If you only need to check the number of events recorded, you can use
+ ``TelemetryTestUtils.assertNumberOfEvents(expectedNum, filter, options);``.
+ Both utilities have :searchfox:`helpful inline documentation <toolkit/components/telemetry/tests/utils/TelemetryTestUtils.sys.mjs>`.
+
+
+Version History
+===============
+
+- Firefox 79: ``geckoview`` support removed (see `bug 1620395 <https://bugzilla.mozilla.org/show_bug.cgi?id=1620395>`__).
+- Firefox 52: Initial event support (`bug 1302663 <https://bugzilla.mozilla.org/show_bug.cgi?id=1302663>`_).
+- Firefox 53: Event recording disabled by default (`bug 1329139 <https://bugzilla.mozilla.org/show_bug.cgi?id=1329139>`_).
+- Firefox 54: Added child process events (`bug 1313326 <https://bugzilla.mozilla.org/show_bug.cgi?id=1313326>`_).
+- Firefox 56: Added support for recording new probes from add-ons (`bug 1302681 <bug https://bugzilla.mozilla.org/show_bug.cgi?id=1302681>`_).
+- Firefox 58:
+
+ - Ignore re-registering existing events for a category instead of failing (`bug 1408975 <https://bugzilla.mozilla.org/show_bug.cgi?id=1408975>`_).
+ - Removed support for the ``expiry_date`` property, as it was unused (`bug 1414638 <https://bugzilla.mozilla.org/show_bug.cgi?id=1414638>`_).
+- Firefox 61:
+
+ - Enabled support for adding events in artifact builds and build-faster workflows (`bug 1448945 <https://bugzilla.mozilla.org/show_bug.cgi?id=1448945>`_).
+ - Added summarization of events (`bug 1440673 <https://bugzilla.mozilla.org/show_bug.cgi?id=1440673>`_).
+- Firefox 66: Replace ``cpp_guard`` with ``operating_systems`` (`bug 1482912 <https://bugzilla.mozilla.org/show_bug.cgi?id=1482912>`_)`
diff --git a/toolkit/components/telemetry/docs/collection/experiments.rst b/toolkit/components/telemetry/docs/collection/experiments.rst
new file mode 100644
index 0000000000..d9c926fcea
--- /dev/null
+++ b/toolkit/components/telemetry/docs/collection/experiments.rst
@@ -0,0 +1,41 @@
+=====================
+Experiment Annotation
+=====================
+This API allows privileged JavaScript to annotate the :doc:`../data/environment` with any experiments a client is participating in.
+
+The experiment annotations are sent with any ping that includes the :doc:`../data/environment` data.
+
+The JS API
+==========
+Privileged JavaScript code can annotate experiments using the functions exposed by ``TelemetryEnvironment.sys.mjs``.
+
+The following function adds an annotation to the environment for the provided ``id``, ``branch`` and ``options``. Calling this function repeatedly with the same ``id`` will overwrite the state and trigger new subsessions (subject to throttling).
+``options`` is an object that may contain ``type`` to tag the experiment with a specific type or ``enrollmentId`` to tag the enrollment in this experiment with an identifier.
+
+.. code-block:: js
+
+ TelemetryEnvironment.setExperimentActive(id, branch, [options={}}])
+
+This removes the annotation for the experiment with the provided ``id``.
+
+.. code-block:: js
+
+ TelemetryEnvironment.setExperimentInactive(id)
+
+This synchronously returns a dictionary containing the information for each active experiment.
+
+.. code-block:: js
+
+ TelemetryEnvironment.getActiveExperiments()
+
+.. note::
+
+ Both ``setExperimentActive`` and ``setExperimentInactive`` trigger a new subsession. However
+ the latter only does so if there was an active experiment with the provided ``id``.
+
+Limits and restrictions
+-----------------------
+To prevent abuses, the content of the experiment ``id`` and ``branch`` is limited to
+100 characters in length.
+``type`` is limited to a length of 20 characters.
+``enrollmentId`` is limited to 40 characters (chosen to be just a little longer than the 36-character long GUID text representation).
diff --git a/toolkit/components/telemetry/docs/collection/histograms.rst b/toolkit/components/telemetry/docs/collection/histograms.rst
new file mode 100644
index 0000000000..282ee4e07f
--- /dev/null
+++ b/toolkit/components/telemetry/docs/collection/histograms.rst
@@ -0,0 +1,410 @@
+==========
+Histograms
+==========
+
+In Firefox, the Telemetry system collects various measures of Firefox performance, hardware, usage and customizations and submits it to Mozilla. The Telemetry data collected by a single client can be examined from the integrated ``about:telemetry`` browser page, while the aggregated reports across entire user populations are publicly available at `telemetry.mozilla.org <https://telemetry.mozilla.org>`_.
+
+.. important::
+
+ Every new or changed data collection in Firefox needs a `data collection review <https://wiki.mozilla.org/Firefox/Data_Collection>`__ from a Data Steward.
+
+The following sections explain how to add a new measurement to Telemetry.
+
+Overview
+========
+
+Telemetry histograms are an efficient way to collect numeric measurements like multiple counts or timings.
+They are collected through a common API and automatically submitted with the :doc:`main ping <../data/main-ping>`.
+
+.. hint::
+
+ Before adding a new histogram, you should consider using other collection mechanisms. For example, if the need is to track a single scalar value (e.g. number, boolean or string), you should use :doc:`scalars`.
+
+The histogram below is taken from Firefox's ``about:telemetry`` page. It shows a histogram used for tracking plugin shutdown times and the data collected over a single Firefox session. The timing data is grouped into buckets where the height of the blue bars represents the number of items in each bucket. The tallest bar, for example, indicates that there were 63 plugin shutdowns lasting between 129ms and 204ms.
+
+.. image:: sampleHistogram.png
+
+The histograms on the ``about:telemetry`` page only show the non-empty buckets in a histogram, except for the bucket to the left of the first non-empty bucket and the bucket to the right of the last non-empty bucket.
+
+.. _choosing-histogram-type:
+
+Choosing a Histogram Type
+=========================
+
+The first step to adding a new histogram is to choose the histogram type that best represents the data being measured. The sample histogram used above is an "exponential" histogram.
+
+.. note::
+
+ Only ``flag`` and ``count`` histograms have default values. All other histograms start out empty and are only submitted if a value is recorded.
+
+``boolean``
+-----------
+These histograms only record boolean values. Multiple boolean entries can be recorded in the same histogram during a single browsing session, e.g. if a histogram is measuring user choices in a dialog box with options "Yes" or "No", a new boolean value is added every time the dialog is displayed.
+
+``linear``
+----------
+Linear histograms are similar to enumerated histograms, except each bucket is associated with a range of values instead of a single enum value. The range of values covered by each bucket increases linearly from the previous bucket, e.g. one bucket might count the number of occurrences of values between 0 to 9, the next bucket would cover values 10-19, the next 20-29, etc. This bucket type is useful if there aren't orders of magnitude differences between the minimum and maximum values stored in the histogram, e.g. if the values you are storing are percentages 0-100%.
+
+.. note::
+
+ If you need a linear histogram with buckets < 0, 1, 2 ... N >, then you should declare an enumerated histogram. This restriction was added to prevent developers from making a common off-by-one mistake when specifying the number of buckets in a linear histogram.
+
+``exponential``
+---------------
+Exponential histograms are similar to linear histograms but the range of values covered by each bucket increases exponentially. As an example of its use, consider the timings of an I/O operation whose duration might normally fall in the range of 0ms-50ms but extreme cases might have durations in seconds or minutes. For such measurements, you would want finer-grained bucketing in the normal range but coarser-grained bucketing for the extremely large values. An exponential histogram fits this requirement since it has "narrow" buckets near the minimum value and significantly "wider" buckets near the maximum value.
+
+``categorical``
+---------------
+Categorical histograms are similar to enumerated histograms. However, instead of specifying ``n_buckets``, you specify an array of strings in the ``labels`` field. From JavaScript, the label values or their indices can be passed as strings to ``histogram.add()``. From C++ you can use ``AccumulateCategorical`` with passing a value from the corresponding ``Telemetry::LABEL_*`` enum, or, in exceptional cases the string values.
+
+.. note::
+
+ You can add new labels to a categorical histogram later on,
+ up to the configured maximum.
+ Categorical histograms by default support up to 50 labels,
+ but you can set it higher using the ``n_values`` property.
+ If you need to add labels beyond the maximum later,
+ you need to use a new histogram name.
+ See `Changing a Histogram`_ for details.
+
+``enumerated``
+--------------
+This histogram type is intended for storing "enum" values, when you can't specify labels and thus cannot use ``categorical`` histograms. An enumerated histogram consists of a fixed number of *buckets* (specified by ``n_values``), each of which is associated with a consecutive integer value (the bucket's *label*), `0` to `n_values`. Each bucket corresponds to an enum value and counts the number of times its particular enum value was recorded; except for the `n_values` bucket, which counts all values greater than or equal to n_values.
+
+You might use this type of histogram if, for example, you wanted to track the relative popularity of SSL handshake types. Whenever the browser started an SSL handshake, it would record one of a limited number of enum values which uniquely identifies the handshake type.
+
+.. note::
+
+ Set ``n_values`` to a slightly larger value than needed to allow for new enum values in the future. See `Changing a histogram`_ if you need to add more enums later.
+
+``flag``
+--------
+*Deprecated* (please use boolean :doc:`scalars`).
+
+This histogram type allows you to record a single value (`0` or `1`, default `0`). This type is useful if you need to track whether a feature was ever used during a Firefox session. You only need to add a single line of code which sets the flag when the feature is used because the histogram is initialized with a default value of `0`/`false` (flag not set). Thus, recording a value of `0` is not allowed and asserts.
+
+Flag histograms will ignore any changes after the flag is set, so once the flag is set, it cannot be unset.
+
+``count``
+---------
+*Deprecated* (please use uint :doc:`scalars`).
+
+This histogram type is used when you want to record a count of something. It only stores a single value and defaults to `0`.
+
+.. _histogram-type-keyed:
+
+Keyed Histograms
+----------------
+
+Keyed histograms are collections of one of the histogram types above, indexed by a string key. This is for example useful when you want to break down certain counts by a name, like how often searches happen with which search engine.
+Note that when you need to record for a small set of known keys, using separate plain histograms is more efficient.
+
+.. warning::
+
+ Keyed histograms are currently not supported in the `histogram change detector <https://alerts.telemetry.mozilla.org/index.html>`_.
+
+Declaring a Histogram
+=====================
+
+Histograms should be declared in the `Histograms.json <https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/Histograms.json>`_ file. These declarations are checked for correctness at `compile time <https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/gen_histogram_data.py>`_ and used to generate C++ code.
+
+The following is a sample histogram declaration from ``Histograms.json`` for a histogram named ``MEMORY_RESIDENT`` which tracks the amount of resident memory used by a process:
+
+
+.. code-block:: json
+
+ {
+ "MEMORY_RESIDENT": {
+ "record_in_processes": ["main", "content"],
+ "alert_emails": ["team@mozilla.xyz"],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 32768,
+ "high": 1048576,
+ "n_buckets": 50,
+ "bug_numbers": [12345],
+ "description": "Resident memory size (KB)"
+ }
+ }
+
+Histograms which track timings in milliseconds or microseconds should suffix their names with ``"_MS"`` and ``"_US"`` respectively. Flag-type histograms should have the suffix ``"_FLAG"`` in their name.
+
+The possible fields in a histogram declaration are listed below.
+
+``record_in_processes``
+-----------------------
+Required. This field is a list of processes this histogram can be recorded in. Currently-supported values are:
+
+- ``main``
+- ``content``
+- ``gpu``
+- ``all_childs`` (record in all child processes)
+- ``all`` (record in all processes)
+
+``alert_emails``
+----------------
+Required. This field is a list of e-mail addresses that should be notified when the distribution of the histogram changes significantly from one build-id to the other. This can be useful to detect regressions. Note that all alerts will be sent automatically to mozilla.dev.telemetry-alerts.
+
+``expires_in_version``
+----------------------
+Required. The version number in which the histogram expires; e.g. a value of `"30"` will mean that the histogram stops recording from Firefox 30 on. A version number of type ``"N"`` is automatically converted to ``"N.0a1"`` in order to expire the histogram also in the development channels. For histograms that never expire the value ``"never"`` can be used as in the example above. Accumulating data into an expired histogram is effectively a non-op and will not record anything.
+
+``kind``
+--------
+Required. One of the histogram types described in the previous section. Different histogram types require different fields to be present in the declaration.
+
+``keyed``
+---------
+Optional, boolean, defaults to ``false``. Determines whether this is a *keyed histogram*.
+
+``keys``
+---------
+Optional, list of strings. Only valid for *keyed histograms*. Defines a case sensitive list of allowed keys that can be used for this histogram. The list is limited to 30 keys with a maximum length of 20 characters. When using a key that is not in the list, the accumulation is discarded and a warning is printed to the browser console.
+
+``low``
+-------
+Optional, the default value is ``1``. This field represents the minimum value expected in the histogram. Note that all histograms automatically get a bucket with label ``0`` for counting values below the ``low`` value. If a histogram does not specify a ``low`` value, it will always have a ``"0"`` bucket (for negative or zero values) and a ``"1"`` bucket (for values between ``1`` and the next bucket).
+
+
+``high``
+--------
+Required for linear and exponential histograms. The maximum value to be stored in a linear or exponential histogram. Any recorded values greater than this maximum will be counted in the last bucket.
+
+``n_buckets``
+-------------
+Required for linear and exponential histograms. The number of buckets in a linear or exponential histogram.
+
+.. note::
+
+ The maximum value for ``n_buckets`` is 100. The more buckets, the larger the storage and transfer costs borne by our users and our pipeline.
+
+``n_values``
+------------
+Required for enumerated histograms. Similar to n_buckets, it represent the number of elements in the enum.
+
+.. note::
+
+ The maximum value for ``n_values`` is 100. The more values, the larger the storage and transfer costs borne by our users and our pipeline.
+
+``labels``
+----------
+Required for categorical histograms. This is an array of strings which are the labels for different values in this histograms. The labels are restricted to a C++-friendly subset of characters (``^[a-z][a-z0-9_]+[a-z0-9]$``). This field is limited to 100 strings, each with a maximum length of 20 characters.
+
+``bug_numbers``
+---------------
+Required for all new histograms. This is an array of integers and should at least contain the bug number that added the probe and additionally other bug numbers that affected its behavior.
+
+``description``
+---------------
+Required. A description of the data tracked by the histogram, e.g. _"Resident memory size"_
+
+``cpp_guard`` (obsolete, use ``operating_systems``)
+---------------------------------------------------
+Optional. This field inserts an #ifdef directive around the histogram's C++ declaration. This is typically used for platform-specific histograms, e.g. ``"cpp_guard": "ANDROID"``
+
+``operating_systems``
+---------------------
+Optional. This field restricts recording to certain operating systems only. Use that in-place of previous ``cpp_guards`` to avoid inclusion on not-specified operating systems.
+Currently supported values are:
+
+- ``mac``
+- ``linux``
+- ``windows``
+- ``android``
+- ``unix``
+- ``all`` (record on all operating systems)
+
+If this field is left out it defaults to ``all``.
+
+``releaseChannelCollection``
+----------------------------
+Optional. This is one of:
+
+* ``"opt-in"``: (default value) This histogram is submitted by default on pre-release channels, unless the user opts out.
+* ``"opt-out"``: This histogram is submitted by default on release and pre-release channels, unless the user opts out.
+
+.. warning::
+
+ Because they are collected by default, opt-out probes need to meet a higher "user benefit" threshold than opt-in probes during data collection review.
+
+
+ Every new or changed data collection in Firefox needs a `data collection review <https://wiki.mozilla.org/Firefox/Data_Collection>`__ from a Data Steward.
+
+.. _histogram-products:
+
+``products``
+-------------
+Required. This field is a list of products this histogram can be recorded on. Currently-supported values are:
+
+- ``firefox`` - Collected in Firefox Desktop for submission via Firefox Telemetry.
+- ``thunderbird`` - Collected in Thunderbird for submission via Thunderbird Telemetry.
+
+``record_into_store``
+---------------------
+
+Optional. This field is a list of stores this histogram should be recorded into.
+If this field is left out it defaults to ``[main]``.
+
+Changing a histogram
+====================
+
+Changing a histogram declaration after the histogram has been released is tricky.
+Many tools
+(like `the aggregator <https://github.com/mozilla/python_mozaggregator>`_)
+assume histograms don't change.
+The current recommended procedure is to change the name of the histogram.
+
+* When changing existing histograms, the recommended pattern is to use a versioned name (``PROBE``, ``PROBE_2``, ``PROBE_3``, ...).
+* For enum histograms, it's recommended to set "n_buckets" to a slightly larger value than needed since new elements may be added to the enum in the future.
+
+The one exception is `Categorical`_ histograms.
+They can be changed by adding labels until it reaches the configured maximum
+(default of 50, or the value of ``n_values``).
+If you need to change the configured maximum,
+then you must change the histogram name as mentioned above.
+
+Histogram values
+================
+
+The values you can accumulate to Histograms are limited by their internal representation.
+
+Telemetry Histograms do not record negative values, instead clamping them to 0 before recording.
+
+Telemetry Histograms do not record values greater than 2^31, instead clamping them to INT_MAX before recording.
+
+Adding a JavaScript Probe
+=========================
+
+A Telemetry probe is the code that measures and stores values in a histogram. Probes in privileged JavaScript code can make use of the `nsITelemetry <https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/nsITelemetry.idl>`_ interface to get references to histogram objects. A new value is recorded in the histogram by calling ``add`` on the histogram object:
+
+.. code-block:: js
+
+ let histogram = Services.telemetry.getHistogramById("PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS");
+ histogram.add(measuredDuration);
+
+ let keyed = Services.telemetry.getKeyedHistogramById("TAG_SEEN_COUNTS");
+ keyed.add("blink");
+
+Note that ``nsITelemetry.getHistogramById()`` will throw an ``NS_ERROR_FAILURE`` JavaScript exception if it is called with an invalid histogram ID. The ``add()`` function will not throw if it fails, instead it prints an error in the browser console.
+
+.. warning::
+
+ Adding a new Telemetry probe is not possible with Artifact builds. A full build is needed.
+
+For histograms measuring time, TelemetryStopwatch can be used to avoid working with Dates manually:
+
+.. code-block:: js
+
+ TelemetryStopwatch.start("SEARCH_SERVICE_INIT2_MS");
+ TelemetryStopwatch.finish("SEARCH_SERVICE_INIT2_MS");
+
+ TelemetryStopwatch.start("FX_TAB_SWITCH_TOTAL_MS");
+ TelemetryStopwatch.cancel("FX_TAB_SWITCH_TOTAL_MS");
+
+Adding a C++ Probe
+==================
+
+Probes in native code can also use the `nsITelemetry <https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/nsITelemetry.idl>`_ interface, but the helper functions declared in `Telemetry.h <https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/Telemetry.h>`_ are more convenient:
+
+.. code-block:: cpp
+
+ #include "mozilla/Telemetry.h"
+
+ /**
+ * Adds sample to a histogram defined in Histograms.json
+ *
+ * @param id - histogram id
+ * @param sample - value to record.
+ */
+ void Accumulate(HistogramID id, uint32_t sample);
+
+ /**
+ * Adds samples to a histogram defined in Histograms.json
+ *
+ * @param id - histogram id
+ * @param samples - values to record.
+ */
+ void Accumulate(HistogramID id, const nsTArray<uint32_t>& samples);
+
+ /**
+ * Adds sample to a keyed histogram defined in Histograms.h
+ *
+ * @param id - keyed histogram id
+ * @param key - the string key
+ * @param sample - (optional) value to record, defaults to 1.
+ */
+ void Accumulate(HistogramID id, const nsCString& key, uint32_t sample = 1);
+
+ /**
+ * Adds time delta in milliseconds to a histogram defined in Histograms.json
+ *
+ * @param id - histogram id
+ * @param start - start time
+ * @param end - (optional) end time, defaults to TimeStamp::Now().
+ */
+ void AccumulateTimeDelta(HistogramID id, TimeStamp start, TimeStamp end = TimeStamp::Now());
+
+ /**
+ * Adds time delta in milliseconds to a keyed histogram defined in Histograms.json
+ *
+ * @param id - histogram id
+ * @param key - the string key
+ * @param start - start time
+ * @param end - (optional) end time, defaults to TimeStamp::Now().
+ */
+ void AccumulateTimeDelta(HistogramID id, const cs TimeStamp start, TimeStamp end = TimeStamp::Now());
+
+ /** Adds time delta in milliseconds to a histogram defined in TelemetryHistogramEnums.h
+ *
+ * @param id - histogram id
+ * @param key - the string key
+ * @param start - start time
+ * @param end - (optional) end time, defaults to TimeStamp::Now().
+ */
+ void AccumulateTimeDelta(HistogramID id, const nsCString& key, TimeStamp start, TimeStamp end = TimeStamp::Now());
+
+The histogram names declared in ``Histograms.json`` are translated into constants in the ``mozilla::Telemetry`` namespace:
+
+.. code-block:: cpp
+
+ mozilla::Telemetry::Accumulate(mozilla::Telemetry::STARTUP_CRASH_DETECTED, true);
+
+.. warning::
+
+ Telemetry accumulations are designed to be cheap, not free. If you wish to accumulate values in a performance-sensitive piece of code, store the accumualtions locally and accumulate after the performance-sensitive piece ("hot path") has completed.
+
+The ``Telemetry.h`` header also declares the helper classes ``AutoTimer`` and ``AutoCounter``. Objects of these types automatically record a histogram value when they go out of scope:
+
+.. code-block:: cpp
+
+ nsresult
+ nsPluginHost::StopPluginInstance(nsNPAPIPluginInstance* aInstance)
+ {
+ Telemetry::AutoTimer<Telemetry::PLUGIN_SHUTDOWN_MS> timer;
+ ...
+ return NS_OK;
+ }
+
+If the HistogramID is not known at compile time, one can use the ``RuntimeAutoTimer`` and ``RuntimeAutoCounter`` classes, which behave like the template parameterized ``AutoTimer`` and ``AutoCounter`` ones.
+
+.. code-block:: cpp
+
+ void
+ FunctionWithTiming(Telemetry::HistogramID aTelemetryID)
+ {
+ ...
+ Telemetry::RuntimeAutoTimer timer(aTelemetryID);
+ ...
+ }
+
+ int32_t
+ FunctionWithCounter(Telemetry::HistogramID aTelemetryID)
+ {
+ ...
+ Telemetry::RuntimeAutoCounter myCounter(aTelemetryID);
+ ++myCounter;
+ myCounter += 42;
+ ...
+ }
+
+Prefer using the template parameterized ``AutoTimer`` and ``AutoCounter`` on hot paths, if possible.
diff --git a/toolkit/components/telemetry/docs/collection/index.rst b/toolkit/components/telemetry/docs/collection/index.rst
new file mode 100644
index 0000000000..96df328317
--- /dev/null
+++ b/toolkit/components/telemetry/docs/collection/index.rst
@@ -0,0 +1,48 @@
+===============
+Data collection
+===============
+
+There are different APIs and formats to collect data in Firefox, all suiting different use cases.
+
+In general, we aim to submit data in a common format where possible. This has several advantages; from common code and tooling to sharing analysis know-how.
+
+In cases where this isn't possible and more flexibility is needed, we can submit custom pings or consider adding different data formats to existing pings.
+
+*Note:* Every new data collection must go through a `data collection review <https://wiki.mozilla.org/Firefox/Data_Collection>`_.
+
+The current data collection possibilities include:
+
+* :doc:`scalars` allow recording of a single value (string, boolean, a number)
+* :doc:`histograms` can efficiently record multiple data points
+* ``environment`` data records information about the system and settings a session occurs in
+* :doc:`events` can record richer data on individual occurrences of specific actions
+* :doc:`Measuring elapsed time <measuring-time>`
+* :doc:`Custom pings <custom-pings>`
+* :doc:`Experiment annotations <experiments>`
+* :doc:`Remote content uptake <uptake>`
+* :doc:`WebExtension API <webextension-api>` can be used in privileged webextensions
+* :doc:`User Interactions <user-interactions>` allow annotating hang report pings with information on what the user was interacting with at the time
+
+.. toctree::
+ :maxdepth: 2
+ :titlesonly:
+ :hidden:
+ :glob:
+
+ scalars
+ histograms
+ events
+ measuring-time
+ custom-pings
+ experiments
+ uptake
+ *
+
+Browser Usage Telemetry
+~~~~~~~~~~~~~~~~~~~~~~~
+For more information, see :ref:`browserusagetelemetry`.
+
+Version History
+~~~~~~~~~~~~~~~
+
+- Firefox 61: Stopped reporting Telemetry Log items (`bug 1443614 <https://bugzilla.mozilla.org/show_bug.cgi?id=1443614>`_).
diff --git a/toolkit/components/telemetry/docs/collection/measuring-time.rst b/toolkit/components/telemetry/docs/collection/measuring-time.rst
new file mode 100644
index 0000000000..c2d972b378
--- /dev/null
+++ b/toolkit/components/telemetry/docs/collection/measuring-time.rst
@@ -0,0 +1,116 @@
+======================
+Measuring elapsed time
+======================
+
+To make it easier to measure how long operations take, we have helpers for both JavaScript and C++.
+These helpers record the elapsed time into histograms, so you have to create suitable :doc:`histograms` for them first.
+
+From JavaScript
+===============
+JavaScript can measure elapsed time using TelemetryStopwatch.
+
+``TelemetryStopwatch`` is a helper that simplifies recording elapsed time (in milliseconds) into histograms (plain or keyed).
+
+API:
+
+.. code-block:: js
+
+ TelemetryStopwatch = {
+ // Start, check if running, cancel & finish recording elapsed time into a
+ // histogram.
+ // |aObject| is optional. If specified, the timer is associated with this
+ // object, so multiple time measurements can be done concurrently.
+ start(histogramId, aObject);
+ running(histogramId, aObject);
+ cancel(histogramId, aObject);
+ finish(histogramId, aObject);
+ // Start, check if running, cancel & finish recording elapsed time into a
+ // keyed histogram.
+ // |key| specifies the key to record into.
+ // |aObject| is optional and used as above.
+ startKeyed(histogramId, key, aObject);
+ runningKeyed(histogramId, key, aObject);
+ cancelKeyed(histogramId, key, aObject);
+ finishKeyed(histogramId, key, aObject);
+ };
+
+Example:
+
+.. code-block:: js
+
+ TelemetryStopwatch.start("SAMPLE_FILE_LOAD_TIME_MS");
+ // ... start loading file.
+ if (failedToOpenFile) {
+ // Cancel this if the operation failed early etc.
+ TelemetryStopwatch.cancel("SAMPLE_FILE_LOAD_TIME_MS");
+ return;
+ }
+ // ... do more work.
+ TelemetryStopwatch.finish("SAMPLE_FILE_LOAD_TIME_MS");
+
+ // Another loading attempt? Start stopwatch again if
+ // not already running.
+ if (!TelemetryStopwatch.running("SAMPLE_FILE_LOAD_TIME_MS")) {
+ TelemetryStopwatch.start("SAMPLE_FILE_LOAD_TIME_MS");
+ }
+
+ // Periodically, it's necessary to attempt to finish a
+ // TelemetryStopwatch that's already been canceled or
+ // finished. Normally, that throws a warning to the
+ // console. If the TelemetryStopwatch being possibly
+ // canceled or finished is expected behaviour, the
+ // warning can be suppressed by passing the optional
+ // aCanceledOkay argument.
+
+ // ... suppress warning on a previously finished
+ // TelemetryStopwatch
+ TelemetryStopwatch.finish("SAMPLE_FILE_LOAD_TIME_MS", null,
+ true /* aCanceledOkay */);
+
+From C++
+========
+
+API:
+
+.. code-block:: cpp
+
+ // This helper class is the preferred way to record elapsed time.
+ template<HistogramID id>
+ class AutoTimer {
+ // Record into a plain histogram.
+ explicit AutoTimer(TimeStamp aStart = TimeStamp::Now());
+ // Record into a keyed histogram, with key |aKey|.
+ explicit AutoTimer(const nsCString& aKey,
+ TimeStamp aStart = TimeStamp::Now());
+ };
+
+ // If the Histogram id is not known at compile time:
+ class RuntimeAutoTimer {
+ // Record into a plain histogram.
+ explicit RuntimeAutoTimer(Telemetry::HistogramID aId,
+ TimeStamp aStart = TimeStamp::Now());
+ // Record into a keyed histogram, with key |aKey|.
+ explicit RuntimeAutoTimer(Telemetry::HistogramID aId,
+ const nsCString& aKey,
+ TimeStamp aStart = TimeStamp::Now());
+ };
+
+ void AccumulateTimeDelta(HistogramID id, TimeStamp start, TimeStamp end = TimeStamp::Now());
+ void AccumulateTimeDelta(HistogramID id, const nsCString& key, TimeStamp start, TimeStamp end = TimeStamp::Now());
+
+Example:
+
+.. code-block:: cpp
+
+ {
+ Telemetry::AutoTimer<Telemetry::FIND_PLUGINS> telemetry;
+ // ... scan disk for plugins.
+ }
+ // When leaving the scope, AutoTimers destructor will record the time that passed.
+
+ // If the histogram id is not known at compile time.
+ {
+ Telemetry::RuntimeAutoTimer telemetry(Telemetry::FIND_PLUGINS);
+ // ... scan disk for plugins.
+ }
+ // When leaving the scope, AutoTimers destructor will record the time that passed.
diff --git a/toolkit/components/telemetry/docs/collection/sampleHistogram.png b/toolkit/components/telemetry/docs/collection/sampleHistogram.png
new file mode 100644
index 0000000000..8bb185930a
--- /dev/null
+++ b/toolkit/components/telemetry/docs/collection/sampleHistogram.png
Binary files differ
diff --git a/toolkit/components/telemetry/docs/collection/scalars.rst b/toolkit/components/telemetry/docs/collection/scalars.rst
new file mode 100644
index 0000000000..7880cd1872
--- /dev/null
+++ b/toolkit/components/telemetry/docs/collection/scalars.rst
@@ -0,0 +1,326 @@
+=======
+Scalars
+=======
+
+A *scalar* metric can be used to track a single value. Unlike
+histograms, which collect every measurement taken, a scalar only
+tracks a single value, with later values completely replacing earlier
+ones.
+
+Historically we started to overload our histogram mechanism to also collect scalar data,
+such as flag values, counts, labels and others.
+The scalar measurement types are the suggested way to collect that kind of scalar data.
+The serialized scalar data is submitted with the :doc:`main pings <../data/main-ping>`. Adding scalars is supported in artifact builds and build faster workflows.
+
+.. important::
+
+ Every new or changed data collection in Firefox needs a `data collection review <https://wiki.mozilla.org/Firefox/Data_Collection>`__ from a Data Steward.
+
+The API
+=======
+Scalar probes can be managed either through the `nsITelemetry interface <https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/core/nsITelemetry.idl>`_
+or the `C++ API <https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/core/Telemetry.h>`_.
+
+JS API
+------
+Probes in privileged JavaScript code can use the following functions to manipulate scalars:
+
+.. code-block:: js
+
+ Services.telemetry.scalarAdd(aName, aValue);
+ Services.telemetry.scalarSet(aName, aValue);
+ Services.telemetry.scalarSetMaximum(aName, aValue);
+
+ Services.telemetry.keyedScalarAdd(aName, aKey, aValue);
+ Services.telemetry.keyedScalarSet(aName, aKey, aValue);
+ Services.telemetry.keyedScalarSetMaximum(aName, aKey, aValue);
+
+These functions can throw if, for example, an operation is performed on a scalar type that doesn't support it
+(e.g. calling scalarSetMaximum on a scalar of the string kind). Please look at the `code documentation <https://searchfox.org/mozilla-central/search?q=TelemetryScalar%3A%3A%28Set%7CAdd%29&path=TelemetryScalar.cpp&case=false&regexp=true>`_ for
+additional information.
+
+.. _registerscalars:
+
+``registerScalars()``
+~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: js
+
+ Services.telemetry.registerScalars(category, scalarData);
+
+Register new scalars from add-ons.
+
+* ``category`` - *(required, string)* The unique category the scalars are registered in (see :ref:`limitations <scalar-limitations>`).
+* ``scalarData`` - *(required, object)* An object of the form ``{scalarName1: scalar1Data, ...}`` that contains registration data for multiple scalars; ``scalarName1`` is subject to :ref:`limitations <scalar-limitations>`; each scalar is an object with the following properties:
+
+ * ``kind`` - *(required, uint)* One of the scalar types (nsITelemetry::SCALAR_TYPE_*).
+ * ``keyed`` - *(optional, bool)* Whether this is a keyed scalar or not. Defaults to false.
+ * ``record_on_release`` - *(optional, bool)* Whether to record this data on release. Defaults to false.
+ * ``expired`` - *(optional, bool)* Whether this scalar entry is expired. This allows recording it without error, but it will be discarded. Defaults to false.
+
+For scalars recorded from add-ons, registration happens at runtime. Any new scalar must first be registered through this function before they can be recorded.
+
+After registration, the scalars can be recorded through the usual scalar JS API. If the accumulation happens in a content process right after the registration and the definition still has to reach this process, it will be discarded: one way to work around the problem is to send an IPC message to the content process and start accumulating data once this message has been received. The accumulated data will be submitted in the main pings payload under ``processes.dynamic.scalars``.
+
+.. note::
+
+ Accumulating in dynamic scalars only works in content child processes and in the parent process. All the accumulations (parent and content children) are aggregated together .
+
+New scalars registered here are subject to the same :ref:`limitations <scalar-limitations>` as the ones registered through ``Scalars.yaml``, e.g. the length of the category name or the allowed characters.
+
+When add-ons are updated, they may re-register all of their scalars. In that case, any changes to scalars that are already registered are ignored. The only exception is expiry; a scalar that is re-registered with ``expired: true`` will not be recorded anymore.
+
+Example:
+
+.. code-block:: js
+
+ Services.telemetry.registerScalars("myAddon.category", {
+ "counter_scalar": {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ keyed: false,
+ record_on_release: false
+ },
+ });
+ // Now scalars can be recorded.
+ Services.telemetry.scalarSet("myAddon.category.counter_scalar", 37);
+
+
+.. _scalars-c++-API:
+
+C++ API
+-------
+Probes in native code can use the more convenient helper functions declared in `Telemetry.h <https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/core/Telemetry.h>`_:
+
+.. code-block:: cpp
+
+ void ScalarAdd(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
+ void ScalarSet(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
+ void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aValue);
+ void ScalarSet(mozilla::Telemetry::ScalarID aId, bool aValue);
+ void ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, uint32_t aValue);
+
+ void ScalarAdd(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue);
+ void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue);
+ void ScalarSet(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, bool aValue);
+ void ScalarSetMaximum(mozilla::Telemetry::ScalarID aId, const nsAString& aKey, uint32_t aValue);
+
+.. warning::
+
+ Scalar operations are designed to be cheap, not free. If you wish to manipulate Scalars in a performance-sensitive piece of code, store the operations locally and change the Scalar only after the performance-sensitive piece ("hot path") has completed.
+
+The YAML definition file
+========================
+Scalar probes are required to be registered, both for validation and transparency reasons,
+in the `Scalars.yaml <https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/Scalars.yaml>`_
+definition file.
+
+The probes in the definition file are represented in a fixed-depth, two-level structure:
+
+.. code-block:: yaml
+
+ # The following is a category.
+ a.category.hierarchy:
+ a_probe_name:
+ kind: uint
+ ...
+ another_probe:
+ kind: string
+ ...
+ ...
+ category2:
+ probe:
+ kind: int
+ ...
+
+.. _scalar-limitations:
+
+Category and probe names need to follow a few rules:
+
+- they cannot exceed 40 characters each;
+- category names must be alphanumeric + ``.``, with no leading/trailing digit or ``.``;
+- probe names must be alphanumeric + ``_``, with no leading/trailing digit or ``_``.
+
+A probe can be defined as follows:
+
+.. code-block:: yaml
+
+ a.category.hierarchy:
+ a_scalar:
+ bug_numbers:
+ - 1276190
+ description: A nice one-line description.
+ expires: never
+ kind: uint
+ notification_emails:
+ - telemetry-client-dev@mozilla.com
+
+.. _scalars-required-fields:
+
+Required Fields
+---------------
+
+- ``bug_numbers``: A list of unsigned integers representing the number of the bugs the probe was introduced in.
+- ``description``: A single or multi-line string describing what data the probe collects and when it gets collected.
+- ``expires``: The version number in which the scalar expires, e.g. "30"; a version number of type "N" is automatically converted to "N.0a1" in order to expire the scalar also in the development channels. A telemetry probe acting on an expired scalar will print a warning into the browser console. For scalars that never expire the value ``never`` can be used.
+- ``kind``: A string representing the scalar type. Allowed values are ``uint``, ``string`` and ``boolean``.
+- ``notification_emails``: A list of email addresses to notify with alerts of expiring probes. More importantly, these are used by the data steward to verify that the probe is still useful.
+- ``products``: A list of products the scalar can be recorded on. Currently supported values are:
+
+ - ``firefox`` - Collected in Firefox Desktop for submission via Firefox Telemetry.
+ - ``thunderbird`` - Collected in Thunderbird for submission via Thunderbird Telemetry.
+
+- ``record_in_processes``: A list of processes the scalar is allowed to record in. Currently supported values are:
+
+ - ``main``;
+ - ``content``;
+ - ``gpu``;
+ - ``all_children`` (record in all the child processes);
+ - ``all`` (record in all the processes).
+
+Optional Fields
+---------------
+
+- ``release_channel_collection``: This can be either ``opt-in`` (default) or ``opt-out``. With the former the scalar is submitted by default on pre-release channels, unless the user has opted out. With the latter the scalar is submitted by default on release and pre-release channels, unless the user has opted out.
+- ``keyed``: A boolean that determines whether this is a keyed scalar. It defaults to ``false``.
+- ``keys``: A string list. Only valid for *keyed scalars*. Defines a case insensitive list of allowed keys that can be used for this scalar. The list is limited to 100 keys with a maximum length of 72 characters each. When using a key that is not in the list, an error is returned.
+- ``record_into_store``: A list of stores this scalar should be recorded into. It defaults to ``[main]``.
+- ``operating_systems``: This field restricts recording to certain operating systems only. Use that in-place of previous ``cpp_guards`` to avoid inclusion on not-specified operating systems. It defaults to ``all``. Currently supported values are:
+
+ - ``mac``
+ - ``linux``
+ - ``windows``
+ - ``android``
+ - ``unix``
+ - ``all`` (record on all operating systems)
+
+String type restrictions
+------------------------
+To prevent abuses, the content of a string scalar is limited to 50 characters in length. Trying
+to set a longer string will result in an error and no string being set.
+
+.. _scalars-keyed-scalars:
+
+Keyed Scalars
+-------------
+Keyed scalars are collections of ``uint`` or ``boolean`` scalar types, indexed by a string key that can contain UTF8 characters and cannot be longer than 72 characters. Keyed scalars can contain up to 100 keys. This scalar type is for example useful when you want to break down certain counts by a name, like how often searches happen with which search engine.
+
+Keyed ``string`` scalars are not supported.
+
+Keyed scalars should only be used if the set of keys are not known beforehand. If the keys are from a known set of strings, other options are preferred if suitable, like categorical histograms or splitting measurements up into separate scalars.
+
+Multiple processes caveats
+--------------------------
+When recording data in different processes of the same type (e.g. multiple content processes), the user is responsible for preventing races between the operations on the scalars.
+Races can happen because scalar changes are sent from each child process to the parent process, and then merged into the final storage location. Since there's no synchronization between the processes, operations like ``setMaximum`` can potentially produce different results if sent from more than one child process.
+
+The processor scripts
+=====================
+The scalar definition file is processed and checked for correctness at compile time. If it
+conforms to the specification, the processor scripts generate two C++ headers files, included
+by the Telemetry C++ core.
+
+gen_scalar_data.py
+------------------
+This script is called by the build system to generate the ``TelemetryScalarData.h`` C++ header
+file out of the scalar definitions.
+This header file contains an array holding the scalar names and version strings, in addition
+to an array of ``ScalarInfo`` structures representing all the scalars.
+
+gen_scalar_enum.py
+------------------
+This script is called by the build system to generate the ``TelemetryScalarEnums.h`` C++ header
+file out of the scalar definitions.
+This header file contains an enum class with all the scalar identifiers used to access them
+from code through the C++ API.
+
+Adding a new probe
+==================
+Making a scalar measurement is a two step process:
+
+1. add the probe definition to the scalar registry;
+2. record into the scalar using the API.
+
+Registering the scalar
+----------------------
+Let's start by registering two probes in the `Scalars.yaml <https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/Scalars.yaml>`_ definition file: a simple boolean scalar and a keyed unsigned scalar.
+
+.. code-block:: yaml
+
+ # The following section contains the demo scalars.
+ profile:
+ was_reset:
+ bug_numbers:
+ - 1301364
+ description: True if the profile was reset.
+ expires: "60"
+ kind: boolean
+ notification_emails:
+ - change-me@allizom.com
+ release_channel_collection: opt-out
+ record_in_processes:
+ - 'main'
+
+ ui:
+ download_button_activated:
+ bug_numbers:
+ - 1301364
+ description: >
+ The number of times the download button was activated, per
+ input type (e.g. 'mouse_click', 'touchscreen', ...).
+ expires: "60"
+ kind: uint
+ keyed: true
+ notification_emails:
+ - change-me@allizom.com
+ release_channel_collection: opt-in
+ record_in_processes:
+ - 'main'
+
+These two scalars have different collection policies and are both constrained to recording only in the main process.
+For example, the ``ui.download_button_activated`` can be recorded only by users on running pre-release builds of Firefox.
+
+Using the JS API
+----------------
+Changing the demo scalars from privileged JavaScript code is straightforward:
+
+.. code-block:: js
+
+ // Set the scalar value: trying to use a non-boolean value doesn't throw
+ // but rather prints a warning to the browser console
+ Services.telemetry.scalarSet("profile.was_reset", true);
+
+ // This call increments the value stored in "mouse_click" within the
+ // "ui.download_button_activated" scalar, by 1.
+ Services.telemetry.keyedScalarAdd("ui.download_button_activated", "mouse_click", 1);
+
+More usage examples can be found in the tests covering the `JS Scalars API <https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/tests/unit/test_TelemetryScalars.js>`_ and `child processes scalars <https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/tests/unit/test_ChildScalars.js>`_.
+
+Using the C++ API
+-----------------
+Native code can take advantage of Scalars as well, by including the ``Telemetry.h`` header file.
+
+.. code-block:: cpp
+
+ Telemetry::ScalarSet(Telemetry::ScalarID::PROFILE_WAS_RESET, false);
+
+ Telemetry::ScalarAdd(Telemetry::ScalarID::UI_DOWNLOAD_BUTTON_ACTIVATED,
+ u"touchscreen"_ns, 1);
+
+The ``ScalarID`` enum is automatically generated by the build process, with an example being available `here <https://searchfox.org/mozilla-central/search?q=path%3ATelemetryScalarEnums.h&redirect=false>`_ .
+
+Other examples can be found in the `test coverage <https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/tests/gtest/TestScalars.cpp>`_ for the scalars C++ API.
+
+Version History
+===============
+
+- Firefox 79: ``geckoview`` support removed (see `bug 1620395 <https://bugzilla.mozilla.org/show_bug.cgi?id=1620395>`__).
+- Firefox 50: Initial scalar support (`bug 1276195 <https://bugzilla.mozilla.org/show_bug.cgi?id=1276195>`_).
+- Firefox 51: Added keyed scalars (`bug 1277806 <https://bugzilla.mozilla.org/show_bug.cgi?id=1277806>`_).
+- Firefox 53: Added child process scalars (`bug 1278556 <https://bugzilla.mozilla.org/show_bug.cgi?id=1278556>`_).
+- Firefox 58
+
+ - Added support for recording new scalars from add-ons (`bug 1393801 <bug https://bugzilla.mozilla.org/show_bug.cgi?id=1393801>`_).
+ - Ignore re-registering existing scalars for a category instead of failing (`bug 1409323 <https://bugzilla.mozilla.org/show_bug.cgi?id=1409323>`_).
+
+- Firefox 60: Enabled support for adding scalars in artifact builds and build-faster workflows (`bug 1425909 <https://bugzilla.mozilla.org/show_bug.cgi?id=1425909>`_).
+- Firefox 66: Replace ``cpp_guard`` with ``operating_systems`` (`bug 1482912 <https://bugzilla.mozilla.org/show_bug.cgi?id=1482912>`_)`
diff --git a/toolkit/components/telemetry/docs/collection/uptake.rst b/toolkit/components/telemetry/docs/collection/uptake.rst
new file mode 100644
index 0000000000..b9ee803c30
--- /dev/null
+++ b/toolkit/components/telemetry/docs/collection/uptake.rst
@@ -0,0 +1,114 @@
+.. _telemetry/collection/uptake:
+
+================
+Uptake Telemetry
+================
+
+Firefox continuously pulls data from different remote sources (eg. settings, system add-ons, …). In order to have consistent insights about the *uptake rate* of these *update sources*, our clients can use a unified Telemetry helper to report their *update status*.
+
+The helper — described below — reports predefined update status, which eventually gives a unified way to obtain:
+
+* the proportion of success among clients;
+* its evolution over time;
+* the distribution of error causes.
+
+.. note::
+
+ Examples of update sources: *remote settings, add-ons update, add-ons, gfx, and plugins blocklists, certificate revocation, certificate pinning, system add-ons delivery…*
+
+ Examples of update status: *up-to-date, success, network error, server error, signature error, server backoff, unknown error…*
+
+Every call to the UptakeTelemetry helper may send a :ref:`Telemetry Event <eventtelemetry>`. Because events are expensive, we take some measures to avoid overwhelming Mozilla systems with the flood of data that this produces. We always send events when not on release channel. On release channel, we only send events from 1% of clients.
+
+Usage
+-----
+
+.. code-block:: js
+
+ const { UptakeTelemetry } = ChromeUtils.import("resource://services-common/uptake-telemetry.js", {});
+
+ UptakeTelemetry.report(component, status, { source });
+
+- ``component``, a ``string`` that identifies the calling component (eg. ``"remotesettings"``, ``"normandy"``). Arbitrary components have to be previously declared in the :ref:`Telemetry Events definition file <eventdefinition>`.
+- ``source``, a ``string`` to distinguish what is being pulled or updated in the component (eg. ``"blocklists/addons"``, ``"recipes/33"``)
+- ``status``, one of the following status constants:
+
+ - ``UptakeTelemetry.STATUS.UP_TO_DATE``: Local content was already up-to-date with remote content.
+ - ``UptakeTelemetry.STATUS.SUCCESS``: Local content was updated successfully.
+ - ``UptakeTelemetry.STATUS.BACKOFF``: Remote server asked clients to backoff.
+ - ``UptakeTelemetry.STATUS.PREF_DISABLED``: Update is disabled in user preferences.
+ - ``UptakeTelemetry.STATUS.PARSE_ERROR``: Parsing server response has failed.
+ - ``UptakeTelemetry.STATUS.CONTENT_ERROR``: Server response has unexpected content.
+ - ``UptakeTelemetry.STATUS.CORRUPTION_ERROR``: Error related to corrupted local data.
+ - ``UptakeTelemetry.STATUS.SIGNATURE_ERROR``: Signature verification after diff-based sync has failed.
+ - ``UptakeTelemetry.STATUS.SIGNATURE_RETRY_ERROR``: Signature verification after full fetch has failed.
+ - ``UptakeTelemetry.STATUS.CONFLICT_ERROR``: Some remote changes are in conflict with local changes.
+ - ``UptakeTelemetry.STATUS.SYNC_ERROR``: Synchronization of remote changes has failed.
+ - ``UptakeTelemetry.STATUS.APPLY_ERROR``: Application of changes locally has failed.
+ - ``UptakeTelemetry.STATUS.SERVER_ERROR``: Server failed to respond.
+ - ``UptakeTelemetry.STATUS.CERTIFICATE_ERROR``: Server certificate verification has failed.
+ - ``UptakeTelemetry.STATUS.DOWNLOAD_ERROR``: Data could not be fully retrieved.
+ - ``UptakeTelemetry.STATUS.TIMEOUT_ERROR``: Server response has timed out.
+ - ``UptakeTelemetry.STATUS.NETWORK_ERROR``: Communication with server has failed.
+ - ``UptakeTelemetry.STATUS.NETWORK_OFFLINE_ERROR``: Network not available.
+ - ``UptakeTelemetry.STATUS.CLEANUP_ERROR``: Clean-up of temporary files has failed.
+ - ``UptakeTelemetry.STATUS.SHUTDOWN_ERROR``: Error occurring during shutdown.
+ - ``UptakeTelemetry.STATUS.UNKNOWN_ERROR``: Uncategorized error.
+ - ``UptakeTelemetry.STATUS.CUSTOM_1_ERROR``: Error #1 specific to this update source.
+ - ``UptakeTelemetry.STATUS.CUSTOM_2_ERROR``: Error #2 specific to this update source.
+ - ``UptakeTelemetry.STATUS.CUSTOM_3_ERROR``: Error #3 specific to this update source.
+ - ``UptakeTelemetry.STATUS.CUSTOM_4_ERROR``: Error #4 specific to this update source.
+ - ``UptakeTelemetry.STATUS.CUSTOM_5_ERROR``: Error #5 specific to this update source.
+
+Example:
+
+.. code-block:: js
+
+ const COMPONENT = "normandy";
+ const UPDATE_SOURCE = "update-monitoring";
+
+ let status;
+ try {
+ const data = await fetch(uri);
+ status = UptakeTelemetry.STATUS.SUCCESS;
+ } catch (e) {
+ status = /NetworkError/.test(e) ?
+ UptakeTelemetry.STATUS.NETWORK_ERROR :
+ UptakeTelemetry.STATUS.SERVER_ERROR ;
+ }
+ UptakeTelemetry.report(COMPONENT, status, { source: UPDATE_SOURCE });
+
+
+Additional Event Info
+'''''''''''''''''''''
+
+Events sent using the telemetry events API can contain additional information. Uptake Telemetry allows you to add the following extra fields to events by adding them to the ``options`` argument:
+
+- ``trigger``: A label to distinguish what triggered the polling/fetching of remote content (eg. ``"broadcast"``, ``"timer"``, ``"forced"``, ``"manual"``)
+- ``age``: The age of pulled data in seconds (ie. difference between publication time and fetch time).
+- ``duration``: The duration of the synchronization process in milliseconds.
+
+.. code-block:: js
+
+ UptakeTelemetry.report(component, status, { source, trigger: "timer", age: 138 });
+
+Remember that events are sampled on release channel. Those calls to uptake telemetry that do not produce events will ignore these extra fields.
+
+
+Use-cases
+---------
+
+The following remote data sources are already using this unified histogram.
+
+* remote settings changes monitoring
+* add-ons blocklist
+* gfx blocklist
+* plugins blocklist
+* certificate revocation
+* certificate pinning
+* :ref:`Normandy Recipe client <components/normandy>`
+
+Obviously, the goal is to eventually converge and avoid ad-hoc Telemetry probes for measuring uptake of remote content. Some notable potential use-cases are:
+
+* nsUpdateService
+* mozapps extensions update
diff --git a/toolkit/components/telemetry/docs/collection/user-interactions.rst b/toolkit/components/telemetry/docs/collection/user-interactions.rst
new file mode 100644
index 0000000000..16d1d53977
--- /dev/null
+++ b/toolkit/components/telemetry/docs/collection/user-interactions.rst
@@ -0,0 +1,272 @@
+.. _userinteractionstelemetry:
+
+=================
+User Interactions
+=================
+
+The Background Hang Reporter is a tool that collects stacks during hangs on pre-release channels.
+User Interactions are a way of annotating Background Hang Reports with additional information about what the user was doing when a hang occurs.
+This allows for grouping and prioritization of hangs based on the user interactions that they occur during.
+
+Since the built-in profiler is often the first tool that developers reach for to debug performance issues,
+User Interactions also will add profiler markers for each recording.
+
+.. important::
+
+ Every new or changed data collection in Firefox needs a `data collection review <https://wiki.mozilla.org/Firefox/Data_Collection>`__ from a Data Steward.
+
+.. _userinteractionsserializationformat:
+
+Serialization format
+====================
+
+User Interactions are submitted in a :doc:`../data/backgroundhangmonitor-ping` as a property under the `annotations` for a hang, e.g.:
+
+.. code-block:: js
+
+ ...
+ {
+ "duration": 105.547582,
+ // ...
+ "annotations": [
+ ["UserInteracting", "true"]
+ ["browser.tabs.opening", "animated"]
+ ],
+ "stack": [
+ "XREMain::XRE_main",
+ "-[GeckoNSApplication nextEventMatchingMask:untilDate:inMode:dequeue:]",
+ "nsAppShell::ProcessGeckoEvents",
+ "nsRefreshDriver::Tick",
+ "EventDispatcher::Dispatch",
+ "EventDispatcher::Dispatch",
+ "",
+ "browser/content/tabbrowser-tabs.js:1699",
+ "browser/content/tabbrowser-tabs.js:1725",
+ "browser/content/tabbrowser-tabs.js:142",
+ "browser/content/tabbrowser-tabs.js:153",
+ "(jit frame)",
+ "(unresolved)",
+ [
+ 1,
+ "418de17"
+ ],
+ [
+ 1,
+ "418de91"
+ ],
+ [
+ 1,
+ "4382e56"
+ ],
+ [
+ 8,
+ "108e3"
+ ],
+ [
+ 9,
+ "2624"
+ ],
+ [
+ 9,
+ "129f"
+ ]
+ ]
+ // ...
+ },
+
+Each User Interaction is of the form:
+
+.. code-block:: js
+
+ ["User Interaction ID", "value"]
+
+A `User Interaction ID` is its category concatenated with its name.
+For example, a User Interaction with category `browser.tabs` and name `opening` has an ID of `browser.tabs.opening`.
+
+.. _userinteractionslimits:
+
+Limits
+------
+
+Each ``String`` marked as an identifier (the User Interaction ``name``, ``category``, ``value``) is restricted to be composed of alphanumeric ASCII characters ([a-zA-Z0-9]) plus infix underscores ('_' characters that aren't the first or last).
+``category`` is also permitted infix periods ('.' characters, so long as they aren't the first or last character).
+
+Several fields are subject to length limits:
+
+- ``category``: Max. byte length is ``40``.
+- ``User Interaction`` name: Max. byte length is ``40``.
+- ``value``: A UTF-8 string with max. byte length of ``50``.
+
+Any ``String`` going over its limit will be reported as an error and the operation aborted.
+
+
+.. _userinteractionsdefinition:
+
+The YAML definition file
+========================
+
+Any User Interaction recorded into Firefox Telemetry must be registered before it can be recorded.
+For any code that ships as part of Firefox that happens in `UserInteractions.yaml <https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/UserInteractions.yaml>`_.
+
+The User Interactions in the definition file are represented in a fixed-depth, three-level structure.
+The first level contains *category* names (grouping multiple User Interactions together),
+the second level contains User Interaction IDs, under which the User Interaction properties are listed. E.g.:
+
+.. code-block:: yaml
+
+ # The following is a category of User Interactions named "browser.tabs".
+ browser.tabs:
+ opening: # This is the name of the User Interaction. The ID for the
+ # User Interaction is browser.tabs.opening
+ description: >
+ Describes this User Interaction in detail, potentially over
+ multiple lines.
+ # ... and more User Interaction properties.
+ # ... and more User Interactions.
+ # This is the "browser.places" category.
+ browser.places:
+ # And the "history" search User Interaction. Its User Interaction ID is
+ # browser.places.history_async
+ history_async:
+ # ...
+ description: Session History is searched asynchronously.
+ # ... and more User Interaction properties.
+ # ...
+
+Category and User Interaction names are subject to the limits :ref:`specified above <userinteractionslimits>`.
+
+
+Profiler markers
+================
+
+The profiler markers automatically added for each User Interaction will have a starting point and ending point corresponding with the recording of the User Interaction.
+The name of the marker will be the User Interaction category plus the User Interaction ID.
+The value of the marker will be the value passed through the `UserInteraction` API, plus any additional text that is optionally added when the recording is finished.
+
+Further details on what the profiler is and what profiler markers are can be found `here <https://profiler.firefox.com/docs/#/>`_.
+
+
+The API
+=======
+
+Public JS API
+-------------
+
+This API is main-thread only, and all functions will return `false` if accessed off of the main thread.
+
+``start()``
+~~~~~~~~~~~~~~~~~
+
+.. code-block:: js
+
+ UserInteraction.start(id, value, object);
+
+Starts recording a User Interaction.
+Any hangs that occur on the main thread while recording this User Interaction result in an annotation being added to the background hang report.
+
+If a pre-existing UserInteraction already exists with the same ``id`` and the same ``object``, that pre-existing UserInteraction will be overwritten.
+The newly created UserInteraction will include a "(clobbered)" suffix on its BHR annotation name.
+
+* ``id``: Required. A string value, limited to 80 characters. This is the category name concatenated with the User Interaction name.
+* ``value``: Required. A string value, limited to 50 characters.
+* ``object``: Optional. If specified, the User Interaction is associated with this object, so multiple recordings can be done concurrently.
+
+Example:
+
+.. code-block:: js
+
+ UserInteraction.start("browser.tabs.opening", "animated", window1);
+ UserInteraction.start("browser.tabs.opening", "animated", window2);
+
+Returns `false` and logs a message to the browser console if the recording does not start for some reason.
+
+Example:
+
+.. code-block:: js
+
+ UserInteraction.start("browser.tabs.opening", "animated", window);
+ UserInteraction.start("browser.places.history_search", "synchronous");
+
+``update()``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: js
+
+ UserInteraction.update(id, value, object);
+
+Updates a User Interaction that's already being recorded with a new value.
+Any hangs that occur on the main thread will be annotated using the new value.
+Updating only works for User Interactions that are already being recorded.
+
+* ``id``: Required. A string value, limited to 80 characters. This is the category name concatenated with the User Interaction name.
+* ``value``: Required. The new string value, limited to 50 characters.
+* ``object``: Optional. If specified, the User Interaction is associated with this object, so multiple recordings can be done concurrently.
+
+Returns `false` and logs a message to the browser console if the update cannot be done for some reason.
+
+
+Example:
+
+.. code-block:: js
+
+ // At this point, we don't know if the tab will open with animation
+ // or not.
+ UserInteraction.start("browser.tabs.opening", "initting", window);
+ // ...
+ if (animating) {
+ UserInteraction.update("browser.tabs.opening", "animating", window);
+ } else {
+ UserInteraction.update("browser.tabs.opening", "not-animating", window);
+ }
+
+``cancel()``
+~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: js
+
+ UserInteraction.cancel(id, object);
+
+Cancels a recording User Interaction.
+No profiler marker will be added in this case, and no further hangs will be annotated.
+Hangs that occurred before the User Interaction was cancelled will not, however, be expunged.
+
+* ``id``: Required. A string value, limited to 80 characters. This is the category name concatenated with the User Interaction name.
+* ``object``: Optional. If specified, the User Interaction is associated with this object, so multiple recordings can be done concurrently.
+
+Returns `false` and logs a message to the browser console if the cancellation cannot be completed for some reason.
+
+``running()``
+~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: js
+
+ UserInteraction.running(id, object);
+
+Checks to see if a UserInteraction is already running.
+
+* ``id``: Required. A string value, limited to 80 characters. This is the category name concatenated with the User Interaction name.
+* ``object``: Optional. If specified, the User Interaction is associated with this object, so multiple recordings can be done concurrently. If you're checking for a running timer that was started with an object, you'll need to pass in that same object here to check its running state.
+
+Returns `true` if a UserInteraction is already running.
+
+``finish()``
+~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: js
+
+ UserInteraction.finish(id, object, additionalText);
+
+Finishes recording the User Interaction.
+Any hangs that occur on the main thread will no longer be annotated with this User Interaction.
+A profiler marker will also be added, starting at the `UserInteraction.start` point and ending at the `UserInteraction.finish` point, along with any additional text that the author wants to include.
+
+* ``id``: Required. A string value, limited to 80 characters. This is the category name concatenated with the User Interaction name.
+* ``object``: Optional. If specified, the User Interaction is associated with this object, so multiple recordings can be done concurrently.
+* ``additionalText``: Optional. If specified, the profile marker will have this text appended to the `value`, separated with a comma.
+
+Returns `false` and logs a message to the browser console if finishing cannot be completed for some reason.
+
+Version History
+===============
+
+- Firefox 84: Initial User Interaction support (see `bug 1661304 <https://bugzilla.mozilla.org/show_bug.cgi?id=1661304>`__).
diff --git a/toolkit/components/telemetry/docs/collection/webextension-api.rst b/toolkit/components/telemetry/docs/collection/webextension-api.rst
new file mode 100644
index 0000000000..a3e73e11fa
--- /dev/null
+++ b/toolkit/components/telemetry/docs/collection/webextension-api.rst
@@ -0,0 +1,158 @@
+.. _webextension-telemetry:
+
+==============================
+WebExtension API for Telemetry
+==============================
+
+Use the ``browser.telemetry`` API to send telemetry data to the Mozilla Telemetry service. Restricted to Mozilla privileged webextensions.
+
+Types
+-----
+
+``ScalarType``
+~~~~~~~~~~~~~~
+
+Type of scalar: 'count' for numeric values, 'string' for string values, 'boolean' for boolean values. Maps to ``nsITelemetry.SCALAR_TYPE_*``.
+
+``ScalarData``
+~~~~~~~~~~~~~~
+
+Represents registration data for a Telemetry scalar.
+
+Properties:
+
+* ``kind`` - See ScalarType_.
+* ``keyed`` - *(optional, boolean)* True if this is a keyed scalar. Defaults to ``false``.
+* ``record_on_release`` - *(optional, boolean)* True if this data should be recorded on release. Defaults to ``false``.
+* ``expired`` - *(optional, boolean)* True if this scalar entry is expired. Operations on an expired scalar don't error (operations on an undefined scalar do), but the operations are no-ops. No data will be recorded. Defaults to ``false``.
+
+``EventData``
+~~~~~~~~~~~~~
+
+Represents registration data for a Telemetry event.
+
+Properties:
+
+* ``methods`` - *(array)* List of methods for this event entry.
+* ``objects`` - *(array)* List of objects for this event entry.
+* ``extra_keys`` - *(array)* List of allowed extra keys for this event entry.
+* ``record_on_release`` - *(optional, boolean)* True if this data should be recorded on release. Defaults to ``false``.
+* ``expired`` - *(optional, boolean)* True if this event entry is expired. Recording an expired event doesn't error (operations on undefined events do). No data will be recorded. Defaults to ``false``.
+
+Functions
+---------
+
+``submitPing``
+~~~~~~~~~~~~~~
+
+.. code-block:: js
+
+ browser.telemetry.submitPing(type, message, options);
+
+Submits a custom ping to the Telemetry backend. See :ref:`submitting-customping`.
+
+* ``type`` - *(string)* The type of the ping.
+* ``message`` - *(object)* The data payload for the ping.
+* ``options`` - *(optional, object)* Options object.
+
+ * ``addClientId`` - *(optional, boolean)* True if the ping should contain the client id. Defaults to ``false``.
+ * ``addEnvironment`` - *(optional, boolean)* True if the ping should contain the environment data. Defaults to ``false``.
+ * ``overrideEnvironment`` - *(optional, object)* Set to override the environment data. Default: not set.
+ * ``usePingSender`` - *(optional, boolean)* If true, send the ping using the PingSender. Defaults to ``false``.
+
+
+``canUpload``
+~~~~~~~~~~~~~
+
+.. code-block:: js
+
+ browser.telemetry.canUpload();
+
+Checks if Telemetry upload is enabled.
+
+``scalarAdd``
+~~~~~~~~~~~~~
+
+.. code-block:: js
+
+ browser.telemetry.scalarAdd(name, value);
+
+Adds the value to the given scalar.
+
+* ``name`` - *(string)* The scalar name.
+* ``value`` - *(integer)* The numeric value to add to the scalar. Only unsigned integers supported.
+
+``scalarSet``
+~~~~~~~~~~~~~
+
+.. code-block:: js
+
+ browser.telemetry.scalarSet(name, value);
+
+Sets the named scalar to the given value. Throws if the value type doesn't match the scalar type.
+
+* ``name`` - *(string)* The scalar name.
+* ``value`` - *(string|boolean|integer|object)* The value to set the scalar to.
+
+``scalarSetMaximum``
+~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: js
+
+ browser.telemetry.scalarSetMaximum(name, value);
+
+Sets the scalar to the maximum of the current and the passed value
+
+* ``name`` - *(string)* The scalar name.
+* ``value`` - *(integer)* The numeric value to set the scalar to. Only unsigned integers supported.
+
+``recordEvent``
+~~~~~~~~~~~~~~~
+
+.. code-block:: js
+
+ browser.telemetry.recordEvent(category, method, object, value, extra);
+
+Record an event in Telemetry. Throws when trying to record an unknown event.
+
+* ``category`` - *(string)* The category name.
+* ``method`` - *(string)* The method name.
+* ``object`` - *(string)* The object name.
+* ``value`` - *(optional, string)* An optional string value to record.
+* ``extra`` - *(optional, object)* An optional object of the form (string -> string). It should only contain registered extra keys.
+
+``registerScalars``
+~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: js
+
+ browser.telemetry.registerScalars(category, data);
+
+Register new scalars to record them from addons. See :ref:`registerscalars` for more details.
+
+* ``category`` - *(string)* The unique category the scalars are registered in.
+* ``data`` - *(object)* An object that contains registration data for multiple scalars. Each property name is the scalar name, and the corresponding property value is an object of ScalarData_ type.
+
+``registerEvents``
+~~~~~~~~~~~~~~~~~~
+
+.. code-block:: js
+
+ browser.telemetry.registerEvents(category, data);
+
+Register new events to record them from addons. See :ref:`registerevents` for more details.
+
+* ``category`` - *(string)* The unique category the events are registered in.
+* ``data`` - *(object)* An object that contains registration data for 1+ events. Each property name is the category name, and the corresponding property value is an object of EventData_ type.
+
+``setEventRecordingEnabled``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: js
+
+ browser.telemetry.setEventRecordingEnabled(category, enabled);
+
+Enable recording of events in a category. Events default to recording disabled. This allows to toggle recording for all events in the specified category.
+
+* ``category`` - *(string)* The category name.
+* ``enabled`` - *(boolean)* Whether recording is enabled for events in that category.
diff --git a/toolkit/components/telemetry/docs/concepts/archiving.rst b/toolkit/components/telemetry/docs/concepts/archiving.rst
new file mode 100644
index 0000000000..0466f13769
--- /dev/null
+++ b/toolkit/components/telemetry/docs/concepts/archiving.rst
@@ -0,0 +1,23 @@
+=========
+Archiving
+=========
+
+When archiving is enabled through the relevant pref (``toolkit.telemetry.archive.enabled``), pings submitted to ``TelemetryController`` are also stored locally in the user profile directory, in ``<profile-dir>/datareporting/archived``.
+
+To allow for cheaper lookup of archived pings, storage follows a specific naming scheme for both the directory and the ping file name: `<YYYY-MM>/<timestamp>.<UUID>.<type>.jsonlz4`.
+
+* ``<YYYY-MM>`` - The subdirectory name, generated from the ping creation date.
+* ``<timestamp>`` - Timestamp of the ping creation date.
+* ``<UUID>`` - The ping identifier.
+* ``<type>`` - The ping type.
+
+Archived data can be viewed on ``about:telemetry``.
+
+Cleanup
+-------
+
+Archived pings are not kept around forever.
+After startup of Firefox and initialization of Telemetry, the archive is cleaned up if necessary.
+
+* Old ping data is removed by month if it is older than 60 days.
+* If the total size of the archive exceeds the quota of 120 MB, pings are removed to reduce the size of the archive again.
diff --git a/toolkit/components/telemetry/docs/concepts/crashes.rst b/toolkit/components/telemetry/docs/concepts/crashes.rst
new file mode 100644
index 0000000000..4ea40f89c4
--- /dev/null
+++ b/toolkit/components/telemetry/docs/concepts/crashes.rst
@@ -0,0 +1,25 @@
+=======
+Crashes
+=======
+
+There are many different kinds of crashes for Firefox, there is not a single system used to record all of them.
+
+Main process crashes
+====================
+
+If the Firefox main process dies, that should be recorded as an aborted session. We would submit a :doc:`main ping <../data/main-ping>` with the reason ``aborted-session``.
+If we have a crash dump for that crash, we should also submit a :doc:`crash ping <../data/crash-ping>`.
+
+The ``aborted-session`` information is first written to disk 60 seconds after startup, any earlier crashes will not trigger an ``aborted-session`` ping.
+Also, the ``aborted-session`` is updated at least every 5 minutes, so it may lag behind the last session state.
+
+Crashes during startup should be recorded in the next sessions main ping in the ``STARTUP_CRASH_DETECTED`` histogram.
+
+Child process crashes
+=====================
+
+If a Firefox plugin, content, gmplugin, or any other type of child process dies unexpectedly, this is recorded in the main ping's ``SUBPROCESS_ABNORMAL_ABORT`` keyed histogram.
+
+If we catch a crash report for this, then additionally the ``SUBPROCESS_CRASHES_WITH_DUMP`` keyed histogram is incremented.
+
+Some processes also generate :doc:`crash pings <../data/crash-ping>` when they crash and generate a crash dump. See `bug 1352496 <https://bugzilla.mozilla.org/show_bug.cgi?id=1352496>`_ for an example of how to allow crash pings for new process types.
diff --git a/toolkit/components/telemetry/docs/concepts/index.rst b/toolkit/components/telemetry/docs/concepts/index.rst
new file mode 100644
index 0000000000..a49466f8d0
--- /dev/null
+++ b/toolkit/components/telemetry/docs/concepts/index.rst
@@ -0,0 +1,23 @@
+========
+Concepts
+========
+
+There are common concepts used throughout Telemetry:
+
+* :doc:`pings <pings>` - the packets we use to submit data
+* :doc:`sessions & subsessions <sessions>` - how we slice a users' time in the browser
+* *measurements* - how we :doc:`collect data <../collection/index>`
+* *opt-in* & *opt-out* - the different sets of data we collect
+* :doc:`submission <submission>` - how we send data to the servers
+* :doc:`archiving <archiving>` - retaining ping data locally
+* :doc:`crashes <crashes>` - the different data crashes generate
+
+.. toctree::
+ :maxdepth: 2
+ :titlesonly:
+ :glob:
+ :hidden:
+
+ pings
+ crashes
+ *
diff --git a/toolkit/components/telemetry/docs/concepts/pings.rst b/toolkit/components/telemetry/docs/concepts/pings.rst
new file mode 100644
index 0000000000..092f7b13fa
--- /dev/null
+++ b/toolkit/components/telemetry/docs/concepts/pings.rst
@@ -0,0 +1,29 @@
+.. _telemetry_pings:
+
+=====================
+Telemetry pings
+=====================
+
+A *Telemetry ping* is the data that we send to Mozilla's Telemetry servers.
+
+The top-level structure is defined by the :doc:`common ping format <../data/common-ping>`. This is a JSON object which contains:
+
+* some basic information shared between different ping types
+* the :doc:`environment data <../data/environment>` (optional)
+* the data specific to the *ping type*, the *payload*.
+
+Ping types
+==========
+
+We send Telemetry with different ping types. The :doc:`main <../data/main-ping>` ping is the ping that contains the bulk of the Telemetry measurements for Firefox. For more specific use-cases, we send other ping types.
+
+Pings sent from code that ships with Firefox are listed in the :doc:`data documentation <../data/index>`.
+
+Important examples are:
+
+* :doc:`main <../data/main-ping>` - contains the information collected by Telemetry (Histograms, Scalars, ...)
+* :doc:`saved-session <../data/main-ping>` - has the same format as a main ping, but it contains the *"classic"* Telemetry payload with measurements covering the whole browser session. This is only a separate type to make storage of saved-session easier server-side. As of Firefox 61 this is sent on Android only.
+* :doc:`crash <../data/crash-ping>` - a ping that is captured and sent after a Firefox process crashes.
+* :doc:`new-profile <../data/new-profile-ping>` - sent on the first run of a new profile.
+* :doc:`update <../data/update-ping>` - sent right after an update is downloaded.
+* :doc:`deletion-request <../data/deletion-request-ping>` - sent when FHR upload is disabled
diff --git a/toolkit/components/telemetry/docs/concepts/sessions.rst b/toolkit/components/telemetry/docs/concepts/sessions.rst
new file mode 100644
index 0000000000..39d6df961d
--- /dev/null
+++ b/toolkit/components/telemetry/docs/concepts/sessions.rst
@@ -0,0 +1,37 @@
+========
+Sessions
+========
+
+A *session* is the time from when Firefox starts until it shuts down.
+A session can be very long-running. E.g. for users that always put their computers into sleep-mode, Firefox may run for weeks.
+We slice the sessions into smaller logical units called *subsessions*.
+
+Subsessions
+===========
+
+The first subsession starts when the browser starts. After that, we split the subsession for different reasons:
+
+* ``daily``, when crossing local midnight. This keeps latency acceptable by triggering a ping at least daily for most active users.
+* ``environment-change``, when a change to the *environment* happens. This happens for important changes to the Firefox settings and when add-ons activate or deactivate.
+
+On a subsession split, a :doc:`main ping <../data/main-ping>` with that reason will be submitted. We store the reason in the pings payload, to see what triggered it.
+
+A session always ends with a subsession with one of two reason:
+
+* ``shutdown``, when the browser was cleanly shut down. To avoid delaying shutdown, we only save this ping to disk and send it at the next opportunity (typically the next browsing session).
+* ``aborted-session``, when the browser crashed. While Firefox is active, we write the current ``main`` ping data to disk every 5 minutes. If the browser crashes, we find this data on disk on the next start and send it with this reason.
+
+.. image:: subsession_triggers.png
+
+Subsession data
+===============
+
+A subsessions data consists of:
+
+* general information: the date the subsession started, how long it lasted, etc.
+* specific measurements: histogram & scalar data, etc.
+
+This has some advantages:
+
+* Latency - Sending a ping with all the data of a subsession immediately after it ends means we get the data from installs faster. For ``main`` pings, we aim to send a ping at least daily by starting a new subsession at local midnight.
+* Correlation - By starting new subsessions when fundamental settings change (i.e. changes to the *environment*), we can better correlate a subsession's data to those settings.
diff --git a/toolkit/components/telemetry/docs/concepts/submission.rst b/toolkit/components/telemetry/docs/concepts/submission.rst
new file mode 100644
index 0000000000..6f3ba3b0f0
--- /dev/null
+++ b/toolkit/components/telemetry/docs/concepts/submission.rst
@@ -0,0 +1,42 @@
+==========
+Submission
+==========
+
+*Note:* The server-side behaviour is documented in the `HTTP Edge Server specification <https://wiki.mozilla.org/CloudServices/DataPipeline/HTTPEdgeServerSpecification>`_.
+
+Pings are submitted via a common API on ``TelemetryController``.
+If a ping fails to successfully submit to the server immediately (e.g. because
+of missing internet connection), Telemetry will store it on disk and retry to
+send it until the maximum ping age is exceeded (14 days).
+
+.. note::
+
+ The :doc:`main pings <../data/main-ping>` are kept locally even after successful submission to enable the HealthReport feature. They will be deleted after their retention period of 180 days.
+
+Submission logic
+================
+
+Sending of pending pings starts as soon as the delayed startup is finished. They are sent in batches, newest-first, with up
+to 10 persisted pings per batch plus all unpersisted pings.
+The send logic then waits for each batch to complete.
+
+If it succeeds we trigger the next send of a ping batch. This is delayed as needed to only trigger one batch send per minute.
+
+If ping sending encounters an error that means retrying later, a backoff timeout behavior is
+triggered, exponentially increasing the timeout for the next try from 1 minute up to a limit of 120 minutes.
+Any new ping submissions and "idle-daily" events reset this behavior as a safety mechanism and trigger immediate ping sending.
+
+Pingsender
+==========
+Some pings (e.g. :doc:`crash pings <../data/crash-ping>` and :doc:`main pings <../data/main-ping>` with reason `shutdown`) are submitted using the :doc:`../internals/pingsender`.
+
+The pingsender tries to send each ping once and, if it fails, no additional attempt is performed: ``TelemetrySend`` will take care of retrying using the previously described submission logic.
+
+Status codes
+============
+
+The telemetry server team is working towards `the common services status codes <https://wiki.mozilla.org/CloudServices/DataPipeline/HTTPEdgeServerSpecification#Server_Responses>`_, but for now the following logic is sufficient for Telemetry:
+
+* `2XX` - success, don't resubmit
+* `4XX` - there was some problem with the request - the client should not try to resubmit as it would just receive the same response
+* `5XX` - there was a server-side error, the client should try to resubmit later
diff --git a/toolkit/components/telemetry/docs/concepts/subsession_triggers.png b/toolkit/components/telemetry/docs/concepts/subsession_triggers.png
new file mode 100644
index 0000000000..0a1dae2c23
--- /dev/null
+++ b/toolkit/components/telemetry/docs/concepts/subsession_triggers.png
Binary files differ
diff --git a/toolkit/components/telemetry/docs/data/addons-malware-ping.rst b/toolkit/components/telemetry/docs/data/addons-malware-ping.rst
new file mode 100644
index 0000000000..18502d7489
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/addons-malware-ping.rst
@@ -0,0 +1,42 @@
+
+Add-ons malware ping
+====================
+
+This ping is generated by an add-on created by Mozilla and shipped to users on older versions of Firefox (44-46). The ping contains information about the profile that might have been altered by a third party malicious add-on.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ type: "malware-addon-states",
+ ...
+ clientId: <UUID>,
+ environment: { ... },
+ // Common ping data.
+ payload: {
+ // True if the blocklist was disabled at startup time.
+ blocklistDisabled: <bool>,
+ // True if the malicious add-on exists and is enabled. False if it
+ // exists and is disabled or null if the add-on was not found.
+ mainAddonActive: <bool | null>,
+ // A value of the malicious add-on block list state, or null if the
+ // add-on was not found.
+ mainAddonBlocked: <int | null>,
+ // True if a malicious user.js file was found in the profile.
+ foundUserJS: <bool>,
+ // If a malicious secmodd.db file was found the extension ID that the // file contained..
+ secmoddAddon: <string | null>, .
+ // A list of IDs for extensions which were hidden by malicious CSS.
+ hiddenAddons: [
+ <string>,
+ ...
+ ],
+ // A mapping of installed add-on IDs with known malicious
+ // update URL patterns to their exact update URLs.
+ updateURLs: {
+ <extensionID>: <updateURL>,
+ ...
+ }
+ }
+ }
diff --git a/toolkit/components/telemetry/docs/data/anonymous-ping.rst b/toolkit/components/telemetry/docs/data/anonymous-ping.rst
new file mode 100644
index 0000000000..53e217fc9d
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/anonymous-ping.rst
@@ -0,0 +1,68 @@
+
+"anonymous" ping
+================
+
+.. note::
+
+ This ping is no longer sent by Firefox or Fennec.
+
+This ping is only for product survey purpose and will not track/associate client ID. It's used
+to evaluate custom tab usage and see which app is using our custom tab.
+
+Submission interval & triggers
+Since this ping is used to measure the feature usage, it should be sent each time the client app uses our custom tab.
+
+Dataset:
+Only opt-in users will send out this ping.
+Since all other pings will collect client ID. We need this custom ping to not do that.
+
+Size and volume:
+The size of submitted payload is small. And this custom ping should be deprecated after it's released for 6 months.
+
+Privacy:
+We won't collect customer information so there'll be no PI leak.
+
+Data contents:
+The content of this ping will let us know which app is using our custom tab.
+Just like other feature usage measurement, we only need it for opt-in users (which consider as heavy users).
+
+Structure:
+
+.. code-block:: js
+
+ {
+ "payload": {
+ "client": <string> // The package name of the caller app.
+ }
+ type: <string>, // "anonymous", "activation", "deletion", "saved-session", ...
+ id: <UUID>, // a UUID that identifies this ping
+ creationDate: <ISO date>, // the date the ping was generated
+ version: <number>, // the version of the ping format, currently 4
+
+ application: {
+ architecture: <string>, // build architecture, e.g. x86
+ buildId: <string>, // "20141126041045"
+ name: <string>, // "Firefox"
+ version: <string>, // "35.0"
+ displayVersion: <string>, // "35.0b3"
+ vendor: <string>, // "Mozilla"
+ platformVersion: <string>, // "35.0"
+ xpcomAbi: <string>, // e.g. "x86-msvc"
+ channel: <string>, // "beta"
+ },
+ }
+
+Field details
+-------------
+
+client
+~~~~~~
+It could be ``com.example.app``, which is the identifier of the app.
+
+Version history
+---------------
+* v1: initial version - Will be shipped in `Fennec 55 <https://bugzilla.mozilla.org/show_bug.cgi?id=1329157>`_.
+
+Notes
+~~~~~
+There's no option in this custom ping since we don't collect clientId nor environment data.
diff --git a/toolkit/components/telemetry/docs/data/backgroundhangmonitor-ping.rst b/toolkit/components/telemetry/docs/data/backgroundhangmonitor-ping.rst
new file mode 100644
index 0000000000..a85de6e30c
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/backgroundhangmonitor-ping.rst
@@ -0,0 +1,162 @@
+
+"backgroundhangmonitor" ping
+============================
+
+Whenever a thread monitored by the Background Hang Monitor hangs, a stack and
+some non-identifying information about the hang is captured. When 50 of these
+hangs are collected across all processes, or the browser exits, this ping is
+transmitted with the collected hang information.
+
+This ping is only collected in nightly builds, to avoid the high volume of pings
+which would be produced in Beta.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ "type": "backgroundhangmonitor",
+ ... // common ping data
+ "environment": { ... },
+ "payload": {
+ "timeSinceLastPing": <number>, // uptime since last backgroundhangmonitor ping (ms).
+ "modules": [
+ [
+ <string>, // Name of the file holding the debug information.
+ <string> // Breakpad ID of this module.
+ ],
+ ...
+ ],
+ "hangs": [
+ {
+ "duration": <number>, // duration of the hang in milliseconds.
+ "thread": <string>, // name of the hanging thread.
+ "runnableName": <string>, // name of the runnable executing during the hang.
+ // Runnable names are only collected for the XPCOM main thread.
+ "process": <string>, // Type of process that hung, see below for a list of types.
+ "remoteType": <string>, // Remote type of process which hung, see below.
+ "annotations": [ ... ], // A list of annotations on the hang, see below.
+ "pseudoStack": [ ... ], // List of label stack frames and js frames.
+ "stack": [ ... ], // interleaved hang stack, see below.
+ },
+ ...
+ ]
+ }
+ }
+
+.. note::
+
+ Hangs are collected whenever the current runnable takes over 128ms.
+
+Process Types
+-------------
+
+The ``process`` field is a string denoting the kind of process that hung. Hangs
+are currently sent only for the processes below:
+
++---------------+---------------------------------------------------+
+| Kind | Description |
++===============+===================================================+
+| default | Main process, also known as the browser process |
++---------------+---------------------------------------------------+
+| tab | Content process |
++---------------+---------------------------------------------------+
+| gpu | GPU process |
++---------------+---------------------------------------------------+
+
+Remote Type
+-----------
+
+The ``remoteType`` field is a string denoting the type of content process that
+hung. As such it is only non-null if ``processType`` contains the ``tab`` value.
+
+The supported ``remoteType`` values are documented in the crash ping
+documentation: :ref:`remote-process-types`.
+
+Stack Traces
+------------
+
+Each hang object contains a ``stack`` field which has been populated with an
+interleaved stack trace of the hung thread. An interleaved stack consists of a
+native backtrace with additional frames interleaved, representing chrome JS and
+label stack entries.
+
+The structure that manages the label stack and the JS stack was called
+"PseudoStack" in the past and is now called "ProfilingStack".
+
+Note that this field only contains native stack frames, label stack and chrome
+JS script frames. If native stacks can not be collected on the target platform,
+or stackwalking was not initialized, there will be no native frames present, and
+the stack will consist only of label stack and chrome JS script frames.
+
+A single frame in the stack is either a raw string, representing a label stack
+or chrome JS script frame, or a native stack frame.
+
+Label stack frames contain the static string from all instances of the
+AUTO_PROFILER_LABEL* macros. Additionally, dynamic strings are collected from
+all usages of the AUTO_PROFILER_LABEL_DYNAMIC*_NONSENSITIVE macros. The dynamic
+strings are simply appended to the static strings after a space character.
+
+Current dynamic string collections are as follows:
+
++--------------------------------------------------+-----------------------------------------+
+| Static string | Dynamic |
++==================================================+=========================================+
+| ChromeUtils::Import | Associated chrome:// or resource:// URI |
++--------------------------------------------------+-----------------------------------------+
+| nsJSContext::GarbageCollectNow | GC reason string |
++--------------------------------------------------+-----------------------------------------+
+| mozJSSubScriptLoader::DoLoadSubScriptWithOptions | Associated chrome:// or resource:// URI |
++--------------------------------------------------+-----------------------------------------+
+| PresShell::DoFlushPendingNotifications | Flush type |
++--------------------------------------------------+-----------------------------------------+
+| nsObserverService::NotifyObservers | Associated observer topic |
++--------------------------------------------------+-----------------------------------------+
+
+Native stack frames are as such:
+
+.. code-block:: js
+
+ [
+ <number>, // Index in the payload.modules list of the module description.
+ // -1 if this frame was not in a valid module.
+ <string> // Hex string (e.g. "FF0F") of the frame offset in the module.
+ ]
+
+Annotations
+-----------
+
+The annotations field is an array of key-value pairs, for example if the user
+was interacting during a hang the annotations field would look something like
+this:
+
+.. code-block:: js
+
+ [
+ [
+ "UserInteracting",
+ "true"
+ ]
+ ]
+
+The following annotations are currently present in tree:
+
++-----------------+-------------------------------------------------+
+| Name | Description |
++=================+=================================================+
+| UserInteracting | "true" if the user was actively interacting |
++-----------------+-------------------------------------------------+
+| pluginName | Name of the currently running plugin |
++-----------------+-------------------------------------------------+
+| pluginVersion | Version of the currently running plugin |
++-----------------+-------------------------------------------------+
+| HangUIShown | "true" if the hang UI was shown |
++-----------------+-------------------------------------------------+
+| HangUIContinued | "true" if continue was selected in the hang UI |
++-----------------+-------------------------------------------------+
+| HangUIDontShow | "true" if the hang UI was not shown |
++-----------------+-------------------------------------------------+
+| Unrecovered | "true" if the hang persisted until process exit |
++-----------------+-------------------------------------------------+
+
+Additional annotations can be added at run-time via :doc:`../collection/user-interactions`.
diff --git a/toolkit/components/telemetry/docs/data/common-ping.rst b/toolkit/components/telemetry/docs/data/common-ping.rst
new file mode 100644
index 0000000000..1587131808
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/common-ping.rst
@@ -0,0 +1,42 @@
+
+Common ping format
+==================
+
+This defines the top-level structure of a Telemetry ping.
+It contains basic information shared between different ping types, which enables proper storage and processing of the raw pings server-side.
+
+It also contains optional further information:
+
+* the :doc:`environment data <../data/environment>`, which contains important info to correlate the measurements against
+* the ``clientId``, a UUID identifying a profile and allowing user-oriented correlation of data
+
+*Note:* Both are not submitted with all ping types due to privacy concerns. This and the data it that can be correlated against is inspected under the `data collection policy <https://wiki.mozilla.org/Firefox/Data_Collection>`_.
+
+Finally, the structure also contains the `payload`, which is the specific data submitted for the respective *ping type*.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ type: <string>, // "main", "activation", "optout", "saved-session", ...
+ id: <UUID>, // a UUID that identifies this ping
+ creationDate: <ISO date>, // the date the ping was generated
+ version: <number>, // the version of the ping format, currently 4
+
+ application: {
+ architecture: <string>, // build architecture, e.g. x86
+ buildId: <string>, // "20141126041045"
+ name: <string>, // "Firefox"
+ version: <string>, // "35.0"
+ displayVersion: <string>, // "35.0b3"
+ vendor: <string>, // "Mozilla"
+ platformVersion: <string>, // "35.0"
+ xpcomAbi: <string>, // e.g. "x86-msvc"
+ channel: <string>, // "beta"
+ },
+
+ clientId: <UUID>, // optional
+ environment: { ... }, // optional, not all pings contain the environment
+ payload: { ... }, // the actual payload data for this ping type
+ }
diff --git a/toolkit/components/telemetry/docs/data/coverage-ping.rst b/toolkit/components/telemetry/docs/data/coverage-ping.rst
new file mode 100644
index 0000000000..3db93d9074
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/coverage-ping.rst
@@ -0,0 +1,40 @@
+
+"coverage" ping
+===============
+
+This ping is not enabled by default. When enabled, a ping is generated a total of once per profile, as a diagnostic tool
+to determine whether Telemetry is working for users.
+
+This ping contains no client id and no environment data.
+
+You can find more background information in `this blog post <https://blog.mozilla.org/data/2018/08/20/effectively-measuring-search-in-firefox/>`_.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ "appVersion": "63.0a1",
+ "appUpdateChannel": "nightly",
+ "osName": "Darwin",
+ "osVersion": "17.7.0",
+ "telemetryEnabled": 1
+ }
+
+Expected behaviours
+-------------------
+The following is a list of expected behaviours for the ``coverage`` ping:
+
+- The ping will only be sent once per ping version, per profile.
+- If sending the ping fails, it will be retried on startup.
+- A totally arbitrary UUID is generated on first run on a new profile, to use for filtering duplicates.
+- The ping is sent to a different endpoint not using existing Telemetry.
+- The ping does not honor the Telemetry enabled preference, but provides its own opt-out preference: `toolkit.coverage.opt-out`.
+- The ping is disabled by default. It is intended to be enabled for users on an experimental basis using the preference `toolkit.coverage.enabled`.
+
+Version History
+---------------
+
+- Firefox 64:
+
+ - "coverage" ping shipped (`bug 1492656 <https://bugzilla.mozilla.org/show_bug.cgi?id=1492656>`_).
diff --git a/toolkit/components/telemetry/docs/data/crash-ping.rst b/toolkit/components/telemetry/docs/data/crash-ping.rst
new file mode 100644
index 0000000000..38f5d6aaef
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/crash-ping.rst
@@ -0,0 +1,264 @@
+Crash ping
+==========
+
+This ping is captured after the main Firefox process crashes or after a child process
+process crashes, whether or not the crash report is submitted to
+crash-stats.mozilla.org. It includes non-identifying metadata about the crash.
+
+This ping is sent either by the ``CrashManager`` or by the crash reporter
+client. The ``CrashManager`` is responsible for sending crash pings for the
+child processes crashes, which are sent right after the crash is detected,
+as well as for main process crashes, which are sent after Firefox restarts
+successfully. The crash reporter client sends crash pings only for main process
+crashes whether or not the user also reports the crash. The crash reporter
+client will not send the crash ping if telemetry has been disabled in Firefox.
+
+The environment block that is sent with this ping varies: if Firefox was running
+long enough to record the environment block before the crash, then the environment
+at the time of the crash will be recorded and ``hasCrashEnvironment`` will be true.
+If Firefox crashed before the environment was recorded, ``hasCrashEnvironment`` will
+be false and the recorded environment will be the environment at time of submission.
+
+The client ID is submitted with this ping.
+
+The metadata field holds a subset of the crash annotations, all field values
+are stored as strings but some may be interpreted either as numbers or
+boolean values. Numbers are integral unless stated otherwise in the
+description. Boolean values are set to "1" when true, "0" when false. If
+they're absent then they should be interpreted as false.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ type: "crash",
+ ... common ping data
+ clientId: <UUID>,
+ environment: { ... },
+ payload: {
+ crashDate: "YYYY-MM-DD",
+ crashTime: <ISO Date>, // per-hour resolution
+ version: 1,
+ sessionId: <UUID>, // Telemetry ID of crashing session. May be missing for crashes that happen early in startup
+ crashId: <UUID>, // Optional, ID of the associated crash
+ minidumpSha256Hash: <hash>, // SHA256 hash of the minidump file
+ processType: <type>, // Type of process that crashed, see below for a list of types
+ stackTraces: { ... }, // Optional, see below
+ metadata: { // Annotations saved while Firefox was running. See CrashAnnotations.yaml for more information
+ ProductID: "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}",
+ ProductName: "Firefox",
+ ReleaseChannel: <channel>,
+ Version: <version number>,
+ BuildID: "YYYYMMDDHHMMSS",
+ AsyncShutdownTimeout: <json>, // Optional, present when a shutdown blocker failed to respond within a reasonable amount of time
+ AvailablePageFile: <size>, // Windows-only, available paging file in bytes
+ AvailablePhysicalMemory: <size>, // Windows-only, available physical memory in bytes
+ AvailableSwapMemory: <size>, // macOS- and Linux-only, available swap space
+ AvailableVirtualMemory: <size>, // Windows-only, available virtual memory in bytes
+ BackgroundTaskName: "task_name", // Optional, if the app was invoked in background task mode via `--backgroundtask task_name`
+ BlockedDllList: <list>, // Windows-only, see WindowsDllBlocklist.cpp for details
+ BlocklistInitFailed: "1", // Windows-only, present only if the DLL blocklist initialization failed
+ CrashTime: <time>, // Seconds since the Epoch
+ DOMFissionEnabled: "1", // Optional, if set indicates that a Fission window had been opened
+ EventLoopNestingLevel: <levels>, // Optional, present only if >0, indicates the nesting level of the event-loop
+ ExperimentalFeatures: <features>, // Optional, a comma-separated string that specifies the enabled experimental features from about:preferences#experimental
+ FontName: <name>, // Optional, the font family name that is being loaded when the crash occurred
+ GPUProcessLaunchCount: <num>, // Number of times the GPU process was launched
+ HeadlessMode: "1", // Optional, "1" if the app was invoked in headless mode via `--headless ...` or `--backgroundtask ...`
+ ipc_channel_error: <error string>, // Optional, contains the string processing error reason for an ipc-based content crash
+ IsGarbageCollecting: "1", // Optional, if set indicates that the crash occurred while the garbage collector was running
+ LowCommitSpaceEvents: <num>, // Windows-only, present only if >0, number of low commit space events detected by the available memory tracker
+ MainThreadRunnableName: <name>, // Optional, Nightly-only, name of the currently executing nsIRunnable on the main thread
+ MozCrashReason: <reason>, // Optional, contains the string passed to MOZ_CRASH()
+ OOMAllocationSize: <size>, // Size of the allocation that caused an OOM
+ ProfilerChildShutdownPhase: <string>, // Profiler shutdown phase
+ PurgeablePhysicalMemory: <size>, // macOS-only, amount of memory that can be deallocated by the OS in case of memory pressure
+ QuotaManagerShutdownTimeout: <log-string>, // Optional, contains a list of shutdown steps and status of the quota manager clients
+ RemoteType: <type>, // Optional, type of content process, see below for a list of types
+ SecondsSinceLastCrash: <duration>, // Seconds elapsed since the last crash occurred
+ ShutdownProgress: <phase>, // Optional, contains a string describing the shutdown phase in which the crash occurred
+ SystemMemoryUsePercentage: <percentage>, // Windows-only, percent of memory in use
+ StartupCrash: "1", // Optional, if set indicates that Firefox crashed during startup
+ TextureUsage: <usage>, // Optional, usage of texture memory in bytes
+ TotalPageFile: <size>, // Windows-only, paging file in use expressed in bytes
+ TotalPhysicalMemory: <size>, // Windows-only, physical memory in use expressed in bytes
+ TotalVirtualMemory: <size>, // Windows-only, virtual memory in use expressed in bytes
+ UptimeTS: <duration>, // Seconds since Firefox was started, this can have a fractional component
+ User32BeforeBlocklist: "1", // Windows-only, present only if user32.dll was loaded before the DLL blocklist has been initialized
+ WindowsErrorReporting: "1", // Windows-only, present only if the crash was intercepted by the WER runtime exception module
+ WindowsFileDialogErrorCode: <error code>, // Windows-only, optional, present only if file-dialog IPC failed
+ WindowsPackageFamilyName: <string>, // Windows-only, a string containing the "Package Family Name" of Firefox, if installed through an MSIX package
+ },
+ hasCrashEnvironment: bool
+ }
+ }
+
+.. note::
+
+ For "crash" pings generated by the crashreporter we are deliberately truncating the ``creationTime``
+ field to hours. See `bug 1345108 <https://bugzilla.mozilla.org/show_bug.cgi?id=1345108>`_ for context.
+
+Process Types
+-------------
+
+The ``processType`` field contains the type of process that crashed. There are
+currently multiple process types defined in ``nsICrashService`` but crash pings
+are sent only for the ones below:
+
++---------------+-------------------------------------------------------------------------------+
+| Type | Description |
++===============+===============================================================================+
+| main | :ref:`Main process <parent-process>`, also known as parent or browser process |
++---------------+-------------------------------------------------------------------------------+
+| content | :ref:`Content process <content-process>` |
++---------------+-------------------------------------------------------------------------------+
+| gmplugin | :ref:`Gecko media plugin <gecko-media-plugins-process>` |
++---------------+-------------------------------------------------------------------------------+
+| gpu | :ref:`GPU process <gpu-process>` |
++---------------+-------------------------------------------------------------------------------+
+| vr | :ref:`VR process <vr-process>` |
++---------------+-------------------------------------------------------------------------------+
+| rdd | :ref:`Data decoder process <data-decoder-process>` |
++---------------+-------------------------------------------------------------------------------+
+| socket | :ref:`Network socket process <network-socket-process>` |
++---------------+-------------------------------------------------------------------------------+
+| sandboxbroker | :ref:`Remote sandbox broker <remote-sandbox-process>` |
++---------------+-------------------------------------------------------------------------------+
+| forkserver | :ref:`Fork server <fork-server>` |
++---------------+-------------------------------------------------------------------------------+
+| utility | :ref:`Utility process <utility-process>` |
++---------------+-------------------------------------------------------------------------------+
+
+.. _remote-process-types:
+
+Remote Process Types
+--------------------
+
+The optional ``remoteType`` field contains the type of the content process that
+crashed. As such it is present only if ``processType`` contains the ``content``
+value. The following content process types are currently defined:
+
++-----------+--------------------------------------------------------+
+| Type | Description |
++===========+========================================================+
+| web | The content process was running code from a web page |
++-----------+--------------------------------------------------------+
+| file | The content process was running code from a local file |
++-----------+--------------------------------------------------------+
+| extension | The content process was running code from an extension |
++-----------+--------------------------------------------------------+
+
+Stack Traces
+------------
+
+The crash ping may contain a ``stackTraces`` field which has been populated
+with stack traces for all threads in the crashed process. The format of this
+field is similar to the one used by Socorro for representing a crash. The main
+differences are that redundant fields are not stored and that the module a
+frame belongs to is referenced by index in the module array rather than by its
+file name.
+
+Note that this field does not contain data from the application; only bare
+stack traces and module lists are stored.
+
+.. code-block:: js
+
+ {
+ status: <string>, // Status of the analysis, "OK" or an error message
+ crash_info: { // Basic crash information
+ type: <string>, // Type of crash, SIGSEGV, assertion, etc...
+ address: <addr>, // Crash address crash, hex format, see the notes below
+ crashing_thread: <index> // Index in the thread array below
+ },
+ main_module: <index>, // Index of Firefox' executable in the module list
+ modules: [{
+ base_addr: <addr>, // Base address of the module, hex format
+ end_addr: <addr>, // End address of the module, hex format
+ code_id: <string>, // Unique ID of this module, see the notes below
+ debug_file: <string>, // Name of the file holding the debug information
+ debug_id: <string>, // ID or hash of the debug information file
+ filename: <string>, // File name
+ version: <string>, // Library/executable version
+ },
+ ... // List of modules ordered by base memory address
+ ],
+ threads: [{ // Stack traces for every thread
+ frames: [{
+ module_index: <index>, // Index of the module this frame belongs to
+ ip: <ip>, // Program counter, hex format
+ trust: <string> // Trust of this frame, see the notes below
+ },
+ ... // List of frames, the first frame is the topmost
+ ]
+ }]
+ }
+
+Notes
+~~~~~
+
+Memory addresses and instruction pointers are always stored as strings in
+hexadecimal format (e.g. "0x4000"). They can be made of up to 16 characters for
+64-bit addresses.
+
+The crash type is both OS and CPU dependent and can be either a descriptive
+string (e.g. SIGSEGV, EXCEPTION_ACCESS_VIOLATION) or a raw numeric value. The
+crash address meaning depends on the type of crash. In a segmentation fault the
+crash address will be the memory address whose access caused the fault; in a
+crash triggered by an illegal instruction exception the address will be the
+instruction pointer where the invalid instruction resides.
+See `breakpad <https://chromium.googlesource.com/breakpad/breakpad/+/c99d374dde62654a024840accfb357b2851daea0/src/processor/minidump_processor.cc#675>`__'s
+relevant code for further information.
+
+Since it's not always possible to establish with certainty the address of the
+previous frame while walking the stack, every frame has a trust value that
+represents how it was found and thus how certain we are that it's a real frame.
+The trust levels are (from least trusted to most trusted):
+
++---------------+---------------------------------------------------+
+| Trust | Description |
++===============+===================================================+
+| context | Given as instruction pointer in a context |
++---------------+---------------------------------------------------+
+| prewalked | Explicitly provided by some external stack walker |
++---------------+---------------------------------------------------+
+| cfi | Derived from call frame info |
++---------------+---------------------------------------------------+
+| frame_pointer | Derived from frame pointer |
++---------------+---------------------------------------------------+
+| cfi_scan | Found while scanning stack using call frame info |
++---------------+---------------------------------------------------+
+| scan | Scanned the stack, found this |
++---------------+---------------------------------------------------+
+| none | Unknown, this is most likely not a valid frame |
++---------------+---------------------------------------------------+
+
+The ``code_id`` field holds a unique ID used to distinguish between different
+versions and builds of the same module. See `breakpad <https://chromium.googlesource.com/breakpad/breakpad/+/24f5931c5e0120982c0cbf1896641e3ef2bdd52f/src/google_breakpad/processor/code_module.h#60>`__'s
+description for further information. This field is populated only on Windows.
+
+Version History
+---------------
+
+- Firefox 58: Added ipc_channel_error (`bug 1410143 <https://bugzilla.mozilla.org/show_bug.cgi?id=1410143>`_).
+- Firefox 62: Added LowCommitSpaceEvents (`bug 1464773 <https://bugzilla.mozilla.org/show_bug.cgi?id=1464773>`_).
+- Firefox 63: Added RecordReplayError (`bug 1481009 <https://bugzilla.mozilla.org/show_bug.cgi?id=1481009>`_).
+- Firefox 64: Added MemoryErrorCorrection (`bug 1498609 <https://bugzilla.mozilla.org/show_bug.cgi?id=1498609>`_).
+- Firefox 68: Added IndexedDBShutdownTimeout and LocalStorageShutdownTimeout
+ (`bug 1539750 <https://bugzilla.mozilla.org/show_bug.cgi?id=1539750>`_).
+- Firefox 74: Added AvailableSwapMemory and PurgeablePhysicalMemory
+ (`bug 1587721 <https://bugzilla.mozilla.org/show_bug.cgi?id=1587721>`_).
+- Firefox 74: Added MainThreadRunnableName (`bug 1608158 <https://bugzilla.mozilla.org/show_bug.cgi?id=1608158>`_).
+- Firefox 76: Added DOMFissionEnabled (`bug 1602918 <https://bugzilla.mozilla.org/show_bug.cgi?id=1602918>`_).
+- Firefox 79: Added ExperimentalFeatures (`bug 1644544 <https://bugzilla.mozilla.org/show_bug.cgi?id=1644544>`_).
+- Firefox 85: Added QuotaManagerShutdownTimeout, removed IndexedDBShutdownTimeout and LocalStorageShutdownTimeout
+ (`bug 1672369 <https://bugzilla.mozilla.org/show_bug.cgi?id=1672369>`_).
+- Firefox 89: Added GPUProcessLaunchCount (`bug 1710448 <https://bugzilla.mozilla.org/show_bug.cgi?id=1710448>`_)
+ and ProfilerChildShutdownPhase (`bug 1704680 <https://bugzilla.mozilla.org/show_bug.cgi?id=1704680>`_).
+- Firefox 90: Removed MemoryErrorCorrection (`bug 1710152 <https://bugzilla.mozilla.org/show_bug.cgi?id=1710152>`_)
+ and added WindowsErrorReporting (`bug 1703761 <https://bugzilla.mozilla.org/show_bug.cgi?id=1703761>`_).
+- Firefox 95: Added HeadlessMode and BackgroundTaskName (`bug 1697875 <https://bugzilla.mozilla.org/show_bug.cgi?id=1697875>`_).
+- Firefox 96: Added WindowsPackageFamilyName (`bug 1738375 <https://bugzilla.mozilla.org/show_bug.cgi?id=1738375>`_).
+- Firefox 103: Removed ContainsMemoryReport (`bug 1776279 <https://bugzilla.mozilla.org/show_bug.cgi?id=1776279>`_).
+- Firefox 107: Added UtilityActorsName (`bug 1788596 <https://bugzilla.mozilla.org/show_bug.cgi?id=1788596>`_).
+- Firefox 119: Added WindowsFileDialogErrorCode (`bug 1837079 <https://bugzilla.mozilla.org/show_bug.cgi?id=1837079>`_)
diff --git a/toolkit/components/telemetry/docs/data/default-browser-ping.rst b/toolkit/components/telemetry/docs/data/default-browser-ping.rst
new file mode 100644
index 0000000000..89a5f1485e
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/default-browser-ping.rst
@@ -0,0 +1,99 @@
+======================
+"default-browser" ping
+======================
+
+This opt-out ping is sent from the Default Browser Agent, which is a Windows-only program that registers itself during Firefox installation with the Windows scheduled tasks system so that it runs automatically every 24 hours, whether Firefox is running or not. The scheduled task gathers the data for this ping and then sends it by handing it off to :doc:`../internals/pingsender`.
+
+Even though this ping is generated by a binary separate from Firefox itself, opting out of telemetry does disable it; the pref value is copied to the registry so that the default browser agent can read it without needing to work with profiles. Relevant policies are consulted as well. The agent also has its own pref, ``default-agent.enabled``, which if set to false disables all agent functionality, including generating this ping.
+
+Each installation of Firefox has its own copy of the agent and its own scheduled task, so one ping will be sent every day for each installation on a given machine. This is needed because the default browser setting is per-user, and different installations may have been created by different users. If multiple operating system-level users are all using one copy of Firefox, only one scheduled task will have been created and only one ping will be sent, even though the users might have different default browser settings.
+
+The namespace this ping is in is called ``default-browser-agent``.
+
+For more information about the default browser agent itself, see :doc:`its documentation </toolkit/mozapps/defaultagent/default-browser-agent/index>`.
+
+Structure
+=========
+
+Since this ping is sent from an external binary, it's structured as its own ping document type and not in the standard Firefox telemetry format. It's also missing lots of data that would normally be present; for instance, there is no ``clientId``, because the agent does not load any profile and so has no way to find any, and no environment block because the agent doesn't contain the telemetry library code to build it.
+
+Here's the format of the ping data, with example values for each property:
+
+.. code-block:: js
+
+ {
+ build_channel: <string>, // ex. "nightly", or "beta", or "release"
+ version: <string>, // ex. "72.0.2"
+ os_version: <string>, // ex. 10.0.18363.592
+ previous_os_version: <string>, // ex. 10.0.18363.591
+ os_locale: <string>, // ex. en-US
+ default_browser: <string>, // ex. "firefox"
+ previous_default_browser: <string>, // ex. "edge"
+ default_pdf_viewer_raw: <string>, // ex. "firefox"
+ notification_type: <string>, // ex. "initial" or "followup"
+ notification_shown: <string>, // ex. "shown", or "not-shown", or "error"
+ notification_action: <string>, // ex. "no-action" or "make-firefox-default-button"
+ previous_notification_action: <string>, // Same possible values as notification_action
+ }
+
+``build_channel``
+-----------------
+The Firefox channel.
+
+``version``
+-----------
+The Firefox version.
+
+``os_version``
+--------------
+The current Windows version number. Below Windows 10, this is in the format ``[major].[minor].[build]``; for Windows 10, the format is ``10.0.[build].[UBR]``.
+
+``previous_os_version``
+-----------------------
+The Windows OS version before it was changed to the current setting. The possible values are the same as for ``os_version``.
+
+The OS does not keep track of the previous OS version, so the agent records this information itself. That means that it will be inaccurate until the first time the default is changed after the agent task begins running. Before then, the value of ``previous_os_version`` will be the same as ``os_version``.
+
+This value is updated every time the Default Agent runs, so when the default browser is first changed the values for ``os_version`` and ``previous_os_version`` will be different. But on subsequent executions of the Default Agent, the two values will be the same.
+
+``os_locale``
+-------------
+The locale that the user has selected for the operating system (NOT for Firefox).
+
+``default_browser``
+-------------------
+Which browser is currently set as the system default web browser. This is simply a string with the name of the browser; the possible values include "firefox", "chrome", "edge", "edge-chrome", "ie", "opera", and "brave".
+
+``previous_default_browser``
+----------------------------
+Which browser was set as the system default before it was changed to the current setting. The possible values are the same as for ``default_browser``.
+
+The OS does not keep track of previous default settings, so the agent records this information itself. That means that it will be inaccurate until the first time the default is changed after the agent task begins running. Before then, the value of ``previous_default_browser`` will be the same as ``default_browser``.
+
+This value is updated every time the Default Browser Agent runs, so when the default browser is first changed the values for ``default_browser`` and ``previous_default_browser`` will be different. But on subsequent executions of the Default Browser Agent, the two values will be the same.
+
+``default_pdf_viewer_raw``
+--------------------------
+Which pdf viewer is currently set as the system default. This is simply a string with the name of the pdf viewer.
+
+``notification_type``
+---------------------
+Which notification type was shown. There are currently two types of notifications, "initial" and "followup". The initial notification is shown first and has a "Remind me later" button. The followup notification is only shown if the "Remind me later" button is clicked and has a "Never ask again" button instead of the "Remind me later" button. Note that the value of ``notification_shown`` should be consulted to determine whether the notification type specified was actually shown.
+
+``notification_shown``
+----------------------
+Whether a notification was shown or not. Possible value include "shown", "not-shown", and "error".
+
+``notification_action``
+-----------------------
+The action that the user took in response to the notification. Possible values currently include "dismissed-by-timeout", "dismissed-to-action-center", "dismissed-by-button", "dismissed-by-application-hidden", "remind-me-later", "make-firefox-default-button", "toast-clicked", "no-action".
+
+Many of the values correspond to buttons on the notification and should be pretty self explanatory, but a few are less so. The action "no-action" will be used if and only if the value of ``notification_shown`` is not "shown" to indicate that no action was taken because no notification was displayed. The action "dismissed-to-action-center" will be used if the user clicks the arrow in the top right corner of the notification to dismiss it to the action center. The action "dismissed-by-application-hidden" is provided because that is a method of dismissal that the notification API could give but, in practice, should never be seen. The action "dismissed-by-timeout" indicates that the user did not interact with the notification and it timed out.
+
+``previous_notification_action``
+--------------------------------
+The action that the user took in response to the previous notification. Possible values are the same as those of ``notification_action``.
+
+If no notification has ever been shown, this will be "no-action". If ``notification_shown`` is "shown", this will be the action that was taken on the notification before the one that was just shown (or "no-action" if there was no previous notification). Otherwise, this will be the action that the user took the last time a notification was shown.
+
+Note that because this feature was added later, there may be people in configurations that might seem impossible, like having the combination of ``notification_type`` being "followup" with a ``previous_notification_action`` of "no-action", because the first notification action was taken before we started storing that value.
diff --git a/toolkit/components/telemetry/docs/data/deletion-request-ping.rst b/toolkit/components/telemetry/docs/data/deletion-request-ping.rst
new file mode 100644
index 0000000000..0c1c00c301
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/deletion-request-ping.rst
@@ -0,0 +1,68 @@
+"deletion-request" ping
+=======================
+
+This ping is submitted when a user opts out of sending technical and interaction data to Mozilla.
+(In other words, when the
+``datareporting.healthreport.uploadEnabled``
+:doc:`preference <../internals/preferences>` is set to ``false``.)
+
+This ping contains the client id.
+This ping does not contain any environment data.
+
+This ping is intended to communicate to the Data Pipeline that the user wishes
+to have their reported Telemetry data deleted.
+As such it attempts to send itself at the moment the user opts out of data collection,
+and continues to try and send itself.
+
+This ping contains scalars present in the "deletion-request" store.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ version: 4,
+ type: "deletion-request",
+ ... common ping data (including clientId)
+ payload: {
+ scalars: {
+ <process-type>: { // like "parent" or "content"
+ <id name>: <id>, // like "deletion.request.impression_id": "<RFC 4122 GUID>"
+ },
+ },
+ }
+ }
+
+Expected behaviours
+-------------------
+The following is a list of expected behaviours for the ``deletion-request`` ping:
+
+- Telemetry will try to send the ping even if upload is disabled.
+- Telemetry may persist this ping if it can't be immediately sent, and may try to resend it later.
+
+Analysis Gotchas
+----------------
+It is `known <https://bugzilla.mozilla.org/show_bug.cgi?id=1741252>`_ that,
+on a release week, "deletion-request" pings from the previous release version will spike in volume.
+
+There is a strong geo component to these spikes (China and Russia mostly).
+The pings behave like they're coming from real Firefox instances (one per ``client_id``).
+However, we've received no "main" pings from these clients from the previous 28 days
+(per ``clients_last_seen``) which makes them rather unusual.
+
+We suspect this is some sort of automation or pseudo-automation.
+
+Including these pings in our self-service deletion processes doesn't make them any slower,
+and ingesting them isn't breaking the bank, so at time of writing we're inclined to take a
+"document and then ignore" approach.
+
+Version History
+---------------
+
+- Firefox 72:
+
+ - "deletion-request" ping replaces the "optout" ping (`bug 1585410 <https://bugzilla.mozilla.org/show_bug.cgi?id=1585410>`_).
+
+- Firefox 73:
+
+ - Added support for subordinate ids in the "deletion-request" store (`bug 1604312 <https://bugzilla.mozilla.org/show_bug.cgi?id=1604312>`_).
diff --git a/toolkit/components/telemetry/docs/data/downgrade-ping.rst b/toolkit/components/telemetry/docs/data/downgrade-ping.rst
new file mode 100644
index 0000000000..5366255834
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/downgrade-ping.rst
@@ -0,0 +1,30 @@
+
+"downgrade" ping
+================
+
+This ping is captured when attempting to use a profile that was previously used
+with a newer version of the application.
+
+This ping is submitted directly through the ```pingsender```. The common ping
+data relates to the profile and application that the user attempted to use.
+
+The client ID is submitted with this ping. No environment block is included with
+this ping.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ type: "downgrade",
+ ... common ping data
+ clientId: <UUID>,
+ payload: {
+ lastVersion: "", // The last version of the application that ran this profile
+ hasSync: <bool>, // Whether the profile is signed in to sync
+ hasBinary: <bool>, // Whether the last version of the application is available to run
+ button: <int> // The button the user chose to click from the UI:
+ // 0 - Quit
+ // 1 - Create new profile
+ }
+ }
diff --git a/toolkit/components/telemetry/docs/data/environment.rst b/toolkit/components/telemetry/docs/data/environment.rst
new file mode 100644
index 0000000000..77d3d1ea6e
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/environment.rst
@@ -0,0 +1,622 @@
+
+Environment
+===========
+
+The environment consists of data that is expected to be characteristic for performance and other behavior and not expected to change too often.
+
+Changes to most of these data points are detected (where possible and sensible) and will lead to a session split in the :doc:`main-ping`.
+The environment data may also be submitted by other ping types.
+
+*Note:* This is not submitted with all ping types due to privacy concerns. This and other data is inspected under the `data collection policy <https://wiki.mozilla.org/Firefox/Data_Collection>`_.
+
+Some parts of the environment must be fetched asynchronously at startup. We don't want other Telemetry components to block on waiting for the environment, so some items may be missing from it until the async fetching finished.
+This currently affects the following sections:
+
+- profile
+- add-ons
+- services
+
+
+Structure:
+
+.. code-block:: js
+
+ {
+ build: {
+ applicationId: <string>, // nsIXULAppInfo.ID
+ applicationName: <string>, // "Firefox"
+ architecture: <string>, // e.g. "x86", build architecture for the active build
+ buildId: <string>, // e.g. "20141126041045"
+ version: <string>, // e.g. "35.0"
+ vendor: <string>, // e.g. "Mozilla"
+ displayVersion: <string>, // e.g. "35.0b1"
+ platformVersion: <string>, // e.g. "35.0"
+ xpcomAbi: <string>, // e.g. "x86-msvc"
+ updaterAvailable: <bool>, // Whether the app was built with app update available (MOZ_UPDATER)
+ },
+ settings: {
+ addonCompatibilityCheckEnabled: <bool>, // Whether application compatibility is respected for add-ons
+ blocklistEnabled: <bool>, // true on failure
+ isDefaultBrowser: <bool>, // whether Firefox is the default browser. On Windows, this is operationalized as whether Firefox is the default HTTP protocol handler and the default HTML file handler.
+ defaultSearchEngine: <string>, // e.g. "yahoo"
+ defaultSearchEngineData: {, // data about the current default engine
+ name: <string>, // engine name, e.g. "Yahoo"; or "NONE" if no default
+ loadPath: <string>, // where the engine line is located; missing if no default
+ origin: <string>, // 'default', 'verified', 'unverified', or 'invalid'; based on the presence and validity of the engine's loadPath verification hash.
+ submissionURL: <string> // set for default engines or well known search domains
+ },
+ defaultPrivateSearchEngine: <string>, // e.g. "duckduckgo"
+ defaultPrivateSearchEngine: {,
+ // data about the current default engine for private browsing mode. Same as defaultSearchEngineData.
+ },
+ launcherProcessState: <integer>, // optional, values correspond to values of mozilla::LauncherRegistryInfo::EnabledState enum
+ e10sEnabled: <bool>, // whether e10s is on, i.e. browser tabs open by default in a different process
+ e10sMultiProcesses: <integer>, // Maximum number of processes that will be launched for regular web content
+ fissionEnabled: <bool>, // whether fission is enabled this session, and subframes can load in a different process
+ telemetryEnabled: <bool>, // false on failure
+ locale: <string>, // e.g. "it", null on failure
+ intl: {
+ requestedLocales: [ <string>, ... ], // The locales that are being requested.
+ availableLocales: [ <string>, ... ], // The locales that are available for use.
+ appLocales: [ <string>, ... ], // The negotiated locales that are being used.
+ systemLocales: [ <string>, ... ], // The locales for the OS.
+ regionalPrefsLocales: [ <string>, ... ], // The regional preferences for the OS.
+ acceptLanguages: [ <string>, ... ], // The languages for the Accept-Languages header.
+ },
+ update: {
+ channel: <string>, // e.g. "release", null on failure
+ enabled: <bool>, // true on failure
+ autoDownload: <bool>, // true on failure
+ background: <bool>, // Indicates whether updates may be installed when Firefox is not running.
+ },
+ userPrefs: {
+ // Only prefs which are changed are listed in this block
+ "pref.name.value": value // some prefs send the value
+ "pref.name.url": "<user-set>" // For some privacy-sensitive prefs
+ // only the fact that the value has been changed is recorded
+ },
+ attribution: { // optional, only present if the installation has attribution data
+ // all of these values are optional.
+ source: <string>, // referring partner domain, when install happens via a known partner
+ 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
+ ua: <string>, // identifier derived from the user agent downloading the installer, e.g., chrome, Google Chrome 123
+ dltoken: <string>, // Unique token created at Firefox download time. ex: c18f86a3-f228-4d98-91bb-f90135c0aa9c
+ msstoresignedin: <boolean>, // optional, only present if the installation was done through the Microsoft Store, and was able to retrieve the "campaign ID" it was first installed with. this value is "true" if the user was signed into the Microsoft Store when they first installed, and false otherwise
+ dlsource: <string>, // identifier that indicate where installations of Firefox originate
+ },
+ sandbox: {
+ effectiveContentProcessLevel: <integer>,
+ contentWin32kLockdownState: <integer>,
+ }
+ },
+ // Optional, missing if fetching the information failed or had not yet completed.
+ services: {
+ // True if the user has a firefox account
+ accountEnabled: <bool>,
+ // True if the user has sync enabled.
+ syncEnabled: <bool>
+ },
+ profile: {
+ creationDate: <integer>, // integer days since UNIX epoch, e.g. 16446
+ resetDate: <integer>, // integer days since UNIX epoch, e.g. 16446 - optional
+ firstUseDate: <integer>, // integer days since UNIX epoch, e.g. 16446 - optional
+ },
+ partner: { // This section may not be immediately available on startup
+ distributionId: <string>, // pref "distribution.id", null on failure
+ distributionVersion: <string>, // pref "distribution.version", null on failure
+ partnerId: <string>, // pref mozilla.partner.id, null on failure
+ distributor: <string>, // pref app.distributor, null on failure
+ distributorChannel: <string>, // pref app.distributor.channel, null on failure
+ partnerNames: [
+ // list from prefs app.partner.<name>=<name>
+ ],
+ },
+ system: {
+ memoryMB: <number>,
+ virtualMaxMB: <number>, // windows-only
+ isWow64: <bool>, // windows-only
+ isWowARM64: <bool>, // windows-only
+ hasWinPackageId: <bool>, // windows-only
+ winPackageFamilyName: <string>, // windows-only
+ cpu: {
+ count: <number>, // desktop only, e.g. 8, or null on failure - logical cpus
+ cores: <number>, // desktop only, e.g., 4, or null on failure - physical cores
+ vendor: <string>, // desktop only, e.g. "GenuineIntel", or null on failure
+ name: <string>, // desktop only, e.g. "Intel(R) Core(TM) i9-8950HK CPU @ 2.90GHz",
+ // or null on failure
+ family: <number>, // desktop only, null on failure
+ model: <number, // desktop only, null on failure
+ stepping: <number>, // desktop only, null on failure
+ l2cacheKB: <number>, // L2 cache size in KB, only on windows & mac
+ l3cacheKB: <number>, // desktop only, L3 cache size in KB
+ speedMHz: <number>, // desktop only, cpu clock speed in MHz
+ extensions: [
+ <string>,
+ ...
+ // as applicable:
+ // "hasMMX", "hasSSE", "hasSSE2", "hasSSE3", "hasSSSE3",
+ // "hasSSE4A", "hasSSE4_1", "hasSSE4_2", "hasAVX", "hasAVX2",
+ // "hasAES", "hasEDSP", "hasARMv6", "hasARMv7", "hasNEON"
+ ],
+ },
+ device: { // This section is only available on mobile devices.
+ model: <string>, // the "device" from FHR, null on failure
+ manufacturer: <string>, // null on failure
+ hardware: <string>, // null on failure
+ isTablet: <bool>, // null on failure
+ },
+ os: {
+ name: <string>, // "Windows_NT" or null on failure
+ version: <string>, // e.g. "6.1", null on failure
+ kernelVersion: <string>, // android only or null on failure
+ servicePackMajor: <number>, // windows only or null on failure
+ servicePackMinor: <number>, // windows only or null on failure
+ windowsBuildNumber: <number>, // windows only or null on failure
+ windowsUBR: <number>, // windows 10 only or null on failure
+ installYear: <number>, // windows only or null on failure
+ locale: <string>, // "en" or null on failure
+ hasPrefetch: <bool>, // windows only, or null on failure
+ hasSuperfetch: <bool>, // windows only, or null on failure
+ distro: <string>, // linux only, or null on failure
+ distroVersion: <string>, // linux only, or null on failure
+ },
+ hdd: {
+ profile: { // hdd where the profile folder is located
+ model: <string>, // windows only or null on failure
+ revision: <string>, // windows only or null on failure
+ type: <string>, // "SSD" or "HDD" windows only or null on failure
+ },
+ binary: { // hdd where the application binary is located
+ model: <string>, // windows only or null on failure
+ revision: <string>, // windows only or null on failure
+ type: <string>, // "SSD" or "HDD" windows only or null on failure
+ },
+ system: { // hdd where the system files are located
+ model: <string>, // windows only or null on failure
+ revision: <string>, // windows only or null on failure
+ type: <string>, // "SSD" or "HDD" windows only or null on failure
+ },
+ },
+ gfx: {
+ D2DEnabled: <bool>, // null on failure
+ DWriteEnabled: <bool>, // null on failure
+ ContentBackend: <string> // One of "Cairo", "Skia", or "Direct2D 1.1"
+ Headless: <bool>, // null on failure
+ TargetFrameRate: <number>, // frame rate in Hz, typically 60 or more
+ //DWriteVersion: <string>, // temporarily removed, pending bug 1154500
+ adapters: [
+ {
+ description: <string>, // e.g. "Intel(R) HD Graphics 4600", null on failure
+ vendorID: <string>, // null on failure
+ deviceID: <string>, // null on failure
+ subsysID: <string>, // null on failure
+ RAM: <number>, // in MB, null on failure
+ driver: <string>, // null on failure
+ driverVendor: <string>, // null on failure
+ driverVersion: <string>, // null on failure
+ driverDate: <string>, // null on failure
+ GPUActive: <bool>, // currently always true for the first adapter
+ },
+ ...
+ ],
+ // Note: currently only added on Desktop. On Linux, only a single
+ // monitor is returned for the primary screen.
+ monitors: [
+ {
+ screenWidth: <number>, // screen width in pixels
+ screenHeight: <number>, // screen height in pixels
+ refreshRate: <number>, // refresh rate in hertz (present on Windows only).
+ // (values <= 1 indicate an unknown value)
+ pseudoDisplay: <bool>, // networked screen (present on Windows only)
+ scale: <number>, // backing scale factor (present on Mac only)
+ },
+ ...
+ ],
+ features: {
+ compositor: <string>, // Layers backend for compositing (e.g. "d3d11", "none", "opengl", "webrender")
+
+ // Each the following features can have one of the following statuses:
+ // "unused" - This feature has not been requested.
+ // "unavailable" - Safe Mode or OS restriction prevents use.
+ // "blocked" - Blocked due to an internal condition such as safe mode.
+ // "blacklisted" - Blocked due to a blacklist restriction.
+ // "denied" - Blocked due to allowlist restrictions.
+ // "disabled" - User explicitly disabled this default feature.
+ // "failed" - This feature was attempted but failed to initialize.
+ // "available" - User has this feature available.
+ // The status can also include a ":" followed by a reason
+ // e.g. "FEATURE_FAILURE_WEBRENDER_VIDEO_CRASH_INTEL_23.20.16.4973"
+ d3d11: { // This feature is Windows-only.
+ status: <string>,
+ warp: <bool>, // Software rendering (WARP) mode was chosen.
+ textureSharing: <bool> // Whether or not texture sharing works.
+ version: <number>, // The D3D11 device feature level.
+ blacklisted: <bool>, // Whether D3D11 is blacklisted; use to see whether WARP
+ // was blacklist induced or driver-failure induced.
+ },
+ d2d: { // This feature is Windows-only.
+ status: <string>,
+ version: <string>, // Either "1.0" or "1.1".
+ },
+ gpuProcess: { // Out-of-process compositing ("GPU process") feature
+ status: <string>, // "Available" means currently in use
+ },
+ hwCompositing: { // hardware acceleration. i.e. whether we try using the GPU
+ status: <string>
+ },
+ wrCompositor: { // native OS compositor (CA, DComp, etc.)
+ status: <string>
+ }
+ wrSoftware: { // Software backend for WebRender, only computed when 'compositor' is 'webrender'
+ status: <string>
+ }
+ openglCompositing: { // OpenGL compositing.
+ status: <string>
+ }
+ },
+ },
+ appleModelId: <string>, // Mac only or null on failure
+ sec: { // This feature is Windows 8+ only
+ antivirus: [ <string>, ... ], // null if unavailable on platform: Product name(s) of registered antivirus programs
+ antispyware: [ <string>, ... ], // null if unavailable on platform: Product name(s) of registered antispyware programs
+ firewall: [ <string>, ... ], // null if unavailable on platform: Product name(s) of registered firewall programs
+ },
+ },
+ addons: {
+ activeAddons: { // the currently enabled add-ons
+ <addon id>: {
+ blocklisted: <bool>,
+ description: <string>, // null if not available
+ name: <string>,
+ userDisabled: <bool>,
+ appDisabled: <bool>,
+ version: <string>,
+ scope: <integer>,
+ type: <string>, // "extension", "locale", ...
+ foreignInstall: <bool>,
+ hasBinaryComponents: <bool>,
+ installDay: <number>, // days since UNIX epoch, 0 on failure
+ updateDay: <number>, // days since UNIX epoch, 0 on failure
+ signedState: <integer>, // whether the add-on is signed by AMO, only present for extensions
+ isSystem: <bool>, // true if this is a System Add-on
+ isWebExtension: <bool>, // true if this is a WebExtension
+ multiprocessCompatible: <bool>, // true if this add-on does *not* require e10s shims
+ },
+ ...
+ },
+ theme: { // the active theme
+ id: <string>,
+ blocklisted: <bool>,
+ description: <string>,
+ name: <string>,
+ userDisabled: <bool>,
+ appDisabled: <bool>,
+ version: <string>,
+ scope: <integer>,
+ foreignInstall: <bool>,
+ hasBinaryComponents: <bool>
+ installDay: <number>, // days since UNIX epoch, 0 on failure
+ updateDay: <number>, // days since UNIX epoch, 0 on failure
+ },
+ activeGMPlugins: {
+ <gmp id>: {
+ version: <string>,
+ userDisabled: <bool>,
+ applyBackgroundUpdates: <integer>,
+ },
+ ...
+ },
+ },
+ experiments: {
+ "<experiment id>": { branch: "<branch>", type: "<type>", enrollmentId: "<id>" },
+ // ...
+ }
+ }
+
+build
+-----
+
+buildId
+~~~~~~~
+Firefox builds downloaded from mozilla.org use a 14-digit buildId. Builds included in other distributions may have a different format (e.g. only 10 digits).
+
+Settings
+--------
+
+defaultSearchEngine
+~~~~~~~~~~~~~~~~~~~
+Note: Deprecated, use defaultSearchEngineData instead.
+
+Contains the string identifier or name of the default search engine provider. This will not be present in environment data collected before the Search Service initialization.
+
+The special value ``NONE`` could occur if there is no default search engine.
+
+The special value ``UNDEFINED`` could occur if a default search engine exists but its identifier could not be determined.
+
+This field's contents are ``Services.search.defaultEngine.identifier`` (if defined) or ``"other-"`` + ``Services.search.defaultEngine.name`` if not. In other words, search engines without an ``.identifier`` are prefixed with ``other-``.
+
+defaultSearchEngineData
+~~~~~~~~~~~~~~~~~~~~~~~
+Contains data identifying the engine currently set as the default.
+
+The object contains:
+
+- a ``name`` property with the name of the engine, or ``NONE`` if no
+ engine is currently set as the default.
+
+- a ``loadPath`` property: an anonymized path of the engine xml file, e.g.
+ jar:[app]/omni.ja!browser/engine.xml
+ (where 'browser' is the name of the chrome package, not a folder)
+ [profile]/searchplugins/engine.xml
+ [distribution]/searchplugins/common/engine.xml
+ [other]/engine.xml
+ [other]/addEngineWithDetails
+ [other]/addEngineWithDetails:extensionID
+ [http/https]example.com/engine-name.xml
+ [http/https]example.com/engine-name.xml:extensionID
+
+- an ``origin`` property: the value will be ``default`` for engines that are built-in or from distribution partners, ``verified`` for user-installed engines with valid verification hashes, ``unverified`` for non-default engines without verification hash, and ``invalid`` for engines with broken verification hashes.
+
+- a ``submissionURL`` property with the HTTP url we would use to search.
+ For privacy, we don't record this for user-installed engines.
+
+``loadPath`` and ``submissionURL`` are not present if ``name`` is ``NONE``.
+
+defaultPrivateSearchEngineData
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+This contains the data identifying the engine current set as the default for
+private browsing mode. This may be the same engine as set for normal browsing
+mode.
+
+This object contains the same information as ``defaultSearchEngineData``. It
+is only reported if the ``browser.search.separatePrivateDefault`` preference is
+set to ``true``.
+
+userPrefs
+~~~~~~~~~
+
+This object contains user preferences.
+
+Each key in the object is the name of a preference. A key's value depends on the policy with which the preference was collected. There are three such policies, "value", "state", and "default value". For preferences collected under the "value" policy, the value will be the preference's value. For preferences collected under the "state" policy, the value will be an opaque marker signifying only that the preference has a user value. The "state" policy is therefore used when user privacy is a concern. For preferences collected under the "default value" policy, the value will be the preference's default value, if the preference exists. If the preference does not exist, there is no key or value.
+
+The following is a partial list of `collected preferences <https://searchfox.org/mozilla-central/search?q=const+DEFAULT_ENVIRONMENT_PREFS&path=>`_.
+
+- ``browser.migrate.interactions.bookmarks``: True if the user has imported bookmarks from another browser before. This preference gets transferred during profile resets.
+
+- ``browser.migrate.interactions.csvpasswords``: True if the user has imported passwords through the migration wizard from a CSV file. This preference gets transferred during profile resets.
+
+- ``browser.migrate.interactions.history``: True if the user has imported history from another browser before. This preference gets transferred during profile resets.
+
+- ``browser.migrate.interactions.passwords``: True if the user has imported passwords from another browser before. This preference gets transferred during profile resets.
+
+- ``browser.privatebrowsing.autostart``: True if the user has enabled the permanent private browsing mode. Defaults to false.
+
+- ``browser.search.suggest.enabled``: The "master switch" for search suggestions everywhere in Firefox (search bar, urlbar, etc.). Defaults to true.
+
+- ``browser.urlbar.autoFill``: The global preference for whether autofill in the urlbar is enabled. When false, all types of autofill are disabled.
+
+- ``browser.urlbar.autoFill.adaptiveHistory.enabled``: True if adaptive history autofill in the urlbar is enabled.
+
+- ``browser.urlbar.dnsResolveSingleWordsAfterSearch``: Controls when to DNS resolve single word search strings, after they were searched for. If the string is resolved as a valid host, show a "Did you mean to go to 'host'" prompt. 0: Never resolve, 1: Use heuristics, 2. Always resolve. Defaults to 0.
+
+- ``browser.urlbar.quicksuggest.onboardingDialogChoice``: The user's choice in the Firefox Suggest onboarding dialog. If the dialog was shown multiple times, this records the user's most recent choice. Values are the following. Empty string: The user has not made a choice (e.g., because the dialog hasn't been shown). ``accept_2`` is recorded when the user accepts the dialog and opts in, ``reject_2`` is recorded when the user rejects the dialog and opts out, ``learn_more_1`` is recorded when the user clicks "Learn more" on the introduction section (the user remains opted out), ``learn_more_2`` is recorded when the user clicks "Learn more" on the main section (the user remains opted out), ``close_1`` is recorded when the user clicks close button on the introduction section (the user remains opted out), ``not_now_2`` is recorded when the user clicks "Not now" link on main section (the user remains opted out), ``dismiss_1`` recorded when the user dismisses the dialog on the introduction section (the user remains opted out), ``dismiss_2`` recorded when the user dismisses the dialog on main (the user remains opted out).
+
+- ``browser.urlbar.quicksuggest.dataCollection.enabled``: Whether the user has opted in to data collection for Firefox Suggest. This pref is set to true when the user opts in to the Firefox Suggest onboarding dialog modal. The user can also toggle the pref using a toggle switch in the Firefox Suggest preferences UI.
+
+- ``browser.urlbar.showSearchTerms.enabled``: True if to show the search term in the urlbar while on a default search engine results page.
+
+- ``browser.urlbar.suggest.quicksuggest.nonsponsored``: True if non-sponsored Firefox Suggest suggestions are enabled in the urlbar.
+
+- ``browser.urlbar.suggest.quicksuggest.sponsored``: True if sponsored Firefox Suggest suggestions are enabled in the urlbar.
+
+- ``browser.urlbar.suggest.searches``: True if search suggestions are enabled in the urlbar. Defaults to false.
+
+- ``browser.zoom.full`` (deprecated): True if zoom is enabled for both text and images, that is if "Zoom Text Only" is not enabled. Defaults to true. This preference was collected in Firefox 50 to 52 (`Bug 979323 <https://bugzilla.mozilla.org/show_bug.cgi?id=979323>`_).
+
+- ``security.tls.version.enable-deprecated``: True if deprecated versions of TLS (1.0 and 1.1) have been enabled by the user. Defaults to false.
+
+- ``privacy.firstparty.isolate``: True if the user has changed the (unsupported, hidden) First Party Isolation preference. Defaults to false.
+
+- ``privacy.resistFingerprinting``: True if the user has changed the (unsupported, hidden) Resist Fingerprinting preference. Defaults to false.
+
+- ``toolkit.telemetry.pioneerId``: The state of the Pioneer ID. If set, then user is enrolled in Pioneer. Note that this does *not* collect the value.
+
+- ``app.normandy.test-prefs.bool``: Test pref that will help troubleshoot uneven unenrollment in experiments. Defaults to false.
+
+- ``app.normandy.test-prefs.integer``: Test pref that will help troubleshoot uneven unenrollment in experiments. Defaults to 0.
+
+- ``app.normandy.test-prefs.string``: Test pref that will help troubleshoot uneven unenrollment in experiments. Defaults to "".
+
+- ``network.trr.mode``: User-set DNS over HTTPS mode. Defaults to 0.
+
+- ``network.trr.strict_native_fallback``: Whether strict fallback mode is enabled for DoH mode 2. Defaults to true on Nightly, false elsewhere.
+
+- ``extensions.InstallTriggerImpl.enabled``: Whether the InstallTrigger implementation should be enabled (or hidden and none of its methods available).
+
+- ``extensions.InstallTrigger.enabled``: Whether the InstallTrigger implementation should be enabled (or completely hidden), separate from InstallTriggerImpl because InstallTrigger is improperly used also for UA detection.
+
+- ``extensions.eventPages.enabled``: Whether non-persistent background pages (also known as Event pages) should be enabled for `"manifest_version": 2` extensions.
+
+- ``extensions.quarantinedDomains.enabled``: Whether "Quarantined Domains" is enabled.
+
+- ``extensions.manifestV3.enabled``: Whether `"manifest_version": 3` extensions should be allowed to install successfully.
+
+- ``media.gmp-gmpopenh264.enabled``: Whether OpenH264 is enabled.
+
+- ``media.gmp-gmpopenh264.lastDownload``: When OpenH264 was last downloaded as seconds since Jan 1, 1970.
+
+- ``media.gmp-gmpopenh264.lastDownloadFailed``: When OpenH264 was last downloaded unsuccessfully as seconds since Jan 1, 1970.
+
+- ``media.gmp-gmpopenh264.lastDownloadFailReason``: The exception value when OpenH264 was last failed to downloaded.
+
+- ``media.gmp-gmpopenh264.lastInstallFailed``: When OpenH264 installation last failed as seconds since Jan 1, 1970.
+
+- ``media.gmp-gmpopenh264.lastInstallStart``: When OpenH264 installation was last started as seconds since Jan 1, 1970.
+
+- ``media.gmp-gmpopenh264.lastUpdate``: When OpenH264 was last updated as seconds since Jan 1, 1970.
+
+- ``media.gmp-gmpopenh264.visible``: Whether OpenH264 is visible.
+
+- ``media.gmp-manager.lastCheck``: When the gmp-manager last checked for updates as seconds since Jan 1, 1970.
+
+- ``media.gmp-manager.lastEmptyCheck``: When the gmp-manager last checked for updates and there was nothing to install as seconds since Jan 1, 1970.
+
+- ``nimbus.qa.pref-1``: Used to monitor the results of pref-setting test experiments.
+
+- ``nimbus.qa.pref-2``: Used to monitor the results of pref-setting test experiments.
+
+- ``signon.firefoxRelay.feature``: User choice regarding Firefox Relay integration with Firefox Password Manager. Can be one of undefined, "available", "offered", "enabled" or "disabled".
+
+- ``dom.popup_allowed_events``: Which events should allow popups. Only exposed with about:config.
+
+- ``intl.ime.use_composition_events_for_insert_text``: Whether a set of composition events is fired when user inserts text without keyboard events nor composing state of a composition (only on Linux and macOS).
+
+attribution
+~~~~~~~~~~~
+
+This object contains the attribution data for the product installation.
+
+Attribution data is used to link installations of Firefox with the source that the user arrived at the Firefox download page from. It would indicate, for instance, when a user executed a web search for Firefox and arrived at the download page from there, directly navigated to the site, clicked on a link from a particular social media campaign, etc.
+
+The attribution data is included in some versions of the default Firefox installer for Windows (the "stub" installer) and stored as part of the installation. All platforms other than Windows and also Windows installations that did not use the stub installer do not have this data and will not include the ``attribution`` object.
+
+sandbox
+~~~~~~~
+
+This object contains data about the state of Firefox's sandbox.
+
+Specific keys are:
+
+- ``effectiveContentProcessLevel``: The meanings of the values are OS dependent. Details of the meanings can be found in the `Firefox prefs file <https://hg.mozilla.org/mozilla-central/file/tip/browser/app/profile/firefox.js>`_. The value here is the effective value, not the raw value, some platforms enforce a minimum sandbox level. If there is an error calculating this, it will be ``null``.
+- ``contentWin32kLockdownState``: The status of Win32k Lockdown for Content process.
+
+ - LockdownEnabled = 1 - After Firefox 98, this value will no longer appear in Telemetry.
+ - MissingWebRender = 2
+ - OperatingSystemNotSupported = 3
+ - PrefNotSet = 4 - After Firefox 98, this value will no longer appear in Telemetry.
+ - MissingRemoteWebGL = 5
+ - MissingNonNativeTheming = 6
+ - DisabledByEnvVar = 7 - MOZ_ENABLE_WIN32K is set
+ - DisabledBySafeMode = 8
+ - DisabledByE10S = 9 - E10S is disabled for whatever reason
+ - DisabledByUserPref = 10 - The user manually set security.sandbox.content.win32k-disable to false
+ - EnabledByUserPref = 11 - The user manually set security.sandbox.content.win32k-disable to true
+ - DisabledByControlGroup = 12 - The user is in the Control Group, so it is disabled
+ - EnabledByTreatmentGroup = 13 - The user is in the Treatment Group, so it is enabled
+ - DisabledByDefault = 14 - The default value of the pref is false
+ - EnabledByDefault = 15 - The default value of the pref is true
+ - DecodersArentRemote = 16 - Some decoder is not remoted to RDD Process (checks PDMFactory::AllDecodersAreRemote)
+ - IncompatibleMitigationPolicy = 17 - Some incompatible Windows Exploit Mitigation policies are enabled
+
+
+profile
+-------
+
+creationDate
+~~~~~~~~~~~~
+
+The assumed creation date of this client's profile.
+It's read from a file-stored timestamp from the client's profile directory.
+
+.. note::
+
+ If the timestamp file does not exist all files in the profile directory are scanned.
+ The oldest creation or modification date of the scanned files is then taken to be the profile creation date.
+ This has been shown to sometimes be inaccurate (`bug 1449739 <https://bugzilla.mozilla.org/show_bug.cgi?id=1449739>`_).
+
+resetDate
+~~~~~~~~~~~~
+
+The time of the last reset time for the profile. If the profile has never been
+reset this field will not be present.
+It's read from a file-stored timestamp from the client's profile directory.
+
+firstUseDate
+~~~~~~~~~~~~
+
+The time of the first use of profile. If this is an old profile where we can't
+determine this this field will not be present.
+It's read from a file-stored timestamp from the client's profile directory.
+
+partner
+-------
+
+If the user is using a partner repack, this contains information identifying the repack being used, otherwise "partnerNames" will be an empty array and other entries will be null. The information may be missing when the profile just becomes available. In Firefox for desktop, the information along with other customizations defined in distribution.ini are processed later in the startup phase, and will be fully applied when "distribution-customization-complete" notification is sent.
+
+Distributions are most reliably identified by the ``distributionId`` field. Partner information can be found in the `partner repacks <https://github.com/mozilla-partners>`_ (`the old one <https://hg.mozilla.org/build/partner-repacks/>`_ is deprecated): it contains one private repository per partner.
+Important values for ``distributionId`` include:
+
+- "MozillaOnline" for the Mozilla China repack.
+- "canonical", for the `Ubuntu Firefox repack <http://bazaar.launchpad.net/~mozillateam/firefox/firefox.trusty/view/head:/debian/distribution.ini>`_.
+- "yandex", for the Firefox Build by Yandex.
+
+system
+------
+
+os
+~~
+
+This object contains operating system information.
+
+- ``name``: the name of the OS.
+- ``version``: a string representing the OS version.
+- ``kernelVersion``: an Android only string representing the kernel version.
+- ``servicePackMajor``: the Windows only major version number for the installed service pack.
+- ``servicePackMinor``: the Windows only minor version number for the installed service pack.
+- ``windowsBuildNumber``: the Windows build number.
+- ``windowsUBR``: the Windows UBR number, only available for Windows >= 10. This value is incremented by Windows cumulative updates patches.
+- ``installYear``: the Windows only integer representing the year the OS was installed.
+- ``locale``: the string representing the OS locale.
+- ``hasPrefetch``: the Windows-only boolean representing whether or not the OS-based prefetch application start-up optimization is set to use the default settings.
+- ``hasSuperfetch``: the Windows-only boolean representing whether or not the OS-based superfetch application start-up optimization service is running and using the default settings.
+
+addons
+------
+
+activeAddons
+~~~~~~~~~~~~
+
+Starting from Firefox 44, the length of the following string fields: ``name``, ``description`` and ``version`` is limited to 100 characters. The same limitation applies to the same fields in ``theme``.
+
+Some of the fields in the record for each add-on are not available during startup. The fields that will always be present are ``id``, ``version``, ``type``, ``updateDate``, ``scope``, ``isSystem``, ``isWebExtension``, and ``multiprocessCompatible``. All the other fields documented above become present shortly after the ``sessionstore-windows-restored`` observer topic is notified.
+
+activeGMPPlugins
+~~~~~~~~~~~~~~~~
+
+Up-to-date information is not available immediately during startup. The field will be populated with dummy information until the blocklist is loaded. At the latest, this will happen just after the ``sessionstore-windows-restored`` observer topic is notified.
+
+experiments
+-----------
+For each experiment we collect the
+
+- ``id`` (Like ``hotfix-reset-xpi-verification-timestamp-1548973``, max length 100 characters)
+- ``branch`` (Like ``control``, max length 100 characters)
+- ``type`` (Optional. Like ``normandy-exp``, max length 20 characters)
+- ``enrollmentId`` (Optional. Like ``5bae2134-e121-46c2-aa00-232f3f5855c5``, max length 40 characters)
+
+In the event any of these fields are truncated, a warning is printed to the console
+
+Note that this list includes other types of deliveries, including Normandy rollouts and Nimbus feature defaults.
+
+Version History
+---------------
+
+- Firefox 88:
+
+ - Removed ``addons.activePlugins`` as part of removing NPAPI plugin support. (`bug 1682030 <https://bugzilla.mozilla.org/show_bug.cgi?id=1682030>`_)
+
+- Firefox 70:
+
+ - Added ``experiments.<experiment id>.enrollmentId``. (`bug 1555172 <https://bugzilla.mozilla.org/show_bug.cgi?id=1555172>`_)
+
+- Firefox 67:
+
+ - Removed ``persona``. The ``addons.activeAddons`` list should be used instead. (`bug 1525511 <https://bugzilla.mozilla.org/show_bug.cgi?id=1525511>`_)
+
+- Firefox 61:
+
+ - Removed empty ``addons.activeExperiment`` (`bug 1452935 <https://bugzilla.mozilla.org/show_bug.cgi?id=1452935>`_).
diff --git a/toolkit/components/telemetry/docs/data/event-ping.rst b/toolkit/components/telemetry/docs/data/event-ping.rst
new file mode 100644
index 0000000000..05fdd60a0f
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/event-ping.rst
@@ -0,0 +1,92 @@
+
+"event" ping
+=============
+
+This ping transmits :ref:`Telemetry Event <eventtelemetry>` records.
+
+The client id is submitted with this ping.
+The :doc:`Telemetry Environment <../data/environment>` is submitted in this ping.
+
+.. code-block:: js
+
+ {
+ "type": "event",
+ ... common ping data
+ "clientId": <UUID>,
+ "environment": { ... },
+ "payload": {
+ "reason": {periodic, max, shutdown}, // Why the ping was submitted
+ "processStartTimestamp": <UNIX Timestamp>, // Minute precision, for calculating absolute time across pings
+ "sessionId": <UUID>, // For linking to "main" pings
+ "subsessionId": <UUID>, // For linking to "main" pings
+ "lostEventsCount": <number>, // How many events we had to drop. Valid only for reasons "max" and "shutdown"
+ "events": {
+ "parent": [ // process name, one of the keys from Processes.yaml
+ [timestamp, category, method, object, value, extra],
+ ... // At most 1000
+ ]
+ }
+ }
+ }
+
+Send behavior
+-------------
+
+The ping is submitted at most once per ten minute interval, and at least once per hour in
+which an event was recorded. Upon reaching 1000 events, the ping is sent immediately
+unless it would be within ten minutes of the previous ping, in which case some event
+records may be lost. A count of these lost records is included in the ping.
+to avoid losing collected data.
+
+On shutdown, during profile-before-change, a final ping is sent with any remaining event
+records, regardless of frequency but obeying the event record limit.
+
+The 1000-record limit and one-hour and ten-minute frequencies are controlled by
+:doc:`preferences <../internals/preferences>`.
+
+Field details
+-------------
+
+reason
+~~~~~~
+The ``reason`` field contains the information about why the "event" ping was submitted:
+
+* ``periodic``: The event ping was submitted because at least one event happened in the past hour.
+* ``max``: The event ping was submitted because the 1000-record limit was reached.
+* ``shutdown``: The event ping was submitted because Firefox is shutting down and some events
+ have yet to be submitted.
+
+processStartTimestamp
+~~~~~~~~~~~~~~~~~~~~~
+The minute the user's Firefox main process was created. Event record timestamps are recorded
+relative to Firefox's main process start. This provides the basis for reconstructing a user's full
+session of events in order, as well as offering a mechanism for grouping event pings.
+
+sessionId
+~~~~~~~~~
+The id of the session that was current when the ping was sent.
+
+subsessionId
+~~~~~~~~~~~~
+The id of the subsession that was current when the ping was sent.
+
+.. note::
+
+ This may not be the same subsession that the events occurred in if a
+ :ref:`session split <sessionsplit>` happened in between.
+
+lostEventsCount
+~~~~~~~~~~~~~~~
+The number of events we had to discard because we reached the 1000-per-ping limit before
+we were able to send the ping. Should only have a non-zero value on "event" pings with
+reason set to "max" or "shutdown". Which events are missing should be calculable via the
+client's "main" pings using :ref:`Event Summary <events.event-summary>`.
+
+events
+~~~~~~
+A map from process names to arrays of event records that have been :ref:`serialized <events.serializationformat>`.
+
+Version History
+---------------
+
+- Firefox 62: Started sending the "event" ping (`bug 1460595 <https://bugzilla.mozilla.org/show_bug.cgi?id=1460595>`_).
diff --git a/toolkit/components/telemetry/docs/data/first-shutdown-ping.rst b/toolkit/components/telemetry/docs/data/first-shutdown-ping.rst
new file mode 100644
index 0000000000..fe20c5df45
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/first-shutdown-ping.rst
@@ -0,0 +1,11 @@
+
+"first-shutdown" ping
+=====================
+
+This ping is a duplicate of the main-ping sent on first shutdown. Enabling pingsender
+for first sessions will impact existing engagement metrics. The ``first-shutdown`` ping enables a
+more accurate view of users that churn after the first session. This ping exists as a
+stopgap until existing metrics are re-evaluated, allowing us to use the first session
+``main-pings`` directly.
+
+See :doc:`main-ping` for details about this payload.
diff --git a/toolkit/components/telemetry/docs/data/health-ping.rst b/toolkit/components/telemetry/docs/data/health-ping.rst
new file mode 100644
index 0000000000..e5655924e1
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/health-ping.rst
@@ -0,0 +1,92 @@
+
+"health" ping
+=============
+
+This ping is intended to provide data about problems arise when submitting other pings.
+The ping is submitted at most once per hour. On shutdown an additional ping is submitted
+to avoid losing collected data.
+
+This ping is intended to be really small.
+The client id is submitted with this ping.
+
+.. code-block:: js
+
+ {
+ "type": "health", // type
+ ... common ping data
+ "clientId": <UUID>, // client id, e.g.
+ // "c641eacf-c30c-4171-b403-f077724e848a"
+ "payload": {
+ "os": {
+ "name": <string>, // OS name
+ "version": <string> // OS version
+ },
+ "reason": <string>, // When ping was triggered, e.g. "immediate" or "shutdown".
+ "pingDiscardedForSize": {
+ "main": <number>, // Amount of occurrences for a specific ping type.
+ "core": <number>
+ ...
+ },
+ "sendFailure": {
+ "timeout": <number>, // Amount of occurrences for a specific failure.
+ "abort": <number>
+ ...
+ }
+ }
+ }
+
+Send behavior
+-------------
+
+``HealthPing.jsm`` tracks several problems:
+
+* The size of other assembled pings exceeds the ping limit.
+* Failures while sending other pings.
+
+After recording the data, a health ping will be sent:
+
+* immediately, with the reason ``immediate`` , if it is first ping in the session or it passed at least one hour from the previous submission.
+* after 1 hour minus the time passed from previous submission, with the reason ``delayed`` , if less than an hour passed from the previous submission.
+* on shutdown, with the reason ``shutdown`` using :doc:`../internals/pingsender`, if recorded data is not empty.
+
+Field details
+-------------
+
+reason
+~~~~~~
+The ``reason`` field contains the information about why the "health" ping was submitted. It presently supports three reasons:
+
+* immediate: The health ping was submitted immediately after recording a failure.
+* delayed: The health ping was submitted after a delay.
+* shutdown: The health ping was submitted on shutdown.
+
+pingDiscardedForSize
+~~~~~~~~~~~~~~~~~~~~
+The ``pingDiscardedForSize`` field contains the information about the top ten pings whose size exceeded the
+ping size limit (1 MB). This field lists the number of discarded pings per ping type.
+
+The ping type "<unknown>" is used to indicate that a pending pings size exceeded the limit. This is because we don't have the pending pings type available cheaply at the moment.
+
+This field is optional.
+
+sendFailure
+~~~~~~~~~~~
+The ``sendFailure`` field contains the information about pings, which had failures on sending.
+This field lists the number of failed pings per ping send failure type.
+
+The recorded failure types are:
+
+* "eOK" - No error.
+* "eRequest" - There was some error in the request before we started to service it.
+* "eUnreachable" - The remote server was unreachable.
+* "eChannelOpen" - The connection failed when we tried to open the channel.
+* "eRedirect" - The connection failed when being redirected.
+* "abort" - What XMLHttpRequest means by "abort" (see `MDN <https://developer.mozilla.org/en-US/docs/Web/Events/abort>`__)
+* "timeout" - What XMLHttpRequest means by "timeout" (see `MDN <https://developer.mozilla.org/en-US/docs/Web/Events/timeout>`__)
+
+This field is optional.
+
+.. note::
+
+ Although both ``pingDiscardedForSize`` and ``sendFailure`` fields are optional, the health ping will only
+ be submitted if one of this field not empty.
diff --git a/toolkit/components/telemetry/docs/data/heartbeat-ping.rst b/toolkit/components/telemetry/docs/data/heartbeat-ping.rst
new file mode 100644
index 0000000000..f1920111fd
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/heartbeat-ping.rst
@@ -0,0 +1,62 @@
+
+"heartbeat" ping
+=================
+
+This ping is submitted after a Firefox Heartbeat survey. Even if the user exits
+the browser, closes the survey window, or ignores the survey, Heartbeat will
+provide a ping to Telemetry for sending during the same session.
+
+The payload contains the user's survey response (if any) as well as timestamps
+of various Heartbeat events (survey shown, survey closed, link clicked, etc).
+
+The ping will also report the "surveyId", "surveyVersion" and "testing"
+Heartbeat survey parameters (if they are present in the survey config).
+These "meta fields" will be repeated verbatim in the payload section.
+
+The environment block and client ID are submitted with this ping.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ type: "heartbeat",
+ version: 4,
+ clientId: <UUID>,
+ environment: { /* ... */ }
+ // ... common ping data
+ payload: {
+ version: 1,
+ flowId: <string>,
+ ... timestamps below ...
+ offeredTS: <integer epoch timestamp>,
+ learnMoreTS: <integer epoch timestamp>,
+ votedTS: <integer epoch timestamp>,
+ engagedTS: <integer epoch timestamp>,
+ closedTS: <integer epoch timestamp>,
+ expiredTS: <integer epoch timestamp>,
+ windowClosedTS: <integer epoch timestamp>,
+ // ... user's rating below
+ score: <integer>,
+ // ... survey meta fields below
+ surveyId: <string>,
+ surveyVersion: <integer>,
+ testing: <boolean>
+ }
+ }
+
+Notes:
+
+* Pings will **NOT** have all possible timestamps, timestamps are only reported for events that actually occurred.
+* Timestamp meanings:
+ * offeredTS: when the survey was shown to the user
+ * learnMoreTS: when the user clicked on the "Learn More" link
+ * votedTS: when the user voted
+ * engagedTS: when the user clicked on the survey-provided button (alternative to voting feature)
+ * closedTS: when the Heartbeat notification bar was closed
+ * expiredTS: indicates that the survey expired after 2 hours of no interaction (threshold regulated by "browser.uitour.surveyDuration" pref)
+ * windowClosedTS: the user closed the entire Firefox window containing the survey, thus ending the survey. This timestamp will also be reported when the survey is ended by the browser being shut down.
+* The surveyId/surveyVersion fields identify a specific survey (like a "1040EZ" tax paper form). The flowID is a UUID that uniquely identifies a single user's interaction with the survey. Think of it as a session token.
+* The self-support page cannot include additional data in this payload. Only the the 4 flowId/surveyId/surveyVersion/testing fields are under the self-support page's control.
+
+See also: :doc:`common ping fields <common-ping>`
diff --git a/toolkit/components/telemetry/docs/data/index.rst b/toolkit/components/telemetry/docs/data/index.rst
new file mode 100644
index 0000000000..c03027d431
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/index.rst
@@ -0,0 +1,19 @@
+==================
+Data documentation
+==================
+
+.. toctree::
+ :maxdepth: 2
+ :titlesonly:
+ :glob:
+
+ common-ping
+ environment
+ main-ping
+ crash-ping
+ backgroundhangmonitor-ping
+ anonymous-ping
+ first-shutdown-ping
+ *-ping
+
+The `mozilla-pipeline-schemas repository <https://github.com/mozilla-services/mozilla-pipeline-schemas/>`_ contains schemas for some of the pings.
diff --git a/toolkit/components/telemetry/docs/data/install-ping.rst b/toolkit/components/telemetry/docs/data/install-ping.rst
new file mode 100644
index 0000000000..a5b002ba7b
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/install-ping.rst
@@ -0,0 +1,234 @@
+============
+Install Ping
+============
+
+The install pings contain some data about the system and the installation process, sent whenever the installer exits [#earlyexit]_.
+
+---------
+Stub Ping
+---------
+
+The :doc:`Stub Installer </browser/installer/windows/installer/StubInstaller>` sends a ping just before it exits, in function SendPing of `stub.nsi <https://searchfox.org/mozilla-central/source/browser/installer/windows/nsis/stub.nsi>`_. This is sent as an HTTP GET request to DSMO (download-stats.mozilla.org).
+
+Ingestion is handled in `gcp-ingestion <https://mozilla.github.io/gcp-ingestion/>`_ at class StubUri within `ParseUri <https://github.com/mozilla/gcp-ingestion/blob/master/ingestion-beam/src/main/java/com/mozilla/telemetry/decoder/ParseUri.java>`_. Several of the fields are codes which are broken out into multiple boolean columns in the database table.
+
+-----------------
+Full Install Ping
+-----------------
+
+The :doc:`Full Installer </browser/installer/windows/installer/FullInstaller>` sends a ping just before it exits, in function SendPing of `installer.nsi <https://searchfox.org/mozilla-central/source/browser/installer/windows/nsis/installer.nsi>`_. This is an HTTP POST request with a JSON document, sent to the standard Telemetry endpoint (incoming.telemetry.mozilla.org).
+
+To avoid double counting, the full installer does not send a ping when it is launched from the stub installer, so pings where ``installer_type = "full"`` correspond to installs that did not use the stub.
+
+--------------------------
+Querying the install pings
+--------------------------
+
+The pings are recorded in the ``firefox_installer.install`` table, accessible in `Redash <https://sql.telemetry.mozilla.org>`_ [#redashlogin]_ using the default "Telemetry (BigQuery)" data source.
+
+Some of the columns are marked [DEPRECATED] because they involve features that were removed, mostly when the stub installer was `streamlined <https://bugzilla.mozilla.org/show_bug.cgi?id=1328445>`_ in Firefox 55. These columns were not removed to keep compatibility and so we could continue to use the old data, but they should no longer be used.
+
+The columns are annotated with "(stub)", "(full)", or "(both)" to indicate which types of installer provide meaningful values.
+
+See also the `JSON schema <https://github.com/mozilla-services/mozilla-pipeline-schemas/blob/master/templates/firefox-installer/install/install.1.schema.json>`_.
+
+submission_timestamp (both)
+ Time the ping was received
+
+installer_type (both)
+ Which type of installer generated this ping (full or stub)
+
+installer_version (full)
+ Version of the installer itself [#stubversion]_
+
+build_channel (both)
+ Channel the installer was built with the branding for ("release", "beta", "nightly", or "default")
+
+update_channel (both)
+ Value of MOZ_UPDATE_CHANNEL for the installer build; should generally be the same as build_channel
+
+version, build_id (both)
+ Version number and Build ID of the installed product, from ``application.ini``. This is **not** the version of the installer itself.
+
+ stub: 0 if the installation failed
+
+ full: ``""`` if not found [#versionfailure]_
+
+locale (both)
+ Locale of the installer and of the installed product, in AB_CD format
+
+from_msi (full)
+ True if the install was launched from an MSI wrapper.
+
+_64bit_build (both)
+ True if a 64-bit build was selected for installation.
+
+ stub: This means the OS is 64-bit, the RAM requirement was met, and no third-party software that blocks 64-bit installations was found
+
+ full: Hardcoded based on the architecture to be installed
+
+_64bit_os (both)
+ True if the version of Windows on the machine was 64-bit.
+
+os_version (both)
+ Version number of Windows in ``major.minor.build`` format [#win10build]_
+
+service_pack (stub)
+ Latest Windows service pack installed on the machine.
+
+server_os (both)
+ True if the installed OS is a server version of Windows.
+
+admin_user (both)
+ True if the installer was run by a user with administrator privileges (and the UAC prompt was accepted). Specifically, this reports whether :abbr:`HKLM (HKEY_LOCAL_MACHINE)` was writeable.
+
+default_path (both)
+ True if the default installation path was not changed.
+
+ stub: [DEPRECATED] [#stubdefaultpath]_
+
+set_default (both)
+ True if the option to set the new installation as the default browser was left selected.
+
+ stub: [DEPRECATED] [#stubsetdefault]_
+
+new_default (both)
+ True if the new installation is now the default browser (registered to handle the http protocol).
+
+ full: Checks the association using ``AppAssocReg::QueryCurrentDefault`` and :abbr:`HKCU (HKEY_CURRENT_USER)`.
+
+ stub: [DEPRECATED] [#stubnewdefault]_
+
+old_default (both)
+ True if firefox.exe in a different directory is now the default browser, mutually exclusive with new_default. The details are the same as new_default.
+
+had_old_install (both)
+ True if at least one existing installation of Firefox was found on the system prior to this installation.
+
+ full: Checks for the installation directory given in the ``Software\Mozilla\${BrandFullName}`` registry keys, either :abbr:`HKLM` or :abbr:`HKCU`
+
+ stub: Checks for the top level profile directory ``%LOCALAPPDATA%\Mozilla\Firefox``
+
+old_version, old_build_id (stub)
+ Version number and Build ID (from ``application.ini``) of a previous installation of Firefox in the install directory, 0 if not found
+
+bytes_downloaded (stub)
+ Size of the full installer data that was transferred before the download ended (whether it failed, was cancelled, or completed normally)
+
+download_size (stub)
+ Expected size of the full installer download according to the HTTP response headers
+
+download_retries (stub)
+ Number of times the full installer download was retried or resumed. 10 retries is the maximum.
+
+download_time (stub)
+ Number of seconds spent downloading the full installer [#downloadphase]_
+
+download_latency (stub)
+ Seconds between sending the full installer download request and receiving the first response data
+
+download_ip (stub)
+ IP address of the server the full installer was download from (can be either IPv4 or IPv6)
+
+manual_download (stub)
+ True if the user clicked on the button that opens the manual download page. The prompt to do that is shown after the installation fails or is cancelled.
+
+intro_time (both)
+ Seconds the user spent on the intro screen.
+
+ stub: [DEPRECATED] The streamlined stub no longer has this screen, so this should always be 0.
+
+options_time (both)
+ Seconds the user spent on the options screen.
+
+ stub: [DEPRECATED] The streamlined stub no longer has this screen, so this should always be 0.
+
+preinstall_time (stub)
+ Seconds spent verifying the downloaded full installer and preparing to run it
+
+install_time (both)
+ full: Seconds taken by the installation phase.
+
+ stub: Seconds taken by the full installer.
+
+finish_time (both)
+ full: Seconds the user spent on the finish page.
+
+ stub: Seconds spent waiting for the installed application to launch.
+
+succeeded (both)
+ True if a new installation was successfully created. False if that didn't happen for any reason, including when the user closed the installer window.
+
+disk_space_error (stub)
+ [DEPRECATED] True if the installation failed because the drive we're trying to install to does not have enough space. The streamlined stub no longer sends a ping in this case, because the installation drive can no longer be selected.
+
+no_write_access (stub)
+ [DEPRECATED] True if the installation failed because the user doesn't have permission to write to the path we're trying to install to. The streamlined stub no longer sends a ping in this case, because the installation directory can no longer be selected.
+
+user_cancelled (both)
+ True if the installation failed because the user cancelled it or closed the window.
+
+out_of_retries (stub)
+ True if the installation failed because the download had to be retried too many times (currently 10)
+
+file_error (stub)
+ True if the installation failed because the downloaded file couldn't be read from
+
+sig_not_trusted (stub)
+ True if the installation failed because the signature on the downloaded file wasn't valid or wasn't signed by a trusted authority
+
+sig_unexpected (stub)
+ True if the installation failed because the signature on the downloaded file didn't have the expected subject and issuer names
+
+install_timeout (stub)
+ True if the installation failed because running the full installer timed out. Currently that means it ran for more than 200 seconds for a new installation, or 215 seconds for a paveover installation.
+
+new_launched (both)
+ True if the installation succeeded and tried to launch the newly installed application.
+
+old_running (stub)
+ [DEPRECATED] True if the installation succeeded and we weren't able to launch the newly installed application because a copy of Firefox was already running. This should always be false since the check for a running copy was `removed <https://bugzilla.mozilla.org/show_bug.cgi?id=1601806>`_ in Firefox 74.
+
+attribution (both)
+ Any attribution data that was included with the installer
+
+profile_cleanup_prompt (stub)
+ 0: neither profile cleanup prompt was shown
+
+ 1: the "reinstall" version of the profile cleanup prompt was shown (no existing installation was found, but the user did have an old Firefox profile)
+
+ 2: the "paveover" version of the profile cleanup prompt was shown (an installation of Firefox was already present, but it's an older version)
+
+profile_cleanup_requested (stub)
+ True if either profile cleanup prompt was shown and the user accepted the prompt
+
+funnelcake (stub)
+ `Funnelcake <https://wiki.mozilla.org/Funnelcake>`_ ID
+
+ping_version (stub)
+ Version of the stub ping, currently 8
+
+silent (full)
+ True if the install was silent (see :ref:`Full Installer Configuration`)
+
+---------
+Footnotes
+---------
+
+.. [#earlyexit] No ping is sent if the installer exits early because initial system requirements checks fail.
+
+.. [#redashlogin] A Mozilla LDAP login is required to access Redash.
+
+.. [#stubversion] The version of the installer would be useful for the stub, but it is not currently sent as part of the stub ping.
+
+.. [#versionfailure] If the installation failed or was cancelled, the full installer will still report the version number of whatever was in the installation directory, or ``""`` on if it couldn't be read.
+
+.. [#win10build] Previous versions of Windows have used a very small set of build numbers through their entire lifecycle. However, Windows 10 gets a new build number with every major update (about every 6 months), and many more builds have been released on its insider channels. So, to prevent a huge amount of noise, queries using this field should generally filter out the build number and only use the major and minor version numbers to differentiate Windows versions, unless the build number is specifically needed.
+
+.. [#stubdefaultpath] ``default_path`` should always be true in the stub, since we no longer support changing the path, but see `bug 1351697 <https://bugzilla.mozilla.org/show_bug.cgi?id=1351697>`_.
+
+.. [#stubsetdefault] We no longer attempt to change the default browser setting in the streamlined stub, so set_default should always be false.
+
+.. [#stubnewdefault] We no longer attempt to change the default browser setting in the streamlined stub, so new_default should usually be false, but the stub still checks the association at ``Software\Classes\http\shell\open\command`` in :abbr:`HKLM` or :abbr:`HKCU`.
+
+.. [#downloadphase] ``download_time`` was previously called ``download_phase_time``, this includes retries during the download phase. There was a different ``download_time`` field that specifically measured only the time of the last download, this is still submitted but it is ignored during ingestion.
diff --git a/toolkit/components/telemetry/docs/data/launcher-process-failure-ping.rst b/toolkit/components/telemetry/docs/data/launcher-process-failure-ping.rst
new file mode 100644
index 0000000000..f09e0422af
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/launcher-process-failure-ping.rst
@@ -0,0 +1,96 @@
+
+Launcher Process Failure ping
+=============================
+
+This ping is generated by the Firefox launcher process when it fails to successfully start the browser, or generated by the browser process when it fails to start a sandboxed process.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ type: "launcher-process-failure",
+ "version": 1,
+ "id": <UUID>,
+ "creationDate": <Date string in ISO format>,
+ "update_channel": <string>,
+ "build_id": <string>,
+ "build_version": <string>,
+ // Windows version number in major.minor.build.UBR format (UBR is optional, only available on Win10)
+ "os_version": <string>,
+ // True if this build was running atop a Windows Server OS
+ "server_os": <bool>,
+ // The current user's OS locale setting
+ "os_locale": <string>,
+ // CPU Architecture. One of the values from the Windows SYSTEM_INFO::wProcessorArchitecture field
+ "cpu_arch": <int>,
+ "num_logical_cpus": <int>,
+ // True if the process was launched with Administrator privileges
+ // but without User Account Control (= UAC)
+ "is_admin_without_uac": <bool>,
+ // The type of the process which failed to start as a sandboxed process.
+ // If the launcher process fails to launch the browser process, this property is not set.
+ "process_type": <string>,
+ "memory": {
+ // Free space available in the page file, in bytes
+ "total_phys": <int>,
+ // Available physical memory on the machine, in bytes
+ "avail_phys": <int>,
+ // Free space available in the page file, in bytes
+ "avail_page_file": <int>,
+ // Available virtual memory on the machine, in bytes
+ "avail_virt": <int>
+ },
+ "xpcom_abi": <string>,
+ "launcher_error": {
+ // The leaf name of the source file where the error was raised
+ "source_file": <string>,
+ // The line number of the source file where the error was raised
+ "source_line": <int>,
+ // The HRESULT error code of the error that was raised
+ "hresult": <int>,
+ // First sixteen bytes of a function that we failed to hook (Nightly-only).
+ // This field is added only on detour failures.
+ "detour_orig_bytes": <string>
+ },
+ "security": {
+ // A list of names of installed antivirus products
+ "av": [
+ <string>,
+ ...
+ ],
+ // A list of names of installed antispyware products
+ "antispyware": [
+ <string>,
+ ...
+ ],
+ // A list of names of installed firewall products
+ "firewall": [
+ <string>,
+ ...
+ ]
+ },
+ // A mapping of all modules present in the failing process, including
+ // their version numbers and an optional index into the signatures array
+ "modules": {
+ <moduleName>: [
+ <string>,
+ <int>
+ ],
+ ...
+ },
+ // A list of all signatures that were used to sign the binaries that are
+ // listed in modules.
+ "signatures": [
+ <string>,
+ ...
+ ]
+ }
+
+Version History
+~~~~~~~~~~~~~~~
+
+- Firefox 82: Added ``detour_orig_bytes`` (`bug 1588245 <https://bugzilla.mozilla.org/show_bug.cgi?id=1588245>`_).
+- Firefox 82: Added ``process_type`` (`bug 1630444 <https://bugzilla.mozilla.org/show_bug.cgi?id=1630444>`_).
+- Firefox 71: Added ``is_admin_without_uac`` (`bug 1567605 <https://bugzilla.mozilla.org/show_bug.cgi?id=1567605>`_).
+- Firefox 67: Initial release (`bug 1460433 <https://bugzilla.mozilla.org/show_bug.cgi?id=1460433>`_).
diff --git a/toolkit/components/telemetry/docs/data/main-ping.rst b/toolkit/components/telemetry/docs/data/main-ping.rst
new file mode 100644
index 0000000000..23f7d8c600
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/main-ping.rst
@@ -0,0 +1,504 @@
+
+"main" ping
+===========
+
+.. toctree::
+ :maxdepth: 2
+
+This is the "main" Telemetry ping type, whose payload contains most of the measurements that are used to track the performance and health of Firefox in the wild.
+It includes histograms and other performance and diagnostic data.
+
+This ping may be triggered for one of many reasons documented by the ``reason`` field:
+
+* ``aborted-session`` - this ping is regularly saved to disk (every 5 minutes), overwriting itself, and deleted at shutdown. If a previous aborted session ping is found at startup, it gets sent to the server. The first aborted-session ping is generated as soon as Telemetry starts
+* ``environment-change`` - the :doc:`environment` changed, so the session measurements got reset and a new subsession starts
+* ``shutdown`` - triggered when the browser session ends. For the first browsing session, this ping is saved to disk and sent on the next browser restart. From the second browsing session on, this ping is sent immediately on shutdown using the :doc:`../internals/pingsender`, unless the OS is shutting down
+* ``daily`` - a session split triggered in 24h hour intervals at local midnight. If an ``environment-change`` ping is generated by the time it should be sent, the daily ping is rescheduled for the next midnight
+* ``saved-session`` - the *"classic"* Telemetry payload with measurements covering the whole browser session (only submitted on Android)
+
+.. _sessionsplit:
+
+Most reasons lead to a session split, initiating a new *subsession*. We reset important measurements for those subsessions.
+
+After a new subsession split, the ``internal-telemetry-after-subsession-split`` topic is notified to all the observers. *This is an internal topic and is only meant for internal Telemetry usage.*
+
+.. note::
+
+ ``saved-session`` is sent with a different ping type (``saved-session``, not ``main``), but otherwise has the same format as discussed here. As of Firefox 61 this is sent on Android only.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ version: 4,
+
+ info: {
+ reason: <string>, // what triggered this ping: "saved-session", "environment-change", "shutdown", ...
+ revision: <string>, // the Histograms.json revision
+ timezoneOffset: <integer>, // time-zone offset from UTC, in minutes, for the current locale
+ previousBuildId: <string>, // null if this is the first run, or the previous build ID is unknown
+
+ sessionId: <uuid>, // random session id, shared by subsessions
+ subsessionId: <uuid>, // random subsession id
+ previousSessionId: <uuid>, // session id of the previous session, null on first run.
+ previousSubsessionId: <uuid>, // subsession id of the previous subsession (even if it was in a different session),
+ // null on first run.
+
+ subsessionCounter: <unsigned integer>, // the running no. of this subsession since the start of the browser session
+ profileSubsessionCounter: <unsigned integer>, // the running no. of all subsessions for the whole profile life time
+
+ sessionStartDate: <ISO date>, // hourly precision, ISO date in local time
+ subsessionStartDate: <ISO date>, // hourly precision, ISO date in local time
+ sessionLength: <integer>, // the session length until now in seconds, monotonic
+ subsessionLength: <integer>, // the subsession length in seconds, monotonic
+
+ addons: <string>, // obsolete, use ``environment.addons``
+ },
+
+ processes: {...},
+ simpleMeasurements: {...},
+
+ // The following properties may all be null if we fail to collect them.
+ histograms: {...},
+ keyedHistograms: {...},
+ chromeHangs: {...}, // removed in firefox 62
+ threadHangStats: [...], // obsolete in firefox 57, use the 'bhr' ping
+ log: [...], // obsolete in firefox 61, use Event Telemetry or Scalars
+ gc: {...},
+ fileIOReports: {...},
+ lateWrites: {...},
+ addonDetails: {...},
+ UIMeasurements: [...], // Android only
+ slowSQL: {...},
+ slowSQLstartup: {...},
+ }
+
+info
+----
+
+sessionLength
+~~~~~~~~~~~~~
+The length of the current session so far in seconds.
+This uses a monotonic clock, so this may mismatch with other measurements that
+are not monotonic like calculations based on ``Date.now()``.
+
+Note that this currently does not behave consistently over our supported platforms:
+
+* On Windows this uses ``GetTickCount64()``, which does increase over sleep periods
+* On macOS this uses ``mach_absolute_time()``, which does not increase over sleep periods
+* On POSIX/Linux this uses ``clock_gettime(CLOCK_MONOTONIC, &ts)``, which should not increase over sleep time
+
+See `bug 1204823 <https://bugzilla.mozilla.org/show_bug.cgi?id=1204823>`_ for details.
+
+subsessionLength
+~~~~~~~~~~~~~~~~
+The length of this subsession in seconds.
+This uses a monotonic clock, so this may mismatch with other measurements that are not monotonic (e.g. based on ``Date.now()``).
+
+Also see the remarks for ``sessionLength`` on platform consistency.
+
+processes
+---------
+This section contains per-process data.
+
+Structure:
+
+.. code-block:: js
+
+ "processes" : {
+ // ... other processes ...
+ "parent": {
+ scalars: {...},
+ keyedScalars: {...},
+ // parent process histograms and keyedHistograms are in main payload
+ },
+ "content": {
+ scalars: {...},
+ keyedScalars: {...},
+ histograms: {...},
+ keyedHistograms: {...},
+ },
+ "gpu": {
+ // ...
+ }
+ }
+
+histograms and keyedHistograms
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+This section contains histograms and keyed histograms accumulated on content processes. Histograms recorded on a content child process have different character than parent histograms. For instance, ``GC_MS`` will be much different in ``processes.content`` as it has to contend with web content, whereas the instance in ``payload.histograms`` has only to contend with browser JS. Also, some histograms may be absent if never recorded on a content child process (``EVENTLOOP_UI_ACTIVITY`` is parent-process-only).
+
+This format was adopted in Firefox 51 via bug 1218576.
+
+scalars and keyedScalars
+~~~~~~~~~~~~~~~~~~~~~~~~
+This section contains the :doc:`../collection/scalars` that are valid for the current platform. Scalars are only submitted if data was added to them, and are only reported with subsession pings. The recorded scalars are described in the `Scalars.yaml <https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/Scalars.yaml>`_ file. The ``info.revision`` field indicates the revision of the file that describes the reported scalars.
+
+simpleMeasurements
+------------------
+This section contains a list of simple measurements, or counters. In addition to the ones highlighted below, Telemetry timestamps (see `here <https://searchfox.org/mozilla-central/search?q=TelemetryTimestamps.add&redirect=false&case=true>`__ and `here <https://searchfox.org/mozilla-central/search?q=recordTimestamp&redirect=false&case=true>`__) can be reported.
+
+totalTime
+~~~~~~~~~
+A non-monotonic integer representing the number of seconds the session has been alive.
+
+addonManager
+~~~~~~~~~~~~
+Only available in the extended set of measures, it contains a set of counters related to Addons. See `here <https://searchfox.org/mozilla-central/search?q=AddonManagerPrivate.recordSimpleMeasure&redirect=false&case=true>`__ for a list of recorded measures.
+
+UITelemetry
+~~~~~~~~~~~
+As of Firefox 61 this section is no longer present.
+
+Only available in the extended set of measures. For more see :ref:`uitelemetry`.
+
+startupInterrupted
+~~~~~~~~~~~~~~~~~~
+A boolean set to true if startup was interrupted by an interactive prompt.
+
+js
+~~
+This section contains a series of counters from the JavaScript engine.
+
+Structure:
+
+.. code-block:: js
+
+ "js" : {
+ // ...
+ }
+
+As of Firefox 59 this section no longer contains any entries, as of Firefox 61 this section is removed.
+
+maximalNumberOfConcurrentThreads
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+An integer representing the highest number of threads encountered so far during the session.
+
+startupSessionRestoreReadBytes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Windows-only integer representing the number of bytes read by the main process up until the session store has finished restoring the windows.
+
+startupSessionRestoreWriteBytes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Windows-only integer representing the number of bytes written by the main process up until the session store has finished restoring the windows.
+
+startupWindowVisibleReadBytes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Windows-only integer representing the number of bytes read by the main process up until after a XUL window is made visible.
+
+startupWindowVisibleWriteBytes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Windows-only integer representing the number of bytes written by the main process up until after a XUL window is made visible.
+
+debuggerAttached
+~~~~~~~~~~~~~~~~
+A boolean set to true if a debugger is attached to the main process.
+
+shutdownDuration
+~~~~~~~~~~~~~~~~
+The time, in milliseconds, it took to complete the last shutdown.
+
+failedProfileLockCount
+~~~~~~~~~~~~~~~~~~~~~~
+The number of times the system failed to lock the user profile.
+
+activeTicks
+~~~~~~~~~~~
+Integer count of the number of five-second intervals ('ticks') the user was considered 'active' (sending UI events to the window). An extra event is fired immediately when the user becomes active after being inactive. This is for some mouse and gamepad events, and all touch, keyboard, wheel, and pointer events (see `EventStateManager.cpp <https://searchfox.org/mozilla-central/source/dom/events/EventStateManager.cpp#504>`__).
+This measure might be useful to give a trend of how much a user actually interacts with the browser when compared to overall session duration. It does not take into account whether or not the window has focus or is in the foreground. Just if it is receiving these interaction events.
+Note that in ``main`` pings, this measure is reset on subsession splits, while in ``saved-session`` pings it covers the whole browser session.
+
+histograms
+----------
+This section contains the histograms that are valid for the current platform. ``Flag`` histograms are always created and submitted with a default value of ``false`` if a value of ``true`` is not recorded during the time period. Other histogram types (see :ref:`choosing-histogram-type`) are not created nor submitted if no data was added to them. The type and format of the reported histograms is described by the ``Histograms.json`` file. Its most recent version is available `here <https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/Histograms.json>`__. The ``info.revision`` field indicates the revision of the file that describes the reported histograms.
+
+keyedHistograms
+---------------
+This section contains the keyed histograms available for the current platform.
+
+As of Firefox 48, this section does not contain empty keyed histograms anymore.
+
+threadHangStats
+---------------
+As of Firefox 57 this section is no longer present, and has been replaced with the :doc:`bhr ping <backgroundhangmonitor-ping>`.
+
+Contains the statistics about the hangs in main and background threads. Note that hangs in this section capture the `label stack <https://developer.mozilla.org/en-US/docs/Mozilla/Performance/Profiling_with_the_Built-in_Profiler#Native_stack_vs._label_stack>`_ and an incomplete JS stack, which is not 100% precise. For particularly egregious hangs, and on nightly, an unsymbolicated native stack is also captured. The amount of time that is considered "egregious" is different from thread to thread, and is set when the BackgroundHangMonitor is constructed for that thread. In general though, hangs from 5 - 10 seconds are generally considered egregious. Shorter hangs (1 - 2s) are considered egregious for other threads (the compositor thread, and the hang monitor that is only enabled during tab switch).
+
+To avoid submitting overly large payloads, some limits are applied:
+
+* Identical, adjacent "(chrome script)" or "(content script)" stack entries are collapsed together. If a stack is reduced, the "(reduced stack)" frame marker is added as the oldest frame.
+* The depth of the reported label stacks is limited to 11 entries. This value represents the 99.9th percentile of the thread hangs stack depths reported by Telemetry.
+* The native stacks are limited to a depth of 25 stack frames.
+
+Structure:
+
+.. code-block:: js
+
+ "threadHangStats" : [
+ {
+ "name" : "Gecko",
+ "activity" : {...}, // a time histogram of all task run times
+ "nativeStacks": { // captured for all hangs on nightly, or egregious hangs on beta
+ "memoryMap": [
+ ["wgdi32.pdb", "08A541B5942242BDB4AEABD8C87E4CFF2"],
+ ["igd10iumd32.pdb", "D36DEBF2E78149B5BE1856B772F1C3991"],
+ // ... other entries in the format ["module name", "breakpad identifier"] ...
+ ],
+ "stacks": [
+ [
+ [
+ 0, // the module index or -1 for invalid module indices
+ 190649 // the offset of this program counter in its module or an absolute pc
+ ],
+ [1, 2540075],
+ // ... other frames ...
+ ],
+ // ... other stacks ...
+ ]
+ },
+ "hangs" : [
+ {
+ "stack" : [
+ "Startup::XRE_Main",
+ "Timer::Fire",
+ "(content script)",
+ "IPDL::PPluginScriptableObject::SendGetChildProperty",
+ ... up to 11 frames ...
+ ],
+ "nativeStack": 0, // index into nativeStacks.stacks array
+ "histogram" : {...}, // the time histogram of the hang times
+ "annotations" : [
+ {
+ "pluginName" : "Shockwave Flash",
+ "pluginVersion" : "18.0.0.209"
+ },
+ ... other annotations ...
+ ]
+ },
+ ],
+ },
+ ... other threads ...
+ ]
+
+.. _chromeHangs:
+
+chromeHangs
+-----------
+As of Firefox 62, chromeHangs has been removed. Please look to the bhr ping for
+similar functionality.
+
+Contains the statistics about the hangs happening exclusively on the main thread of the parent process. Precise C++ stacks are reported. This is only available on Nightly Release on Windows, when building using "--enable-profiling" switch.
+
+Some limits are applied:
+
+* Reported chrome hang stacks are limited in depth to 50 entries.
+* The maximum number of reported stacks is 50.
+
+The module names can contain unicode characters.
+
+Structure:
+
+.. code-block:: js
+
+ "chromeHangs" : {
+ "memoryMap" : [
+ ["wgdi32.pdb", "08A541B5942242BDB4AEABD8C87E4CFF2"],
+ ["igd10iumd32.pdb", "D36DEBF2E78149B5BE1856B772F1C3991"],
+ ... other entries in the format ["module name", "breakpad identifier"] ...
+ ],
+ "stacks" : [
+ [
+ [
+ 0, // the module index or -1 for invalid module indices
+ 190649 // the offset of this program counter in its module or an absolute pc
+ ],
+ [1, 2540075],
+ ... other frames, up to 50 ...
+ ],
+ ... other stacks, up to 50 ...
+ ],
+ "durations" : [8, ...], // the hang durations (in seconds)
+ "systemUptime" : [692, ...], // the system uptime (in minutes) at the time of the hang
+ "firefoxUptime" : [672, ...], // the Firefox uptime (in minutes) at the time of the hang
+ "annotations" : [
+ [
+ [0, ...], // the indices of the related hangs
+ {
+ "pluginName" : "Shockwave Flash",
+ "pluginVersion" : "18.0.0.209",
+ ... other annotations as key:value pairs ...
+ }
+ ],
+ ...
+ ]
+ },
+
+log
+---
+As of Firefox 61 this section is no longer present, use :ref:`eventtelemetry` or :doc:`../collection/scalars`.
+
+This section contains a log of important or unusual events reported through Telemetry.
+
+Structure:
+
+.. code-block:: js
+
+ "log": [
+ [
+ "Event_ID",
+ 3785, // the timestamp (in milliseconds) for the log entry
+ ... other data ...
+ ],
+ ...
+ ]
+
+At present there is one known users of this section: Telemetry Experiments.
+
+Telemetry Experiments uses it to note when experiments are activated and terminated.
+
+fileIOReports
+-------------
+Contains the statistics of main-thread I/O recorded during the execution. Only the I/O stats for the XRE and the profile directories are currently reported, neither of them disclosing the full local path.
+
+Structure:
+
+.. code-block:: js
+
+ "fileIOReports": {
+ "{xre}": [
+ totalTime, // Accumulated duration of all operations
+ creates, // Number of create/open operations
+ reads, // Number of read operations
+ writes, // Number of write operations
+ fsyncs, // Number of fsync operations
+ stats, // Number of stat operations
+ ],
+ "{profile}": [ ... ],
+ ...
+ }
+
+lateWrites
+----------
+This sections reports writes to the file system that happen during shutdown. The reported data contains the stack and the file names of the loaded libraries at the time the writes happened.
+
+The file names of the loaded libraries can contain unicode characters.
+
+Structure:
+
+.. code-block:: js
+
+ "lateWrites" : {
+ "memoryMap" : [
+ ["wgdi32.pdb", "08A541B5942242BDB4AEABD8C87E4CFF2"],
+ ... other entries in the format ["module name", "breakpad identifier"] ...
+ ],
+ "stacks" : [
+ [
+ [
+ 0, // the module index or -1 for invalid module indices
+ 190649 // the offset of this program counter in its module or an absolute pc
+ ],
+ [1, 2540075],
+ ... other frames ...
+ ],
+ ... other stacks ...
+ ],
+ },
+
+addonDetails
+------------
+This section contains per add-on telemetry details, as reported by each add-on provider. The XPI provider is the only one reporting at the time of writing (`see Searchfox <https://searchfox.org/mozilla-central/search?q=setTelemetryDetails&case=true>`_). Telemetry does not manipulate or enforce a specific format for the supplied provider's data.
+
+Structure:
+
+.. code-block:: js
+
+ "addonDetails": {
+ "XPI": {
+ "adbhelper@mozilla.org": {
+ "location": "app-profile",
+ "name": "ADB Helper",
+ "creator": "Mozilla & Android Open Source Project",
+ },
+ ...
+ },
+ ...
+ }
+
+slowSQL
+-------
+This section contains the information about the slow SQL queries for both the main and other threads. The execution of an SQL statement is considered slow if it takes 50ms or more on the main thread or 100ms or more on other threads. Slow SQL statements will be automatically trimmed to 1000 characters. This limit doesn't include the ellipsis and database name, that are appended at the end of the stored statement.
+
+Structure:
+
+.. code-block:: js
+
+ "slowSQL": {
+ "mainThread": {
+ "Sanitized SQL Statement": [
+ 1, // the number of times this statement was hit
+ 200 // the total time (in milliseconds) that was spent on this statement
+ ],
+ ...
+ },
+ "otherThreads": {
+ "VACUUM /* places.sqlite */": [
+ 1,
+ 330
+ ],
+ ...
+ }
+ },
+
+slowSQLStartup
+--------------
+This section contains the slow SQL statements gathered at startup (until the "sessionstore-windows-restored" event is fired). The structure of this section resembles the one for `slowSQL`_.
+
+UIMeasurements
+--------------
+This section is Android-only and contains UI specific Telemetry measurements and events (`see here <https://searchfox.org/mozilla-central/search?q=UITelemetry.%28addEvent|startSession|stopSession%29&redirect=false&case=false&regexp=true>`_).
+
+Structure:
+
+.. code-block:: js
+
+ "UIMeasurements": [
+ {
+ "type": "event", // either "session" or "event"
+ "action": "action.1",
+ "method": "menu",
+ "sessions": [],
+ "timestamp": 12345,
+ "extras": "settings"
+ },
+ {
+ "type": "session",
+ "name": "awesomescreen.1",
+ "reason": "commit",
+ "start": 123,
+ "end": 456
+ }
+ ...
+ ],
+
+Version History
+---------------
+
+- Firefox 88:
+
+ - Stopped reporting ``flashVersion`` since Flash is no longer supported. (`bug 1682030 <https://bugzilla.mozilla.org/show_bug.cgi?id=1682030>`_)
+
+- Firefox 61:
+
+ - Stopped reporting ``childPayloads`` (`bug 1443599 <https://bugzilla.mozilla.org/show_bug.cgi?id=1443599>`_).
+ - Stopped reporting ``saved-session`` pings on Firefox Desktop (`bug 1443603 <https://bugzilla.mozilla.org/show_bug.cgi?id=1443603>`_).
+ - Stopped reporting ``simpleMeasurements.js`` (`bug 1278920 <https://bugzilla.mozilla.org/show_bug.cgi?id=1278920>`_).
+ - Stopped reporting ``UITelemetry`` (`bug 1443605 <https://bugzilla.mozilla.org/show_bug.cgi?id=1443605>`_)
+
+- Firefox 62:
+
+ - ``events`` are now reported via the :doc:`../data/event-ping` (`bug 1460595 <https://bugzilla.mozilla.org/show_bug.cgi?id=1460595>`_).
+
+- Firefox 80:
+
+ - Stopped reporting ``GCTelemetry`` (`bug 1482089 <https://bugzilla.mozilla.org/show_bug.cgi?id=1482089>`_).
diff --git a/toolkit/components/telemetry/docs/data/modules-ping.rst b/toolkit/components/telemetry/docs/data/modules-ping.rst
new file mode 100644
index 0000000000..c3b193f8dc
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/modules-ping.rst
@@ -0,0 +1,46 @@
+
+"modules" ping
+==============
+
+This ping is sent once a week and includes the modules loaded in the Firefox process.
+
+The client ID and environment are submitted with this ping.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ type: "modules",
+ ... common ping data
+ clientId: <UUID>,
+ environment: { ... },
+ payload: {
+ version: 1,
+ modules: [
+ {
+ name: <string>, // Name of the module file (e.g. xul.dll)
+ version: <string>, // Version of the module
+ debugID: <string>, // ID of the debug information file
+ debugName: <string>, // Name of the debug information file
+ certSubject: <string>, // Name of the organization that signed the binary (Optional, only defined when present)
+ },
+ ...
+ ],
+ }
+ }
+
+Notes
+~~~~~
+
+The version information is only available on Windows, it is null on other platforms.
+
+The debug name is the name of the PDB on Windows (which isn't always the same as the module name modulo the extension, e.g. the PDB for C:\Windows\SysWOW64\ntdll.dll is wntdll.pdb) and is the same as the module name on other platforms.
+
+The debug ID is platform-dependent. It is compatible with the Breakpad ID used on Socorro.
+
+Sometimes the debug name and debug ID are missing for Windows modules (often with malware). In this case, they will be "null".
+
+The length of the modules array is limited to 512 entries.
+
+The name and debug name are length limited, with a maximum of 64 characters.
diff --git a/toolkit/components/telemetry/docs/data/new-profile-ping.rst b/toolkit/components/telemetry/docs/data/new-profile-ping.rst
new file mode 100644
index 0000000000..67a95b45b4
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/new-profile-ping.rst
@@ -0,0 +1,83 @@
+
+"new-profile" ping
+==================
+
+This opt-out ping is sent from Firefox Desktop 30 minutes after the browser is started, on the first session
+of a newly created profile. If the first session of a newly-created profile was shorter than 30 minutes, it
+gets sent using the :doc:`../internals/pingsender` at shutdown.
+
+.. note::
+
+ We don't sent the ping immediately after Telemetry completes initialization to give the user enough
+ time to tweak their data collection preferences.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ type: "new-profile",
+ ... common ping data
+ clientId: <UUID>,
+ environment: { ... },
+ payload: {
+ reason: "startup", // or "shutdown"
+ processes: { ... }
+ }
+ }
+
+payload.reason
+--------------
+If this field contains ``startup``, then the ping was generated at the scheduled time after
+startup. If it contains ``shutdown``, then the browser was closed before the time the
+ping was scheduled. In the latter case, the ping is generated during shutdown and sent
+using the :doc:`../internals/pingsender`.
+
+processes
+---------
+This section contains per-process data.
+
+Structure:
+
+.. code-block:: js
+
+ "processes" : {
+ "parent": {
+ "scalars": {...}
+ }
+ }
+
+scalars
+~~~~~~~
+This section contains the :doc:`../collection/scalars` that are valid for the ``new-profile`` ping,
+that is the ``record_into_store`` list contains ``new-profile``.
+Scalars are only submitted if data was added to them.
+The recorded scalars are described in the `Scalars.yaml <https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/Scalars.yaml>`_ file.
+
+Duplicate pings
+---------------
+We expect a low fraction of duplicates of this ping, mostly due to crashes happening
+right after sending the ping and before the telemetry state gets flushed to the disk. This should
+be fairly low in practice and manageable during the analysis phase.
+
+Expected behaviours
+-------------------
+The following is a list of conditions and expected behaviours for the ``new-profile`` ping:
+
+- **The ping is generated at the browser shutdown on a new profile, after the privacy policy is displayed:**
+
+ - *for an user initiated browser shutdown*, ``new-profile`` is sent immediately using the :doc:`../internals/pingsender`;
+ - *for a browser shutdown triggered by OS shutdown*, ``new-profile`` is saved to disk and sent next time the browser restarts.
+- **The ping is generated before the privacy policy is displayed**: ``new-profile`` is saved to disk and sent
+ next time the browser restarts.
+- **The ping is set to be generated and Telemetry is disabled**: ``new-profile`` is never sent, even if Telemetry is
+ turned back on later.
+- **Firefox crashes before the ping can be generated**: ``new-profile`` will be scheduled to be generated and
+ sent again on the next restart.
+- **User performs a profile refresh**:
+
+ - *the ping was already sent*: ``new-profile`` will not be sent again.
+ - *the ping was not sent*: ``new-profile`` will be generated and sent.
+ - *the refresh happens immediately after the profile creation, before the policy is shown*: ``new-profile`` will not be sent again.
+- **Firefox is run with an old profile that already sent Telemetry data**: ``new-profile`` will not be generated
+ nor sent.
diff --git a/toolkit/components/telemetry/docs/data/pioneer-study.rst b/toolkit/components/telemetry/docs/data/pioneer-study.rst
new file mode 100644
index 0000000000..d592c692ab
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/pioneer-study.rst
@@ -0,0 +1,58 @@
+=============
+Pioneer Study
+=============
+
+The `pioneer-study` ping is the main transport used by the Pioneer components.
+
+-------
+Payload
+-------
+
+It is made up of a clear text payload and an encrypted data payload, following the structure described below.
+
+Structure:
+
+.. code-block:: js
+
+ "payload": {
+ "encryptedData": "<encrypted token>",
+ "schemaVersion": 1,
+ "schemaName": "debug",
+ "schemaNamespace": "<namespace>",
+ "encryptionKeyId": "<key id>",
+ "pioneerId": "<UUID>",
+ "studyName": "pioneer-v2-example"
+ }
+
+See also the `JSON schemas <https://github.com/mozilla-services/mozilla-pipeline-schemas/tree/master/schemas/pioneer-debug>`_.
+
+encryptedData
+ The encrypted data sent using the Pioneer platform.
+
+schemaVersion
+ The payload format version.
+
+schemaName
+ The name of the schema of the encrypted data.
+
+schemaNamespace
+ The namespace used to segregate data on the pipeline.
+
+encryptionKeyId
+ The id of the key used to encrypt the data. If `discarded` is used, then the `encryptedData` will be ignored and not decoded (only possible for `deletion-request` and `pioneer-enrollment` schemas).
+
+pioneerId
+ The id of the pioneer client.
+
+studyName (optional)
+ The id of the study for which the data is being collected.
+
+------------------------
+Special Pioneer Payloads
+------------------------
+
+This ping has two special Pioneer payload configurations, indicated by the different `schemaName`: `deletion-request` and `pioneer-enrollemnt`.
+
+The `deletion-request` is sent when a user opts out from a Pioneer study: it contains the `pioneerId` and the `studyName`.
+
+The `pioneer-enrollment` is sent when a user opts into the Pioneer program: in this case it reports `schemaNamespace: "pioneer-meta"` and will have no `studyName`. It is also sent when enrolling into a study, in which case it reports the same namespace as the `deletion-request` (i.e. the id the study making the request) and the `pioneer-enrollment` schema name.
diff --git a/toolkit/components/telemetry/docs/data/sync-ping.rst b/toolkit/components/telemetry/docs/data/sync-ping.rst
new file mode 100644
index 0000000000..4668b4e695
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/sync-ping.rst
@@ -0,0 +1,357 @@
+
+"sync" ping
+===========
+
+This is an aggregated format that contains information about each sync that occurred during a timeframe. It is submitted every 12 hours, and on browser shutdown, but only if the ``syncs`` property would not be empty. The ping does not contain the environment block, nor the clientId.
+
+Each item in the ``syncs`` property is generated after a sync is completed, for both successful and failed syncs, and contains measurements pertaining to sync performance and error information.
+
+A JSON-schema document describing the exact format of the ping's payload property can be found at `services/sync/tests/unit/sync\_ping\_schema.json <https://searchfox.org/mozilla-central/source/services/sync/tests/unit/sync_ping_schema.json>`_.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ version: 4,
+ type: "sync",
+ ... common ping data
+ payload: {
+ version: 1,
+ os : { ... }, // os data from the current telemetry environment. OS specific, but typically includes name, version and locale.
+ discarded: <integer count> // Number of syncs discarded -- left out if zero.
+ why: <string>, // Why did we submit the ping? Either "shutdown", "schedule", or "idchanged".
+ uid: <string>, // Hashed FxA unique ID, or string of 32 zeros. If this changes between syncs, the payload is submitted.
+ deviceID: <string>, // Hashed FxA Device ID, hex string of 64 characters, not included if the user is not logged in. If this changes between syncs, the payload is submitted.
+ sessionStartDate: <ISO date>, // Hourly precision, ISO date in local time
+ // Array of recorded syncs. The ping is not submitted if this would be empty
+ syncs: [{
+ when: <integer milliseconds since epoch>,
+ took: <integer duration in milliseconds>,
+ didLogin: <bool>, // Optional, is this the first sync after login? Excluded if we don't know.
+ why: <string>, // Optional, why the sync occurred, excluded if we don't know.
+
+ // Optional, excluded if there was no error.
+ failureReason: {
+ name: <string>, // "httperror", "networkerror", "shutdownerror", etc.
+ code: <integer>, // Only present for "httperror" and "networkerror".
+ error: <string>, // Only present for "othererror" and "unexpectederror".
+ from: <string>, // Optional, and only present for "autherror".
+ },
+
+ // Optional, excluded if we couldn't get a valid uid or local device id
+ devices: [{
+ os: <string>, // OS string as reported by Services.appinfo.OS, if known
+ version: <string>, // Firefox version, as reported by Services.appinfo.version if known
+ id: <string>, // Hashed FxA device id for device
+ type: <string>, // broad device "type", as reported by fxa ("mobile", "tv", etc).
+ syncID: <string>, // Hashed Sync device id for device, if the user is a sync user.
+ }],
+
+ // Internal sync status information. Omitted if it would be empty.
+ status: {
+ sync: <string>, // The value of the Status.sync property, unless it indicates success.
+ service: <string>, // The value of the Status.service property, unless it indicates success.
+ },
+ // Information about each engine's sync.
+ engines: [
+ {
+ name: <string>, // "bookmarks", "tabs", etc.
+ took: <integer duration in milliseconds>, // Optional, values of 0 are omitted.
+
+ status: <string>, // The value of Status.engines, if it holds a non-success value.
+
+ // Optional, excluded if all items would be 0. A missing item indicates a value of 0.
+ incoming: {
+ applied: <integer>, // Number of records applied
+ succeeded: <integer>, // Number of records that applied without error
+ failed: <integer>, // Number of records that failed to apply
+ failedReasons: [
+ name: <string> // Error message trying to apply the record
+ count: <integer> // count of how many times this error occurred
+ ],
+ },
+
+ // Optional, excluded if it would be empty. Records that would be
+ // empty (e.g. 0 sent and 0 failed) are omitted.
+ outgoing: [
+ {
+ sent: <integer>, // Number of outgoing records sent. Zero values are omitted.
+ failed: <integer>, // Number that failed to send. Zero values are omitted.
+ failedReasons: [
+ name: <string> // Error message trying to apply the record
+ count: <integer> // count of how many times this error occurred
+ ],
+ }
+ ],
+ // Optional, excluded if there were no errors
+ failureReason: { ... }, // Same as above.
+
+ // Timings and counts for detailed steps that the engine reported
+ // as part of its sync. Optional; omitted if the engine didn't
+ // report any extra steps.
+ steps: {
+ name: <string>, // The step name.
+ took: <integer duration in milliseconds>, // Omitted if 0.
+ // Optional, extra named counts (e.g., number of items handled
+ // in this step). Omitted if the engine didn't report extra
+ // counts.
+ counts: [
+ {
+ name: <string>, // The counter name.
+ count: <integer>, // The counter value.
+ },
+ ],
+ },
+
+ // Optional, excluded if it would be empty or if the engine cannot
+ // or did not run validation on itself.
+ validation: {
+ // Optional validator version, default of 0.
+ version: <integer>,
+ checked: <integer>,
+ took: <non-monotonic integer duration in milliseconds>,
+ // Entries with a count of 0 are excluded, the array is excluded if no problems are found.
+ problems: [
+ {
+ name: <string>, // The problem identified.
+ count: <integer>, // Number of times it occurred.
+ }
+ ],
+ // Format is same as above, this is only included if we tried and failed
+ // to run validation, and if it's present, all other fields in this object are optional.
+ failureReason: { ... },
+ }
+ }
+ ],
+ // Information about any storage migrations that have occurred. Omitted if it would be empty.
+ migrations: [
+ // See the section on the `migrations` array for detailed documentation on what may appear here.
+ {
+ type: <string identifier>,
+ // per-type data
+ }
+ ]
+ }],
+ // The "node type" as reported by the token server. This will not change
+ // from sync to sync, so is reported once per ping. Optional because it
+ // will not appear if the token server omits this information, but in
+ // general, we will expect all "new" pings to have it.
+ syncNodeType: <string>,
+ events: [
+ event_array // See events below.
+ ],
+ histograms: { ... } // See histograms below
+ }
+ }
+
+info
+----
+
+discarded
+~~~~~~~~~
+
+The ping may only contain a certain number of entries in the ``"syncs"`` array, currently 500 (it is determined by the ``"services.sync.telemetry.maxPayloadCount"`` preference). Entries beyond this are discarded, and recorded in the discarded count.
+
+syncs.took
+~~~~~~~~~~
+
+These values should be monotonic. If we can't get a monotonic timestamp, -1 will be reported on the payload, and the values will be omitted from the engines. Additionally, the value will be omitted from an engine if it would be 0 (either due to timer inaccuracy or finishing instantaneously).
+
+uid
+~~~~~~~~~
+
+This property containing a hash of the FxA account identifier, which is a 32 character hexadecimal string. In the case that we are unable to authenticate with FxA and have never authenticated in the past, it will be a placeholder string consisting of 32 repeated ``0`` characters.
+
+syncs.why
+~~~~~~~~~
+
+One of the following values:
+
+- ``startup``: This is the first sync triggered after browser startup.
+- ``schedule``: This is a sync triggered because it has been too long since the last sync.
+- ``score``: This sync is triggered by a high score value one of sync's trackers, indicating that many changes have occurred since the last sync.
+- ``user``: The user manually triggered the sync.
+- ``tabs``: The user opened the synced tabs sidebar, which triggers a sync.
+
+syncs.status
+~~~~~~~~~~~~
+
+The ``engine.status``, ``payload.status.sync``, and ``payload.status.service`` properties are sync error codes, which are listed in `services/sync/modules/constants.js <https://searchfox.org/mozilla-central/source/services/sync/modules/constants.js>`_, and success values are not reported.
+
+syncs.failureReason
+~~~~~~~~~~~~~~~~~~~
+
+Stores error information, if any is present. Always contains the "name" property, which identifies the type of error it is. The types can be.
+
+- ``httperror``: Indicates that we received an HTTP error response code, but are unable to be more specific about the error. Contains the following properties:
+
+ - ``code``: Integer HTTP status code.
+
+- ``nserror``: Indicates that an exception with the provided error code caused sync to fail.
+
+ - ``code``: The nsresult error code (integer).
+
+- ``shutdownerror``: Indicates that the sync failed because we shut down before completion.
+
+- ``autherror``: Indicates an unrecoverable authentication error.
+
+ - ``from``: Where the authentication error occurred, one of the following values: ``tokenserver``, ``fxaccounts``, or ``hawkclient``.
+
+- ``othererror``: Indicates that it is a sync error code that we are unable to give more specific information on. As with the ``syncStatus`` property, it is a sync error code, which are listed in `services/sync/modules/constants.js <https://searchfox.org/mozilla-central/source/services/sync/modules/constants.js>`_.
+
+ - ``error``: String identifying which error was present.
+
+- ``unexpectederror``: Indicates that some other error caused sync to fail, typically an uncaught exception.
+
+ - ``error``: The message provided by the error.
+
+- ``sqlerror``: Indicates that we received a ``mozIStorageError`` from a database query.
+
+ - ``code``: Value of the ``error.result`` property, one of the constants listed `here <https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/MozIStorageError#Constants>`_.
+
+syncs.engine.name
+~~~~~~~~~~~~~~~~~
+
+Third-party engines are not reported, so only the following values are allowed: ``addons``, ``bookmarks``, ``clients``, ``forms``, ``history``, ``passwords``, ``prefs``, and ``tabs``.
+
+syncs.engine.validation.problems
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+For engines that can run validation on themselves, an array of objects describing validation errors that have occurred. Items that would have a count of 0 are excluded. Each engine will have its own set of items that it might put in the ``name`` field, but there are a finite number. See ``BookmarkProblemData.getSummary`` in `services/sync/modules/bookmark\_validator.js <https://searchfox.org/mozilla-central/source/services/sync/modules/bookmark_validator.js>`_ for an example.
+
+syncs.devices
+~~~~~~~~~~~~~
+
+The list of remote devices associated with this account, as reported by the clients collection. The ID of each device is hashed using the same algorithm as the local id.
+
+Events in the "sync" ping
+-------------------------
+
+The sync ping includes events in the same format as they are included in the
+main ping, see :ref:`eventtelemetry`.
+
+All events submitted as part of the sync ping which already include the "extra"
+object (the 6th parameter of the event array described in the event telemetry
+documentation) may also include a "serverTime" parameter, which the most recent
+unix timestamp sent from the sync server (as a string). This arrives in the
+``X-Weave-Timestamp`` HTTP header, and may be omitted in cases where the client
+has not yet made a request to the server, or doesn't have it for any other
+reason. It is included to improve flow analysis across multiple clients.
+
+Every event recorded in this ping will have a category of ``sync``. The following
+events are defined, categorized by the event method.
+
+Histograms in the "sync" ping
+-----------------------------
+
+The sync ping includes histograms relating to measurements of password manager usage.
+These histograms are duplicated in the main ping. Histograms are only included in a ping if they have been set by the pwmgr code.
+Currently, the histograms that can be included are:
+
+PWMGR_BLOCKLIST_NUM_SITES
+PWMGR_FORM_AUTOFILL_RESULT
+PWMGR_LOGIN_LAST_USED_DAYS
+PWMGR_LOGIN_PAGE_SAFETY
+PWMGR_NUM_PASSWORDS_PER_HOSTNAME
+PWMGR_NUM_SAVED_PASSWORDS
+PWMGR_PROMPT_REMEMBER_ACTION
+PWMGR_PROMPT_UPDATE_ACTION
+PWMGR_SAVING_ENABLED
+
+Histograms are objects with the following 6 properties:
+- min - minimum bucket size
+- max - maximum bucket size
+- histogram_type
+- counts - array representing contents of the buckets in the histogram
+- ranges - array with calculated bucket sizes
+
+sendcommand
+~~~~~~~~~~~
+
+Records that Sync wrote a remote "command" to another client. These commands
+cause that other client to take some action, such as resetting Sync on that
+client, or opening a new URL.
+
+- object: The specific command being written.
+- value: Not used (ie, ``null``)
+- extra: An object with the following attributes:
+
+ - deviceID: A GUID which identifies the device the command is being sent to.
+ - flowID: A GUID which uniquely identifies this command invocation. This GUID
+ is the same for every device the tab is sent to.
+ - streamID: A GUID which uniquely identifies this command invocation's
+ specific target. This GUID is unique for every device the tab is
+ sent to (new in Firefox 79).
+ - serverTime: (optional) Most recent server timestamp, as described above.
+
+processcommand
+~~~~~~~~~~~~~~
+
+Records that Sync processed a remote "command" previously sent by another
+client. This is logically the "other end" of ``sendcommand``.
+
+- object: The specific command being processed.
+- value: Not used (ie, ``null``)
+- extra: An object with the following attributes:
+
+ - flowID: A GUID which uniquely identifies this command invocation. The value
+ for this GUID will be the same as the flowID sent to the client via
+ ``sendcommand``.
+ - streamID: A GUID which uniquely identifies this command invocation's
+ specific target. The value for this GUID will be the same as the
+ streamID sent to the client via ``sendcommand`` (new in Firefox 79).
+ - reason: A string value of either ``"poll"``, ``"push"``, or ``"push-missed"``
+ representing an explanation for why the command is being processed.
+ - serverTime: (optional) Most recent server timestamp, as described above.
+
+The ``migrations`` Array
+------------------------
+
+The application-services developers are in the process of oxidizing parts of firefox sync and the related data storage code, which typically requires migrating the old storage into a new database and/or format.
+
+When a migration like this occurs, a record is reported in this list the next time the sync ping is submitted.
+
+Because the format of each data store may be drastically different, we are not attempting to come up with a generic representation here, and currently planning on allowing each migration record to vary independently (at least for now). These records will be distinctly identified by their ``"type"`` field.
+
+They should only appear once per migration (that is, we'd rather fail to report a record than report them multiple times).
+
+migrations.type: ``"webext-storage"``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This indicates a migration was performed from the legacy kinto-based extension-storage database into the new webext-storage rust implementation.
+
+It contains the following fields:
+
+- ``type``: Always the string ``"webext-storage"``.
+
+- ``entries``: The number of entries/preferences in the source (legacy) database, including ones we failed to read. See below for information on the distinction between ``entries`` and ``extensions`` in this record.
+
+- ``entriesSuccessful``: The number of entries/preferences (see below) which we have successfully migrated into the destination database..
+
+- ``extensions``: The number of distinct extensions which have at least one preference in the source (legacy) database.
+
+- ``extensionsSuccessful``: The number of distinct extensions which have at least one preference in the destination (migrated) database.
+
+- ``openFailure``: A boolean flag that is true if we hit a read error prior to . This likely indicates complete corruption, or a bug in an underlying library like rusqlite.
+
+
+Note: "entries" vs "extensions"
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+The ``webext-storage`` migration record detailed above contains counts for both:
+
+- The number of "entries" detected vs successfully migrated.
+- The number of "extensions" detected vs successfully migrated.
+
+This may seem redundant, but these refer to different (but related) things. The distinction here has to do with the way the two databases store extension-storage data:
+
+* The legacy database stores one row for each (``extension_id``, ``preference_name``, ``preference_value``) triple. These are referred to as ``entries``.
+
+* Conversely, the new database stores one row per extension, which is a pair containing both the ``extension_id``, as well as a dictionary holding all preference data, and so are equivalent to extensions.
+
+(The description above is a somewhat simplified view of things, as it ignores a number values each database stores which is irrelevant for migration)
+
+That is, ``entries`` represent each individual preference setting, and ``extensions`` represent the collected set of preferences for a given extension.
+
+Counts for are *both* of these are present as it's likely that the disparity would point to different kinds of issues with the migration code.
diff --git a/toolkit/components/telemetry/docs/data/third-party-modules-ping.rst b/toolkit/components/telemetry/docs/data/third-party-modules-ping.rst
new file mode 100644
index 0000000000..3871b2e4b3
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/third-party-modules-ping.rst
@@ -0,0 +1,135 @@
+.. _third-party-modules-ping:
+
+"third-party-modules" ping
+==========================
+
+This ping contains information about events whereby third-party modules
+were loaded into Firefox processes.
+
+.. code-block:: js
+
+ {
+ "type": "third-party-modules",
+ ... common ping data
+ "clientId": <UUID>,
+ "environment": { ... },
+ "payload": {
+ "structVersion": 1,
+ // List of DLL filenames that are on the dynamic blocklist
+ "blockedModules": [<string>],
+ "modules": [
+ {
+ // The sanitized name of the module as resolved by the Windows loader.
+ "resolvedDllName": <string>,
+ // Version of the DLL as contained in its resources's fixed version information.
+ "fileVersion": <string>,
+ // The value of the CompanyName field as extracted from the DLL's version information. This property is only present when such version info is present, and when the 'signedBy' property is absent.
+ "companyName": <string>,
+ // The organization whose certificate was used to sign the DLL. Only present for signed modules.
+ "signedBy": <string>,
+ // Flags that indicate this module's level of trustworthiness. This corresponds to one or more mozilla::ModuleTrustFlags OR'd together.
+ "trustFlags": <unsigned int>
+ },
+ ... Additional modules (maximum 100)
+ ],
+ "processes": {
+ <string containing processType and pid, formatted as `${processType}.0x${pid}">`: {
+ // Except for Default (which is remapped to "browser"), one of the process string names specified in xpcom/build/GeckoProcessTypes.h.
+ "processType": <string>,
+ // Elapsed time since process creation that this object was generated, in seconds.
+ "elapsed": <number>,
+ // Time spent loading xul.dll in milliseconds.
+ "xulLoadDurationMS": <number>,
+ // Number of dropped events due to failures sanitizing file paths.
+ "sanitizationFailures": <int>,
+ // Number of dropped events due to failures computing trust levels.
+ "trustTestFailures": <int>,
+ // Array of module load events for this process. The entries of this array are ordered to be in sync with the combinedStacks.stacks array (see below)
+ "events": [
+ {
+ // Elapsed time since process creation that this event was generated, in milliseconds.
+ "processUptimeMS": <int>,
+ // Time spent loading this module, in milliseconds.
+ "loadDurationMS": <number>,
+ // Thread ID for the thread that loaded the module.
+ "threadID": <int>,
+ // Name of the thread that loaded the module, when applicable.
+ "threadName": <string>,
+ // The sanitized name of the module that was requested by the invoking code. Only exists when it is different from resolvedDllName.
+ "requestedDllName": <string>,
+ // The base address to which the loader mapped the module.
+ "baseAddress": <string formatted as "0x%x">,
+ // Index of the element in the modules array that contains details about the module that was loaded during this event.
+ "moduleIndex": <int>,
+ // True if the module is included in the executable's Import Directory Table.
+ "isDependent": <bool>,
+ // The status of DLL load. This corresponds to enum ModuleLoadInfo::Status.
+ "loadStatus": <int>
+ },
+ ... Additional events (maximum 50)
+ ],
+ "combinedStacks": [
+ "memoryMap": [
+ [
+ // Name of the module symbol file, e.g. ``xul.pdb``
+ <string>,
+ // Breakpad identifier of the module, e.g. ``08A541B5942242BDB4AEABD8C87E4CFF2``
+ <string>
+ ],
+ ... Additional modules
+ ],
+ // Array of stacks for this process. These entries are ordered to be in sync with the events array
+ "stacks": [
+ [
+ [
+ // The module index or -1 for invalid module indices
+ <integer>,
+ // The program counter relative to its module base, or an absolute pc if the module index is -1
+ <unsigned integer>
+ ],
+ ... Additional stack frames (maximum 512)
+ ],
+ ... Additional stack traces (maximum 50)
+ ]
+ ]
+ },
+ ... Additional processes (maximum 100)
+ }
+ }
+ }
+
+payload.processes[...].events[...].resolvedDllName
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The effective path to the module file, sanitized to remove any potentially
+sensitive information. In most cases, the directory path is removed leaving only
+the leaf name, e.g. ``foo.dll``. There are three exceptions:
+
+* Paths under ``%ProgramFiles%`` are preserved, e.g. ``%ProgramFiles%\FooApplication\foo.dll``
+* Paths under ``%SystemRoot%`` are preserved, e.g. ``%SystemRoot%\System32\DriverStore\FileRepository\nvlt.inf_amd64_97992900c592012e\nvinitx.dll``
+* Paths under the temporary path are preserved, e.g. ``%TEMP%\bin\foo.dll``
+
+payload.processes[...].events[...].requestedDllName
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The name of the module as it was requested from the OS. This string is also
+sanitized in a similar fashion to to ``resolvedDllName``. This string is
+omitted from the ping when it is identical to ``resolvedDllName``.
+
+Notes
+~~~~~
+* The client id is submitted with this ping.
+* The :doc:`Telemetry Environment <../data/environment>` is submitted in this ping.
+* String fields within ``payload`` are limited in length to 260 characters.
+* This ping is sent once daily.
+* If there are no events to report, this ping is not sent.
+
+Version History
+~~~~~~~~~~~~~~~
+- Firefox 110: Added ``blockedModules`` (`bug 1808158 <https://bugzilla.mozilla.org/show_bug.cgi?id=1808158>`_).
+- Firefox 77: Added ``isDependent`` (`bug 1620118 <https://bugzilla.mozilla.org/show_bug.cgi?id=1620118>`_).
+- Firefox 71: Renamed from untrustedModules to third-party-modules with a revised schema (`bug 1542830 <https://bugzilla.mozilla.org/show_bug.cgi?id=1542830>`_).
+- Firefox 70: Added ``%SystemRoot%`` as an exemption to path sanitization (`bug 1573275 <https://bugzilla.mozilla.org/show_bug.cgi?id=1573275>`_).
+- Firefox 66:
+ - Added Windows Side-by-side directory trust flag (`bug 1514694 <https://bugzilla.mozilla.org/show_bug.cgi?id=1514694>`_).
+ - Added module load times (``xulLoadDurationMS``, ``loadDurationMS``) and xul.dll trust flag (`bug 1518490 <https://bugzilla.mozilla.org/show_bug.cgi?id=1518490>`_).
+ - Added SysWOW64 trust flag (`bug 1518798 <https://bugzilla.mozilla.org/show_bug.cgi?id=1518798>`_).
+- Firefox 65: Initial support (`bug 1435827 <https://bugzilla.mozilla.org/show_bug.cgi?id=1435827>`_).
diff --git a/toolkit/components/telemetry/docs/data/uitour-ping.rst b/toolkit/components/telemetry/docs/data/uitour-ping.rst
new file mode 100644
index 0000000000..06e4a9f49b
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/uitour-ping.rst
@@ -0,0 +1,25 @@
+
+"uitour-tag" ping
+=================
+
+This ping is submitted via the UITour ``setTreatmentTag`` API. It may be used by
+the tour to record what settings were made by a user or to track the result of
+A/B experiments.
+
+The client ID is submitted with this ping.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ version: 1,
+ type: "uitour-tag",
+ clientId: <string>,
+ payload: {
+ tagName: <string>,
+ tagValue: <string>
+ }
+ }
+
+See also: :doc:`common ping fields <common-ping>`
diff --git a/toolkit/components/telemetry/docs/data/uninstall-ping.rst b/toolkit/components/telemetry/docs/data/uninstall-ping.rst
new file mode 100644
index 0000000000..281cc3fcec
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/uninstall-ping.rst
@@ -0,0 +1,36 @@
+
+"uninstall" ping
+================
+
+This opt-out ping is sent from the Windows uninstaller when the uninstall finishes. Notably it includes ``clientId`` and the :doc:`Telemetry Environment <environment>`. It follows the :doc:`common ping format <common-ping>`.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ type: "uninstall",
+ ... common ping data
+ clientId: <UUID>,
+ environment: { ... },
+ payload: {
+ otherInstalls: <integer>, // Optional, number of other installs on the system, max 11.
+ }
+ }
+
+See also the `JSON schema <https://github.com/mozilla-services/mozilla-pipeline-schemas/blob/master/templates/telemetry/uninstall/uninstall.4.schema.json>`_. These pings are recorded in the ``telemetry.uninstall`` table in Redash, using the default "Telemetry (BigQuery)" data source.
+
+payload.otherInstalls
+---------------------
+This is a count of how many other installs of Firefox were present on the system at the time the ping was written. It is the number of values in the ``Software\Mozilla\Firefox\TaskBarIDs`` registry key, for both 32-bit and 64-bit architectures, for both HKCU and HKLM, excluding duplicates, and excluding a value for this install (if present). For example, if this is the only install on the system, the value will be 0. It may be missing in case of an error.
+
+This count is capped at 11. This avoids introducing a high-resolution identifier in case of a system with a large, unique number of installs.
+
+Uninstall Ping storage and lifetime
+-----------------------------------
+
+On delayed Telemetry init (about 1 minute into each run of Firefox), if opt-out telemetry is enabled, this ping is written to disk. There is a single ping for each install, any uninstall pings from the same install are removed before the new ping is written.
+
+The ping is removed if Firefox notices that opt-out telemetry has been disabled, either when the ``datareporting.healthreport.uploadEnabled`` pref goes false or when it is false on delayed init. Conversely, when opt-out telemetry is re-enabled, the ping is written as Telemetry is setting itself up again.
+
+The ping is sent by the uninstaller some arbitrary time after it is written to disk by Firefox, so it could be significantly out of date when it is submitted. There should be little impact from stale data, since analysis is likely to focus on clients that uninstalled soon after running Firefox, and this ping mostly changes when Firefox itself is updated.
diff --git a/toolkit/components/telemetry/docs/data/update-ping.rst b/toolkit/components/telemetry/docs/data/update-ping.rst
new file mode 100644
index 0000000000..037d521019
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/update-ping.rst
@@ -0,0 +1,79 @@
+
+"update" ping
+==================
+
+This opt-out ping is sent from Firefox Desktop when a browser update is ready to be applied and after it was correctly applied.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ type: "update",
+ ... common ping data
+ clientId: <UUID>,
+ environment: { ... },
+ payload: {
+ reason: <string>, // "ready", "success"
+ targetChannel: <string>, // "nightly" (only present for reason = "ready")
+ targetVersion: <string>, // "56.0a1" (only present for reason = "ready")
+ targetBuildId: <string>, // "20080811053724" (only present for reason = "ready")
+ targetDisplayVersion: <string>, // "56.0a1" (only present for reason = "ready")
+ previousChannel: <string>, // "nightly" or null (only present for reason = "success")
+ previousVersion: <string>, // "55.0a1" (only present for reason = "success")
+ previousBuildId: <string>, // "20080810053724" (only present for reason = "success")
+ }
+ }
+
+payload.reason
+--------------
+This field supports the following values:
+
+- ``ready`` meaning that the ping was generated after an update was downloaded and marked as ready to be processed. For *non-staged* updates this happens as soon as the download finishes and is verified while for *staged* updates this happens before the staging step is started.
+- ``success`` the ping was generated after the browser was restarted and the update correctly applied.
+
+payload.targetChannel
+-----------------------
+The Firefox channel the update was fetched from (only valid for pings with reason "ready").
+
+payload.targetVersion
+-----------------------
+The Firefox version the browser is updating to. Follows the same format as application.version (only valid for pings with reason "ready").
+
+payload.targetBuildId
+-----------------------
+The Firefox build id the browser is updating to. Follows the same format as application.buildId (only valid for pings with reason "ready").
+
+payload.targetDisplayVersion
+----------------------------
+The Firefox display version the browser is updating to. This may contain a different value than ``targetVersion``, e.g. for the ``Beta`` channel this field will report the beta suffix while ``targetVersion`` will only report the version number.
+
+payload.previousChannel
+-----------------------
+The Firefox channel the profile was on before the update was applied (only valid for pings with reason "success").
+This can be ``null``.
+
+payload.previousVersion
+-----------------------
+The Firefox version the browser is updating from. Follows the same format as application.version (only valid for pings with reason "success").
+
+payload.previousBuildId
+-----------------------
+The Firefox build id the browser is updating from. Follows the same format as application.buildId (only valid for pings with reason "success").
+
+Expected behaviours
+-------------------
+The following is a list of conditions and expected behaviours for the ``update`` ping:
+
+- **The ping is generated once every time an update is downloaded, after it was verified:**
+
+ - *for users who saw the privacy policy*, the ``update`` ping is sent immediately;
+ - *for users who did not see the privacy policy*, the ``update`` ping is saved to disk and sent after the policy is displayed.
+- **If the download of the update retries or other fallback occurs**: the ``update`` ping will not be generated
+ multiple times, but only one time once the download is complete and verified.
+- **If automatic updates are disabled**: when the user forces a manual update, no ``update`` ping will be generated.
+- **If updates fail to apply**: in some cases the client will download the same update blob and generate a new ``update`` ping for the same target version and build id, with a different document id.
+- **If the build update channel contains the CCK keyword**, the update ping will not report it but rather report a vanilla channel name (e.g. ``mozilla-cck-test-beta`` gets reported as ``beta``).
+- **If a profile refresh occurs before the update is applied**, the update ping with ``reason = success`` will not be generated.
+- **If the update is applied on a new profile, different then the one it was downloaded in**, the update ping with ``reason = success`` will not be generated.
+- **If a newer browser version is installed over an older**, the update ping with ``reason = success`` will not be generated.
diff --git a/toolkit/components/telemetry/docs/data/xfocsp-error-report-ping.rst b/toolkit/components/telemetry/docs/data/xfocsp-error-report-ping.rst
new file mode 100644
index 0000000000..f1b6f928a3
--- /dev/null
+++ b/toolkit/components/telemetry/docs/data/xfocsp-error-report-ping.rst
@@ -0,0 +1,69 @@
+
+"xfocsp-error-report" ping
+==========================
+
+This opt-in ping is sent when an X-Frame-Options error or a CSP: frame-ancestors
+happens to report the error. Users can opt-in this by checking the reporting
+checkbox. After users opt-in, this ping will be sent every time the error
+happens. Users can opt-out this by un-checking the reporting checkbox on the
+error page. The client_id and environment are not sent with this ping.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ "type": "xfocsp-error-report",
+ ... common ping data
+ "payload": {
+ "error_type": <string>,
+ "xfo_header": <string>,
+ "csp_header": <string>,
+ "frame_hostname": <string>,
+ "top_hostname": <string>,
+ "frame_uri": <string>,
+ "top_uri": <string>,
+ }
+ }
+
+info
+----
+
+error_type
+~~~~~~~~~~
+
+The type of what error triggers this ping. This could be either "xfo" or "csp".
+
+xfo_header
+~~~~~~~~~~
+
+The X-Frame-Options value in the response HTTP header.
+
+csp_header
+~~~~~~~~~~
+
+The CSP: frame-ancestors value in the response HTTP header.
+
+frame_hostname
+~~~~~~~~~~~~~~
+
+The hostname of the frame which triggers the error.
+
+top_hostname
+~~~~~~~~~~~~
+
+The hostname of the top-level page which loads the frame.
+
+frame_uri
+~~~~~~~~~
+
+The uri of the frame which triggers the error. This excludes the query strings.
+
+top_uri
+~~~~~~~
+
+The uri of the top-level page which loads the frame. This excludes the query
+strings.
+
+
+See also: :doc:`common ping fields <common-ping>`
diff --git a/toolkit/components/telemetry/docs/index.rst b/toolkit/components/telemetry/docs/index.rst
new file mode 100644
index 0000000000..2c587fd3d4
--- /dev/null
+++ b/toolkit/components/telemetry/docs/index.rst
@@ -0,0 +1,31 @@
+=========
+Telemetry
+=========
+
+Telemetry is a feature that allows data collection.
+This is being used to collect performance metrics and other information about how Firefox performs in the wild, e.g. update events or session lengths.
+
+There are two main ways of gathering data, Desktop Telemetry - documented here - which is used in Firefox Desktop
+and `Glean <https://docs.telemetry.mozilla.org/concepts/glean/glean.html>`__ which is
+Mozilla’s newer telemetry framework and used in all Mozilla projects needing data collection.
+Information which is gathered is called a probe in Desktop Telemetry or a metric in Glean.
+The data is being sent in so-called pings. When pings cannot be sent immediately, caching is implemented as well.
+
+In many cases, `Firefox on Glean (FOG) <../glean/index.html>`__
+(the Firefox Desktop integration of Glean) is to be preferred over Telemetry.
+If your data would benefit from being in Telemetry as well as Glean,
+please consult the documentation for the
+`Glean Interface For Firefox Telemetry (GIFFT) <../glean/user/gifft.html>`__.
+
+*Note:* Mozilla's `data collection policy <https://wiki.mozilla.org/Firefox/Data_Collection>`_ documents the process and requirements that are applied here.
+
+.. toctree::
+ :maxdepth: 5
+ :titlesonly:
+
+ start/index
+ concepts/index
+ collection/index
+ data/index
+ internals/index
+ obsolete/index
diff --git a/toolkit/components/telemetry/docs/internals/index.rst b/toolkit/components/telemetry/docs/internals/index.rst
new file mode 100644
index 0000000000..78679bda0d
--- /dev/null
+++ b/toolkit/components/telemetry/docs/internals/index.rst
@@ -0,0 +1,25 @@
+=========
+Internals
+=========
+
+Here is a quick overview of the most important code parts. They can be found in the telemetry folder.
+
+* TelemetryController: Main telemetry logic, e.g. assembling pings, local storage (when archiving is enabled), preference changes, testing
+* Telemetry.cpp contains most of the public interface, implements the IDL
+* The different data types for telemetry are handled in TelemetryHistogram, TelemetryScalar, TelemetryEvent.
+* TelemetryEnvironment: A helper for gathering environment data, like build version or graphics data
+* TelemetryScheduler: Starts regular jobs for collecting and sending data
+* TelemetrySend: Sending and caching of pings
+* TelemetryStorage: Handles writing pings to disk for TelemetrySend
+* TelemetrySession: Collects data for a browsing session, includes many of the most important probes (aka metrics)
+* Policy: A layer of indirection added to provide testability. A common pattern in many files
+* pings/: Contains definitions and handling for most ping types, like EventPing
+
+More details on different topics can be found in these chapters:
+
+.. toctree::
+ :maxdepth: 2
+ :titlesonly:
+ :glob:
+
+ **
diff --git a/toolkit/components/telemetry/docs/internals/integration_tests/index.rst b/toolkit/components/telemetry/docs/internals/integration_tests/index.rst
new file mode 100644
index 0000000000..389fa8b80e
--- /dev/null
+++ b/toolkit/components/telemetry/docs/internals/integration_tests/index.rst
@@ -0,0 +1,143 @@
+=================
+Integration Tests
+=================
+
+The aim of the telemetry-tests-client suite is to verify Firefox collects telemetry probes, aggregates that data, and submits telemetry
+pings containing the data to a HTTP server. The integration tests try to make no assumptions about the internal workings of Firefox and
+use automation to mimic user behavior.
+
+The integration test suite for Firefox Client Telemetry runs on CI `tier 1 <https://wiki.mozilla.org/Sheriffing/Job_Visibility_Policy>`_
+with treeherder symbol `tt(c)`
+and is checked in to version control at mozilla-central under
+`toolkit/components/telemetry/tests/marionette/tests/client <https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/tests/marionette/tests/client/>`_.
+
+Test Main Tab Scalars
+---------------------
+
+- PATH: ``telemetry/tests/marionette/tests/client/test_main_tab_scalars.py``
+- This test opens and closes a number of browser tabs,
+ restarts the browser in a new session
+ and then verifies the correctness of scalar data in the resulting `main` ping.
+
+Test Search Counts
+------------------
+
+- PATH: ``toolkit/telemetry/tests/marionette/tests/client/test_search_counts_across_subsessions.py``
+- This test performs a search in a new tab,
+ restarts Firefox in a new session and verifies the correctness of client, session and subsession IDs,
+ as well as scalar and keyed histogram data in the `shutdown` ping,
+ installs an addon, verifies the `environment-change` ping, and performs three additional search actions
+ before restarting and verifying the new `main` ping.
+
+
+Test Deletion Request Ping
+--------------------------
+
+- PATH: ``toolkit/telemetry/tests/marionette/tests/client/test_deletion_request_ping.py``
+- This test installs an addon and verifies a ping is received. The test takes note of the client ID.
+ It then disables telemetry and checks for a `deletion-request` ping.
+ After it receives the correct ping it makes sure that no other pings are sent.
+ Telemetry is then re-enabled and the `main` ping is checked to see if the client ID has changed.
+ The test asserts that the user has opted back in to telemetry.
+
+Test Event Ping
+---------------
+
+- PATH: ``toolkit/telemetry/tests/marionette/tests/client/test_event_ping.py``
+- This test checks for a basic `event` ping. It opens firefox, performs a search and checks the `event`
+ ping for the correct number of searches performed (1) and the correct search engine.
+
+Test Fog Custom Ping
+--------------------
+
+- PATH: ``toolkit/telemetry/tests/marionette/tests/client/test_fog_custom_ping.py``
+- This test creates a custom ping using the Glean API and asserts this ping is sent correctly.
+
+Test Fog Deletion Request Ping
+------------------------------
+
+- PATH: ``toolkit/telemetry/tests/marionette/tests/client/test_fog_deletion_request_ping.py``
+- This test opens the browser, performs a search and disables telemetry after the search.
+ It asserts that the telemetry is disabled and no pings exist.
+ The browser is restarted and telemetry is then re-enabled.
+ Then we set a `debug tag <https://mozilla.github.io/glean/book/user/debugging/debug-ping-view.html>`_
+ which is attached to the ping.
+ Telemetry is then disabled again to trigger a `deletion-request` ping.
+ We verify that 1) The debug tag is present; and 2) that the client ID
+ in the second `deletion-request` ping is different from the first client ID.
+
+Test Fog User Activity
+----------------------
+
+- PATH: ``toolkit/telemetry/tests/marionette/tests/client/test_fog_user_activity.py``
+- This test checks that a `baseline` ping is sent when the user starts or stops using Firefox.
+
+Test Background Update Ping
+---------------------------
+
+- PATH: ``toolkit/telemetry/tests/marionette/tests/client/test_fog_user_activity.py``
+- In this test we launch Firefox to prepare a profile and to disable the background update setting.
+ We exit Firefox,
+ leaving the (unlocked) profile to be used as the default profile for the background update task (and not having multiple instances running).
+ The task will not try to update, but it will send a ping.
+ Then we restart Firefox to unwind the background update setting and allow shutdown to proceed cleanly.
+
+Running the tests locally
+-------------------------
+
+You can run the tests on your local machine using
+`mach <https://firefox-source-docs.mozilla.org/mach/index.html>`__:
+
+``./mach telemetry-tests-client``
+
+Running the tests on try
+------------------------
+
+You can run the tests across all platforms on the try server using
+`mach <https://firefox-source-docs.mozilla.org/mach/index.html>`__:
+
+``./mach try fuzzy -q "'telemetry-tests-client"``
+
+Disabling an individual failing test
+------------------------------------
+
+The telemetry-tests-client suite is implemented in Python and uses Marionette for browser automation and wptserve for the HTTP ping server.
+The integration tests are based on Python's unittest testing library and can be disabled by calling
+`self.skipTest("reason") <https://docs.python.org/3/library/unittest.html#skipping-tests-and-expected-failures>`_ in a test method.
+
+The example below demonstrates how to disable test_main_ping2:
+
+.. code-block:: python
+
+ import unittest
+
+ from telemetry_harness.testcase import TelemetryTestCase
+
+ class TestMainPingExample(TelemetryTestCase):
+ """Example tests for the telemetry main ping."""
+
+ def test_main_ping1(self):
+ """Example test that we want to run."""
+
+ self.search_in_new_tab("mozilla firefox")
+
+ def test_main_ping2(self):
+ """Example test that we want to skip."""
+
+ self.skipTest("demonstrating skipping")
+
+ self.search_in_new_tab("firefox telemetry")
+
+
+Who to contact for help
+-----------------------
+
+- The test cases are owned by Chris Hutten-Czapski (chutten on matrix) from the Firefox Telemetry team
+ (`#telemetry <https://chat.mozilla.org/#/room/#telemetry:mozilla.org>`__ on matrix)
+- The test harness is owned by Raphael Pierzina (raphael on matrix) from the Ecosystem Test Engineering team
+ (`#telemetry <https://chat.mozilla.org/#/room/#telemetry:mozilla.org>`__ on matrix)
+
+Bugzilla
+--------
+
+Bugs can be filed under the Toolkit product for the Telemetry component.
diff --git a/toolkit/components/telemetry/docs/internals/mentored-bugs.rst b/toolkit/components/telemetry/docs/internals/mentored-bugs.rst
new file mode 100644
index 0000000000..bdc2de96b2
--- /dev/null
+++ b/toolkit/components/telemetry/docs/internals/mentored-bugs.rst
@@ -0,0 +1,49 @@
+==========================
+Template for Mentored Bugs
+==========================
+
+We like to encourage external contributions to the Firefox code base and the Telemetry module specifically.
+In order to set up a mentored bug, you can use the following template.
+Post it as a comment and add relevant steps in part 3.
+
+.. code-block:: md
+
+ To help Mozilla out with this bug, here's the steps:
+
+ 1. Comment here on the bug that you want to volunteer to help.
+ This will tell others that you're working on the next steps.
+ 2. [Download and build the Firefox source code](https://firefox-source-docs.mozilla.org/setup/index.html)
+ * If you have any problems, please ask on
+ [Element/Matrix](https://chat.mozilla.org/#/room/#introduction:mozilla.org)
+ in the `#introduction` channel. They're there to help you get started.
+ * You can also read the
+ [Firefox Contributors' Quick Reference](https://firefox-source-docs.mozilla.org/contributing/contribution_quickref.html),
+ which has answers to most development questions.
+ 3. Start working on this bug. <SPECIFIC STEPS RELEVANT TO THIS BUG>
+ * If you have any problems with this bug,
+ please comment on this bug and set the needinfo flag for me.
+ Also, you can find me and my teammates on the `#telemetry` channel on
+ [Element/Matrix](https://chat.mozilla.org/#/room/#telemetry:mozilla.org)
+ most hours of most days.
+ 4. Build your change with `mach build` and test your change with
+ `mach test toolkit/components/telemetry/tests/`.
+ Also check your changes for adherence to our style guidelines by using `mach lint`
+ 5. Submit the patch (including an automated test, if applicable) for review.
+ Mark me as a reviewer so I'll get an email to come look at your code.
+ * [Getting your code reviewed](https://firefox-source-docs.mozilla.org/setup/contributing_code.html#getting-your-code-reviewed)
+ * This is when the bug will be assigned to you.
+ 6. After a series of reviews and changes to your patch,
+ I'll mark it for checkin or push it to autoland.
+ Your code will soon be shipping to Firefox users worldwide!
+ 7. ...now you get to think about what kind of bug you'd like to work on next.
+ Let me know what you're interested in and I can help you find your next contribution.
+
+
+Don't forget to add yourself as a :code:`Mentor` on the bug,
+and add these tags to the :code:`Whiteboard`:
+
+* Add :code:`[lang=<language>]` to show what languages solving this bug will involve.
+* Add one of :code:`[good first bug]`, :code:`[good second bug]`, :code:`[good next bug]`
+ to indicate for whom this bug might be a good point for contribution.
+
+If this is a Good First Bug, be sure to also add the :code:`good-first-bug` :code:`Keyword`.
diff --git a/toolkit/components/telemetry/docs/internals/pingsender.rst b/toolkit/components/telemetry/docs/internals/pingsender.rst
new file mode 100644
index 0000000000..be60f699ff
--- /dev/null
+++ b/toolkit/components/telemetry/docs/internals/pingsender.rst
@@ -0,0 +1,36 @@
+Ping Sender
+===========
+
+The ping sender is a minimalistic program whose sole purpose is to deliver a
+telemetry ping. It accepts the following parameters:
+
+- the URL the ping will be sent to (as an HTTP POST command);
+- the path to an uncompressed file holding the ping contents.
+
+Once the ping has been read from disk the ping sender will try to post it once, exiting
+with a non-zero value if it fails. If sending the ping succeeds then the ping file is removed.
+
+The content of the HTTP request is *gzip* encoded. The request comes with a few
+additional headers:
+
+- ``User-Agent: pingsender/1.0``
+- ``X-PingSender-Version: 1.0``. Even if this data is already included by the user agent, this
+ header is needed as the pipeline is not currently storing use agent strings and doing that
+ could require storing a fair chunk of redundant extra data. We need to discern between pings
+ sent using the ping sender and the ones sent using the normal flow, at the end of the
+ ingestion pipeline, for validation purposes.
+
+.. note::
+
+ The ping sender relies on libcurl for Linux and Mac build and on WinInet for
+ Windows ones for its HTTP functionality. It currently ignores Firefox or the
+ system proxy configuration.
+
+In non-debug mode the ping sender doesn't print anything, not even on error,
+this is done deliberately to prevent startling the user on architectures such
+as Windows that would open a separate console window just to display the
+program output. If you need runtime information to be printed out compile the
+ping sender with debugging enabled.
+
+The pingsender is not supported on Firefox for Android
+(see `bug 1335917 <https://bugzilla.mozilla.org/show_bug.cgi?id=1335917>`_)
diff --git a/toolkit/components/telemetry/docs/internals/preferences.rst b/toolkit/components/telemetry/docs/internals/preferences.rst
new file mode 100644
index 0000000000..0c63ae031d
--- /dev/null
+++ b/toolkit/components/telemetry/docs/internals/preferences.rst
@@ -0,0 +1,280 @@
+Preferences and Defines
+=======================
+
+Telemetry behaviour is controlled through the mozconfig defines and preferences listed here.
+
+mozconfig Defines
+-----------------
+
+``MOZ_TELEMETRY_REPORTING``
+
+ When Defined (which it is for official builds):
+
+ * If ``RELEASE_OR_BETA`` is not defined, defines ``MOZ_TELEMETRY_ON_BY_DEFAULT``
+
+ When Not Defined:
+
+ * If ``datareporting.healthreport.uploadEnabled`` is locked, we print a message in the Privacy settings that you cannot turn on data submission and disabled the checkbox so you don't try.
+ * Android: hides the data submission UI to prevent users from thinking they can turn it on
+ * Disables Telemetry from being sent (due to ``Telemetry::IsOfficialTelemetry``)
+
+``MOZ_TELEMETRY_ON_BY_DEFAULT``
+
+ When Defined:
+
+ * Android: enables ``toolkit.telemetry.enabled``
+
+``MOZ_SERVICES_HEALTHREPORT``
+
+ When Defined (which it is on most platforms):
+
+ * Sets ``datareporting.healthreport.{infoURL|uploadEnabled}`` in ``modules/libpref/init/all.js``.
+
+``MOZ_DATA_REPORTING``
+
+ When Defined (which it is when ``MOZ_TELEMETRY_REPORTING``, ``MOZ_SERVICES_HEALTHREPORT``, or ``MOZ_CRASHREPORTER`` is defined (so, on most platforms, but not typically on developer builds)):
+
+ * Enables ``app.shield.optoutstudies.enabled``
+
+ When Not Defined:
+
+ * Disables ``app.shield.optoutstudies.enabled``
+ * Removes parts of the Data Collection Preferences UI in ``privacy.xhtml``
+
+``MOZILLA_OFFICIAL``
+
+ When Not Defined (defined on our own external builds and builds from several Linux distros, but not typically on defeloper builds):
+
+ * Disables Telemetry from being sent (due to ``Telemetry::IsOfficialTelemetry``)
+
+``MOZ_UPDATE_CHANNEL``
+
+ When not ``release`` or ``beta``:
+
+ * If ``MOZ_TELEMETRY_REPORTING`` is also defined, defines ``MOZ_TELEMETRY_ON_BY_DEFAULT``
+
+ When ``beta``:
+
+ * If ``toolkit.telemetry.enabled`` is otherwise unset at startup, ``toolkit.telemetry.enabled`` is defaulted to ``true`` (this is irrespective of ``MOZ_TELEMETRY_REPORTING``)
+
+ When ``nightly`` or ``aurora`` or ``beta`` or ``default``:
+
+ * Desktop: Locks ``toolkit.telemetry.enabled`` to ``true``. All other values for ``MOZ_UPDATE_CHANNEL`` on Desktop locks ``toolkit.telemetry.enabled`` to ``false``.
+ * Desktop: Defaults ``Telemetry::CanRecordExtended`` (and, thus ``Telemetry::CanRecordReleaseData``) to ``true``. All other values of ``MOZ_UPDATE_CHANNEL`` on Desktop defaults these to ``false``.
+
+``DEBUG``
+
+ When Defined:
+
+ * Disables Telemetry from being sent (due to ``Telemetry::IsOfficialTelemetry``)
+
+**In Short:**
+
+ For builds downloaded from mozilla.com ``MOZ_TELEMETRY_REPORTING`` is defined, ``MOZ_TELEMETRY_ON_BY_DEFAULT`` is on if you downloaded Nightly or Developer Edition, ``MOZ_SERVICES_HEALTHREPORT`` is defined, ``MOZ_DATA_REPORTING`` is defined, ``MOZILLA_OFFICIAL`` is defined, ``MOZ_UPDATE_CHANNEL`` is set to the channel you downloaded, and ``DEBUG`` is false. This means Telemetry is, by default, collecting some amount of information and is sending it to Mozilla.
+
+ For builds you make yourself with a blank mozconfig, ``MOZ_UPDATE_CHANNEL`` is set to ``default`` and everything else is undefined. This means Telemetry is, by default, collecting an extended amount of information but isn't sending it anywhere.
+
+Preferences
+-----------
+
+``toolkit.telemetry.unified``
+
+ This controls whether unified behavior is enabled. If true:
+
+ * Telemetry is always enabled and recording *base* data.
+ * Telemetry will send additional ``main`` pings.
+
+ It defaults to ``true``, but is ``false`` on Android (Fennec) builds.
+
+``toolkit.telemetry.enabled``
+
+ If ``unified`` is off, this controls whether the Telemetry module is enabled. It can be set or unset via the `Preferences` dialog in Firefox for Android (Fennec).
+ If ``unified`` is on, this is locked to ``true`` if ``MOZ_UPDATE_CHANNEL`` is ``nightly`` or ``aurora`` or ``beta`` or ``default`` (which is the default value of ``MOZ_UPDATE_CHANNEL`` for developer builds). Otherwise it is locked to ``false``. This controls a diminishing number of things and is intended to be deprecated, and then removed.
+
+``datareporting.healthreport.uploadEnabled``
+
+ If ``unified`` is true, this controls whether we send Telemetry data.
+ If ``unified`` is false, we don't use this value.
+
+``toolkit.telemetry.archive.enabled``
+
+ Allow pings to be archived locally. This can only be enabled if ``unified`` is on.
+
+``toolkit.telemetry.server``
+
+ The server Telemetry pings are sent to. Change requires restart.
+
+``toolkit.telemetry.log.level``
+
+ This sets the Telemetry logging verbosity per ``Log.sys.mjs``. The available levels, in descending order of verbosity, are ``Trace``, ``Debug``, ``Config``, ``Info``, ``Warn``, ``Error`` and ``Fatal`` with the default being ``Warn``.
+
+ By default logging goes only the console service.
+
+``toolkit.telemetry.log.dump``
+
+ Sets whether to dump Telemetry log messages to ``stdout`` too.
+
+``toolkit.telemetry.shutdownPingSender.enabled``
+
+ Allow the ``shutdown`` ping to be sent when the browser shuts down, from the second browsing session on, instead of the next restart, using the :doc:`ping sender <pingsender>`.
+
+``toolkit.telemetry.shutdownPingSender.enabledFirstSession``
+
+ Allow the ``shutdown`` ping to be sent using the :doc:`ping sender <pingsender>` from the first browsing session.
+
+``toolkit.telemetry.firstShutdownPing.enabled``
+
+ Allow a duplicate of the ``main`` shutdown ping from the first browsing session to be sent as a separate ``first-shutdown`` ping.
+
+``toolkit.telemetry.newProfilePing.enabled``
+
+ Enable the :doc:`../data/new-profile-ping` on new profiles.
+
+``toolkit.telemetry.newProfilePing.delay``
+
+ Controls the delay after which the :doc:`../data/new-profile-ping` is sent on new profiles.
+
+``toolkit.telemetry.updatePing.enabled``
+
+ Enable the :doc:`../data/update-ping` on browser updates.
+
+``toolkit.telemetry.eventping.minimumFrequency``
+
+ The minimum frequency at which an :doc:`../data/event-ping` will be sent.
+ Default is 60 (minutes).
+
+``toolkit.telemetry.eventping.maximumFrequency``
+
+ The maximum frequency at which an :doc:`../data/event-ping` will be sent.
+ Default is 10 (minutes).
+
+``toolkit.telemetry.overrideUpdateChannel``
+
+ Override the ``channel`` value that is reported via Telemetry.
+ This is useful for distinguishing different types of builds that otherwise still report as the same update channel.
+
+``toolkit.telemetry.ipcBatchTimeout``
+
+ How long, in milliseconds, we batch accumulations from child processes before
+ sending them to the parent process.
+ Default is 2000 (milliseconds).
+
+``toolkit.telemetry.pioneerId``
+
+ If a user has opted into the Pioneer program, this will contain their Pioneer ID.
+
+Data-choices notification
+-------------------------
+
+``toolkit.telemetry.reportingpolicy.firstRun``
+
+ This preference is not present until the first run. After, its value is set to false. This is used to show the infobar with a more aggressive timeout if it wasn't shown yet.
+
+``datareporting.policy.firstRunURL``
+
+ If set, a browser tab will be opened on first run instead of the infobar.
+
+``datareporting.policy.dataSubmissionEnabled``
+
+ This is the data submission master kill switch. If disabled, no policy is shown or upload takes place, ever.
+
+``datareporting.policy.dataSubmissionPolicyNotifiedTime``
+
+ Records the date user was shown the policy. This preference is also used on Android.
+
+``datareporting.policy.dataSubmissionPolicyAcceptedVersion``
+
+ Records the version of the policy notified to the user. This preference is also used on Android.
+
+``datareporting.policy.dataSubmissionPolicyBypassNotification``
+
+ Used in tests, it allows to skip the notification check.
+
+``datareporting.policy.currentPolicyVersion``
+
+ Stores the current policy version, overrides the default value defined in TelemetryReportingPolicy.sys.mjs.
+
+``datareporting.policy.minimumPolicyVersion``
+
+ The minimum policy version that is accepted for the current policy. This can be set per channel.
+
+``datareporting.policy.minimumPolicyVersion.channel-NAME``
+
+ This is the only channel-specific version that we currently use for the minimum policy version.
+
+GeckoView
+---------
+
+``toolkit.telemetry.geckoview.streaming``
+
+ Whether the GeckoView mode we're running in is the variety that uses the :doc:`GeckoView Streaming Telemetry API <../internals/geckoview-streaming>` or not.
+ Defaults to false.
+
+``toolkit.telemetry.geckoview.batchDurationMS``
+
+ The duration in milliseconds over which :doc:`GeckoView Streaming Telemetry <../internals/geckoview-streaming>` will batch accumulations before passing it on to its delegate.
+ Defaults to 5000.
+
+``toolkit.telemetry.geckoview.maxBatchStalenessMS``
+
+ The maximum time (in milliseconds) between flushes of the
+ :doc:`GeckoView Streaming Telemetry <../internals/geckoview-streaming>`
+ batch to its delegate.
+ Defaults to 60000.
+
+Testing
+-------
+
+The following prefs are for testing purpose only.
+
+``toolkit.telemetry.initDelay``
+
+ Delay before initializing telemetry (seconds).
+
+``toolkit.telemetry.minSubsessionLength``
+
+ Minimum length of a telemetry subsession and throttling time for common environment changes (seconds).
+
+``toolkit.telemetry.collectInterval``
+
+ Minimum interval between data collection (seconds).
+
+``toolkit.telemetry.scheduler.tickInterval``
+
+ Interval between scheduler ticks (seconds).
+
+``toolkit.telemetry.scheduler.idleTickInterval``
+
+ Interval between scheduler ticks when the user is idle (seconds).
+
+``toolkit.telemetry.idleTimeout``
+
+ Timeout until we decide whether a user is idle or not (seconds).
+
+``toolkit.telemetry.modulesPing.interval``
+
+ Interval between "modules" ping transmissions.
+
+``toolkit.telemetry.send.overrideOfficialCheck``
+
+ If true, allows sending pings on unofficial builds. Requires a restart.
+
+``toolkit.telemetry.testing.overridePreRelease``
+
+ If true, allows recording opt-in Telemetry on the Release channel. Requires a restart.
+
+``toolkit.telemetry.untrustedModulesPing.frequency``
+
+ Interval, in seconds, between "untrustedModules" ping transmissions.
+
+``toolkit.telemetry.healthping.enabled``
+
+ If false, sending health pings is disabled. Defaults to true.
+
+``toolkit.telemetry.testing.disableFuzzingDelay``
+
+ If true, ping sending is not delayed when sending between 0am and 1am local time.
+
+``toolkit.telemetry.testing.overrideProductsCheck``
+
+ If true, allow all probes to be recorded no matter what the current product is.
diff --git a/toolkit/components/telemetry/docs/internals/review.rst b/toolkit/components/telemetry/docs/internals/review.rst
new file mode 100644
index 0000000000..80a3bd57de
--- /dev/null
+++ b/toolkit/components/telemetry/docs/internals/review.rst
@@ -0,0 +1,144 @@
+===========================
+Telemetry review guidelines
+===========================
+
+General guidelines for reviewing changes
+========================================
+
+These are the general principles we follow when reviewing changes.
+
+- *Be constructive.* Both reviewers and patch authors should be allies that aim to get the change landed together.
+- *Consider the impact.* We deliver critical data that is processed and used by many systems and people. Any disruption should be planned and intentional.
+- *Be diligent.* All changes should be tested under typical conditions.
+- *Know your limits.* Defer to other peers or experts where sensible.
+
+Main considerations for any change
+========================================
+
+For any change, these are the fundamental questions that we always need satisfactory answers for.
+
+- Does this have a plan?
+
+ - Is there a specific need to do this?
+ - Does this change need `a proposal <https://github.com/mozilla/Fx-Data-Planning/blob/master/process/ProposalProcess.md>`_ first?
+ - Do we need to announce this before we do this? (e.g. for `deprecations <https://github.com/mozilla/Fx-Data-Planning/blob/master/process/Deprecation.md>`_)
+
+- Does this involve the right people?
+
+ - Does this change need input from... A data engineer? A data scientist?
+ - Does this change need data review?
+
+- Is this change complete?
+
+ - Does this change have sufficient test coverage?
+ - Does this change have sufficient documentation?
+
+- Do we need follow-ups?
+
+ - Do we need to file validation bugs? Or other follow-up bugs?
+ - Do we need to communicate this to our users? Or other groups?
+
+Additional considerations
+=========================
+
+Besides the basic considerations above, these are additional detailed considerations that help with reviewing changes.
+
+Considerations for all changes
+------------------------------
+
+- Follow our standards and best practices.
+
+ - Firefox Desktop:
+
+ - :ref:`The Mozilla coding style <Coding style>`
+ - `The toolkit code review guidelines <https://wiki.mozilla.org/Toolkit/Code_Review>`_
+
+ - Mobile:
+
+ - `Android/Kotlin code style <https://kotlinlang.org/docs/reference/coding-conventions.html>`_
+ - `iOS/Swift code style <https://github.com/mozilla-mobile/firefox-ios/wiki/Swift-Style-Guides>`_
+
+- Does this impact performance significantly?:
+
+ - Don't delay application initialisation (unless absolutely necessary).
+ - Don't ever block on network requests.
+ - Make longer tasks async whenever feasible.
+
+- Does this affect products more broadly than expected?
+
+ - Consider all our platforms: Windows, Mac, Linux, Android.
+ - Consider all our products: Firefox, Fennec, GeckoView, Glean.
+
+- Does this fall afoul of common architectural failures?
+
+ - Prefer APIs that take non-String types unless writing a parser.
+
+- Sanity checking:
+
+ - How does this behave in a release build? Have you tested this?
+ - Does this change contain test coverage? We require test coverage for every new code, changes and bug fixes.
+
+- Does this need documentation updates?
+
+ - To the :ref:`in-tree docs <Telemetry>`?
+ - To the `firefox-data-docs <https://docs.telemetry.mozilla.org/>`_ (`repository <https://github.com/mozilla/firefox-data-docs>`_)?
+ - To the `glean documentation <https://github.com/mozilla-mobile/android-components/tree/master/components/service/glean>`_?
+
+- Following up:
+
+ - Do we have a validation bug filed yet?
+ - Do all TODOs have follow-up bugs filed?
+ - Do we need to communicate this to our users?
+
+ - `fx-data-dev <https://mail.mozilla.org/listinfo/fx-data-dev>`_ (Main Firefox data list)
+ - `firefox-dev <https://mail.mozilla.org/listinfo/firefox-dev>`_ (Firefox application developers)
+ - `dev-platform <https://lists.mozilla.org/listinfo/dev-platform>`_ (Gecko / platform developers)
+ - `mobile-firefox-dev <https://mail.mozilla.org/listinfo/mobile-firefox-dev>`_ (Mobile developers)
+ - fx-team (Firefox staff)
+
+ - Do we need to communicate this to other groups?
+
+ - Data engineering, data science, data stewards, ...?
+
+Consider the impact on others
+-----------------------------
+
+- Could this change break upstream pipeline jobs?
+
+ - Does this change the format of outgoing data in an unhandled way?
+
+ - E.g. by adding, removing, or changing the type of a non-metric payload field.
+
+ - Does this require ping schema updates?
+ - Does this break jobs that parse the registry files for metrics? (Scalars.yaml, metrics.yaml, etc.)
+
+- Do we need to involve others?
+
+ - Changes to data formats, ping contents, ping semantics etc. require involving a data engineer.
+ - Changes to any outgoing data that is in active use require involving the stakeholders (e.g. data scientists).
+
+Considerations for Firefox Desktop
+----------------------------------
+
+- For profiles:
+
+ - How does using different profiles affect this?
+ - How does switching between profiles affect this?
+ - What happens when users switch between different channels?
+
+- Footguns:
+
+ - Does this have side-effects on Fennec? (Unified Telemetry is off there, so behavior is pretty different.)
+ - Is your code gated on prefs, build info, channels? Tests should cover that.
+ - If test is gated on isUnified, code should be too (and vice-versa)
+
+ - Test for the other case
+
+ - Any code using `new Date()` should get additional scrutiny
+ - Code using `new Date()` should be using Policy so it can be mocked
+ - Tests using `new Date()` should use specific dates, not the current one
+
+ - How does this impact Build Faster support/Artifact builds/Dynamic builtin scalars or events? Will this be testable by others on artifact builds?
+ - We work in the open: Does the change include words that might scare end users?
+ - How does this handle client id resets?
+ - How does this handle users opting out of data collection?
diff --git a/toolkit/components/telemetry/docs/internals/tests.rst b/toolkit/components/telemetry/docs/internals/tests.rst
new file mode 100644
index 0000000000..58adbb94af
--- /dev/null
+++ b/toolkit/components/telemetry/docs/internals/tests.rst
@@ -0,0 +1,99 @@
+Tests
+=====
+
+A high-level test strategy for Firefox Telemetry is defined in the
+`Test Strategy document <https://docs.google.com/document/d/1Mi6va3gE4HSv5LjXNREvMa2V4q-LKIFDTwA2o4yeo_c/edit>`_.
+
+Firefox Telemetry is a complicated and old component.
+So too are the organization and expanse of its tests.
+Let’s break them down by harness.
+
+Unless otherwise mentioned the tests live in subdirectories of
+``toolkit/components/telemetry/tests``.
+
+Mochitest
+---------
+:Location: ``t/c/t/t/browser/``
+:Language: Javascript
+ (`mochitest <https://firefox-source-docs.mozilla.org/testing/mochitest-plain>`__)
+
+This test harness runs nearly the entire Firefox and gives access to multiple tabs and browser chrome APIs.
+It requires window focus to complete correctly,
+so it isn’t recommended to add new tests here.
+The tests that are here maybe would be more at home as telemetry-tests-client tests as they tend to be integration tests.
+
+Google Test
+-----------
+:Location: ``t/c/t/t/gtest/``
+:Language: C++
+ (`googletest <https://github.com/google/googletest>`_)
+
+This test harness runs a specially-built gtest shell around libxul which allows you to write unit tests against public C++ APIs.
+It should be used to test the C++ API and core of Firefox Telemetry.
+This is for tests like
+“Do we correctly accumulate to bucket 0 if I pass -1 to ``Telemetry::Accumulate``?”
+
+Integration Tests (telemetry-tests-client and telemetry-integration-tests)
+--------------------------------------------------------------------------
+:Location: ``t/c/t/t/marionette/tests/client`` and ``t/c/t/t/integration/``
+:Language: Python
+ (`unittest <https://docs.python.org/3/library/unittest.html>`__,
+ `pytest <https://docs.pytest.org/en/latest/>`_)
+
+The most modern of the test harnesses,
+telemetry-integration-tests uses marionette to puppet the entire browser allowing us to write integration tests that include ping servers and multiple browser runs.
+You should use this if you’re testing Big Picture things like
+“Does Firefox resend its “deletion-request” ping if the network is down when Telemetry is first disabled?”.
+
+At time of writing there are two “editions” of integration tests.
+Prefer writing new tests in telemetry-tests-client
+(the unittest-based one in ``t/c/t/t/marionette/tests/client``)
+while we evaluate CI support for telemetry-integration-tests.
+
+More info: :doc:`./integration_tests/index`
+
+Definitions Files Tests
+-----------------------
+:Location: ``t/c/t/t/python``
+:Language: Python
+ (`unittest <https://docs.python.org/3/library/unittest.html>`__)
+
+This harness pulls in the parsers and scripts used to turn JSON and YAML probe definitions into code.
+It should be used to test the build scripts and formats of the definitions files
+Histograms.json, Scalars.yaml, and Events.yaml.
+This is for tests like
+“Does the build fail if someone forgot to put in a bug number for a new Histogram?”.
+
+xpcshell
+--------
+:Location: ``t/c/t/t/unit``
+:Language: Javascript
+ (`xpcshell <https://firefox-source-docs.mozilla.org/testing/xpcshell>`__)
+
+This test harness uses a stripped-down shell of the Firefox browser to run privileged Javascript.
+It should be used to write unit tests for the Javascript API and app-level logic of Firefox Telemetry.
+This is for tests like
+“Do we correctly accumulate to bucket 0 if I pass -1 to ``Telemetry.getHistogramById(...).add``?”
+and
+“Do we reschedule pings that want to be sent near local midnight?”.
+
+Since these tests are easy to write and quick to run we have in the past bent this harness in a few interesting shapes
+(see PingServer)
+to have it support integration tests as well.
+New integration tests should use telemetry-tests-client instead.
+
+Instrumentation Tests
+---------------------
+:Location: Various
+:Language: Usually Javascript
+ (`xpcshell <https://firefox-source-docs.mozilla.org/testing/xpcshell>`__ or
+ `mochitest <https://firefox-source-docs.mozilla.org/testing/mochitest-plain>`__)
+
+In addition to the tests of Firefox Telemetry,
+other code owners have written tests that ensure that their code records appropriate values to Telemetry.
+They should use the
+``toolkit/components/telemetry/tests/unit/TelemetryTestUtils.sys.mjs``
+module to make their lives easier.
+This can be used for tests like
+“If five bookmarks are read from the database,
+does the bookmark count Histogram have a value of 5 in it?”.
diff --git a/toolkit/components/telemetry/docs/obsolete/activation-ping.rst b/toolkit/components/telemetry/docs/obsolete/activation-ping.rst
new file mode 100644
index 0000000000..a2c241aec4
--- /dev/null
+++ b/toolkit/components/telemetry/docs/obsolete/activation-ping.rst
@@ -0,0 +1,69 @@
+
+"activation" ping (obsolete)
+============================
+
+This mobile-specific ping is intendend to track activations of distributions of mobile products
+with a small error rate.
+
+This will be sent at Startup. Minimally, we want to get this ping at least once from every client.
+
+Submission will be per the Edge server specification::
+
+ /submit/mobile/activation/v/docId
+
+This is a modern “structure ingestion” ping (the namespace is not telemetry). For structured
+ingestion, we capture the schema version as one of the URI parameters, so we don’t need to
+include it in the body of the message.
+
+* ``v`` is the ping format version
+* ``docId`` is a UUID for deduping
+
+Structure:
+
+.. code-block:: js
+
+ {
+ "identifier": <string>, // Googled Ad ID hashed using bcrypt
+ "clientId": <string>, // client id, e.g. "c641eacf-c30c-4171-b403-f077724e848a"
+ //included only if identifier was unabled to be retrieved
+ "manufacturer": <string>, // Build.MANUFACTURER
+ "model": <string>, // Build.MODEL
+ "locale": <string>, // application locale, e.g. "en-US"
+ "os": <string>, // OS name.
+ "osversion": <string>, // OS version.
+ "created": <string>, // date the ping was created
+ // in local time, "yyyy-mm-dd"
+ "tz": <integer>, // timezone offset (in minutes) of the
+ // device when the ping was created
+ "app_name": <string>, // "Fennec"
+ "channel": <string>, // Android package name e.g. "org.mozilla.firefox"
+ "distributionId": <string> // Distribution identifier (optional)
+ }
+
+
+Field details
+-------------
+
+identifier
+~~~~~~~~~~
+The ``identifier`` field is the Google Ad ID hashed using bcrypt. Ideally we want to send this instead of the
+client_id but not all distributions have Google Play Services enabled.
+
+client_id
+~~~~~~~~~~
+The ``client_id`` field represents the telemetry client id and it is only included if the identifier is empty.
+
+channel
+~~~~~~~
+The ``channel`` field represents the Android package name.
+
+Version history
+---------------
+* v1: initial version - shipped in `Fennec 68 <https://bugzilla.mozilla.org/show_bug.cgi?id=1534451>`_.
+
+Android implementation notes
+----------------------------
+On Android, the uploader has a high probability of delivering the complete data
+for a given client but not a 100% probability. This was a conscious decision to
+keep the code simple. Even if we drop a ping, it will be resent on future startups
+until we have confirmation that it has been uploaded.
diff --git a/toolkit/components/telemetry/docs/obsolete/core-ping.rst b/toolkit/components/telemetry/docs/obsolete/core-ping.rst
new file mode 100644
index 0000000000..6fee237805
--- /dev/null
+++ b/toolkit/components/telemetry/docs/obsolete/core-ping.rst
@@ -0,0 +1,510 @@
+
+"core" ping (obsolete)
+======================
+
+This mobile-specific ping is intended to provide the most critical
+data in a concise format, allowing for frequent uploads.
+
+Since this ping is used to measure retention, it should be sent
+each time the browser is opened.
+
+Submission will be per the Edge server specification::
+
+ /submit/telemetry/docId/docType/appName/appVersion/appUpdateChannel/appBuildID
+
+* ``docId`` is a UUID for deduping
+* ``docType`` is “core”
+* ``appName`` is “Fennec”
+* ``appVersion`` is the version of the application (e.g. "46.0a1")
+* ``appUpdateChannel`` is “release”, “beta”, etc.
+* ``appBuildID`` is the build number
+
+Note: Counts below (e.g. search & usage times) are “since the last
+ping”, not total for the whole application lifetime.
+
+Structure:
+
+.. code-block::
+
+ {
+ "v": 10, // ping format version
+ "clientId": <string>, // client id, e.g.
+ // "c641eacf-c30c-4171-b403-f077724e848a"
+ "seq": <positive integer>, // running ping counter, e.g. 3
+ "locale": <string>, // application locale, e.g. "en-US"
+ "os": <string>, // OS name.
+ "osversion": <string>, // OS version.
+ "device": <string>, // Build.MANUFACTURER + " - " + Build.MODEL
+ // where manufacturer is truncated to 12 characters
+ // & model is truncated to 19 characters
+ "arch": <string>, // e.g. "arm", "x86"
+ "profileDate": <pos integer>, // Profile creation date in days since
+ // UNIX epoch.
+ "defaultSearch": <string>, // Identifier of the default search engine,
+ // e.g. "yahoo".
+ "displayVersion": <string>, // Version displayed to user, e.g. 57.0b3 (optional)
+ "distributionId": <string>, // Distribution identifier (optional)
+ "campaignId": <string>, // Adjust's campaign identifier (optional)
+ "created": <string>, // date the ping was created
+ // in local time, "yyyy-mm-dd"
+ "tz": <integer>, // timezone offset (in minutes) of the
+ // device when the ping was created
+ "sessions": <integer>, // number of sessions since last upload
+ "durations": <integer>, // combined duration, in seconds, of all
+ // sessions since last upload
+ "searches": <object>, // Optional, object of search use counts in the
+ // format: { "engine.source": <pos integer> }
+ // e.g.: { "yahoo.suggestion": 3, "other.listitem": 1 }
+ "experiments": [<string>, /* … */], // Optional, array of identifiers
+ // for the active experiments
+ "accessibilityServices": [<string>, /* … */], // Optional, array of identifiers for
+ // enabled accessibility services that
+ // interact with our android products.
+ "flashUsage": <integer>, // number of times flash plugin is played since last upload
+ "defaultBrowser": <boolean> // true if the user has set Firefox as default browser
+ "bug_1501329_affected": <boolean> // true if Firefox previously used canary clientId
+ // when submitting telemetry
+ "fennec": <object> // Fennec only.
+ // Block of a variety of fields of different types.
+ // Used to understand the usage of Fennec features in the release population
+ // to understand when Fenix is ready to support Fennec users.
+ {
+ "new_tab": {
+ "top_sites_clicked": <int>, // Number of times a Top Site was opened from the Awesome Screen.
+ // Resets after each sent core ping.
+ "pocket_stories_clicked": <int>, // Number of time a Pocket Recommended website was opened
+ // from the Awesome Screen.
+ // Resets after each sent core ping.
+ }
+ "settings_advanced": {
+ "restore_tabs": <boolean>, // State of the "Settings/Advanced/Restore Tabs" setting
+ "show_images": <string>, // State of the "Settings/Advanced/Show images" setting
+ // Value will be be "user-specified" for any non-default values
+ "show_web_fonts": <boolean>, // State of the "Settings/Advanced/Show web fonts" setting
+ },
+ "settings_general": {
+ "full_screen_browsing": <boolean>, // Current state of the
+ // "Settings/General/Full-screen browsing" setting.
+ "tab_queue": <boolean>, // State of the "Settings/General/Tab queue" setting.
+ "tab_queue_usage_count": <int>, // Number of tabs opened through Tab Queue.
+ // Resets after each sent core ping.
+ "compact_tabs": <boolean>, // State of the "Settings/General/Compact tabs" setting.
+ "homepage": {
+ "custom_homepage": <boolean>, // "true" if not "about:home".
+ "custom_homepage_use_for_newtab": <boolean>, // If the "Settings/General/Home/Also use for new tabs"
+ // setting is enabled.
+ "topsites_enabled": <boolean>, // If the "Settings/General/Home/Top Sites"
+ // setting is set to "Show".
+ "pocket_enabled": <boolean>, // If the "Settings/General/Home/Top Sites/Recommended by Pocket"
+ // setting is enabled.
+ "recent_bookmarks_enabled": <boolean>, // If the "Settings/General/Home/Top Sites/
+ // Additional Content/Recent Bookmarks"
+ // setting is enabled.
+ "visited_enabled": <boolean>, // If the "Settings/General/Home/Top Sites/Additional Content/Visited"
+ // setting is enabled.
+ bookmarks_enabled": <boolean>, // If the "Settings/General/Home/Bookmarks" setting is set to "Show".
+ "history_enabled": <boolean>, // If the "Settings/General/Home/History" setting is set to "Show".
+ }
+ },
+ "settings_privacy": {
+ "do_not_track": <boolean>, // If the "Settings/Privacy/Do not track" is enabled.
+ "master_password": <boolean>, // If the "Settings/Privacy/Use master password" is enabled.
+ "master_password_usage_count": <int>, // Number of times the user has entered their master password.
+ // Resets after each sent core ping.
+ },
+ "settings_notifications": {
+ "product_feature_tips": <boolean>, // If the "Settings/Notifications/Product and feature tips"
+ // setting is enabled.
+ },
+ "addons": {
+ "active": [addon_id_1, addon_id_2, …, ], // From all installed addons, which ones are active.
+ "disabled": [addon_id_1, addon_id_2, …], // From all installed addons, which ones are disabled.
+ },
+ "page_options": {
+ "save_as_pdf": <int>, // Number of times the user has used "Page/Save to PDF".
+ // Resets after each sent core ping.
+ "print": <int>, // Number of times the user has used the "Page/Print".
+ // Resets after each sent core ping.
+ "total_added_search_engines": <int>, // The absolute number of user added search engines,
+ // not just those added during this session.
+ "total_sites_pinned_to_topsites": <int>, // The absolute number of times the user has used
+ // the "Pin Site" functionality.
+ // Not just those added during this session.
+ "view_source": <int>, // Number of times the user has used the "Page/View Page Source".
+ // Resets after each sent core ping.
+ "bookmark_with_star": <int>, // The number of times the user has used the "Menu / <Star>".
+ // Resets after each sent core ping.
+ "current_pwas_count": <int>, // On Android >=25 - a positive number of PWAs currently on
+ // homescreen, installed from this app.
+ // On Android <25 - a default of "-1".
+ },
+ "sync": {
+ "only_over_wifi": <boolean>, // "true" if the "Settings/Sync/Sync only over Wi-Fi"
+ // setting is enabled.
+ // null if the user is not signed into Sync.
+ }
+ }
+ }
+
+Field details
+-------------
+
+device
+~~~~~~
+The ``device`` field is filled in with information specified by the hardware
+manufacturer. As such, it could be excessively long and use excessive amounts
+of limited user data. To avoid this, we limit the length of the field. We're
+more likely have collisions for models within a manufacturer (e.g. "Galaxy S5"
+vs. "Galaxy Note") than we are for shortened manufacturer names so we provide
+more characters for the model than the manufacturer.
+
+distributionId
+~~~~~~~~~~~~~~
+The ``distributionId`` contains the distribution ID as specified by
+preferences.json for a given distribution. More information on distributions
+can be found `here <https://wiki.mozilla.org/Mobile/Distribution_Files>`_.
+
+It is optional.
+
+campaignId
+~~~~~~~~~~~~~~
+The ``campaignId`` contains the campaign identifier like '3ly8t0'.
+It's generated by `Adjust <https://docs.adjust.com/en/tracker-generation/#segmenting-users-dynamically-with-campaign-structure-parameters>`_,
+It can only used to identify a campaign, but can't target to a specific user.
+
+It is optional because not everyone has a campaign to begin with.
+
+defaultSearch
+~~~~~~~~~~~~~
+On Android, this field may be ``null``. To get the engine, we rely on
+``SearchEngineManager#getDefaultEngine``, which searches in several places in
+order to find the search engine identifier:
+
+* Shared Preferences
+* The distribution (if it exists)
+* The localized default engine
+
+If the identifier could not be retrieved, this field is ``null``. If the
+identifier is retrieved, we attempt to create an instance of the search
+engine from the search plugins (in order):
+
+* In the distribution
+* From the localized plugins shipped with the browser
+* The third-party plugins that are installed in the profile directory
+
+If the plugins fail to create a search engine instance, this field is also
+``null``.
+
+This field can also be ``null`` when a custom search engine is set as the
+default.
+
+sessions & durations
+~~~~~~~~~~~~~~~~~~~~
+On Android, a session is the time when Firefox is focused in the foreground.
+`sessions` tracks the number of sessions since the last upload and
+`durations` is the accumulated duration in seconds of all of these
+sessions. Note that showing a dialog (including a Firefox dialog) will
+take Firefox out of focus & end the current session.
+
+An implementation that records a session when Firefox is completely hidden is
+preferable (e.g. to avoid the dialog issue above), however, it's more complex
+to implement and so we chose not to, at least for the initial implementation.
+
+profileDate
+~~~~~~~~~~~
+On Android, this value is created at profile creation time and retrieved or,
+for legacy profiles, taken from the package install time (note: this is not the
+same exact metric as profile creation time but we compromised in favor of ease
+of implementation).
+
+Additionally on Android, this field may be ``null`` in the unlikely event that
+all of the following events occur:
+
+#. The times.json file does not exist
+#. The package install date could not be persisted to disk
+
+The reason we don't just return the package install time even if the date could
+not be persisted to disk is to ensure the value doesn't change once we start
+sending it: we only want to send consistent values.
+
+searches
+~~~~~~~~
+This describes the search engine usage(count). The format is { "<engine identifier>.<source>"" : count }
+This is optional because the users may have never used the search feature.
+There's no difference if extended telemetry is enabled (prerelease builds) or not.
+
+Possible value :
+
+.. code-block:: js
+
+ {
+ "yahoo.listitem":2,
+ "duckduckgo.listitem":1,
+ "google.suggestion":1
+ }
+
+**<engine identifier>**: the identifier of the the search engine. The identifier is collected the way same as desktop.
+we only record the search engine name when:
+
+* builtin or suggested search engines with an ID (includes partner search engines in various distribution scenarios).
+ If it's not a built-in engine, we show "null" or "other".
+* If the user has "Health Report" and core ping enabled.
+
+**<sources>**: it's from one of the 'method's in UI telemetry. Possible values:
+
+* actionbar: the user types in the url bar and hits enter to use the default
+ search engine
+* listitem: the user selects a search engine from the list of secondary search
+ engines at the bottom of the screen
+* suggestion: the user clicks on a search suggestion or, in the case that
+ suggestions are disabled, the row corresponding with the main engine
+
+accessibilityServices
+~~~~~~~~~~~~~~~~~~~~~
+This describes which accessibility services are currently enabled on user's device and could be interacting with our
+products. This is optional because users often do not have any accessibility services enabled. If present, the value is
+a list of accessibility service ids.
+
+fennec.new_tab.top_sites_clicked
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `top_sites_clicked` field contains the number of times a top site was
+opened from the new tab page since the last time the core ping was sent.
+This counter is reset when the core ping has been sent.
+
+
+Fennec.new_tab.pocket_stories_clicked
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `pocket_stories_clicked` contains the number of times a pocket story was
+opened from the new tab page since the last time the core ping was sent.
+This counter is reset when the core ping has been sent.
+
+Fennec.settings_advanced.restore_tabs
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `restore_tabs` field contains state of the "Settings/Advanced/Restore Tabs"
+setting. It is true for "Always Restore" and false for "Don’t restore after
+quitting Firefox".
+The value is determined at the time of sending the core ping.
+
+Fennec.settings_advanced.show_images
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `show_images` field contains the state of the
+"Settings/Advanced/Show images" settings.
+It is a string value set to "default" if the setting is "Always", or
+"user~specified" for any of the other options.
+The value is determined at the time of sending the core ping.
+
+Fennec.settings_advanced.show_web_fonts
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `show_web_fonts` field is a boolean that contains the current state of the
+"Settings/Advanced/Show web fonts" setting.
+The value is determined at the time of sending the core ping.
+
+Fennec.settings_general.full_screen_browsing
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `full_screen_browsing` field is a boolean that contains the current state
+of the "Settings/General/Full~screen browsing" setting.
+The value is determined at the time of sending the core ping.
+
+Fennec.settings_general.tab_queue
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `tab_queue` field is a boolean that contains the current state of the
+"Settings/General/Tab queue" setting.
+The value is determined at the time of sending the core ping.
+
+Fennec.settings_general.tab_queue_usage_count
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `tab_queue_usage_count` is a counter that increments with the number of
+tabs opened through the tab queue.
+It contains the total number of queued tabs opened since the last time the
+Core Ping was sent.
+This counter is reset when the core ping has been sent.
+
+Fennec.settings_general.compact_tabs
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `compact_tabs` field is a boolean that contains the current state of the
+"Settings/General/Compact tabs" setting.
+The value is determined at the time of sending the core ping.
+
+Fennec.settings_general.homepage.custom_homepage
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `custom_homepage` field is set to true if the homepage is not set to the
+the default `about:home`.
+The value is determined at the time of sending the core ping.
+
+Fennec.settings_general.homepage.custom_homepage_use_for_newtab
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `custom_homepage_use_for_newtab` field is set to true if the
+"Settings/General/Home/Also use for new tabs" setting is enabled.
+The value is determined at the time of sending the core ping.
+
+Fennec.settings_general.homepage.topsites_enabled
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `topsites_enabled` setting is true if the "Settings/General/Home/Top Sites"
+setting is set to "Show".
+The value is determined at the time of sending the core ping.
+
+Fennec.settings_general.homepage.pocket_enabled
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `pocket_enabled` setting is true if the
+"Settings/General/Home/Top Sites/Recommended by Pocket" setting is enabled.
+The value is determined at the time of sending the core ping.
+
+Fennec.settings_general.homepage.recent_bookmarks_enabled
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `recent_bookmarks_enabled` setting is true if the
+"Settings/General/Home/Top Sites/Additional Content/Recent Bookmarks" setting
+is enabled.
+The value is determined at the time of sending the core ping.
+
+Fennec.settings_general.homepage.visited_enabled
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `visited_enabled` setting is true if the
+"Settings/General/Home/Top Sites/Additional Content/Visited" setting is
+enabled.
+The value is determined at the time of sending the core ping.
+
+Fennec.settings_general.homepage.bookmarks_enabled
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `bookmarks_enabled` setting is true if the
+"Settings/General/Home/Bookmarks" setting is set to "Show".
+The value is determined at the time of sending the core ping.
+
+Fennec.settings_general.homepage.history_enabled
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `history_enabled` setting is true if the "Settings/General/Home/History"
+setting is set to "Show".
+The value is determined at the time of sending the core ping.
+
+Fennec.settings_privacy.do_not_track
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `do_not_track` setting is true if the "Settings/Privacy/Do not track" is
+enabled.
+The value is determined at the time of sending the core ping.
+
+Fennec.settings_privacy.master_password
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `master_password` setting is true if the
+"Settings/Privacy/Use master password" is enabled.
+The value is determined at the time of sending the core ping.
+
+Fennec.settings_privacy.master_password_usage_count
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `master_password_usage_count` field contains the number of times the user
+has entered their master password since the last time the core ping was sent.
+This counter is reset when the core ping has been sent.
+
+Fennec.settings_notifications.product_feature_tips
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `product_feature_tips` setting is true if the
+"Settings/Notifications/Product and feature tips" setting is enabled.
+The value is determined at the time of sending the core ping.
+
+fennec.page_options.save_as_pdf
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `save_as_pdf` field contains the number of times the user has used the
+"Page/Save to PDF" feature since the last time the core ping was sent.
+This counter is reset when the core ping has been sent.
+
+fennec.page_options.print
+~~~~~~~~~~~~~~~~~~~~~~~~~
+The `print` field contains the number of times the user has used the
+"Page/Print" feature since the last time the core ping was sent.
+This counter is reset when the core ping has been sent.
+
+fennec.page_options.total_added_search_engines
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `total_added_search_engines` is an absolute value that contains the number
+of search engines the user has added manually.
+The value is determined at the time of sending the core ping and never reset
+to zero.
+
+fennec.page_options.total_sites_pinned_to_topsites
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `total_sites_pinned_to_topsites` is an absolute value that contains the
+number of sites the user has pinned to top sites.
+The value is determined at the time of sending the core ping and never reset
+to zero.
+
+fennec.page_options.view_source
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `view_source` field contains the number of times the user has used the
+"Page/View Page Source" feature since the last time the core ping was sent.
+This counter is reset when the core ping has been sent.
+
+Fennec.page_options.bookmark_with_star
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `bookmark_with_star` field contains the number of times the user has used
+the "Menu / <Star>"" feature since the last time the core ping was sent.
+This counter is reset when the core ping has been sent.
+
+Fennec.page_options.current_pwas_count
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `current_pwas_count` field contains the number of currently installed PWAs
+from this application.
+As Android APIs for querying this are only available on Android >=25 for lower
+versions of Android the value of this key will be "-1".
+The value is determined at the time of sending the core ping.
+
+Fennec.sync.only_over_wifi
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+The `only_over_wifi` setting is true if the
+"Settings/Sync/Sync only over Wi~Fi" setting is enabled.
+The value is determined at the time of sending the core ping.
+If the user is not signed into sync, then this value is set to `null`.
+The value is determined at the time of sending the core ping.
+
+Other parameters
+----------------
+
+HTTP "Date" header
+~~~~~~~~~~~~~~~~~~
+This header is used to track the submission date of the core ping in the format
+specified by
+`rfc 2616 sec 14.18 <https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.18>`_,
+et al (e.g. "Tue, 01 Feb 2011 14:00:00 GMT").
+
+
+Version history
+---------------
+* v10: added ``bug_1501329_affected``
+* v9:
+
+ - Apr 2017: changed ``arch`` to contain device arch rather than the one we
+ built against & ``accessibilityServices``
+ - Dec 2017: added ``defaultBrowser`` to know if the user has set Firefox as
+ default browser (Dec 2017)
+ - May 2018: added (optional) ``displayVersion`` to distinguish Firefox beta versions easily
+
+* v8: added ``flashUsage``
+* v7: added ``sessionCount`` & ``sessionDuration`` & ``campaignId``
+* v6: added ``searches``
+* v5: added ``created`` & ``tz``
+* v4: ``profileDate`` will return package install time when times.json is not available
+* v3: added ``defaultSearch``
+* v2: added ``distributionId``
+* v1: initial version - shipped in `Fennec 45 <https://bugzilla.mozilla.org/show_bug.cgi?id=1205835>`_.
+
+Notes
+~~~~~
+
+* ``distributionId`` (v2) actually landed after ``profileDate`` (v4) but was
+ uplifted to 46, whereas ``profileDate`` landed on 47. The version numbers in
+ code were updated to be increasing (bug 1264492) and the version history docs
+ rearranged accordingly.
+
+Android implementation notes
+----------------------------
+On Android, the uploader has a high probability of delivering the complete data
+for a given client but not a 100% probability. This was a conscious decision to
+keep the code simple. The cases where we can lose data:
+
+* Resetting the field measurements (including incrementing the sequence number)
+ and storing a ping for upload are not atomic. Android can kill our process
+ for memory pressure in between these distinct operations so we can just lose
+ a ping's worth of data. That sequence number will be missing on the server.
+* If we exceed some number of pings on disk that have not yet been uploaded,
+ we remove old pings to save storage space. For those pings, we will lose
+ their data and their sequence numbers will be missing on the server.
+
+Note: we never expect to drop data without also dropping a sequence number so
+we are able to determine when data loss occurs.
diff --git a/toolkit/components/telemetry/docs/obsolete/deletion-ping.rst b/toolkit/components/telemetry/docs/obsolete/deletion-ping.rst
new file mode 100644
index 0000000000..dfa7fefa00
--- /dev/null
+++ b/toolkit/components/telemetry/docs/obsolete/deletion-ping.rst
@@ -0,0 +1,26 @@
+
+"deletion" ping (obsolete)
+==========================
+
+This ping is generated when a user turns off FHR upload from the Preferences panel, changing the related ``datareporting.healthreport.uploadEnabled`` preference. This requests that all associated data from that user be deleted.
+
+This ping contains the client id and no environment data.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ version: 4,
+ type: "deletion",
+ ... common ping data
+ clientId: <UUID>,
+ payload: { }
+ }
+
+Version History
+---------------
+
+- Firefox 63:
+
+ - Replaced by "optout" ping (`bug 1445921 <https://bugzilla.mozilla.org/show_bug.cgi?id=1445921>`_).
diff --git a/toolkit/components/telemetry/docs/obsolete/ecosystem-telemetry.rst b/toolkit/components/telemetry/docs/obsolete/ecosystem-telemetry.rst
new file mode 100644
index 0000000000..e9902ff89c
--- /dev/null
+++ b/toolkit/components/telemetry/docs/obsolete/ecosystem-telemetry.rst
@@ -0,0 +1,109 @@
+Ecosystem Telemetry (obsolete)
+==============================
+
+This module transmits Ecosystem Telemetry from Firefox Desktop.
+It is only sent for Firefox Account users, using a single ping type
+"account-ecosystem"
+
+.. note::
+
+ You might like to read the `background information on Ecosystem
+ Telemetry <https://mozilla.github.io/ecosystem-platform/docs/features/firefox-accounts/ecosystem-telemetry/>`_
+
+The existing telemetry client id is **not** submitted with the ping, but an
+"ecosystem client id" is - this has the same semantics as the existing
+client id, although is a different value, and is not sent in any other ping.
+
+An anonymized user ID is submitted with each ping - `read more about these
+IDs and how they're designed to safeguard user privacy <https://mozilla.github.io/ecosystem-platform/docs/features/firefox-accounts/ecosystem-telemetry/>`_
+
+A reduced Telemetry environment is submitted in the ping, as described below.
+
+Environment
+-----------
+
+In an effort to reduce the possibility of fingerprinting, we only provide the
+following environment subset:
+
+.. code-block:: js
+
+ {
+ settings: {
+ locale: <string>, // e.g. "it", null on failure
+ },
+ system: {
+ memoryMB: <number>,
+ os: {
+ name: <string>, // e.g. "Windows_NT", null on failure
+ version: <string>, // e.g. "6.1", null on failure
+ locale: <string>, // "en" or null on failure
+ },
+ cpu: {
+ speedMHz: <number>, // cpu clock speed in MHz
+ }
+ },
+ profile: {
+ creationDate: <integer>, // integer days since UNIX epoch, e.g. 16446
+ firstUseDate: <integer>, // integer days since UNIX epoch, e.g. 16446 - optional
+ }
+ }
+
+account-ecosystem ping
+----------------------
+
+.. code-block:: js
+
+ {
+ "type": "account-ecosystem",
+ ... common ping data
+ "environment": { ... }, // as above
+ "payload": {
+ "reason": <string>, // Why the ping was submitted
+ "ecosystemAnonId": <string>, // The anonymized ID, as described above.
+ "ecosystemClientId": <guid>, // The ecosystem client ID as described above.
+ "duration": <number>, // duration since ping was last sent or since the beginning of the Firefox session in seconds
+ "histograms": {...},
+ "keyedHistograms": {...},
+ "scalars": {...},
+ "keyedScalars": {...},
+ }
+ }
+
+reason
+~~~~~~
+The ``reason`` field contains the information about why the "account-ecosystem" ping was submitted:
+
+* ``periodic`` - Sent roughly every 24 hours
+* ``shutdown`` - Sent on shutdown
+* ``logout`` - Sent when the user logs out
+
+histograms and keyedHistograms
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+This section contains the :doc:`../collection/histograms` that are valid for the account-ecosystem ping, per process.
+The recorded histograms are described in `Histograms.json <https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/Histograms.json>`_, marked with the `account-ecosystem` store.
+
+scalars and keyedScalars
+~~~~~~~~~~~~~~~~~~~~~~~~
+This section contains the :doc:`../collection/scalars` that are valid for the account-ecosystem ping, per process.
+Scalars are only submitted if data was added to them.
+The recorded scalars are described in `Scalars.yaml <https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/Scalars.yaml>`_, marked with the `account-ecosystem` store.
+
+Send behavior
+-------------
+
+Without an account
+~~~~~~~~~~~~~~~~~~
+
+Never.
+
+When a user logs into Firefox Accounts, this ping is submitted as described in
+"With an account" below. No ping is immediately sent.
+
+With an account
+~~~~~~~~~~~~~~~
+
+The ping is submitted; roughly every 24 hours with reason *periodic*. On
+shutdown this ping is submitted with reason *shutdown*.
+
+If the user logs out and disconnects the account, this ping is submitted with
+reason *logout*. While logged out, no pings will be submitted.
diff --git a/toolkit/components/telemetry/docs/obsolete/fhr/architecture.rst b/toolkit/components/telemetry/docs/obsolete/fhr/architecture.rst
new file mode 100644
index 0000000000..2e9c37f3d3
--- /dev/null
+++ b/toolkit/components/telemetry/docs/obsolete/fhr/architecture.rst
@@ -0,0 +1,226 @@
+.. _healthreport_architecture:
+
+============
+Architecture
+============
+
+``healthreporter.jsm`` contains the main interface for FHR, the
+``HealthReporter`` type. An instance of this is created by the
+``data_reporting_service``.
+
+``providers.jsm`` contains numerous ``Metrics.Provider`` and
+``Metrics.Measurement`` used for collecting application metrics. If you
+are looking for the FHR probes, this is where they are.
+
+Storage
+=======
+
+Firefox Health Report stores data in 3 locations:
+
+* Metrics measurements and provider state is stored in a SQLite database
+ (via ``Metrics.Storage``).
+* Service state (such as the IDs of documents uploaded) is stored in a
+ JSON file on disk (via OS.File).
+* Lesser state and run-time options are stored in preferences.
+
+Preferences
+===========
+
+Preferences controlling behavior of Firefox Health Report live in the
+``datareporting.healthreport.*`` branch.
+
+Service and Data Control
+------------------------
+
+The follow preferences control behavior of the service and data upload.
+
+service.enabled
+ Controls whether the entire health report service runs. The overall
+ service performs data collection, storing, and submission.
+
+ This is the primary kill switch for Firefox Health Report
+ outside of the build system variable. i.e. if you are using an
+ official Firefox build and wish to disable FHR, this is what you
+ should set to false to prevent FHR from not only submitting but
+ also collecting data.
+
+uploadEnabled
+ Whether uploading of data is enabled. This is the preference the
+ checkbox in the preferences UI reflects. If this is
+ disabled, FHR still collects data - it just doesn't upload it.
+
+service.loadDelayMsec
+ How long (in milliseconds) after initial application start should FHR
+ wait before initializing.
+
+ FHR may initialize sooner than this if the FHR service is requested.
+ This will happen if e.g. the user goes to ``about:healthreport``.
+
+service.loadDelayFirstRunMsec
+ How long (in milliseconds) FHR should wait to initialize on first
+ application run.
+
+ FHR waits longer than normal to initialize on first application run
+ because first-time initialization can use a lot of I/O to initialize
+ the SQLite database and this I/O should not interfere with the
+ first-run user experience.
+
+documentServerURI
+ The URI of a Bagheera server that FHR should interface with for
+ submitting documents.
+
+ You typically do not need to change this.
+
+documentServerNamespace
+ The namespace on the document server FHR should upload documents to.
+
+ You typically do not need to change this.
+
+infoURL
+ The URL of a page containing more info about FHR, it's privacy
+ policy, etc.
+
+about.reportUrl
+ The URL to load in ``about:healthreport``.
+
+about.reportUrlUnified
+ The URL to load in ``about:healthreport``. This is used instead of ``reportUrl`` for UnifiedTelemetry when it is not opt-in.
+
+service.providerCategories
+ A comma-delimited list of category manager categories that contain
+ registered ``Metrics.Provider`` records. Read below for how provider
+ registration works.
+
+If the entire service is disabled, you lose data collection. This means
+that **local** data analysis won't be available because there is no data
+to analyze! Keep in mind that Firefox Health Report can be useful even
+if it's not submitting data to remote servers!
+
+Logging
+-------
+
+The following preferences allow you to control the logging behavior of
+Firefox Health Report.
+
+logging.consoleEnabled
+ Whether to write log messages to the web console. This is true by
+ default.
+
+logging.consoleLevel
+ The minimum log level FHR messages must have to be written to the
+ web console. By default, only FHR warnings or errors will be written
+ to the web console. During normal/expected operation, no messages of
+ this type should be produced.
+
+logging.dumpEnabled
+ Whether to write log messages via ``dump()``. If true, FHR will write
+ messages to stdout/stderr.
+
+ This is typically only enabled when developing FHR.
+
+logging.dumpLevel
+ The minimum log level messages must have to be written via
+ ``dump()``.
+
+State
+-----
+
+currentDaySubmissionFailureCount
+ How many submission failures the client has encountered while
+ attempting to upload the most recent document.
+
+lastDataSubmissionFailureTime
+ The time of the last failed document upload.
+
+lastDataSubmissionRequestedTime
+ The time of the last document upload attempt.
+
+lastDataSubmissionSuccessfulTime
+ The time of the last successful document upload.
+
+nextDataSubmissionTime
+ The time the next data submission is scheduled for. FHR will not
+ attempt to upload a new document before this time.
+
+pendingDeleteRemoteData
+ Whether the client currently has a pending request to delete remote
+ data. If true, the client will attempt to delete all remote data
+ before an upload is performed.
+
+FHR stores various state in preferences.
+
+Registering Providers
+=====================
+
+Firefox Health Report providers are registered via the category manager.
+See ``HealthReportComponents.manifest`` for providers defined in this
+directory.
+
+Essentially, the category manager receives the name of a JS type and the
+URI of a JSM to import that exports this symbol. At run-time, the
+providers registered in the category manager are instantiated.
+
+Providers are registered via the category manager to make registration
+simple and less prone to errors. Any XPCOM component can create a
+category manager entry. Therefore, new data providers can be added
+without having to touch core Firefox Health Report code. Additionally,
+category manager registration means providers are more likely to be
+registered on FHR's terms, when it wants. If providers were registered
+in code at application run-time, there would be the risk of other
+components prematurely instantiating FHR (causing a performance hit if
+performed at an inopportune time) or semi-complicated code around
+observers or listeners. Category manager entries are only 1 line per
+provider and leave FHR in control: they are simple and safe.
+
+Document Generation and Lifecycle
+=================================
+
+FHR will attempt to submit a JSON document containing data every 24 wall
+clock hours.
+
+At upload time, FHR will query the database for **all** information from
+the last 180 days and assemble this data into a JSON document. We
+attempt to upload this JSON document with a client-generated UUID to the
+configured server.
+
+Before we attempt upload, the generated UUID is stored in the JSON state
+file on local disk. At this point, the client assumes the document with
+that UUID has been successfully stored on the server.
+
+If the client is aware of other document UUIDs that presumably exist on
+the server, those UUIDs are sent with the upload request so the client
+can request those UUIDs be deleted. This helps ensure that each client
+only has 1 document/UUID on the server at any one time.
+
+Importance of Persisting UUIDs
+------------------------------
+
+The choices of how, where, and when document UUIDs are stored and updated
+are very important. One should not attempt to change things unless she
+has a very detailed understanding of why things are the way they are.
+
+The client is purposefully very conservative about forgetting about
+generated UUIDs. In other words, once a UUID is generated, the client
+deliberately holds on to that UUID until it's very confident that UUID
+is no longer stored on the server. The reason we do this is because
+*orphaned* documents/UUIDs on the server can lead to faulty analysis,
+such as over-reporting the number of Firefox installs that stop being
+used.
+
+When uploading a new UUID, we update the state and save the state file
+to disk *before* an upload attempt because if the upload succeeds but
+the response never makes it back to the client, we want the client to
+know about the uploaded UUID so it can delete it later to prevent an
+orphan.
+
+We maintain a list of UUIDs locally (not simply the last UUID) because
+multiple upload attempts could fail the same way as the previous
+paragraph describes and we have no way of knowing which (if any)
+actually succeeded. The safest approach is to assume every document
+produced managed to get uploaded some how.
+
+We store the UUIDs on a file on disk and not anywhere else because we
+want storage to be robust. We originally stored UUIDs in preferences,
+which only flush to disk periodically. Writes to preferences were
+apparently getting lost. We switched to writing directly to files to
+eliminate this window.
diff --git a/toolkit/components/telemetry/docs/obsolete/fhr/dataformat.rst b/toolkit/components/telemetry/docs/obsolete/fhr/dataformat.rst
new file mode 100644
index 0000000000..730d7514b8
--- /dev/null
+++ b/toolkit/components/telemetry/docs/obsolete/fhr/dataformat.rst
@@ -0,0 +1,1998 @@
+.. _healthreport_dataformat:
+
+==============
+Payload Format
+==============
+
+Currently, the Firefox Health Report is submitted as a compressed JSON
+document. The root JSON element is an object. A *version* field defines
+the version of the payload which in turn defines the expected contents
+the object.
+
+As of 2013-07-03, desktop submits Version 2, and Firefox for Android submits
+Version 3 payloads.
+
+Version 3
+=========
+
+Version 3 is a complete rebuild of the document format. Events are tracked in
+an "environment". Environments are computed from a large swath of local data
+(e.g., add-ons, CPU count, versions), and a new environment comes into being
+when one of its attributes changes.
+
+Client documents, then, will include descriptions of many environments, and
+measurements will be attributed to one particular environment.
+
+A map of environments is present at the top level of the document, with the
+current named "current" in the map. Each environment has a hash identifier and
+a set of attributes. The current environment is completely described, and has
+its hash present in a "hash" attribute. All other environments are represented
+as a tree diff from the current environment, with their hash as the key in the
+"environments" object.
+
+A removed add-on has the value 'null'.
+
+There is no "last" data at present.
+
+Daily data is hierarchical: by day, then by environment, and then by
+measurement, and is present in "data", just as in v2.
+
+Leading by example::
+
+ {
+ "lastPingDate": "2013-06-29",
+ "thisPingDate": "2013-07-03",
+ "version": 3,
+ "environments": {
+ "current": {
+ "org.mozilla.sysinfo.sysinfo": {
+ "memoryMB": 1567,
+ "cpuCount": 4,
+ "architecture": "armeabi-v7a",
+ "_v": 1,
+ "version": "4.1.2",
+ "name": "Android"
+ },
+ "org.mozilla.profile.age": {
+ "_v": 1,
+ "profileCreation": 15827
+ },
+ "org.mozilla.addons.active": {
+ "QuitNow@TWiGSoftware.com": {
+ "appDisabled": false,
+ "userDisabled": false,
+ "scope": 1,
+ "updateDay": 15885,
+ "foreignInstall": false,
+ "hasBinaryComponents": false,
+ "blocklistState": 0,
+ "type": "extension",
+ "installDay": 15885,
+ "version": "1.18.02"
+ },
+ "{dbbf9331-b713-6eda-1006-205efead09dc}": {
+ "appDisabled": false,
+ "userDisabled": "askToActivate",
+ "scope": 8,
+ "updateDay": 15779,
+ "foreignInstall": true,
+ "blocklistState": 0,
+ "type": "plugin",
+ "installDay": 15779,
+ "version": "11.1 r115"
+ },
+ "desktopbydefault@bnicholson.mozilla.org": {
+ "appDisabled": false,
+ "userDisabled": true,
+ "scope": 1,
+ "updateDay": 15870,
+ "foreignInstall": false,
+ "hasBinaryComponents": false,
+ "blocklistState": 0,
+ "type": "extension",
+ "installDay": 15870,
+ "version": "1.1"
+ },
+ "{6e092a7f-ba58-4abb-88c1-1a4e50b217e4}": {
+ "appDisabled": false,
+ "userDisabled": false,
+ "scope": 1,
+ "updateDay": 15828,
+ "foreignInstall": false,
+ "hasBinaryComponents": false,
+ "blocklistState": 0,
+ "type": "extension",
+ "installDay": 15828,
+ "version": "1.1.0"
+ },
+ "{46551EC9-40F0-4e47-8E18-8E5CF550CFB8}": {
+ "appDisabled": false,
+ "userDisabled": true,
+ "scope": 1,
+ "updateDay": 15879,
+ "foreignInstall": false,
+ "hasBinaryComponents": false,
+ "blocklistState": 0,
+ "type": "extension",
+ "installDay": 15879,
+ "version": "1.3.2"
+ },
+ "_v": 1
+ },
+ "org.mozilla.appInfo.appinfo": {
+ "_v": 3,
+ "appLocale": "en_us",
+ "osLocale": "en_us",
+ "distribution": "",
+ "acceptLangIsUserSet": 0,
+ "isTelemetryEnabled": 1,
+ "isBlocklistEnabled": 1
+ },
+ "geckoAppInfo": {
+ "updateChannel": "nightly",
+ "id": "{aa3c5121-dab2-40e2-81ca-7ea25febc110}",
+ "os": "Android",
+ "platformBuildID": "20130703031323",
+ "platformVersion": "25.0a1",
+ "vendor": "Mozilla",
+ "name": "fennec",
+ "xpcomabi": "arm-eabi-gcc3",
+ "appBuildID": "20130703031323",
+ "_v": 1,
+ "version": "25.0a1"
+ },
+ "hash": "tB4Pnnep9yTxnMDymc3dAB2RRB0=",
+ "org.mozilla.addons.counts": {
+ "extension": 4,
+ "plugin": 1,
+ "_v": 1,
+ "theme": 0
+ }
+ },
+ "k2O3hlreMeS7L1qtxeMsYWxgWWQ=": {
+ "geckoAppInfo": {
+ "platformBuildID": "20130630031138",
+ "appBuildID": "20130630031138",
+ "_v": 1
+ },
+ "org.mozilla.appInfo.appinfo": {
+ "_v": 2,
+ }
+ },
+ "1+KN9TutMpzdl4TJEl+aCxK+xcw=": {
+ "geckoAppInfo": {
+ "platformBuildID": "20130626031100",
+ "appBuildID": "20130626031100",
+ "_v": 1
+ },
+ "org.mozilla.addons.active": {
+ "QuitNow@TWiGSoftware.com": null,
+ "{dbbf9331-b713-6eda-1006-205efead09dc}": null,
+ "desktopbydefault@bnicholson.mozilla.org": null,
+ "{6e092a7f-ba58-4abb-88c1-1a4e50b217e4}": null,
+ "{46551EC9-40F0-4e47-8E18-8E5CF550CFB8}": null,
+ "_v": 1
+ },
+ "org.mozilla.addons.counts": {
+ "extension": 0,
+ "plugin": 0,
+ "_v": 1
+ }
+ }
+ },
+ "data": {
+ "last": {},
+ "days": {
+ "2013-07-03": {
+ "tB4Pnnep9yTxnMDymc3dAB2RRB0=": {
+ "org.mozilla.appSessions": {
+ "normal": [
+ {
+ "r": "P",
+ "d": 2,
+ "sj": 653
+ },
+ {
+ "r": "P",
+ "d": 22
+ },
+ {
+ "r": "P",
+ "d": 5
+ },
+ {
+ "r": "P",
+ "d": 0
+ },
+ {
+ "r": "P",
+ "sg": 3560,
+ "d": 171,
+ "sj": 518
+ },
+ {
+ "r": "P",
+ "d": 16
+ },
+ {
+ "r": "P",
+ "d": 1079
+ }
+ ],
+ "_v": "4"
+ }
+ },
+ "k2O3hlreMeS7L1qtxeMsYWxgWWQ=": {
+ "org.mozilla.appSessions": {
+ "normal": [
+ {
+ "r": "P",
+ "d": 27
+ },
+ {
+ "r": "P",
+ "d": 19
+ },
+ {
+ "r": "P",
+ "d": 55
+ }
+ ],
+ "_v": "4"
+ },
+ "org.mozilla.searches.counts": {
+ "bartext": {
+ "google": 1
+ },
+ "_v": "4"
+ },
+ "org.mozilla.experiment": {
+ "lastActive": "some.experiment.id"
+ "_v": "1"
+ }
+ }
+ }
+ }
+ }
+ }
+
+App sessions in Version 3
+-------------------------
+
+Sessions are divided into "normal" and "abnormal". Session objects are stored as discrete JSON::
+
+ "org.mozilla.appSessions": {
+ _v: 4,
+ "normal": [
+ {"r":"P", "d": 123},
+ ],
+ "abnormal": [
+ {"r":"A", "oom": true, "stopped": false}
+ ]
+ }
+
+Keys are:
+
+"r"
+ reason. Values are "P" (activity paused), "A" (abnormal termination).
+"d"
+ duration. Value in seconds.
+"sg"
+ Gecko startup time (msec). Present if this is a clean launch. This
+ corresponds to the telemetry timer *FENNEC_STARTUP_TIME_GECKOREADY*.
+"sj"
+ Java activity init time (msec). Present if this is a clean launch. This
+ corresponds to the telemetry timer *FENNEC_STARTUP_TIME_JAVAUI*,
+ and includes initialization tasks beyond initial
+ *onWindowFocusChanged*.
+
+Abnormal terminations will be missing a duration and will feature these keys:
+
+"oom"
+ was the session killed by an OOM exception?
+"stopped"
+ was the session stopped gently?
+
+Version 3.2
+-----------
+
+As of Firefox 35, the search counts measurement is now bumped to v6, including the *activity* location for the search activity.
+
+Version 3.1
+-----------
+
+As of Firefox 27, *appinfo* is now bumped to v3, including *osLocale*,
+*appLocale* (currently always the same as *osLocale*), *distribution* (a string
+containing the distribution ID and version, separated by a colon), and
+*acceptLangIsUserSet*, an integer-boolean that describes whether the user set
+an *intl.accept_languages* preference.
+
+The search counts measurement is now at version 5, which indicates that
+non-partner searches are recorded. You'll see identifiers like "other-Foo Bar"
+rather than "other".
+
+
+Version 3.2
+-----------
+
+In Firefox 32, Firefox for Android includes a device configuration section
+in the environment description::
+
+ "org.mozilla.device.config": {
+ "hasHardwareKeyboard": false,
+ "screenXInMM": 58,
+ "screenLayout": 2,
+ "uiType": "default",
+ "screenYInMM": 103,
+ "_v": 1,
+ "uiMode": 1
+ }
+
+Of these, the only keys that need explanation are:
+
+uiType
+ One of "default", "smalltablet", "largetablet".
+uiMode
+ A mask of the Android *Configuration.uiMode* value, e.g.,
+ *UI_MODE_TYPE_CAR*.
+screenLayout
+ A mask of the Android *Configuration.screenLayout* value. One of the
+ *SCREENLAYOUT_SIZE_* constants.
+
+Note that screen dimensions can be incorrect due to device inaccuracies and platform limitations.
+
+Other notable differences from Version 2
+----------------------------------------
+
+* There is no default browser indicator on Android.
+* Add-ons include a *blocklistState* attribute, as returned by AddonManager.
+* Searches are now version 4, and are hierarchical: how the search was started
+ (bartext, barkeyword, barsuggest), and then counts per provider.
+
+Version 2
+=========
+
+Version 2 is the same as version 1 with the exception that it has an additional
+top-level field, *geckoAppInfo*, which contains basic application info.
+
+geckoAppInfo
+------------
+
+This field is an object that is a simple map of string keys and values
+describing basic application metadata. It is very similar to the *appinfo*
+measurement in the *last* section. The difference is this field is almost
+certainly guaranteed to exist whereas the one in the data part of the
+payload may be omitted in certain scenarios (such as catastrophic client
+error).
+
+Its keys are as follows:
+
+appBuildID
+ The build ID/date of the application. e.g. "20130314113542".
+
+version
+ The value of nsXREAppData.version. This is the application's version. e.g.
+ "21.0.0".
+
+vendor
+ The value of nsXREAppData.vendor. Can be empty an empty string. For
+ official Mozilla builds, this will be "Mozilla".
+
+name
+ The value of nsXREAppData.name. For official Firefox builds, this
+ will be "Firefox".
+
+id
+ The value of nsXREAppData.ID.
+
+platformVersion
+ The version of the Gecko platform (as opposed to the app version). For
+ Firefox, this is almost certainly equivalent to the *version* field.
+
+platformBuildID
+ The build ID/date of the Gecko platfor (as opposed to the app version).
+ This is commonly equivalent to *appBuildID*.
+
+os
+ The name of the operating system the application is running on.
+
+xpcomabi
+ The binary architecture of the build.
+
+updateChannel
+ The name of the channel used for application updates. Official Mozilla
+ builds have one of the values {release, beta, aurora, nightly}. Local and
+ test builds have *default* as the channel.
+
+Version 1
+=========
+
+Top-level Properties
+--------------------
+
+The main JSON object contains the following properties:
+
+lastPingDate
+ UTC date of the last upload. If this is the first upload from this client,
+ this will not be present.
+
+thisPingDate
+ UTC date when this payload was constructed.
+
+version
+ Integer version of this payload format. Currently only 1 is defined.
+
+clientID
+ An identifier that identifies the client that is submitting data.
+
+ This property may not be present in older clients.
+
+ See :ref:`healthreport_identifiers` for more info on identifiers.
+
+clientIDVersion
+ Integer version associated with the generation semantics for the
+ ``clientID``.
+
+ If the value is ``1``, ``clientID`` is a randomly-generated UUID.
+
+ This property may not be present in older clients.
+
+data
+ Object holding data constituting health report.
+
+Data Properties
+---------------
+
+The bulk of the health report is contained within the *data* object. This
+object has the following keys:
+
+days
+ Object mapping UTC days to measurements from that day. Keys are in the
+ *YYYY-MM-DD* format. e.g. "2013-03-14"
+
+last
+ Object mapping measurement names to their values.
+
+
+The value of *days* and *last* are objects mapping measurement names to that
+measurement's values. The values are always objects. Each object contains
+a *_v* property. This property defines the version of this measurement.
+Additional non-underscore-prefixed properties are defined by the measurement
+itself (see sections below).
+
+Example
+-------
+
+Here is an example JSON document for version 1::
+
+ {
+ "version": 1,
+ "thisPingDate": "2013-03-11",
+ "lastPingDate": "2013-03-10",
+ "data": {
+ "last": {
+ "org.mozilla.addons.active": {
+ "masspasswordreset@johnathan.nightingale": {
+ "userDisabled": false,
+ "appDisabled": false,
+ "version": "1.05",
+ "type": "extension",
+ "scope": 1,
+ "foreignInstall": false,
+ "hasBinaryComponents": false,
+ "installDay": 14973,
+ "updateDay": 15317
+ },
+ "places-maintenance@bonardo.net": {
+ "userDisabled": false,
+ "appDisabled": false,
+ "version": "1.3",
+ "type": "extension",
+ "scope": 1,
+ "foreignInstall": false,
+ "hasBinaryComponents": false,
+ "installDay": 15268,
+ "updateDay": 15379
+ },
+ "_v": 1
+ },
+ "org.mozilla.appInfo.appinfo": {
+ "_v": 1,
+ "appBuildID": "20130309030841",
+ "distributionID": "",
+ "distributionVersion": "",
+ "hotfixVersion": "",
+ "id": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}",
+ "locale": "en-US",
+ "name": "Firefox",
+ "os": "Darwin",
+ "platformBuildID": "20130309030841",
+ "platformVersion": "22.0a1",
+ "updateChannel": "nightly",
+ "vendor": "Mozilla",
+ "version": "22.0a1",
+ "xpcomabi": "x86_64-gcc3"
+ },
+ "org.mozilla.profile.age": {
+ "_v": 1,
+ "profileCreation": 12444
+ },
+ "org.mozilla.appSessions.current": {
+ "_v": 3,
+ "startDay": 15773,
+ "activeTicks": 522,
+ "totalTime": 70858,
+ "main": 1245,
+ "firstPaint": 2695,
+ "sessionRestored": 3436
+ },
+ "org.mozilla.sysinfo.sysinfo": {
+ "_v": 1,
+ "cpuCount": 8,
+ "memoryMB": 16384,
+ "architecture": "x86-64",
+ "name": "Darwin",
+ "version": "12.2.1"
+ }
+ },
+ "days": {
+ "2013-03-11": {
+ "org.mozilla.addons.counts": {
+ "_v": 1,
+ "extension": 15,
+ "plugin": 12,
+ "theme": 1
+ },
+ "org.mozilla.places.places": {
+ "_v": 1,
+ "bookmarks": 757,
+ "pages": 104858
+ },
+ "org.mozilla.appInfo.appinfo": {
+ "_v": 1,
+ "isDefaultBrowser": 1
+ }
+ },
+ "2013-03-10": {
+ "org.mozilla.addons.counts": {
+ "_v": 1,
+ "extension": 15,
+ "plugin": 12,
+ "theme": 1
+ },
+ "org.mozilla.places.places": {
+ "_v": 1,
+ "bookmarks": 757,
+ "pages": 104857
+ },
+ "org.mozilla.searches.counts": {
+ "_v": 1,
+ "google.urlbar": 4
+ },
+ "org.mozilla.appInfo.appinfo": {
+ "_v": 1,
+ "isDefaultBrowser": 1
+ }
+ }
+ }
+ }
+ }
+
+Measurements
+============
+
+The bulk of payloads consists of measurement data. An individual measurement
+is merely a collection of related values e.g. *statistics about the Places
+database* or *system information*.
+
+Each measurement has an integer version number attached. When the fields in
+a measurement or the semantics of data within that measurement change, the
+version number is incremented.
+
+All measurements are defined alphabetically in the sections below.
+
+org.mozilla.addons.addons
+-------------------------
+
+This measurement contains information about the currently-installed add-ons.
+
+Version 2
+^^^^^^^^^
+
+This version adds the human-readable fields *name* and *description*, both
+coming directly from the Addon instance as most properties in version 1.
+Also, all plugin details are now in org.mozilla.addons.plugins.
+
+Version 1
+^^^^^^^^^
+
+The measurement object is a mapping of add-on IDs to objects containing
+add-on metadata.
+
+Each add-on contains the following properties:
+
+* userDisabled
+* appDisabled
+* version
+* type
+* scope
+* foreignInstall
+* hasBinaryComponents
+* installDay
+* updateDay
+
+With the exception of *installDay* and *updateDay*, all these properties
+come direct from the Addon instance. See https://developer.mozilla.org/en-US/docs/Addons/Add-on_Manager/Addon.
+*installDay* and *updateDay* are the number of days since UNIX epoch of
+the add-ons *installDate* and *updateDate* properties, respectively.
+
+Notes
+^^^^^
+
+Add-ons that have opted out of AMO updates via the
+*extensions._id_.getAddons.cache.enabled* preference are, since Bug 868306
+(Firefox 24), included in the list of submitted add-ons.
+
+Example
+^^^^^^^
+::
+
+ "org.mozilla.addons.addons": {
+ "_v": 2,
+ "{d10d0bf8-f5b5-c8b4-a8b2-2b9879e08c5d}": {
+ "userDisabled": false,
+ "appDisabled": false,
+ "name": "Adblock Plus",
+ "version": "2.4.1",
+ "type": "extension",
+ "scope": 1,
+ "description": "Ads were yesterday!",
+ "foreignInstall": false,
+ "hasBinaryComponents": false,
+ "installDay": 16093,
+ "updateDay": 16093
+ },
+ "{e4a8a97b-f2ed-450b-b12d-ee082ba24781}": {
+ "userDisabled": true,
+ "appDisabled": false,
+ "name": "Greasemonkey",
+ "version": "1.14",
+ "type": "extension",
+ "scope": 1,
+ "description": "A User Script Manager for Firefox",
+ "foreignInstall": false,
+ "hasBinaryComponents": false,
+ "installDay": 16093,
+ "updateDay": 16093
+ }
+ }
+
+org.mozilla.addons.plugins
+--------------------------
+
+This measurement contains information about the currently-installed plugins.
+
+Version 1
+^^^^^^^^^
+
+The measurement object is a mapping of plugin IDs to objects containing
+plugin metadata.
+
+The plugin ID is constructed of the plugins filename, name, version and
+description. Every plugin has at least a filename and a name.
+
+Each plugin contains the following properties:
+
+* name
+* version
+* description
+* blocklisted
+* disabled
+* clicktoplay
+* mimeTypes
+* updateDay
+
+With the exception of *updateDay* and *mimeTypes*, all these properties come
+directly from ``nsIPluginTag`` via ``nsIPluginHost``.
+*updateDay* is the number of days since UNIX epoch of the plugins last modified
+time.
+*mimeTypes* is the list of mimetypes the plugin supports, see
+``nsIPluginTag.getMimeTypes()``.
+
+Example
+^^^^^^^
+
+::
+
+ "org.mozilla.addons.plugins": {
+ "_v": 1,
+ "Flash Player.plugin:Shockwave Flash:12.0.0.38:Shockwave Flash 12.0 r0": {
+ "mimeTypes": [
+ "application/x-shockwave-flash",
+ "application/futuresplash"
+ ],
+ "name": "Shockwave Flash",
+ "version": "12.0.0.38",
+ "description": "Shockwave Flash 12.0 r0",
+ "blocklisted": false,
+ "disabled": false,
+ "clicktoplay": false
+ },
+ "Default Browser.plugin:Default Browser Helper:537:Provides information about the default web browser": {
+ "mimeTypes": [
+ "application/apple-default-browser"
+ ],
+ "name": "Default Browser Helper",
+ "version": "537",
+ "description": "Provides information about the default web browser",
+ "blocklisted": false,
+ "disabled": true,
+ "clicktoplay": false
+ }
+ }
+
+org.mozilla.addons.counts
+-------------------------
+
+This measurement contains information about historical add-on counts.
+
+Version 1
+^^^^^^^^^
+
+The measurement object consists of counts of different add-on types. The
+properties are:
+
+extension
+ Integer count of installed extensions.
+plugin
+ Integer count of installed plugins.
+theme
+ Integer count of installed themes.
+lwtheme
+ Integer count of installed lightweight themes.
+
+Notes
+^^^^^
+
+Add-ons opted out of AMO updates are included in the counts. This differs from
+the behavior of the active add-ons measurement.
+
+If no add-ons of a particular type are installed, the property for that type
+will not be present (as opposed to an explicit property with value of 0).
+
+Example
+^^^^^^^
+
+::
+
+ "2013-03-14": {
+ "org.mozilla.addons.counts": {
+ "_v": 1,
+ "extension": 21,
+ "plugin": 4,
+ "theme": 1
+ }
+ }
+
+
+
+org.mozilla.appInfo.appinfo
+---------------------------
+
+This measurement contains basic XUL application and Gecko platform
+information. It is reported in the *last* section.
+
+Version 2
+^^^^^^^^^
+
+In addition to fields present in version 1, this version has the following
+fields appearing in the *days* section:
+
+isBlocklistEnabled
+ Whether the blocklist ping is enabled. This is an integer, 0 or 1.
+ This does not indicate whether the blocklist ping was sent but merely
+ whether the application will try to send the blocklist ping.
+
+isTelemetryEnabled
+ Whether Telemetry is enabled. This is an integer, 0 or 1.
+
+Version 1
+^^^^^^^^^
+
+The measurement object contains mostly string values describing the
+current application and build. The properties are:
+
+* vendor
+* name
+* id
+* version
+* appBuildID
+* platformVersion
+* platformBuildID
+* os
+* xpcomabi
+* updateChannel
+* distributionID
+* distributionVersion
+* hotfixVersion
+* locale
+* isDefaultBrowser
+
+Notes
+^^^^^
+
+All of the properties appear in the *last* section except for
+*isDefaultBrowser*, which appears under *days*.
+
+Example
+^^^^^^^
+
+This example comes from an official macOS Nightly build::
+
+ "org.mozilla.appInfo.appinfo": {
+ "_v": 1,
+ "appBuildID": "20130311030946",
+ "distributionID": "",
+ "distributionVersion": "",
+ "hotfixVersion": "",
+ "id": "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}",
+ "locale": "en-US",
+ "name": "Firefox",
+ "os": "Darwin",
+ "platformBuildID": "20130311030946",
+ "platformVersion": "22.0a1",
+ "updateChannel": "nightly",
+ "vendor": "Mozilla",
+ "version": "22.0a1",
+ "xpcomabi": "x86_64-gcc3"
+ },
+
+org.mozilla.appInfo.update
+--------------------------
+
+This measurement contains information about the application update mechanism
+in the application.
+
+Version 1
+^^^^^^^^^
+
+The following daily values are reported:
+
+enabled
+ Whether automatic application update checking is enabled. 1 for yes,
+ 0 for no.
+autoDownload
+ Whether automatic download of available updates is enabled.
+
+Notes
+^^^^^
+
+This measurement was merged to mozilla-central for JS FHR on 2013-07-15.
+
+Example
+^^^^^^^
+
+::
+
+ "2013-07-15": {
+ "org.mozilla.appInfo.update": {
+ "_v": 1,
+ "enabled": 1,
+ "autoDownload": 1,
+ }
+ }
+
+org.mozilla.appInfo.versions
+----------------------------
+
+This measurement contains a history of application version numbers.
+
+Version 2
+^^^^^^^^^
+
+Version 2 reports more fields than version 1 and is not backwards compatible.
+The following fields are present in version 2:
+
+appVersion
+ An array of application version strings.
+appBuildID
+ An array of application build ID strings.
+platformVersion
+ An array of platform version strings.
+platformBuildID
+ An array of platform build ID strings.
+
+When the application is upgraded, the new version and/or build IDs are
+appended to their appropriate fields.
+
+Version 1
+^^^^^^^^^
+
+When the application version (*version* from *org.mozilla.appinfo.appinfo*)
+changes, we record the new version on the day the change was seen. The new
+versions for a day are recorded in an array under the *version* property.
+
+Notes
+^^^^^
+
+If the application isn't upgraded, this measurement will not be present.
+This means this measurement will not be present for most days if a user is
+on the release channel (since updates are typically released every 6 weeks).
+However, users on the Nightly and Aurora channels will likely have a lot
+of these entries since those builds are updated every day.
+
+Values for this measurement are collected when performing the daily
+collection (typically occurs at upload time). As a result, it's possible
+the actual upgrade day may not be attributed to the proper day - the
+reported day may lag behind.
+
+The app and platform versions and build IDs should be identical for most
+clients. If they are different, we are possibly looking at a *Frankenfox*.
+
+Example
+^^^^^^^
+
+::
+
+ "2013-03-27": {
+ "org.mozilla.appInfo.versions": {
+ "_v": 2,
+ "appVersion": [
+ "22.0.0"
+ ],
+ "appBuildID": [
+ "20130325031100"
+ ],
+ "platformVersion": [
+ "22.0.0"
+ ],
+ "platformBuildID": [
+ "20130325031100"
+ ]
+ }
+ }
+
+org.mozilla.appSessions.current
+-------------------------------
+
+This measurement contains information about the currently running XUL
+application's session.
+
+Version 3
+^^^^^^^^^
+
+This measurement has the following properties:
+
+startDay
+ Integer days since UNIX epoch when this session began.
+activeTicks
+ Integer count of *ticks* the session was active for. Gecko periodically
+ sends out a signal when the session is active. Session activity
+ involves keyboard or mouse interaction with the application. Each tick
+ represents a window of 5 seconds where there was interaction.
+totalTime
+ Integer seconds the session has been alive.
+main
+ Integer milliseconds it took for the Gecko process to start up.
+firstPaint
+ Integer milliseconds from process start to first paint.
+sessionRestored
+ Integer milliseconds from process start to session restore.
+
+Example
+^^^^^^^
+
+::
+
+ "org.mozilla.appSessions.current": {
+ "_v": 3,
+ "startDay": 15775,
+ "activeTicks": 4282,
+ "totalTime": 249422,
+ "main": 851,
+ "firstPaint": 3271,
+ "sessionRestored": 5998
+ }
+
+org.mozilla.appSessions.previous
+--------------------------------
+
+This measurement contains information about previous XUL application sessions.
+
+Version 3
+^^^^^^^^^
+
+This measurement contains per-day lists of all the sessions started on that
+day. The following properties may be present:
+
+cleanActiveTicks
+ Active ticks of sessions that were properly shut down.
+cleanTotalTime
+ Total number of seconds for sessions that were properly shut down.
+abortedActiveTicks
+ Active ticks of sessions that were not properly shut down.
+abortedTotalTime
+ Total number of seconds for sessions that were not properly shut down.
+main
+ Time in milliseconds from process start to main process initialization.
+firstPaint
+ Time in milliseconds from process start to first paint.
+sessionRestored
+ Time in milliseconds from process start to session restore.
+
+Notes
+^^^^^
+
+Sessions are recorded on the date on which they began.
+
+If a session was aborted/crashed, the total time may be less than the actual
+total time. This is because we don't always update total time during periods
+of inactivity and the abort/crash could occur after a long period of idle,
+before we've updated the total time.
+
+The lengths of the arrays for {cleanActiveTicks, cleanTotalTime},
+{abortedActiveTicks, abortedTotalTime}, and {main, firstPaint, sessionRestored}
+should all be identical.
+
+The length of the clean sessions plus the length of the aborted sessions should
+be equal to the length of the {main, firstPaint, sessionRestored} properties.
+
+It is not possible to distinguish the main, firstPaint, and sessionRestored
+values from a clean vs aborted session: they are all lumped together.
+
+For sessions spanning multiple UTC days, it's not possible to know which
+days the session was active for. It's possible a week long session only
+had activity for 2 days and there's no way for us to tell which days.
+
+Example
+^^^^^^^
+
+::
+
+ "org.mozilla.appSessions.previous": {
+ "_v": 3,
+ "cleanActiveTicks": [
+ 78,
+ 1785
+ ],
+ "cleanTotalTime": [
+ 4472,
+ 88908
+ ],
+ "main": [
+ 32,
+ 952
+ ],
+ "firstPaint": [
+ 2755,
+ 3497
+ ],
+ "sessionRestored": [
+ 5149,
+ 5520
+ ]
+ }
+
+org.mozilla.crashes.crashes
+---------------------------
+
+This measurement contains a historical record of application crashes.
+
+Version 6
+^^^^^^^^^
+
+This version adds tracking for out-of-memory (OOM) crashes in the main process.
+An OOM crash will be counted as both main-crash and main-crash-oom.
+
+This measurement will be reported on each day there was a crash or crash
+submission. Records may contain the following fields, whose values indicate
+the number of crashes, hangs, or submissions that occurred on the given day:
+
+* content-crash
+* content-crash-submission-succeeded
+* content-crash-submission-failed
+* content-hang
+* content-hang-submission-succeeded
+* content-hang-submission-failed
+* gmplugin-crash
+* gmplugin-crash-submission-succeeded
+* gmplugin-crash-submission-failed
+* main-crash
+* main-crash-oom
+* main-crash-submission-succeeded
+* main-crash-submission-failed
+* main-hang
+* main-hang-submission-succeeded
+* main-hang-submission-failed
+* plugin-crash
+* plugin-crash-submission-succeeded
+* plugin-crash-submission-failed
+* plugin-hang
+* plugin-hang-submission-succeeded
+* plugin-hang-submission-failed
+
+Version 5
+^^^^^^^^^
+
+This version adds support for Gecko media plugin (GMP) crashes.
+
+This measurement will be reported on each day there was a crash or crash
+submission. Records may contain the following fields, whose values indicate
+the number of crashes, hangs, or submissions that occurred on the given day:
+
+* content-crash
+* content-crash-submission-succeeded
+* content-crash-submission-failed
+* content-hang
+* content-hang-submission-succeeded
+* content-hang-submission-failed
+* gmplugin-crash
+* gmplugin-crash-submission-succeeded
+* gmplugin-crash-submission-failed
+* main-crash
+* main-crash-submission-succeeded
+* main-crash-submission-failed
+* main-hang
+* main-hang-submission-succeeded
+* main-hang-submission-failed
+* plugin-crash
+* plugin-crash-submission-succeeded
+* plugin-crash-submission-failed
+* plugin-hang
+* plugin-hang-submission-succeeded
+* plugin-hang-submission-failed
+
+Version 4
+^^^^^^^^^
+
+This version follows up from version 3, adding submissions which are now
+tracked by the :ref:`crashes_crashmanager`.
+
+This measurement will be reported on each day there was a crash or crash
+submission. Records may contain the following fields, whose values indicate
+the number of crashes, hangs, or submissions that occurred on the given day:
+
+* main-crash
+* main-crash-submission-succeeded
+* main-crash-submission-failed
+* main-hang
+* main-hang-submission-succeeded
+* main-hang-submission-failed
+* content-crash
+* content-crash-submission-succeeded
+* content-crash-submission-failed
+* content-hang
+* content-hang-submission-succeeded
+* content-hang-submission-failed
+* plugin-crash
+* plugin-crash-submission-succeeded
+* plugin-crash-submission-failed
+* plugin-hang
+* plugin-hang-submission-succeeded
+* plugin-hang-submission-failed
+
+Version 3
+^^^^^^^^^
+
+This version follows up from version 2, building on improvements to
+the :ref:`crashes_crashmanager`.
+
+This measurement will be reported on each day there was a
+crash. Records may contain the following fields, whose values indicate
+the number of crashes or hangs that occurred on the given day:
+
+* main-crash
+* main-hang
+* content-crash
+* content-hang
+* plugin-crash
+* plugin-hang
+
+Version 2
+^^^^^^^^^
+
+The switch to version 2 coincides with the introduction of the
+:ref:`crashes_crashmanager`, which provides a more robust source of
+crash data.
+
+This measurement will be reported on each day there was a crash. The
+following fields may be present in each record:
+
+mainCrash
+ The number of main process crashes that occurred on the given day.
+
+Yes, version 2 does not track submissions like version 1. It is very
+likely submissions will be re-added later.
+
+Also absent from version 2 are plugin crashes and hangs. These will be
+re-added, likely in version 3.
+
+Version 1
+^^^^^^^^^
+
+This measurement will be reported on each day there was a crash. The
+following properties are reported:
+
+pending
+ The number of crash reports that haven't been submitted.
+submitted
+ The number of crash reports that were submitted.
+
+Notes
+^^^^^
+
+Main process crashes are typically submitted immediately after they
+occur (by checking a box in the crash reporter, which should appear
+automatically after a crash). If the crash reporter submits the crash
+successfully, we get a submitted crash. Else, we leave it as pending.
+
+A pending crash does not mean it will eventually be submitted.
+
+Pending crash reports can be submitted post-crash by going to
+about:crashes.
+
+If a pending crash is submitted via about:crashes, the submitted count
+increments but the pending count does not decrement. This is because FHR
+does not know which pending crash was just submitted and therefore it does
+not know which day's pending crash to decrement.
+
+Example
+^^^^^^^
+
+::
+
+ "org.mozilla.crashes.crashes": {
+ "_v": 1,
+ "pending": 1,
+ "submitted": 2
+ },
+ "org.mozilla.crashes.crashes": {
+ "_v": 2,
+ "mainCrash": 2
+ }
+ "org.mozilla.crashes.crashes": {
+ "_v": 4,
+ "main-crash": 2,
+ "main-crash-submission-succeeded": 1,
+ "main-crash-submission-failed": 1,
+ "main-hang": 1,
+ "plugin-crash": 2
+ }
+
+org.mozilla.healthreport.submissions
+------------------------------------
+
+This measurement contains a history of FHR's own data submission activity.
+It was added in Firefox 23 in early May 2013.
+
+Version 2
+^^^^^^^^^
+
+This is the same as version 1 except an additional field has been added.
+
+uploadAlreadyInProgress
+ A request for upload was initiated while another upload was in progress.
+ This should not occur in well-behaving clients. It (along with a lock
+ preventing simultaneous upload) was added to ensure this never occurs.
+
+Version 1
+^^^^^^^^^
+
+Daily counts of upload events are recorded.
+
+firstDocumentUploadAttempt
+ An attempt was made to upload the client's first document to the server.
+ These are uploads where the client is not aware of a previous document ID
+ on the server. Unless the client had disabled upload, there should be at
+ most one of these in the history of the client.
+
+continuationUploadAttempt
+ An attempt was made to upload a document that replaces an existing document
+ on the server. Most upload attempts should be attributed to this as opposed
+ to *firstDocumentUploadAttempt*.
+
+uploadSuccess
+ The upload attempt recorded by *firstDocumentUploadAttempt* or
+ *continuationUploadAttempt* was successful.
+
+uploadTransportFailure
+ An upload attempt failed due to transport failure (network unavailable,
+ etc).
+
+uploadServerFailure
+ An upload attempt failed due to a server-reported failure. Ideally these
+ are failures reported by the FHR server itself. However, intermediate
+ proxies, firewalls, etc may trigger this depending on how things are
+ configured.
+
+uploadClientFailure
+ An upload attempt failued due to an error/exception in the client.
+ This almost certainly points to a bug in the client.
+
+The result for an upload attempt is always attributed to the same day as
+the attempt, even if the result occurred on a different day from the attempt.
+Therefore, the sum of the result counts should equal the result of the attempt
+counts.
+
+org.mozilla.hotfix.update
+-------------------------
+
+This measurement contains results from the Firefox update hotfix.
+
+The Firefox update hotfix bypasses the built-in application update mechanism
+and installs a modern Firefox.
+
+Version 1
+^^^^^^^^^
+
+The fields in this measurement are dynamically created based on which
+versions of the update hotfix state file are found on disk.
+
+The general format of the fields is ``<version>.<thing>`` where ``version``
+is a hotfix version like ``v20140527`` and ``thing`` is a key from the
+hotfix state file, e.g. ``upgradedFrom``. Here are some of the ``things``
+that can be defined.
+
+upgradedFrom
+ String identifying the Firefox version that the hotfix upgraded from.
+ e.g. ``16.0`` or ``17.0.1``.
+
+uninstallReason
+ String with enumerated values identifying why the hotfix was uninstalled.
+ Value will be ``STILL_INSTALLED`` if the hotfix is still installed.
+
+downloadAttempts
+ Integer number of times the hotfix started downloading an installer.
+ Download resumes are part of this count.
+
+downloadFailures
+ Integer count of times a download supposedly completed but couldn't
+ be validated. This likely represents something wrong with the network
+ connection. The ratio of this to ``downloadAttempts`` should be low.
+
+installAttempts
+ Integer count of times the hotfix attempted to run the installer.
+ This should ideally be 1. It should only be greater than 1 if UAC
+ elevation was canceled or not allowed.
+
+installFailures
+ Integer count of total installation failures this client experienced.
+ Can be 0. ``installAttempts - installFailures`` implies install successes.
+
+notificationsShown
+ Integer count of times a notification was displayed to the user that
+ they are running an older Firefox.
+
+org.mozilla.places.places
+-------------------------
+
+This measurement contains information about the Places database (where Firefox
+stores its history and bookmarks).
+
+Version 1
+^^^^^^^^^
+
+Daily counts of items in the database are reported in the following properties:
+
+bookmarks
+ Integer count of bookmarks present.
+pages
+ Integer count of pages in the history database.
+
+Example
+^^^^^^^
+
+::
+
+ "org.mozilla.places.places": {
+ "_v": 1,
+ "bookmarks": 388,
+ "pages": 94870
+ }
+
+org.mozilla.profile.age
+-----------------------
+
+This measurement contains information about the current profile's age (and
+in version 2, the profile's most recent reset date)
+
+Version 2
+^^^^^^^^^
+
+*profileCreation* and *profileReset* properties are present. Both define
+the integer days since UNIX epoch that the current profile was created or
+reset accordingly.
+
+Version 1
+^^^^^^^^^
+
+A single *profileCreation* property is present. It defines the integer
+days since UNIX epoch that the current profile was created.
+
+Notes
+^^^^^
+
+It is somewhat difficult to obtain a reliable *profile born date* due to a
+number of factors, but since Version 2, improvements have been made - on a
+"profile reset" we copy the profileCreation date from the old profile and
+record the time of the reset in profileReset.
+
+Example
+^^^^^^^
+
+::
+
+ "org.mozilla.profile.age": {
+ "_v": 2,
+ "profileCreation": 15176
+ "profileReset": 15576
+ }
+
+org.mozilla.searches.counts
+---------------------------
+
+This measurement contains information about searches performed in the
+application.
+
+Version 6 (mobile)
+^^^^^^^^^^^^^^^^^^
+
+This adds two new search locations: *widget* and *activity*, corresponding to the search widget and search activity respectively.
+
+Version 2
+^^^^^^^^^
+
+This behaves like version 1 except we added all search engines that
+Mozilla has a partner agreement with. Like version 1, we concatenate
+a search engine ID with a search origin.
+
+Another difference with version 2 is we should no longer misattribute
+a search to the *other* bucket if the search engine name is localized.
+
+The set of search engine providers is:
+
+* amazon-co-uk
+* amazon-de
+* amazon-en-GB
+* amazon-france
+* amazon-it
+* amazon-jp
+* amazondotcn
+* amazondotcom
+* amazondotcom-de
+* aol-en-GB
+* aol-web-search
+* bing
+* eBay
+* eBay-de
+* eBay-en-GB
+* eBay-es
+* eBay-fi
+* eBay-france
+* eBay-hu
+* eBay-in
+* eBay-it
+* google
+* google-jp
+* google-ku
+* google-maps-zh-TW
+* mailru
+* mercadolibre-ar
+* mercadolibre-cl
+* mercadolibre-mx
+* seznam-cz
+* twitter
+* twitter-de
+* twitter-ja
+* yahoo
+* yahoo-NO
+* yahoo-answer-zh-TW
+* yahoo-ar
+* yahoo-bid-zh-TW
+* yahoo-br
+* yahoo-ch
+* yahoo-cl
+* yahoo-de
+* yahoo-en-GB
+* yahoo-es
+* yahoo-fi
+* yahoo-france
+* yahoo-fy-NL
+* yahoo-id
+* yahoo-in
+* yahoo-it
+* yahoo-jp
+* yahoo-jp-auctions
+* yahoo-mx
+* yahoo-sv-SE
+* yahoo-zh-TW
+* yandex
+* yandex-ru
+* yandex-slovari
+* yandex-tr
+* yandex.by
+* yandex.ru-be
+
+And of course, *other*.
+
+The sources for searches remain:
+
+* abouthome
+* contextmenu
+* searchbar
+* urlbar
+
+The measurement will only be populated with providers and sources that
+occurred that day.
+
+If a user switches locales, searches from default providers on the older
+locale will still be supported. However, if that same search engine is
+added by the user to the new build and is *not* a default search engine
+provider, its searches will be attributed to the *other* bucket.
+
+Version 1
+^^^^^^^^^
+
+We record counts of performed searches grouped by search engine and search
+origin. Only search engines with which Mozilla has a business relationship
+are explicitly counted. All other search engines are grouped into an
+*other* bucket.
+
+The following search engines are explicitly counted:
+
+* Amazon.com
+* Bing
+* Google
+* Yahoo
+* Other
+
+The following search origins are distinguished:
+
+about:home
+ Searches initiated from the search text box on about:home.
+context menu
+ Searches initiated from the context menu (highlight text, right click,
+ and select "search for...")
+search bar
+ Searches initiated from the search bar (the text field next to the
+ Awesomebar)
+url bar
+ Searches initiated from the awesomebar/url bar.
+
+Due to the localization of search engine names, non en-US locales may wrongly
+attribute searches to the *other* bucket. This is fixed in version 2.
+
+Example
+^^^^^^^
+
+::
+
+ "org.mozilla.searches.counts": {
+ "_v": 1,
+ "google.searchbar": 3,
+ "google.urlbar": 7
+ },
+
+org.mozilla.searches.engines
+----------------------------
+
+This measurement contains information about search engines.
+
+Version 1
+^^^^^^^^^
+
+This version debuted with Firefox 31 on desktop. It contains the
+following properties:
+
+default
+ Daily string identifier or name of the default search engine provider.
+
+ This field will only be collected if Telemetry is enabled. If
+ Telemetry is enabled and then later disabled, this field may
+ disappear from future days in the payload.
+
+ The special value ``NONE`` could occur if there is no default search
+ engine.
+
+ The special value ``UNDEFINED`` could occur if a default search
+ engine exists but its identifier could not be determined.
+
+ This field's contents are
+ ``Services.search.defaultEngine.identifier`` (if defined) or
+ ``"other-"`` + ``Services.search.defaultEngine.name`` if not.
+ In other words, search engines without an ``.identifier``
+ are prefixed with ``other-``.
+
+Version 2
+^^^^^^^^^
+
+Starting with Firefox 40, there is an additional optional value:
+
+cohort
+ Daily cohort string identifier, recorded if the user is part of
+ search defaults A/B testing.
+
+org.mozilla.sync.sync
+---------------------
+
+This daily measurement contains information about the Sync service.
+
+Values should be recorded for every day FHR measurements occurred.
+
+Version 1
+^^^^^^^^^
+
+This version debuted with Firefox 30 on desktop. It contains the following
+properties:
+
+enabled
+ Daily numeric indicating whether Sync is configured and enabled. 1 if so,
+ 0 otherwise.
+
+preferredProtocol
+ String version of the maximum Sync protocol version the client supports.
+ This will be ``1.1`` for for legacy Sync and ``1.5`` for clients that
+ speak the Firefox Accounts protocol.
+
+actualProtocol
+ The actual Sync protocol version the client is configured to use.
+
+ This will be ``1.1`` if the client is configured with the legacy Sync
+ service or if the client only supports ``1.1``.
+
+ It will be ``1.5`` if the client supports ``1.5`` and either a) the
+ client is not configured b) the client is using Firefox Accounts Sync.
+
+syncStart
+ Count of sync operations performed.
+
+syncSuccess
+ Count of sync operations that completed successfully.
+
+syncError
+ Count of sync operations that did not complete successfully.
+
+ This is a measure of overall sync success. This does *not* reflect
+ recoverable errors (such as record conflict) that can occur during
+ sync. This is thus a rough proxy of whether the sync service is
+ operating without error.
+
+org.mozilla.sync.devices
+------------------------
+
+This daily measurement contains information about the device type composition
+for the configured Sync account.
+
+Version 1
+^^^^^^^^^
+
+Version 1 was introduced with Firefox 30.
+
+Field names are dynamic according to the client-reported device types from
+Sync records. All fields are daily last seen integer values corresponding to
+the number of devices of that type.
+
+Common values include:
+
+desktop
+ Corresponds to a Firefox desktop client.
+
+mobile
+ Corresponds to a Fennec client.
+
+org.mozilla.sync.migration
+--------------------------
+
+This daily measurement contains information about sync migration (that is, the
+semi-automated process of migrating a legacy sync account to an FxA account.)
+
+Measurements will start being recorded after a migration is offered by the
+sync server and stop after migration is complete or the user elects to "unlink"
+their sync account. In other words, it is expected that users with Sync setup
+for FxA or with sync unconfigured will not collect data, and that for users
+where data is collected, the collection will only be for a relatively short
+period.
+
+Version 1
+^^^^^^^^^
+
+Version 1 was introduced with Firefox 37 and includes the following properties:
+
+state
+ Corresponds to either a STATE_USER_* string or a STATE_INTERNAL_* string in
+ FxaMigration.jsm. This reflects a state where we are waiting for the user,
+ or waiting for some internal process to complete on the way to completing
+ the migration.
+
+declined
+ Corresponds to the number of times the user closed the migration infobar.
+
+unlinked
+ Set if the user declined to migrate and instead "unlinked" Sync from the
+ browser.
+
+accepted
+ Corresponds to the number of times the user explicitly elected to start or
+ continue the migration - it counts how often the user clicked on any UI
+ created specifically for migration. The "ideal" UX for migration would see
+ this at exactly 1, some known edge-cases (eg, browser restart required to
+ finish) could expect this to be 2, and anything more means we are doing
+ something wrong.
+
+org.mozilla.sysinfo.sysinfo
+---------------------------
+
+This measurement contains basic information about the system the application
+is running on.
+
+Version 2
+^^^^^^^^^
+
+This version debuted with Firefox 29 on desktop.
+
+A single property was introduced.
+
+isWow64
+ If present, this property indicates whether the machine supports WoW64.
+ This property can be used to identify whether the host machine is 64-bit.
+
+ This property is only present on Windows machines. It is the preferred way
+ to identify 32- vs 64-bit support in that environment.
+
+Version 1
+^^^^^^^^^
+
+The following properties may be available:
+
+cpuCount
+ Integer number of CPUs/cores in the machine.
+memoryMB
+ Integer megabytes of memory in the machine.
+manufacturer
+ The manufacturer of the device.
+device
+ The name of the device (like model number).
+hardware
+ Unknown.
+name
+ OS name.
+version
+ OS version.
+architecture
+ OS architecture that the application is built for. This is not the
+ actual system architecture.
+
+Example
+^^^^^^^
+
+::
+
+ "org.mozilla.sysinfo.sysinfo": {
+ "_v": 1,
+ "cpuCount": 8,
+ "memoryMB": 8192,
+ "architecture": "x86-64",
+ "name": "Darwin",
+ "version": "12.2.0"
+ }
+
+
+org.mozilla.translation.translation
+-----------------------------------
+
+This daily measurement contains information about the usage of the translation
+feature. It is a special telemetry measurement which will only be recorded in
+FHR if telemetry is enabled.
+
+Version 1
+^^^^^^^^^
+
+Daily counts are reported in the following properties:
+
+translationOpportunityCount
+ Integer count of the number of opportunities there were to translate a page.
+missedTranslationOpportunityCount
+ Integer count of the number of missed opportunities there were to translate a page.
+ A missed opportunity is when the page language is not supported by the translation
+ provider.
+pageTranslatedCount
+ Integer count of the number of pages translated.
+charactersTranslatedCount
+ Integer count of the number of characters translated.
+detectedLanguageChangedBefore
+ Integer count of the number of times the user manually adjusted the detected
+ language before translating.
+detectedLanguageChangedAfter
+ Integer count of the number of times the user manually adjusted the detected
+ language after having first translated the page.
+targetLanguageChanged
+ Integer count of the number of times the user manually adjusted the target
+ language.
+deniedTranslationOffer
+ Integer count of the number of times the user opted-out offered
+ page translation, either by the Not Now button or by the notification's
+ close button in the "offer" state.
+autoRejectedTranlationOffer
+ Integer count of the number of times the user is not offered page
+ translation because they had previously clicked "Never translate this
+ language" or "Never translate this site".
+showOriginalContent
+ Integer count of the number of times the user activated the Show Original
+ command.
+
+Additional daily counts broken down by language are reported in the following
+properties:
+
+translationOpportunityCountsByLanguage
+ A mapping from language to count of opportunities to translate that
+ language.
+missedTranslationOpportunityCountsByLanguage
+ A mapping from language to count of missed opportunities to translate that
+ language.
+pageTranslatedCountsByLanguage
+ A mapping from language to the counts of pages translated from that
+ language. Each language entry will be an object containing a "total" member
+ along with individual counts for each language translated to.
+
+Other properties:
+
+detectLanguageEnabled
+ Whether automatic language detection is enabled. This is an integer, 0 or 1.
+showTranslationUI
+ Whether the translation feature UI will be shown. This is an integer, 0 or 1.
+
+Example
+^^^^^^^
+
+::
+
+ "org.mozilla.translation.translation": {
+ "_v": 1,
+ "detectLanguageEnabled": 1,
+ "showTranslationUI": 1,
+ "translationOpportunityCount": 134,
+ "missedTranslationOpportunityCount": 32,
+ "pageTranslatedCount": 6,
+ "charactersTranslatedCount": "1126",
+ "detectedLanguageChangedBefore": 1,
+ "detectedLanguageChangedAfter": 2,
+ "targetLanguageChanged": 0,
+ "deniedTranslationOffer": 3,
+ "autoRejectedTranlationOffer": 1,
+ "showOriginalContent": 2,
+ "translationOpportunityCountsByLanguage": {
+ "fr": 100,
+ "es": 34
+ },
+ "missedTranslationOpportunityCountsByLanguage": {
+ "it": 20,
+ "nl": 10,
+ "fi": 2
+ },
+ "pageTranslatedCountsByLanguage": {
+ "fr": {
+ "total": 6,
+ "es": 5,
+ "en": 1
+ }
+ }
+ }
+
+
+org.mozilla.experiments.info
+----------------------------------
+
+Daily measurement reporting information about the Telemetry Experiments service.
+
+Version 1
+^^^^^^^^^
+
+Property:
+
+lastActive
+ ID of the final Telemetry Experiment that is active on a given day, if any.
+
+
+Version 2
+^^^^^^^^^
+
+Adds an additional optional property:
+
+lastActiveBranch
+ If the experiment uses branches, the branch identifier string.
+
+Example
+^^^^^^^
+
+::
+
+ "org.mozilla.experiments.info": {
+ "_v": 2,
+ "lastActive": "some.experiment.id",
+ "lastActiveBranch": "control"
+ }
+
+org.mozilla.uitour.treatment
+----------------------------
+
+Daily measurement reporting information about treatment tagging done
+by the UITour module.
+
+Version 1
+^^^^^^^^^
+
+Daily text values in the following properties:
+
+<tag>:
+ Array of discrete strings corresponding to calls for setTreatmentTag(tag, value).
+
+Example
+^^^^^^^
+
+::
+
+ "org.mozilla.uitour.treatment": {
+ "_v": 1,
+ "treatment": [
+ "optin",
+ "optin-DNT"
+ ],
+ "another-tag": [
+ "foobar-value"
+ ]
+ }
+
+org.mozilla.passwordmgr.passwordmgr
+-----------------------------------
+
+Daily measurement reporting information about the Password Manager
+
+Version 1
+^^^^^^^^^
+
+Property:
+
+numSavedPasswords
+ number of passwords saved in the Password Manager
+
+enabled
+ Whether or not the user has disabled the Password Manager in preferences
+
+Example
+^^^^^^^
+
+::
+
+ "org.mozilla.passwordmgr.passwordmgr": {
+ "_v": 1,
+ "numSavedPasswords": 5,
+ "enabled": 0,
+ }
+
+Version 2
+^^^^^^^^^
+
+More detailed measurements of login forms & their behavior
+
+numNewSavedPasswordsInSession
+ Number of passwords saved to the password manager this session.
+
+numSuccessfulFills
+ Number of times the password manager filled in password fields for user this session.
+
+numTotalLoginsEncountered
+ Number of times a login form was encountered by the user in the session.
+
+Example
+^^^^^^^
+
+::
+
+ "org.mozilla.passwordmgr.passwordmgr": {
+ "_v": 2,
+ "numSavedPasswords": 32,
+ "enabled": 1,
+ "numNewSavedPasswords": 5,
+ "numSuccessfulFills": 11,
+ "numTotalLoginsEncountered": 23,
+ }
diff --git a/toolkit/components/telemetry/docs/obsolete/fhr/identifiers.rst b/toolkit/components/telemetry/docs/obsolete/fhr/identifiers.rst
new file mode 100644
index 0000000000..82ad0e49e6
--- /dev/null
+++ b/toolkit/components/telemetry/docs/obsolete/fhr/identifiers.rst
@@ -0,0 +1,83 @@
+.. _healthreport_identifiers:
+
+===========
+Identifiers
+===========
+
+Firefox Health Report records some identifiers to keep track of clients
+and uploaded documents.
+
+Identifier Types
+================
+
+Document/Upload IDs
+-------------------
+
+A random UUID called the *Document ID* or *Upload ID* is generated when the FHR
+client creates or uploads a new document.
+
+When clients generate a new *Document ID*, they persist this ID to disk
+**before** the upload attempt.
+
+As part of the upload, the client sends all old *Document IDs* to the server
+and asks the server to delete them. In well-behaving clients, the server
+has a single record for each client with a randomly-changing *Document ID*.
+
+Client IDs
+----------
+
+A *Client ID* is an identifier that **attempts** to uniquely identify an
+individual FHR client. Please note the emphasis on *attempts* in that last
+sentence: *Client IDs* do not guarantee uniqueness.
+
+The *Client ID* is generated when the client first runs or as needed.
+
+The *Client ID* is transferred to the server as part of every upload. The
+server is thus able to affiliate multiple document uploads with a single
+*Client ID*.
+
+Client ID Versions
+^^^^^^^^^^^^^^^^^^
+
+The semantics for how a *Client ID* is generated are versioned.
+
+Version 1
+ The *Client ID* is a randomly-generated UUID.
+
+History of Identifiers
+======================
+
+In the beginning, there were just *Document IDs*. The thinking was clients
+would clean up after themselves and leave at most 1 active document on the
+server.
+
+Unfortunately, this did not work out. Using brute force analysis to
+deduplicate records on the server, a number of interesting patterns emerged.
+
+Orphaning
+ Clients would upload a new payload while not deleting the old payload.
+
+Divergent records
+ Records would share data up to a certain date and then the data would
+ almost completely diverge. This appears to be indicative of profile
+ copying.
+
+Rollback
+ Records would share data up to a certain date. Each record in this set
+ would contain data for a day or two but no extra data. This could be
+ explained by filesystem rollback on the client.
+
+A significant percentage of the records on the server belonged to
+misbehaving clients. Identifying these records was extremely resource
+intensive and error-prone. These records were undermining the ability
+to use Firefox Health Report data.
+
+Thus, the *Client ID* was born. The intent of the *Client ID* was to
+uniquely identify clients so the extreme effort required and the
+questionable reliability of deduplicating server data would become
+problems of the past.
+
+The *Client ID* was originally a randomly-generated UUID (version 1). This
+allowed detection of orphaning and rollback. However, these version 1
+*Client IDs* were still susceptible to use on multiple profiles and
+machines if the profile was copied.
diff --git a/toolkit/components/telemetry/docs/obsolete/fhr/index.rst b/toolkit/components/telemetry/docs/obsolete/fhr/index.rst
new file mode 100644
index 0000000000..d114a02814
--- /dev/null
+++ b/toolkit/components/telemetry/docs/obsolete/fhr/index.rst
@@ -0,0 +1,34 @@
+================================
+Firefox Health Report (Obsolete)
+================================
+
+**Firefox Health Report (FHR) is obsolete and no longer ships with Firefox.
+This documentation will live here for a few more cycles.**
+
+Firefox Health Report is a background service that collects application
+metrics and periodically submits them to a central server. The core
+parts of the service are implemented in this directory. However, the
+actual XPCOM service is implemented in the
+``data_reporting_service``.
+
+The core types can actually be instantiated multiple times and used to
+power multiple data submission services within a single Gecko
+application. In other words, everything in this directory is effectively
+a reusable library. However, the terminology and some of the features
+are very specific to what the Firefox Health Report feature requires.
+
+.. toctree::
+ :maxdepth: 1
+
+ architecture
+ dataformat
+ identifiers
+
+Legal and Privacy Concerns
+==========================
+
+Because Firefox Health Report collects and submits data to remote
+servers and is an opt-out feature, there are legal and privacy
+concerns over what data may be collected and submitted. **Additions or
+changes to submitted data should be signed off by responsible
+parties.**
diff --git a/toolkit/components/telemetry/docs/obsolete/geckoview-streaming.rst b/toolkit/components/telemetry/docs/obsolete/geckoview-streaming.rst
new file mode 100644
index 0000000000..10dfcdf860
--- /dev/null
+++ b/toolkit/components/telemetry/docs/obsolete/geckoview-streaming.rst
@@ -0,0 +1,26 @@
+GeckoView Streaming API
+=======================
+
+As an alternative to the normal mode where Firefox Desktop records and sends data,
+Telemetry can instead route Histogram samples and Scalar values out of Gecko to a Telemetry Delegate.
+
+To do this, ``toolkit.telemetry.geckoview.streaming`` must be set to true,
+and Gecko must have been built with ``MOZ_WIDGET_ANDROID`` defined.
+
+See :doc:`this guide <../start/report-gecko-telemetry-in-glean>`
+for how to collect data in this mode.
+
+Details
+=======
+
+Samples accumulated on Histograms and values set
+(``ScalarAdd`` and ``ScalarSetMaximum`` operations are not supported)
+on Scalars that have ``products`` lists that include ``geckoview_streaming``
+will be redirected to a small batching service in
+``toolkit/components/telemetry/geckoview/streaming``.
+The batching service
+(essentially just tables of histogram/scalar names to lists of samples/values)
+will hold on to these lists of samples/values paired to the histogram/scalar names for a length of time
+(``toolkit.telemetry.geckoview.batchDurationMS`` (default 5000))
+after which the next accumulation or ``ScalarSet`` will trigger the whole batch
+(all lists) to be passed over to the ``StreamingTelemetryDelegate``.
diff --git a/toolkit/components/telemetry/docs/obsolete/hybrid-content.rst b/toolkit/components/telemetry/docs/obsolete/hybrid-content.rst
new file mode 100644
index 0000000000..9eb0b00341
--- /dev/null
+++ b/toolkit/components/telemetry/docs/obsolete/hybrid-content.rst
@@ -0,0 +1,374 @@
+===================================
+Hybrid Content Telemetry (obsolete)
+===================================
+
+Hybrid content is web content that is loaded as part of Firefox, appears as part of
+Firefox to the user and is primarily intended to be used in Firefox. This can be
+either a page that ships with Firefox or that can be loaded dynamically from our hosted
+services. Hybrid content telemetry allows Mozilla pages to check whether data
+collection is enabled and to submit Telemetry data.
+
+.. important::
+
+ Every new or changed data collection in Firefox (including hybrid content) needs a `data collection review <https://wiki.mozilla.org/Firefox/Data_Collection>`__ from a Data Steward.
+
+The recorded data will be sent to Mozilla servers by Firefox, if the collection is enabled, with the :doc:`main-ping <../data/main-ping>`.
+
+Adding content data collection
+==============================
+Telemetry can be sent from web content by:
+
+1. granting the web content's host privileges in the Firefox codebase;
+2. including the ``HybridContentTelemetry-lib.js`` file in the page;
+3. registering the probes after the library is loaded;
+4. using the API to send Telemetry.
+
+Granting the privileges
+-----------------------
+For security/privacy reasons `Mozilla.ContentTelemetry` will only work on a list of allowed secure origins.
+The list of allowed origins can be found in
+`browser/app/permissions <https://searchfox.org/mozilla-central/source/browser/app/permissions>`_ .
+A host needs to be given the ``hc_telemetry`` permission in order to be allowed to use the API.
+
+Example:
+
+::
+
+ origin hc_telemetry 1 https://discovery.addons.mozilla.org
+
+Adding an entry to the ``permissions`` file requires riding the trains. If "go-faster" content requires
+granting permissions to a Mozilla page, it can do so by using the `permission manager <https://developer.mozilla.org/en-US/docs/Mozilla/Tech/XPCOM/Reference/Interface/nsIPermissionManager>`_
+
+.. code-block:: js
+
+ function addonInit() {
+ // The following code must be called before attempting to load a page that uses
+ // hybrid content telemetry on https://example.mozilla.org.
+ let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin("https://example.mozilla.org");
+ Services.perms.addFromPrincipal(principal, "hc_telemetry", Services.perms.ALLOW_ACTION);
+ }
+
+ function addonCleanup() {
+ // The permission must be removed if no longer needed (e.g. the add-on is shut down).
+ let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin("https://example.mozilla.org");
+ Services.perms.removeFromPrincipal(principal, "hc_telemetry");
+ }
+
+.. important::
+
+ Granted permissions do not disappear when a "go-faster" add-on is uninstalled but are cleared when the browser is closed. If permissions need to be cleaned without closing the browser, it must be done manually. Moreover, permissions are keyed by origin: ``http://mozilla.com`` and ``https://mozilla.com`` are different things.
+
+Including the library
+---------------------
+To use hybrid content telemetry the related content JS library needs to be included in the page. We have different integration options:
+
+* Add ``mozilla-hybrid-content-telemetry`` as a dependency to the project and require it in the code.
+* Load it directly from the `external unpkg CDN <https://unpkg.com/mozilla-hybrid-content-telemetry/HybridContentTelemetry-lib.js>`_.
+* Manually fetch the latest version from the `main repository <https://hg.mozilla.org/mozilla-central/file/tip/toolkit/components/telemetry/hybrid-content/HybridContentTelemetry-lib.js>`_ and add it to the page repository. Then this file can be deployed along with the page.
+
+Example (manual inclusion):
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: html
+
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <!-- Other head stuff -->
+ <script type="application/javascript" src="HybridContentTelemetry-lib.js"></script>
+ </head>
+ <body> <!-- Other body stuff --> </body>
+ </html>
+
+Example (NPM dependency):
+~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Add the dependency to your project:
+
+.. code-block:: shell
+
+ npm install --save mozilla-hybrid-content-telemetry@1.0.0
+
+In your app load the module and use the :ref:`API <the-api>`:
+
+.. code-block:: js
+
+ const ContentTelemetry = require("mozilla-hybrid-content-telemetry");
+
+ ContentTelemetry.registerEvents("page.interaction", {
+ "click": {
+ methods: ["click"],
+ objects: ["red_button", "blue_button"],
+ }
+ });
+
+ // Now events can be recorded.
+ ContentTelemetry.recordEvent("page.interaction", "click", "red_button");
+
+.. note::
+
+ The following examples assume the manual inclusion of the JS library.
+
+Registering the probes
+----------------------
+Probe registration can happen at any time after the library is loaded in the page, but registering early enough ensures that the definition is available once a recording attempt is made.
+
+Example:
+
+.. code-block:: html
+
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <!-- Other head stuff -->
+ <script type="application/javascript">
+ window.onload = function() {
+ if (!Mozilla || !Mozilla.ContentTelemetry) {
+ // .. uh-oh, was library loaded? Report the error.
+ return;
+ }
+ // Register the probe.
+ Mozilla.ContentTelemetry.registerEvents("page.interaction", {
+ "click": {
+ methods: ["click"],
+ objects: ["red_button", "blue_button"],
+ }
+ });
+ };
+ </script>
+ </head>
+ <body> <!-- Other body stuff --> </body>
+ </html>
+
+Recording the data
+------------------
+Data recording can happen at any time after a probe has been registered. The data will be recorded and sent by Firefox if permitted by the Telemetry :doc:`preferences <../internals/preferences>`.
+
+Example:
+
+.. code-block:: html
+
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <!-- Other head stuff -->
+ <script type="application/javascript">
+ function triggerEvent() {
+ if (!Mozilla || !Mozilla.ContentTelemetry) {
+ // .. uh-oh, was library loaded? Report the error.
+ return;
+ }
+ Mozilla.ContentTelemetry.recordEvent("page.interaction", "click", "red_button");
+ };
+ </script>
+ </head>
+ <body>
+ <!-- Other body stuff -->
+ <div id="content">
+ <button id='event-recording' onclick="triggerEvent();">
+ Trigger Recording
+ </button>
+ </div>
+ </body>
+ </html>
+
+Checking if upload is enabled
+-----------------------------
+Mozilla pages can check if data upload is enabled, as reported by Telemetry :doc:`preferences <../internals/preferences>`. This is useful for pages which are not using Telemetry to collect data, but
+need to comply to our data policy for the collection.
+
+Example:
+
+.. code-block:: html
+
+ <!DOCTYPE html>
+ <html>
+ <head>
+ <!-- Other head stuff -->
+ <script type="application/javascript">
+ function recordData() {
+ if (!Mozilla || !Mozilla.ContentTelemetry) {
+ // .. uh-oh, was library loaded? Report the error.
+ return;
+ }
+
+ if (!Mozilla.ContentTelemetry.canUpload()) {
+ // User has opted-out of Telemetry. No collection must take place.
+ return;
+ }
+
+ // ... perform the collection without Telemetry below this point.
+ };
+ </script>
+ </head>
+ <body>
+ <!-- Other body stuff -->
+ <div id="content">
+ <button id='event-recording' onclick="recordData();">
+ Trigger Recording
+ </button>
+ </div>
+ </body>
+ </html>
+
+
+.. _the-api:
+
+The API
+=======
+The hybrid content API is available to the web content through the inclusion of the `HybridContentTelemetry-lib.js <https://searchfox.org/mozilla-central/source/toolkit/components/telemetry/hybrid-content/HybridContentTelemetry-lib.js>`_ library.
+
+The initial implementation of the API allows the registration and the recording of events.
+
+JS API
+------
+Authorized content can use the following functions:
+
+.. code-block:: js
+
+ Mozilla.ContentTelemetry.canUpload();
+ Mozilla.ContentTelemetry.initPromise();
+ Mozilla.ContentTelemetry.registerEvents(category, eventData);
+ Mozilla.ContentTelemetry.recordEvent(category, method, object, value, extra);
+
+These functions will not throw. If an unsupported operation is performed (e.g. recording an unknown event) an error will be logged to the browser console.
+
+.. note::
+
+ Data collected using this API will always respect the user Telemetry preferences: if a user has chosen to not send Telemetry data to Mozilla servers, Telemetry from hybrid content pages will not be sent either.
+ Like other Telemetry data, it will still be recorded locally and available through ``about:telemetry``.
+
+``Mozilla.ContentTelemetry.canUpload()``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: js
+
+ Mozilla.ContentTelemetry.canUpload();
+
+This function returns true if the browser is allowed to send collected data to Mozilla servers (i.e. ``datareporting.healthreport.uploadEnabled`` is ``true``), false otherwise. See :doc:`preferences <../internals/preferences>`.
+
+.. note::
+
+ The page should use this function to check if it is allowed to collect data. This is only needed in case the Telemetry system is not be being used for collection. If Telemetry is used, then this is taken care of internally by the Telemetry API. The page should not cache the returned value: users can opt in or out from the Data Collection at any time and so the returned value may change.
+
+Example:
+
+.. code-block:: js
+
+ if (Mozilla.ContentTelemetry.canUpload()) {
+ // ... perform the data collection here using another measurement system.
+ }
+
+``Mozilla.ContentTelemetry.initPromise()``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: js
+
+ Mozilla.ContentTelemetry.initPromise();
+
+This function returns a Promise that gets resolved as soon as Hybrid Content Telemetry is correctly initialized and the value from ``canUpload`` can be reliably read. The promise will reject if Hybrid Content Telemetry is disabled or the host doesn't have enough privileges to use the API.
+
+``Mozilla.ContentTelemetry.registerEvents()``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: js
+
+ Mozilla.ContentTelemetry.registerEvents(category, eventData);
+
+Register new dynamic events from the content. This accepts the same parameters and is subject to the same limitation as ``Services.telemetry.registerEvents()``. See the `events` documentation for the definitive reference.
+
+.. note::
+
+ Make sure to call this before recording events, as soon as the library is loaded (e.g. `window load event <https://developer.mozilla.org/en-US/docs/Web/Events/load>`_). This will make sure that the definition will be ready when recording.
+
+The data recorded into events registered with this function will end up in the ``dynamic`` process section of the main ping.
+
+Example:
+
+.. code-block:: js
+
+ Mozilla.ContentTelemetry.registerEvents("page.interaction", {
+ "click": {
+ methods: ["click"],
+ objects: ["red_button", "blue_button"],
+ }
+ });
+ // Now events can be recorded.
+ Mozilla.ContentTelemetry.recordEvent("page.interaction", "click", "red_button");
+
+``Mozilla.ContentTelemetry.recordEvent()``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+.. code-block:: js
+
+ Mozilla.ContentTelemetry.recordEvent(category, method, object, value, extra);
+
+Record a registered event. This accepts the same parameters and is subject to the same limitation as ``Services.telemetry.recordEvent()``. See the `events` documentation for the definitive reference.
+
+Example:
+
+.. code-block:: js
+
+ Mozilla.ContentTelemetry.recordEvent("ui", "click", "reload-btn");
+ // event: [543345, "ui", "click", "reload-btn"]
+ Mozilla.ContentTelemetry.recordEvent("ui", "search", "search-bar", "google");
+ // event: [89438, "ui", "search", "search-bar", "google"]
+ Mozilla.ContentTelemetry.recordEvent("ui", "completion", "search-bar", "yahoo",
+ {"querylen": "7", "results": "23"});
+ // event: [982134, "ui", "completion", "search-bar", "yahoo",
+ // {"qerylen": "7", "results": "23"}]
+
+Data Review
+===========
+
+Adding the ``hc_telemetry`` permission for a new domain in `browser/app/permissions <https://searchfox.org/mozilla-central/source/browser/app/permissions>`_
+requires `Data Collection Review <https://wiki.mozilla.org/Firefox/Data_Collection>`_ as we are enabling a new method of data collection.
+
+Giving a domain permission to use Hybrid Content Telemetry also gives any Extensions running on this domain permission to use Hybrid Content Telemetry.
+If the domain is already on the `list of restricted domains <https://hg.mozilla.org/integration/mozilla-inbound/file/39e131181d44/modules/libpref/init/all.js#l5120>`_
+(configured by the ``extensions.webextensions.restrictedDomains`` preference), Extensions don't run on this domain and therefore cannot access the Hybrid Content Telemetry API.
+No additional approval is necessary.
+
+If the domain is not on that list, you need additional privacy review. In that case request help from the Telemetry team.
+
+Testing
+=======
+
+In order to test Hybrid Content Telemetry integrations, the permission API can be used to enable certain hosts.
+The ``Services.perms.addFromPrincipal`` API is available in the Browser Console as well as in ``xpcshell`` and ``mochi`` tests with access to the ``Services.*`` APIs.
+
+The respective ``hc_telemetry`` permission needs to be set before any pages on that host load the ``HybridContentTelemetry-lib.js`` file.
+
+Manual testing
+--------------
+
+After starting the browser, open the Browser Console (Tools -> Web Developer -> Browser Console).
+To enable Hybrid Content Telemetry on ``https://example.mozilla.org``, execute this code snippet in the console:
+
+.. code-block:: js
+
+ let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin("https://example.mozilla.org");
+ Services.perms.addFromPrincipal(principal, "hc_telemetry", Services.perms.ALLOW_ACTION);
+
+Afterwards load the page on ``https://example.mozilla.org`` and it will be able to record Telemetry data.
+
+.. note::
+
+ Manual testing requires a host that handles HTTPS connections, as this kind of collection is only allowed on secure hosts. To allow for local testing, a local proxy capable of handling HTTPS connection is required.
+
+Automated testing
+-----------------
+
+In test frameworks with privileged access the permission can be set in the ``head.js`` or during test setup.
+Add the code snippet in your ``head.js`` to enable Hybrid Content ContentTelemetry on ``https://example.mozilla.org``:
+
+.. code-block:: js
+
+ let principal = Services.scriptSecurityManager.createContentPrincipalFromOrigin("https://example.mozilla.org");
+ Services.perms.addFromPrincipal(principal, "hc_telemetry", Services.perms.ALLOW_ACTION);
+
+Version History
+===============
+
+- Firefox 59: Initial hybrid content telemetry support (`bug 1417473 <https://bugzilla.mozilla.org/show_bug.cgi?id=1417473>`_).
+- Firefox 71: Hybrid Content Telemetry removed (`bug 1520491 <https://bugzilla.mozilla.org/show_bug.cgi?id=1520491>`_).
diff --git a/toolkit/components/telemetry/docs/obsolete/index.rst b/toolkit/components/telemetry/docs/obsolete/index.rst
new file mode 100644
index 0000000000..fc2acf7615
--- /dev/null
+++ b/toolkit/components/telemetry/docs/obsolete/index.rst
@@ -0,0 +1,15 @@
+.. _telemetry_obsolete:
+
+======================
+Obsolete Documentation
+======================
+
+.. toctree::
+ :glob:
+ :maxdepth: 5
+ :titlesonly:
+
+ uitelemetry/index
+ fhr/index
+ hybrid-content
+ *
diff --git a/toolkit/components/telemetry/docs/obsolete/optout-ping.rst b/toolkit/components/telemetry/docs/obsolete/optout-ping.rst
new file mode 100644
index 0000000000..910bb41220
--- /dev/null
+++ b/toolkit/components/telemetry/docs/obsolete/optout-ping.rst
@@ -0,0 +1,33 @@
+
+"optout" ping (obsolete)
+========================
+
+This ping is generated when a user turns off FHR upload from the Preferences panel, changing the related ``datareporting.healthreport.uploadEnabled`` :doc:`preference <../internals/preferences>`.
+
+This ping contains no client id and no environment data.
+
+Structure:
+
+.. code-block:: js
+
+ {
+ version: 4,
+ type: "optout",
+ ... common ping data
+ payload: { }
+ }
+
+Expected behaviours
+-------------------
+The following is a list of expected behaviours for the ``optout`` ping:
+
+- Sending the "optout" ping is best-effort. Telemetry tries to send the ping once and discards it immediately if sending fails.
+- The ping might be delayed if ping sending is throttled (e.g. around midnight).
+- The ping can be lost if the browser is shutdown before the ping is sent out the "optout" ping is discarded.
+
+Version History
+---------------
+
+- Firefox 63:
+
+ - "optout" ping replaced the "deletion" ping (`bug 1445921 <https://bugzilla.mozilla.org/show_bug.cgi?id=1445921>`_).
diff --git a/toolkit/components/telemetry/docs/obsolete/uitelemetry/index.rst b/toolkit/components/telemetry/docs/obsolete/uitelemetry/index.rst
new file mode 100644
index 0000000000..89447cc9eb
--- /dev/null
+++ b/toolkit/components/telemetry/docs/obsolete/uitelemetry/index.rst
@@ -0,0 +1,146 @@
+.. _uitelemetry:
+
+==================================
+UITelemetry data format (obsolete)
+==================================
+
+.. note::
+
+ ``UITelemetry`` is deprecated. As of Firefox 61, ``UITelemetry`` is no longer reported.
+
+UI Telemetry sends its data as a JSON blob. This document describes the different parts
+of the JSON blob.
+
+``toolbars``
+============
+
+This tracks the state of the user's UI customizations. It has the following properties:
+
+- ``sizemode`` - string indicating whether the window is in maximized, normal (restored) or
+ fullscreen mode;
+- ``bookmarksBarEnabled`` - boolean indicating whether the bookmarks bar is visible;
+- ``menuBarEnabled`` - boolean indicating whether the menu bar is visible (always false on OS X);
+- ``titleBarEnabled`` - boolean indicating whether the (real) titlebar is visible (rather than
+ having tabs in the titlebar);
+- ``defaultKept`` - list of strings identifying toolbar buttons and items that are still in their
+ default position. Only the IDs of builtin widgets are sent (ie not add-on widgets);
+- ``defaultMoved`` - list of strings identifying toolbar buttons and items that are no longer in
+ their default position, but have not been removed to the palette. Only the IDs of builtin widgets
+ are sent (ie not add-on widgets);
+- ``nondefaultAdded`` - list of strings identifying toolbar buttons and items that have been added
+ from the palette. Only the IDs of builtin widgets are sent (ie not add-on widgets);
+- ``defaultRemoved`` - list of strings identifying toolbar buttons and items that are in the
+ palette that are elsewhere by default. Only the IDs of builtin widgets are sent
+ (ie not add-on widgets);
+- ``addonToolbars`` - the number of non-default toolbars that are customizable. 1 by default
+ because it counts the add-on bar shim;
+- ``visibleTabs`` - array of the number of visible tabs per window;
+- ``hiddenTabs`` - array of the number of hidden tabs per window (ie tabs in panorama groups which
+ are not the current group);
+- ``countableEvents`` - please refer to the next section.
+- ``durations`` - an object mapping descriptions to duration records, which records the amount of
+ time a user spent doing something. Currently only has one property:
+
+ - ``customization`` - how long a user spent customizing the browser. This is an array of
+ objects, where each object has a ``duration`` property indicating the time in milliseconds,
+ and a ``bucket`` property indicating a bucket in which the duration info falls.
+
+
+.. _UITelemetry_countableEvents:
+
+``countableEvents``
+===================
+
+Countable events are stored under the ``toolbars`` section. They count the number of times certain
+events happen. No timing or other correlating information is stored - purely the number of times
+things happen.
+
+``countableEvents`` contains a list of buckets as its properties. A bucket represents the state the browser was in when these events occurred, such as currently running an interactive tour. There are 3 types of buckets:
+
+- ``__DEFAULT__`` - No bucket, for times when the browser is not in any special state.
+- ``bucket_<NAME>`` - Normal buckets, for when the browser is in a special state. The ``<NAME>`` in the bucket ID is the name associated with the bucket and may be further broken down into parts by the ``|`` character.
+- ``bucket_<NAME>|<INTERVAL>`` - Expiring buckets, which are similar to a countdown timer. The ``<INTERVAL>`` in the bucket ID describes the time interval the recorded event happened in. The intervals are ``1m`` (one minute), ``3m`` (three minutes), ``10m`` (ten minutes), and ``1h`` (one hour). After one hour, the ``__DEFAULT__`` bucket is automatically used again.
+
+Each bucket is an object with the following properties:
+
+- ``click-builtin-item`` is an object tracking clicks on builtin customizable toolbar items, keyed
+ off the item IDs, with an object for each item with keys ``left``, ``middle`` and ``right`` each
+ storing a number indicating how often the respective type of click has happened.
+- ``click-menu-button`` is the same, except the item ID is always 'button'.
+- ``click-bookmarks-bar`` is the same, with the item IDs being replaced by either ``container`` for
+ clicks on bookmark or livemark folders, and ``item`` for individual bookmarks.
+- ``click-menubar`` is similar, with the item IDs being replaced by one of ``menu``, ``menuitem``
+ or ``other``, depending on the kind of item clicked. Note that this is not tracked on OS X, where
+ we can't listen for these events because of the global menubar.
+- ``click-bookmarks-menu-button`` is also similar, with the item IDs being replaced by:
+
+ - ``menu`` for clicks on the 'menu' part of the item;
+ - ``add`` for clicks that add a bookmark;
+ - ``edit`` for clicks that open the panel to edit an existing bookmark;
+ - ``in-panel`` for clicks when the button is in the menu panel, and clicking it does none of the
+ above;
+- ``customize`` tracks different types of customization events without the ``left``, ``middle`` and
+ ``right`` distinctions. The different events are the following, with each storing a count of the
+ number of times they occurred:
+
+ - ``start`` counts the number of times the user starts customizing;
+ - ``add`` counts the number of times an item is added somewhere from the palette;
+ - ``move`` counts the number of times an item is moved somewhere else (but not to the palette);
+ - ``remove`` counts the number of times an item is removed to the palette;
+ - ``reset`` counts the number of times the 'restore defaults' button is used;
+- ``search`` is an object tracking searches of various types, keyed off the search
+ location, storing a number indicating how often the respective type of search
+ has happened.
+
+ - There are also two special keys that mean slightly different things.
+
+ - ``urlbar-keyword`` records searches that would have been an invalid-protocol
+ error, but are now keyword searches. They are also counted in the ``urlbar``
+ keyword (along with all the other urlbar searches).
+ - ``selection`` searches records selections of search suggestions. They include
+ the source, the index of the selection, and the kind of selection (mouse or
+ enter key). Selection searches are also counted in their sources.
+
+
+
+``UITour``
+==========
+
+The UITour API provides ways for pages on trusted domains to safely interact with the browser UI and request it to perform actions such as opening menus and showing highlights over the browser chrome - for the purposes of interactive tours. We track some usage of this API via the ``UITour`` object in the UI Telemetry output.
+
+Each page is able to register itself with an identifier, a ``Page ID``. A list of Page IDs that have been seen over the last 8 weeks is available via ``seenPageIDs``.
+
+Page IDs are also used to identify buckets for :ref:`UITelemetry_countableEvents`, in the following circumstances:
+
+- The current tab is a tour page. This will be a normal bucket with the name ``UITour|<PAGEID>``, where ``<PAGEID>`` is the page's registered ID. This will result in bucket IDs such as ``bucket_UITour|australis-tour``.
+- A tour tab is open but another tab is active. This will be an expiring bucket with the name ``UITour|<PAGEID>|inactive``. This will result in bucket IDs such as ``bucket_UITour|australis-tour|inactive|1m``.
+- A tour tab has recently been open but has been closed. This will be an expiring bucket with the name ``UITour|<PAGEID>|closed``. This will result in bucket IDs such as ``bucket_UITour|australis-tour|closed|10m``.
+
+
+
+``contextmenu``
+===============
+
+We track context menu interactions to figure out which ones are most often used and/or how
+effective they are. In the ``contextmenu`` object, we first store things per-bucket. Next, we
+divide the following different context menu situations:
+
+- ``selection`` if there is content on the page that's selected on which the user clicks;
+- ``link`` if the user opened the context menu for a link
+- ``image-link`` if the user opened the context menu on an image or canvas that's a link;
+- ``image`` if the user opened the context menu on an image (that isn't a link);
+- ``canvas`` if the user opened the context menu on a canvas (that isn't a link);
+- ``media`` if the user opened the context menu on an HTML video or audio element;
+- ``input`` if the user opened the context menu on a text input element;
+- ``other`` for all other openings of the content menu;
+
+Each of these objects (if they exist) then gets a "withcustom" and/or a "withoutcustom" property
+for context menus opened with custom page-created items and without them, and each of those
+properties holds an object with IDs corresponding to a count of how often an item with that ID was
+activated in the context menu. Only builtin context menu items are tracked, and besides those items
+there are four special items which get counts:
+
+- ``close-without-interaction`` is incremented when the user closes the context menu without interacting with it;
+- ``custom-page-item`` is incremented when the user clicks an item that was created by the page;
+- ``unknown`` is incremented when an item without an ID was clicked;
+- ``other-item`` is incremented when an add-on-provided menuitem is clicked.
diff --git a/toolkit/components/telemetry/docs/start/adding-a-new-probe.rst b/toolkit/components/telemetry/docs/start/adding-a-new-probe.rst
new file mode 100644
index 0000000000..a5b462d5f9
--- /dev/null
+++ b/toolkit/components/telemetry/docs/start/adding-a-new-probe.rst
@@ -0,0 +1,151 @@
+============================
+Adding a new Telemetry probe
+============================
+
+In Firefox, the Telemetry system collects various measures of Firefox performance, hardware, usage and customizations and submit it to Mozilla. This article provides an overview of what is needed to add any new Telemetry data collection.
+
+.. important::
+
+ Every new data collection in Firefox needs a `data collection review <https://wiki.mozilla.org/Firefox/Data_Collection#Requesting_Approval>`__ from a data collection peer. Just set the feedback? flag for one of the data peers. They try to reply within a business day.
+
+What is your goal?
+==================
+
+We have various :doc:`data collection tools <../collection/index>` available, each serving different needs. Before diving right into technical details, it is best to take a step back and consider what you need to achieve.
+
+Your goal could be to answer product questions like “how many people use feature X?” or “what is the error rate of service Y?”.
+You could also be focused more on answering engineering questions, say “which web features are most used?” or “how is the performance of engine Z?”.
+
+From there, questions you should ask are:
+
+- What is the minimum data that can answer your questions?
+- How many people do you need this data from?
+- Is data from the pre-release channels sufficient?
+
+This also informs the `data collection review <https://wiki.mozilla.org/Firefox/Data_Collection>`__, which requires a plan for how to use the data. Data collection review is required for all new data collection.
+
+Data collection levels
+======================
+
+Most of our data collection falls into one of two levels, *release* and *pre-release*.
+
+**Release data** is recorded by default on all channels, users need to explicitly opt out to disable it. This has `stricter constraints <https://wiki.mozilla.org/Firefox/Data_Collection#Requirements>`_ for what data we can collect. "Most" users submit this data.
+
+**Pre-release data** is not recorded on release, but is collected by default on our pre-release channels (Beta and Nightly), so it can be biased.
+
+These levels cover what is described in the `Firefox privacy notice <https://www.mozilla.org/en-US/privacy/firefox/>`_. For other needs, there might be custom mechanisms that clearly require user opt-in and show what data is collected.
+
+Rich data & aggregate data
+==========================
+
+For the recording and transmission of data, we have various data types available. We can divide these data types into two large groups.
+
+**Aggregate data** is aggregated on the client-side and cheap to send, process and analyze. This could e.g. be a simple count of tab opens or a histogram showing how long it takes to switch between tabs. This should be your default choice and is well supported in our analysis tools.
+
+**Rich data** is used when questions can not be answered from aggregate data. When we send more detailed data we can e.g. see when a specific UI interaction happened and in which context.
+
+As a general rule, you can inform the choice of data types from your goals like this:
+
++------------------------+-----------------+-----------------------+
+| Goals | Collection type | Implementation |
++========================+=================+=======================+
+| On-going monitoring | Aggregate data | Histograms |
+| | | |
+| Health tracking | | Scalars |
+| | | |
+| KPI impact | | Environment data |
++------------------------+-----------------+-----------------------+
+| Detailed user behavior | Rich data | Event Telemetry |
+| | | |
+| Funnel analysis | | Detailed custom pings |
+| | | |
+| Diagnostics | | Logs |
+| | | |
+| | | Crash data |
++------------------------+-----------------+-----------------------+
+
+Aggregate data
+--------------
+
+Most of our data collection happens through :doc:`scalars <../collection/scalars>` and :doc:`histograms <../collection/histograms>`:
+
+- Scalars allow collection of simple values, like counts, booleans and strings.
+- Histograms allow collection of multiple different values, but aggregate them into a number of buckets. Each bucket has a value range and a count of how many values we recorded.
+
+Both scalars & histograms allow recording by keys. This allows for more flexible, two-level data collection.
+
+We also collect :doc:`environment data <../data/environment>`. This consists of mostly scalar values that capture the “working environment” a Firefox session lives in, and includes e.g. data on hardware, OS, add-ons and some settings. Any data that is part of the "working environment", or needs to split :doc:`subsessions <../concepts/sessions>`, should go into it.
+
+Rich data
+---------
+
+Aggregate data can tell you that something happened, but is usually lacking details about what exactly. When more details are needed, we can collect them using other tools that submit less efficient data. This usually means that we can't enable the data collection for all users, for cost and performance concerns.
+
+There are multiple mechanisms to collect rich data:
+
+**Stack collection** helps with e.g. diagnosing hangs. Stack data is recorded into chrome hangs and threadhang stats. To diagnose where rarely used code is called from, you can use stack capturing.
+
+:doc:`Event Telemetry <../collection/events>` provides a way to record both when and what happened. This enables e.g. funnel analysis for usage.
+
+:doc:`Custom pings <../collection/custom-pings>` are used when other existing data collection does not cover your need. Submitting a custom ping enables you to submit your own JSON package that will be delivered to the Telemetry servers. However, this loses you access to existing tooling and makes it harder to join your data with other sources.
+
+Setup & building
+================
+
+Every build of Firefox has Telemetry enabled. Local developer builds with no custom build flags will record all Telemetry data, but not send it out.
+
+When adding any new scalar, histogram or event Firefox needs to be built. Artifact builds are currently not supported, even if code changes are limited to JavaScript.
+
+Usually you don't need to send out data to add new Telemetry. In the rare event you do, you need the following in your *.mozconfig*::
+
+ MOZ_TELEMETRY_REPORTING=1
+ MOZILLA_OFFICIAL=1
+
+Testing
+=======
+
+Local confirmation
+------------------
+
+Your first step should always be to confirm your new data collection locally.
+
+The *about:telemetry* page allows to view any data you submitted to Telemetry in the last 60 days, whether it is in existing pings or in new custom pings. You can choose which pings to display on the top-left.
+
+If you need to confirm when - or if - pings are getting sent, you can run an instance of the `gzipServer <https://github.com/mozilla/gzipServer>`_ locally. It emulates roughly how the official Telemetry servers respond, and saves all received pings to disk for inspection.
+
+Test coverage
+-------------
+
+Any data collection that you need to base decisions on needs to have test coverage. Using JS, you can access the recorded values for your data collection. You can use the following functions:
+
+- for scalars, `getSnapshotForScalars() <https://searchfox.org/mozilla-central/rev/f997bd6bbfc4773e774fdb6cd010142370d186f9/toolkit/components/telemetry/core/nsITelemetry.idl#90-102>`_
+ or `getSnapshotForKeyedScalars() <https://searchfox.org/mozilla-central/rev/f997bd6bbfc4773e774fdb6cd010142370d186f9/toolkit/components/telemetry/core/nsITelemetry.idl#104-116>`_
+- for histograms, `getSnapshotForHistograms() <https://searchfox.org/mozilla-central/rev/f997bd6bbfc4773e774fdb6cd010142370d186f9/toolkit/components/telemetry/core/nsITelemetry.idl#54-74>`_
+ or `getSnapshotForKeyedHistograms() <https://searchfox.org/mozilla-central/rev/f997bd6bbfc4773e774fdb6cd010142370d186f9/toolkit/components/telemetry/core/nsITelemetry.idl#76-88>`_
+
+ * Optionally, histogram objects have a `snapshot() <https://searchfox.org/mozilla-central/rev/f997bd6bbfc4773e774fdb6cd010142370d186f9/toolkit/components/telemetry/core/nsITelemetry.idl#285-287,313-315>`_ method.
+
+- for events, `snapshotEvents() <https://searchfox.org/mozilla-central/rev/f997bd6bbfc4773e774fdb6cd010142370d186f9/toolkit/components/telemetry/core/nsITelemetry.idl#542-558>`_
+
+If you need to test that pings were correctly passed to Telemetry, you can use `TelemetryArchiveTesting <https://searchfox.org/mozilla-central/search?q=TelemetryArchiveTesting&redirect=false>`_.
+
+Validation
+----------
+
+While it's important to confirm that the data collection works on your machine, the Firefox user population is very diverse. Before basing decisions on any new data, it should be validated. This could take various forms.
+
+For *new data collection* using existing Telemetry data types, the transport mechanism is already tested. It is sufficient to validate the incoming values. This could happen through `Redash <https://docs.telemetry.mozilla.org/tools/stmo.html>`_ or through `custom analysis <https://docs.telemetry.mozilla.org/tools/spark.html>`_.
+
+For *new custom pings*, you'll want to check schema validation results, as well as that the contents look valid.
+
+Getting help
+============
+
+You can find all important Telemetry resources listed on `telemetry.mozilla.org <https://telemetry.mozilla.org/>`_.
+
+The Telemetry team is there to help with any problems. You can reach us via:
+
+- Matrix in `#telemetry:mozilla.org <https://chat.mozilla.org/#/room/#telemetry:mozilla.org>`_
+- Slack in `#data-help <https://mozilla.slack.com/messages/data-help/>`_
+- the `fx-data-dev mailing list <https://mail.mozilla.org/listinfo/fx-data-dev>`_
+- flags for `one of the peers <https://wiki.mozilla.org/Modules/Toolkit#Telemetry>`_ on Bugzilla or send us an e-mail
diff --git a/toolkit/components/telemetry/docs/start/index.rst b/toolkit/components/telemetry/docs/start/index.rst
new file mode 100644
index 0000000000..9cc544c9a3
--- /dev/null
+++ b/toolkit/components/telemetry/docs/start/index.rst
@@ -0,0 +1,28 @@
+===============
+Getting started
+===============
+
+If you are interested in extending data collection by adding new probes have a look at
+
+.. toctree::
+ :maxdepth: 2
+ :titlesonly:
+ :glob:
+
+ adding-a-new-probe
+ report-gecko-telemetry-in-glean
+
+If you want to work with the telemetry code itself, for example to fix a bug, it is often helpful to start with these steps:
+
+1. Have a look at about:telemetry to see which data is being collected and sent.
+2. Increase the log level in about:config by setting toolkit.telemetry.log.level to Debug or Trace. This will show telemetry information in the browser console. To enable the browser console follow `these instructions <../../../../devtools-user/browser_console/index.html>`__.
+3. Run a local telemetry receiver, e.g. `this one <https://github.com/mozilla/gzipServer>`__ and set ``toolkit.telemetry.server`` to “localhost” (Like the next preference this needs a restart.)
+4. Set ``toolkit.telemetry.send.overrideOfficialCheck = true``, otherwise local debug builds will not send telemetry data. (Requires restart.)
+
+More information about the internals can be found `here <../internals/index.html>`__.
+
+Further Reading
+###############
+
+* `Telemetry Portal <https://telemetry.mozilla.org/>`_ - Discover all important resources for working with data
+* `Telemetry Data Documentation <https://docs.telemetry.mozilla.org/>`_ - Find what data is available & how to use it
diff --git a/toolkit/components/telemetry/docs/start/report-gecko-telemetry-in-glean.rst b/toolkit/components/telemetry/docs/start/report-gecko-telemetry-in-glean.rst
new file mode 100644
index 0000000000..1e7cb4aca5
--- /dev/null
+++ b/toolkit/components/telemetry/docs/start/report-gecko-telemetry-in-glean.rst
@@ -0,0 +1,36 @@
+=======================================================
+How to report Gecko Telemetry in engine-gecko via Glean
+=======================================================
+
+In Gecko, the `Telemetry <../index.html>`__ system collects various measures of Gecko performance, hardware, usage and customizations.
+When the Gecko engine is embedded in Android products that use
+`the Glean SDK <https://docs.telemetry.mozilla.org/concepts/glean/glean.html>`__ for data collection, then Gecko metrics can be reported in `Glean pings <https://mozilla.github.io/glean/book/user/pings/index.html>`__.
+This article provides an overview of what is needed to report any existing or new Telemetry data collection in Glean.
+
+.. important::
+
+ Every new or changed data collection in Firefox needs a `data collection review <https://wiki.mozilla.org/Data_Collection>`__ from a Data Steward.
+
+Using Glean
+===========
+In short, the way to collect data in all Mozilla products using Gecko is to use Glean.
+The Glean SDK is present in Gecko and all of Firefox Desktop via
+`Firefox on Glean (FOG) <../../glean/index.html>`__.
+If you instrument your Gecko data collection using Glean metrics,
+they will be reported by all products that use both Gecko and Glean so long as it is defined in a
+``metrics.yaml`` file in the ``gecko_metrics`` list of ``toolkit/components/glean/metrics_index.py``.
+
+See `these docs <../../glean/user/new_definitions_file.html>`__ for details.
+Additional relevant concepts are covered in
+`this guide for migrating Firefox Telemetry collections to Glean <../../glean/user/migration.html>`__.
+
+To make sure your data is also reported in Telemetry when your code is executed in Firefox Desktop,
+you must be sure to mirror the collection in Telemetry.
+If possible, you should use the
+`Glean Interface For Firefox Telemetry (GIFFT) <../../glean/user/gifft.html>`__
+and let FOG mirror the call for you.
+If that doesn't look like it'll work out for you,
+you can instead add your Glean API calls just next to your Telemetry API calls.
+
+As always, if you have any questions, you can find helpful people in
+`the #telemetry channel on Mozilla's Matrix <https://chat.mozilla.org/#/room/#telemetry:mozilla.org>`__.
diff --git a/toolkit/components/telemetry/geckoview/gtest/TestGeckoViewStreaming.cpp b/toolkit/components/telemetry/geckoview/gtest/TestGeckoViewStreaming.cpp
new file mode 100644
index 0000000000..ebaa7099a8
--- /dev/null
+++ b/toolkit/components/telemetry/geckoview/gtest/TestGeckoViewStreaming.cpp
@@ -0,0 +1,237 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+#include "gmock/gmock.h"
+#include "gtest/gtest.h"
+#include "mozilla/gtest/MozAssertions.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/Services.h"
+#include "mozilla/Telemetry.h"
+#include "nsIObserverService.h"
+#include "nsThreadUtils.h"
+#include "TelemetryFixture.h"
+#include "TelemetryTestHelpers.h"
+#include "streaming/GeckoViewStreamingTelemetry.h"
+
+using namespace mozilla;
+using namespace mozilla::Telemetry;
+using namespace TelemetryTestHelpers;
+using GeckoViewStreamingTelemetry::StreamingTelemetryDelegate;
+using mozilla::Telemetry::ScalarID;
+using ::testing::_;
+using ::testing::Eq;
+using ::testing::StrictMock;
+
+namespace {
+
+const char* kGeckoViewStreamingPref = "toolkit.telemetry.geckoview.streaming";
+const char* kBatchTimeoutPref = "toolkit.telemetry.geckoview.batchDurationMS";
+
+constexpr auto kTestHgramName = "TELEMETRY_TEST_STREAMING"_ns;
+constexpr auto kTestHgramName2 = "TELEMETRY_TEST_STREAMING_2"_ns;
+constexpr auto kTestCategoricalName = "TELEMETRY_TEST_CATEGORICAL_OPTOUT"_ns;
+const HistogramID kTestHgram = Telemetry::TELEMETRY_TEST_STREAMING;
+const HistogramID kTestHgram2 = Telemetry::TELEMETRY_TEST_STREAMING_2;
+
+class TelemetryStreamingFixture : public TelemetryTestFixture {
+ protected:
+ virtual void SetUp() {
+ TelemetryTestFixture::SetUp();
+ Preferences::SetBool(kGeckoViewStreamingPref, true);
+ Preferences::SetInt(kBatchTimeoutPref, 5000);
+ }
+ virtual void TearDown() {
+ TelemetryTestFixture::TearDown();
+ Preferences::SetBool(kGeckoViewStreamingPref, false);
+ GeckoViewStreamingTelemetry::RegisterDelegate(nullptr);
+ }
+};
+
+class MockDelegate : public StreamingTelemetryDelegate {
+ public:
+ MOCK_METHOD2(ReceiveHistogramSamples,
+ void(const nsCString& aHistogramName,
+ const nsTArray<uint32_t>& aSamples));
+ MOCK_METHOD2(ReceiveCategoricalHistogramSamples,
+ void(const nsCString& aHistogramName,
+ const nsTArray<uint32_t>& aSamples));
+ MOCK_METHOD2(ReceiveBoolScalarValue,
+ void(const nsCString& aScalarName, bool aValue));
+ MOCK_METHOD2(ReceiveStringScalarValue,
+ void(const nsCString& aScalarName, const nsCString& aValue));
+ MOCK_METHOD2(ReceiveUintScalarValue,
+ void(const nsCString& aScalarName, uint32_t aValue));
+}; // class MockDelegate
+
+TEST_F(TelemetryStreamingFixture, HistogramSamples) {
+ const uint32_t kSampleOne = 401;
+ const uint32_t kSampleTwo = 2019;
+
+ CopyableTArray<uint32_t> samplesArray;
+ samplesArray.AppendElement(kSampleOne);
+ samplesArray.AppendElement(kSampleTwo);
+
+ auto md = MakeRefPtr<MockDelegate>();
+ EXPECT_CALL(*md, ReceiveHistogramSamples(Eq(kTestHgramName),
+ Eq(std::move(samplesArray))));
+ GeckoViewStreamingTelemetry::RegisterDelegate(md);
+
+ Telemetry::Accumulate(Telemetry::TELEMETRY_TEST_STREAMING, kSampleOne);
+ Preferences::SetInt(kBatchTimeoutPref, 0);
+ Telemetry::Accumulate(Telemetry::TELEMETRY_TEST_STREAMING, kSampleTwo);
+}
+
+TEST_F(TelemetryStreamingFixture, CategoricalHistogramSamples) {
+ auto kSampleOne =
+ Telemetry::LABELS_TELEMETRY_TEST_CATEGORICAL_OPTOUT::CommonLabel;
+ auto kSampleTwo = Telemetry::LABELS_TELEMETRY_TEST_CATEGORICAL_OPTOUT::Label5;
+
+ CopyableTArray<uint32_t> samplesArray;
+ samplesArray.AppendElement(static_cast<uint32_t>(kSampleOne));
+ samplesArray.AppendElement(static_cast<uint32_t>(kSampleOne));
+ samplesArray.AppendElement(static_cast<uint32_t>(kSampleTwo));
+
+ auto md = MakeRefPtr<MockDelegate>();
+ EXPECT_CALL(*md, ReceiveCategoricalHistogramSamples(
+ Eq(kTestCategoricalName), Eq(std::move(samplesArray))));
+ GeckoViewStreamingTelemetry::RegisterDelegate(md);
+
+ Telemetry::AccumulateCategorical(kSampleOne);
+ Telemetry::AccumulateCategorical(kSampleOne);
+ Preferences::SetInt(kBatchTimeoutPref, 0);
+ Telemetry::AccumulateCategorical(kSampleTwo);
+}
+
+TEST_F(TelemetryStreamingFixture, MultipleHistograms) {
+ const uint32_t kSample1 = 400;
+ const uint32_t kSample2 = 1 << 31;
+ const uint32_t kSample3 = 7;
+ CopyableTArray<uint32_t> samplesArray1;
+ samplesArray1.AppendElement(kSample1);
+ samplesArray1.AppendElement(kSample2);
+ CopyableTArray<uint32_t> samplesArray2;
+ samplesArray2.AppendElement(kSample3);
+
+ auto md = MakeRefPtr<MockDelegate>();
+ EXPECT_CALL(*md, ReceiveHistogramSamples(Eq(kTestHgramName),
+ Eq(std::move(samplesArray1))));
+ EXPECT_CALL(*md, ReceiveHistogramSamples(Eq(kTestHgramName2),
+ Eq(std::move(samplesArray2))));
+
+ GeckoViewStreamingTelemetry::RegisterDelegate(md);
+
+ Telemetry::Accumulate(kTestHgram, kSample1);
+ Telemetry::Accumulate(kTestHgram2, kSample3);
+ Preferences::SetInt(kBatchTimeoutPref, 0);
+ Telemetry::Accumulate(kTestHgram, kSample2);
+}
+
+// If we can find a way to convert the expectation's arg into an stl container,
+// we can use gmock's own ::testing::UnorderedElementsAre() instead.
+auto MatchUnordered(uint32_t sample1, uint32_t sample2) {
+ CopyableTArray<uint32_t> samplesArray1;
+ samplesArray1.AppendElement(sample1);
+ samplesArray1.AppendElement(sample2);
+
+ CopyableTArray<uint32_t> samplesArray2;
+ samplesArray2.AppendElement(sample2);
+ samplesArray2.AppendElement(sample1);
+
+ return ::testing::AnyOf(Eq(std::move(samplesArray1)),
+ Eq(std::move(samplesArray2)));
+}
+
+TEST_F(TelemetryStreamingFixture, MultipleThreads) {
+ const uint32_t kSample1 = 4;
+ const uint32_t kSample2 = 14;
+
+ auto md = MakeRefPtr<MockDelegate>();
+ // In this test, samples for the second test hgram are uninteresting.
+ EXPECT_CALL(*md, ReceiveHistogramSamples(Eq(kTestHgramName2), _));
+ EXPECT_CALL(*md, ReceiveHistogramSamples(Eq(kTestHgramName),
+ MatchUnordered(kSample1, kSample2)));
+
+ GeckoViewStreamingTelemetry::RegisterDelegate(md);
+
+ nsCOMPtr<nsIThread> t1;
+ nsCOMPtr<nsIThread> t2;
+ nsCOMPtr<nsIThread> t3;
+
+ nsCOMPtr<nsIRunnable> r1 = NS_NewRunnableFunction(
+ "accumulate 4", [&]() { Telemetry::Accumulate(kTestHgram, kSample1); });
+ nsCOMPtr<nsIRunnable> r2 = NS_NewRunnableFunction(
+ "accumulate 14", [&]() { Telemetry::Accumulate(kTestHgram, kSample2); });
+
+ nsresult rv = NS_NewNamedThread("t1", getter_AddRefs(t1), r1);
+ EXPECT_NS_SUCCEEDED(rv);
+ rv = NS_NewNamedThread("t2", getter_AddRefs(t2), r2);
+ EXPECT_NS_SUCCEEDED(rv);
+
+ // Give the threads a chance to do their work.
+ PR_Sleep(PR_MillisecondsToInterval(1));
+
+ Preferences::SetInt(kBatchTimeoutPref, 0);
+ Telemetry::Accumulate(kTestHgram2, kSample1);
+}
+
+TEST_F(TelemetryStreamingFixture, ScalarValues) {
+ constexpr auto kBoolScalarName = "telemetry.test.boolean_kind"_ns;
+ constexpr auto kStringScalarName = "telemetry.test.string_kind"_ns;
+ constexpr auto kUintScalarName = "telemetry.test.unsigned_int_kind"_ns;
+
+ const bool kBoolScalarValue = true;
+ constexpr auto kStringScalarValue = "a string scalar value"_ns;
+ const uint32_t kUintScalarValue = 42;
+
+ auto md = MakeRefPtr<MockDelegate>();
+ EXPECT_CALL(
+ *md, ReceiveBoolScalarValue(Eq(kBoolScalarName), Eq(kBoolScalarValue)));
+ EXPECT_CALL(*md, ReceiveStringScalarValue(Eq(kStringScalarName),
+ Eq(kStringScalarValue)));
+ EXPECT_CALL(
+ *md, ReceiveUintScalarValue(Eq(kUintScalarName), Eq(kUintScalarValue)));
+
+ GeckoViewStreamingTelemetry::RegisterDelegate(md);
+
+ Telemetry::ScalarSet(ScalarID::TELEMETRY_TEST_BOOLEAN_KIND, kBoolScalarValue);
+ Telemetry::ScalarSet(ScalarID::TELEMETRY_TEST_STRING_KIND,
+ NS_ConvertUTF8toUTF16(kStringScalarValue));
+ Preferences::SetInt(kBatchTimeoutPref,
+ 0); // Trigger batch on next accumulation.
+ Telemetry::ScalarSet(ScalarID::TELEMETRY_TEST_UNSIGNED_INT_KIND,
+ kUintScalarValue);
+}
+
+TEST_F(TelemetryStreamingFixture, ExpiredHistogram) {
+ const HistogramID kExpiredHistogram = Telemetry::TELEMETRY_TEST_EXPIRED;
+ const uint32_t kSample = 401;
+
+ // Strict Mock fails on any method calls.
+ auto md = MakeRefPtr<StrictMock<MockDelegate>>();
+ GeckoViewStreamingTelemetry::RegisterDelegate(md);
+
+ Preferences::SetInt(kBatchTimeoutPref, 0);
+ Telemetry::Accumulate(kExpiredHistogram, kSample);
+}
+
+TEST_F(TelemetryStreamingFixture, SendOnAppBackground) {
+ constexpr auto kBoolScalarName = "telemetry.test.boolean_kind"_ns;
+ const bool kBoolScalarValue = true;
+ const char* kApplicationBackgroundTopic = "application-background";
+
+ auto md = MakeRefPtr<MockDelegate>();
+ EXPECT_CALL(
+ *md, ReceiveBoolScalarValue(Eq(kBoolScalarName), Eq(kBoolScalarValue)));
+
+ GeckoViewStreamingTelemetry::RegisterDelegate(md);
+ Telemetry::ScalarSet(ScalarID::TELEMETRY_TEST_BOOLEAN_KIND, kBoolScalarValue);
+
+ nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+ ASSERT_TRUE(!!os)
+ << "Observer Service unavailable?!?!";
+ os->NotifyObservers(nullptr, kApplicationBackgroundTopic, nullptr);
+}
+
+} // namespace
diff --git a/toolkit/components/telemetry/geckoview/gtest/moz.build b/toolkit/components/telemetry/geckoview/gtest/moz.build
new file mode 100644
index 0000000000..eb6a2f9293
--- /dev/null
+++ b/toolkit/components/telemetry/geckoview/gtest/moz.build
@@ -0,0 +1,28 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, you can obtain one at http://mozilla.org/MPL/2.0/.
+
+Library("telemetrygeckoviewtest")
+
+LOCAL_INCLUDES += [
+ "../",
+ "../..",
+ "../../..",
+ "/toolkit/components/telemetry/tests/gtest",
+ "/xpcom/io",
+]
+
+# GeckoView Streaming Telemetry is only available on Android.
+if CONFIG["MOZ_WIDGET_TOOLKIT"] == "android":
+ UNIFIED_SOURCES += [
+ "TestGeckoViewStreaming.cpp",
+ ]
+
+# We need the following line otherwise including
+# "TelemetryHistogram.h" in tests will fail due to
+# missing headers.
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul-gtest"
diff --git a/toolkit/components/telemetry/geckoview/streaming/GeckoViewStreamingTelemetry.cpp b/toolkit/components/telemetry/geckoview/streaming/GeckoViewStreamingTelemetry.cpp
new file mode 100644
index 0000000000..6c4b9590c0
--- /dev/null
+++ b/toolkit/components/telemetry/geckoview/streaming/GeckoViewStreamingTelemetry.cpp
@@ -0,0 +1,282 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "GeckoViewStreamingTelemetry.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/Services.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/StaticPrefs_toolkit.h"
+#include "mozilla/TimeStamp.h"
+#include "nsTHashMap.h"
+#include "nsIObserver.h"
+#include "nsIObserverService.h"
+#include "nsITimer.h"
+#include "nsTArray.h"
+#include "nsThreadUtils.h"
+
+using mozilla::Runnable;
+using mozilla::StaticMutex;
+using mozilla::StaticMutexAutoLock;
+using mozilla::StaticRefPtr;
+using mozilla::TimeStamp;
+
+// Batches and streams Telemetry samples to a JNI delegate which will
+// (presumably) do something with the data. Expected to be used to route data
+// up to the Android Components layer to be translated into Glean metrics.
+namespace GeckoViewStreamingTelemetry {
+
+class LifecycleObserver;
+void SendBatch(const StaticMutexAutoLock& aLock);
+
+// Topic on which we flush the batch.
+static const char* const kApplicationBackgroundTopic = "application-background";
+
+static StaticMutex gMutex MOZ_UNANNOTATED;
+
+// -- The following state is accessed across threads.
+// -- Do not touch these if you do not hold gMutex.
+
+// The time the batch began.
+TimeStamp gBatchBegan;
+// The batch of histograms and samples.
+typedef nsTHashMap<nsCStringHashKey, nsTArray<uint32_t>> HistogramBatch;
+HistogramBatch gBatch;
+HistogramBatch gCategoricalBatch;
+// The batches of Scalars and their values.
+typedef nsTHashMap<nsCStringHashKey, bool> BoolScalarBatch;
+BoolScalarBatch gBoolScalars;
+typedef nsTHashMap<nsCStringHashKey, nsCString> StringScalarBatch;
+StringScalarBatch gStringScalars;
+typedef nsTHashMap<nsCStringHashKey, uint32_t> UintScalarBatch;
+UintScalarBatch gUintScalars;
+// The delegate to receive the samples and values.
+StaticRefPtr<StreamingTelemetryDelegate> gDelegate;
+// Lifecycle observer used to flush the batch when backgrounded.
+StaticRefPtr<LifecycleObserver> gObserver;
+
+// -- End of gMutex-protected thread-unsafe-accessed data
+
+// Timer that ensures data in the batch never gets too stale.
+// This timer may only be manipulated on the Main Thread.
+StaticRefPtr<nsITimer> gJICTimer;
+
+class LifecycleObserver final : public nsIObserver {
+ public:
+ NS_DECL_ISUPPORTS
+ NS_DECL_NSIOBSERVER
+
+ LifecycleObserver() = default;
+
+ protected:
+ ~LifecycleObserver() = default;
+};
+
+NS_IMPL_ISUPPORTS(LifecycleObserver, nsIObserver);
+
+NS_IMETHODIMP
+LifecycleObserver::Observe(nsISupports* aSubject, const char* aTopic,
+ const char16_t* aData) {
+ if (!strcmp(aTopic, kApplicationBackgroundTopic)) {
+ StaticMutexAutoLock lock(gMutex);
+ SendBatch(lock);
+ }
+ return NS_OK;
+}
+
+void RegisterDelegate(const RefPtr<StreamingTelemetryDelegate>& aDelegate) {
+ StaticMutexAutoLock lock(gMutex);
+ gDelegate = aDelegate;
+}
+
+class SendBatchRunnable : public Runnable {
+ public:
+ explicit SendBatchRunnable(RefPtr<StreamingTelemetryDelegate> aDelegate,
+ HistogramBatch&& aBatch,
+ HistogramBatch&& aCategoricalBatch,
+ BoolScalarBatch&& aBoolScalars,
+ StringScalarBatch&& aStringScalars,
+ UintScalarBatch&& aUintScalars)
+ : Runnable("SendBatchRunnable"),
+ mDelegate(std::move(aDelegate)),
+ mBatch(std::move(aBatch)),
+ mCategoricalBatch(std::move(aCategoricalBatch)),
+ mBoolScalars(std::move(aBoolScalars)),
+ mStringScalars(std::move(aStringScalars)),
+ mUintScalars(std::move(aUintScalars)) {}
+
+ NS_IMETHOD Run() override {
+ MOZ_ASSERT(NS_IsMainThread());
+ MOZ_ASSERT(mDelegate);
+
+ if (gJICTimer) {
+ gJICTimer->Cancel();
+ }
+
+ for (const auto& entry : mBatch) {
+ const nsCString& histogramName = PromiseFlatCString(entry.GetKey());
+ const nsTArray<uint32_t>& samples = entry.GetData();
+
+ mDelegate->ReceiveHistogramSamples(histogramName, samples);
+ }
+ mBatch.Clear();
+
+ for (const auto& entry : mCategoricalBatch) {
+ const nsCString& histogramName = PromiseFlatCString(entry.GetKey());
+ const nsTArray<uint32_t>& samples = entry.GetData();
+
+ mDelegate->ReceiveCategoricalHistogramSamples(histogramName, samples);
+ }
+ mCategoricalBatch.Clear();
+
+ for (const auto& entry : mBoolScalars) {
+ const nsCString& scalarName = PromiseFlatCString(entry.GetKey());
+ mDelegate->ReceiveBoolScalarValue(scalarName, entry.GetData());
+ }
+ mBoolScalars.Clear();
+
+ for (const auto& entry : mStringScalars) {
+ const nsCString& scalarName = PromiseFlatCString(entry.GetKey());
+ const nsCString& scalarValue = PromiseFlatCString(entry.GetData());
+ mDelegate->ReceiveStringScalarValue(scalarName, scalarValue);
+ }
+ mStringScalars.Clear();
+
+ for (const auto& entry : mUintScalars) {
+ const nsCString& scalarName = PromiseFlatCString(entry.GetKey());
+ mDelegate->ReceiveUintScalarValue(scalarName, entry.GetData());
+ }
+ mUintScalars.Clear();
+
+ return NS_OK;
+ }
+
+ private:
+ RefPtr<StreamingTelemetryDelegate> mDelegate;
+ HistogramBatch mBatch;
+ HistogramBatch mCategoricalBatch;
+ BoolScalarBatch mBoolScalars;
+ StringScalarBatch mStringScalars;
+ UintScalarBatch mUintScalars;
+}; // class SendBatchRunnable
+
+// Can be called on any thread.
+// NOTE: Pay special attention to what you call in this method as if it
+// accumulates to a gv-streaming-enabled probe we will deadlock the calling
+// thread.
+void SendBatch(const StaticMutexAutoLock& aLock) {
+ if (!gDelegate) {
+ NS_WARNING(
+ "Being asked to send Streaming Telemetry with no registered Streaming "
+ "Telemetry Delegate. Will try again later.");
+ // Give us another full Batch Duration to register a delegate.
+ gBatchBegan = TimeStamp::Now();
+ return;
+ }
+
+ // To make it so accumulations within the delegation don't deadlock us,
+ // move the batches' contents into the Runner.
+ HistogramBatch histogramCopy;
+ gBatch.SwapElements(histogramCopy);
+ HistogramBatch categoricalCopy;
+ gCategoricalBatch.SwapElements(categoricalCopy);
+ BoolScalarBatch boolScalarCopy;
+ gBoolScalars.SwapElements(boolScalarCopy);
+ StringScalarBatch stringScalarCopy;
+ gStringScalars.SwapElements(stringScalarCopy);
+ UintScalarBatch uintScalarCopy;
+ gUintScalars.SwapElements(uintScalarCopy);
+ RefPtr<SendBatchRunnable> runnable = new SendBatchRunnable(
+ gDelegate, std::move(histogramCopy), std::move(categoricalCopy),
+ std::move(boolScalarCopy), std::move(stringScalarCopy),
+ std::move(uintScalarCopy));
+
+ // To make things easier for the delegate, dispatch to the main thread.
+ NS_DispatchToMainThread(runnable);
+}
+
+// Can be called on any thread.
+void BatchCheck(const StaticMutexAutoLock& aLock) {
+ if (!gObserver) {
+ gObserver = new LifecycleObserver();
+ nsCOMPtr<nsIObserverService> os = mozilla::services::GetObserverService();
+ if (os) {
+ os->AddObserver(gObserver, kApplicationBackgroundTopic, false);
+ }
+ }
+ if (gBatchBegan.IsNull()) {
+ // Time to begin a new batch.
+ gBatchBegan = TimeStamp::Now();
+ // Set a just-in-case timer to enforce an upper-bound on batch staleness.
+ NS_DispatchToMainThread(NS_NewRunnableFunction(
+ "GeckoviewStreamingTelemetry::ArmTimer", []() -> void {
+ if (!gJICTimer) {
+ gJICTimer = NS_NewTimer().take();
+ }
+ if (gJICTimer) {
+ gJICTimer->InitWithNamedFuncCallback(
+ [](nsITimer*, void*) -> void {
+ StaticMutexAutoLock locker(gMutex);
+ SendBatch(locker);
+ },
+ nullptr,
+ mozilla::StaticPrefs::
+ toolkit_telemetry_geckoview_maxBatchStalenessMS(),
+ nsITimer::TYPE_ONE_SHOT_LOW_PRIORITY,
+ "GeckoviewStreamingTelemetry::SendBatch");
+ }
+ }));
+ }
+ double batchDurationMs = (TimeStamp::Now() - gBatchBegan).ToMilliseconds();
+ if (batchDurationMs >
+ mozilla::StaticPrefs::toolkit_telemetry_geckoview_batchDurationMS()) {
+ SendBatch(aLock);
+ gBatchBegan = TimeStamp();
+ }
+}
+
+// Can be called on any thread.
+void HistogramAccumulate(const nsCString& aName, bool aIsCategorical,
+ uint32_t aValue) {
+ StaticMutexAutoLock lock(gMutex);
+
+ if (aIsCategorical) {
+ nsTArray<uint32_t>& samples = gCategoricalBatch.LookupOrInsert(aName);
+ samples.AppendElement(aValue);
+ } else {
+ nsTArray<uint32_t>& samples = gBatch.LookupOrInsert(aName);
+ samples.AppendElement(aValue);
+ }
+
+ BatchCheck(lock);
+}
+
+void BoolScalarSet(const nsCString& aName, bool aValue) {
+ StaticMutexAutoLock lock(gMutex);
+
+ gBoolScalars.InsertOrUpdate(aName, aValue);
+
+ BatchCheck(lock);
+}
+
+void StringScalarSet(const nsCString& aName, const nsCString& aValue) {
+ StaticMutexAutoLock lock(gMutex);
+
+ gStringScalars.InsertOrUpdate(aName, aValue);
+
+ BatchCheck(lock);
+}
+
+void UintScalarSet(const nsCString& aName, uint32_t aValue) {
+ StaticMutexAutoLock lock(gMutex);
+
+ gUintScalars.InsertOrUpdate(aName, aValue);
+
+ BatchCheck(lock);
+}
+
+} // namespace GeckoViewStreamingTelemetry
diff --git a/toolkit/components/telemetry/geckoview/streaming/GeckoViewStreamingTelemetry.h b/toolkit/components/telemetry/geckoview/streaming/GeckoViewStreamingTelemetry.h
new file mode 100644
index 0000000000..458224a3c2
--- /dev/null
+++ b/toolkit/components/telemetry/geckoview/streaming/GeckoViewStreamingTelemetry.h
@@ -0,0 +1,55 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GeckoViewStreamingTelemetry_h__
+#define GeckoViewStreamingTelemetry_h__
+
+#include "mozilla/Assertions.h"
+#include "mozilla/RefPtr.h"
+#include "nsISupports.h"
+#include "nsStringFwd.h"
+#include "nsTArray.h"
+
+#include <cstdint>
+
+namespace GeckoViewStreamingTelemetry {
+
+void HistogramAccumulate(const nsCString& aName, bool aIsCategorical,
+ uint32_t aValue);
+
+void BoolScalarSet(const nsCString& aName, bool aValue);
+void StringScalarSet(const nsCString& aName, const nsCString& aValue);
+void UintScalarSet(const nsCString& aName, uint32_t aValue);
+
+// Classes wishing to receive Streaming Telemetry must implement this interface
+// and register themselves via RegisterDelegate.
+class StreamingTelemetryDelegate {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(StreamingTelemetryDelegate)
+
+ // Receive* methods will be called from time to time on the main thread.
+ virtual void ReceiveHistogramSamples(const nsCString& aName,
+ const nsTArray<uint32_t>& aSamples) = 0;
+ virtual void ReceiveCategoricalHistogramSamples(
+ const nsCString& aName, const nsTArray<uint32_t>& aSamples) = 0;
+ virtual void ReceiveBoolScalarValue(const nsCString& aName, bool aValue) = 0;
+ virtual void ReceiveStringScalarValue(const nsCString& aName,
+ const nsCString& aValue) = 0;
+ virtual void ReceiveUintScalarValue(const nsCString& aName,
+ uint32_t aValue) = 0;
+
+ protected:
+ virtual ~StreamingTelemetryDelegate() = default;
+};
+
+// Registers the provided StreamingTelemetryDelegate to receive Streaming
+// Telemetry, overwriting any previous delegate registration.
+// Call on any thread.
+void RegisterDelegate(const RefPtr<StreamingTelemetryDelegate>& aDelegate);
+
+} // namespace GeckoViewStreamingTelemetry
+
+#endif // GeckoViewStreamingTelemetry_h__
diff --git a/toolkit/components/telemetry/geckoview/streaming/metrics.yaml b/toolkit/components/telemetry/geckoview/streaming/metrics.yaml
new file mode 100644
index 0000000000..2386fb2171
--- /dev/null
+++ b/toolkit/components/telemetry/geckoview/streaming/metrics.yaml
@@ -0,0 +1,13 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# This file defines the metrics that are recorded by the Glean SDK. They are
+# automatically converted to platform-specific code at build time using the
+# `glean_parser` PyPI package.
+
+# Adding a new metric? Please don't!
+# (At least not without the permission of a Telemetry Module Peer)
+
+---
+$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
diff --git a/toolkit/components/telemetry/histogram-allowlists.json b/toolkit/components/telemetry/histogram-allowlists.json
new file mode 100644
index 0000000000..dc5a74fbea
--- /dev/null
+++ b/toolkit/components/telemetry/histogram-allowlists.json
@@ -0,0 +1,941 @@
+{
+ "alert_emails": [
+ "AUTO_REJECTED_TRANSLATION_OFFERS",
+ "BACKGROUNDFILESAVER_THREAD_COUNT",
+ "BAD_FALLBACK_FONT",
+ "BROWSERPROVIDER_XUL_IMPORT_BOOKMARKS",
+ "BROWSER_IS_ASSIST_DEFAULT",
+ "BROWSER_IS_USER_DEFAULT",
+ "BROWSER_IS_USER_DEFAULT_ERROR",
+ "BROWSER_SET_DEFAULT_ALWAYS_CHECK",
+ "BROWSER_SET_DEFAULT_DIALOG_PROMPT_RAWCOUNT",
+ "BROWSER_SET_DEFAULT_ERROR",
+ "BROWSER_SET_DEFAULT_RESULT",
+ "CACHE_DEVICE_SEARCH_2",
+ "CACHE_LM_INCONSISTENT",
+ "CANVAS_2D_USED",
+ "CANVAS_WEBGL_USED",
+ "CHANGES_OF_DETECTED_LANGUAGE",
+ "CHANGES_OF_TARGET_LANGUAGE",
+ "COMPONENTS_SHIM_ACCESSED_BY_CONTENT",
+ "CRASH_STORE_COMPRESSED_BYTES",
+ "DEFECTIVE_PERMISSIONS_SQL_REMOVED",
+ "DEFERRED_FINALIZE_ASYNC",
+ "DENIED_TRANSLATION_OFFERS",
+ "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE_LOCAL_MS",
+ "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE_REMOTE_MS",
+ "DEVTOOLS_HEAP_SNAPSHOT_EDGE_COUNT",
+ "DEVTOOLS_HEAP_SNAPSHOT_NODE_COUNT",
+ "DEVTOOLS_READ_HEAP_SNAPSHOT_MS",
+ "DEVTOOLS_SAVE_HEAP_SNAPSHOT_MS",
+ "DNS_CLEANUP_AGE",
+ "DNS_RENEWAL_TIME",
+ "DNS_RENEWAL_TIME_FOR_TTL",
+ "DNT_USAGE",
+ "DWRITEFONT_DELAYEDINITFONTLIST_COLLECT",
+ "DWRITEFONT_DELAYEDINITFONTLIST_COUNT",
+ "DWRITEFONT_DELAYEDINITFONTLIST_TOTAL",
+ "DWRITEFONT_INIT_PROBLEM",
+ "FONTLIST_INITFACENAMELISTS",
+ "FONTLIST_INITOTHERFAMILYNAMES",
+ "FONTLIST_INITOTHERFAMILYNAMES_NO_DEFERRING",
+ "FONT_CACHE_HIT",
+ "FX_SESSION_RESTORE_NUMBER_OF_EAGER_TABS_RESTORED",
+ "FX_SESSION_RESTORE_NUMBER_OF_TABS_RESTORED",
+ "FX_SESSION_RESTORE_NUMBER_OF_WINDOWS_RESTORED",
+ "FX_TABLETMODE_PAGE_LOAD",
+ "FX_TAB_CLICK_MS",
+ "FX_THUMBNAILS_BG_CAPTURE_CANVAS_DRAW_TIME_MS",
+ "FX_THUMBNAILS_BG_CAPTURE_DONE_REASON_2",
+ "FX_THUMBNAILS_BG_CAPTURE_PAGE_LOAD_TIME_MS",
+ "FX_THUMBNAILS_BG_CAPTURE_QUEUE_TIME_MS",
+ "FX_THUMBNAILS_BG_CAPTURE_SERVICE_TIME_MS",
+ "FX_THUMBNAILS_BG_QUEUE_SIZE_ON_CAPTURE",
+ "FX_THUMBNAILS_CAPTURE_TIME_MS",
+ "FX_THUMBNAILS_HIT_OR_MISS",
+ "FX_THUMBNAILS_STORE_TIME_MS",
+ "FX_TOTAL_TOP_VISITS",
+ "GDI_INITFONTLIST_TOTAL",
+ "GEOLOCATION_ERROR",
+ "GEOLOCATION_OSX_SOURCE_IS_MLS",
+ "GEOLOCATION_WIN8_SOURCE_IS_MLS",
+ "HISTORY_LASTVISITED_TREE_QUERY_TIME_MS",
+ "HTTPCONNMGR_TOTAL_SPECULATIVE_CONN",
+ "HTTPCONNMGR_UNUSED_SPECULATIVE_CONN",
+ "HTTPCONNMGR_USED_SPECULATIVE_CONN",
+ "HTTP_CACHE_ENTRY_ALIVE_TIME",
+ "HTTP_CACHE_ENTRY_RELOAD_TIME",
+ "HTTP_CACHE_ENTRY_REUSE_COUNT",
+ "HTTP_CONNECTION_ENTRY_CACHE_HIT_1",
+ "HTTP_PAGE_CACHE_READ_TIME_V2",
+ "HTTP_PAGE_COMPLETE_LOAD_CACHED_V2",
+ "HTTP_PAGE_COMPLETE_LOAD_NET_V2",
+ "HTTP_PAGE_COMPLETE_LOAD_V2",
+ "HTTP_PAGE_FIRST_SENT_TO_LAST_RECEIVED",
+ "HTTP_PAGE_OPEN_TO_FIRST_RECEIVED",
+ "HTTP_PAGE_OPEN_TO_FIRST_SENT",
+ "HTTP_PAGE_REVALIDATION",
+ "HTTP_PROXY_TYPE",
+ "HTTP_REQUEST_PER_CONN",
+ "HTTP_REQUEST_PER_PAGE",
+ "HTTP_REQUEST_PER_PAGE_FROM_CACHE",
+ "HTTP_SUBITEM_FIRST_BYTE_LATENCY_TIME",
+ "HTTP_SUBITEM_OPEN_LATENCY_TIME",
+ "HTTP_SUB_CACHE_READ_TIME_V2",
+ "HTTP_SUB_COMPLETE_LOAD_CACHED_V2",
+ "HTTP_SUB_COMPLETE_LOAD_NET_V2",
+ "HTTP_SUB_COMPLETE_LOAD_V2",
+ "HTTP_SUB_DNS_ISSUE_TIME",
+ "HTTP_SUB_FIRST_SENT_TO_LAST_RECEIVED",
+ "HTTP_SUB_OPEN_TO_FIRST_FROM_CACHE_V2",
+ "HTTP_SUB_OPEN_TO_FIRST_RECEIVED",
+ "HTTP_SUB_OPEN_TO_FIRST_SENT",
+ "HTTP_SUB_REVALIDATION",
+ "HTTP_TRANSACTION_USE_ALTSVC",
+ "HTTP_TRANSACTION_USE_ALTSVC_OE",
+ "INNERWINDOWS_WITH_MUTATION_LISTENERS",
+ "IPC_SAME_PROCESS_MESSAGE_COPY_OOM_KB",
+ "IPV4_AND_IPV6_ADDRESS_CONNECTIVITY",
+ "LOCALDOMSTORAGE_CLEAR_BLOCKING_MS",
+ "LOCALDOMSTORAGE_GETALLKEYS_BLOCKING_MS",
+ "LOCALDOMSTORAGE_GETKEY_BLOCKING_MS",
+ "LOCALDOMSTORAGE_GETLENGTH_BLOCKING_MS",
+ "LOCALDOMSTORAGE_GETVALUE_BLOCKING_MS",
+ "LOCALDOMSTORAGE_PRELOAD_PENDING_ON_FIRST_ACCESS",
+ "LOCALDOMSTORAGE_REMOVEKEY_BLOCKING_MS",
+ "LOCALDOMSTORAGE_SESSIONONLY_PRELOAD_BLOCKING_MS",
+ "LOCALDOMSTORAGE_SETVALUE_BLOCKING_MS",
+ "LOCALDOMSTORAGE_SHUTDOWN_DATABASE_MS",
+ "LOCALDOMSTORAGE_UNLOAD_BLOCKING_MS",
+ "LONG_REFLOW_INTERRUPTIBLE",
+ "MAC_INITFONTLIST_TOTAL",
+ "MASTER_PASSWORD_ENABLED",
+ "MIXED_CONTENT_PAGE_LOAD",
+ "MIXED_CONTENT_UNBLOCK_COUNTER",
+ "MOZ_SQLITE_COOKIES_OPEN_READAHEAD_MS",
+ "NETWORK_CACHE_HIT_MISS_STAT_PER_CACHE_SIZE",
+ "NETWORK_CACHE_HIT_RATE_PER_CACHE_SIZE",
+ "NETWORK_CACHE_METADATA_FIRST_READ_TIME_MS",
+ "NETWORK_CACHE_METADATA_SECOND_READ_TIME_MS",
+ "NETWORK_CACHE_V1_HIT_TIME_MS",
+ "NETWORK_CACHE_V1_MISS_TIME_MS",
+ "NETWORK_CACHE_V1_TRUNCATE_TIME_MS",
+ "NETWORK_CACHE_V2_INPUT_STREAM_STATUS",
+ "NETWORK_CACHE_V2_OUTPUT_STREAM_STATUS",
+ "NETWORK_DISK_CACHE2_SHUTDOWN_CLEAR_PRIVATE",
+ "NETWORK_DISK_CACHE_DELETEDIR",
+ "NETWORK_DISK_CACHE_DELETEDIR_SHUTDOWN",
+ "NETWORK_DISK_CACHE_SHUTDOWN",
+ "NETWORK_DISK_CACHE_SHUTDOWN_CLEAR_PRIVATE",
+ "NETWORK_DISK_CACHE_SHUTDOWN_V2",
+ "NETWORK_DISK_CACHE_TRASHRENAME",
+ "NEWTAB_PAGE_BLOCKED_SITES_COUNT",
+ "NEWTAB_PAGE_ENABLED",
+ "NEWTAB_PAGE_PINNED_SITES_COUNT",
+ "NTLM_MODULE_USED_2",
+ "PAGE_FAULTS_HARD",
+ "PERMISSIONS_SQL_CORRUPTED",
+ "PLACES_ANNOS_PAGES_COUNT",
+ "PLACES_BACKUPS_BOOKMARKSTREE_MS",
+ "PLACES_BACKUPS_DAYSFROMLAST",
+ "PLACES_BACKUPS_TOJSON_MS",
+ "PLACES_BOOKMARKS_COUNT",
+ "PLACES_EXPIRATION_STEPS_TO_CLEAN2",
+ "PLACES_EXPORT_TOHTML_MS",
+ "PLACES_FAVICON_BMP_SIZES",
+ "PLACES_FAVICON_GIF_SIZES",
+ "PLACES_FAVICON_ICO_SIZES",
+ "PLACES_FAVICON_JPEG_SIZES",
+ "PLACES_FAVICON_OTHER_SIZES",
+ "PLACES_FAVICON_PNG_SIZES",
+ "PLACES_FAVICON_SVG_SIZES",
+ "PLACES_HISTORY_LIBRARY_SEARCH_TIME_MS",
+ "PLACES_IDLE_FRECENCY_DECAY_TIME_MS",
+ "PLACES_IDLE_MAINTENANCE_TIME_MS",
+ "PLACES_KEYWORDS_COUNT",
+ "PLACES_MAINTENANCE_DAYSFROMLAST",
+ "PLACES_PAGES_COUNT",
+ "PLACES_SORTED_BOOKMARKS_PERC",
+ "PLACES_TAGGED_BOOKMARKS_PERC",
+ "PLACES_TAGS_COUNT",
+ "PLUGIN_SHUTDOWN_MS",
+ "PRCLOSE_TCP_BLOCKING_TIME_CONNECTIVITY_CHANGE",
+ "PRCLOSE_TCP_BLOCKING_TIME_LINK_CHANGE",
+ "PRCLOSE_TCP_BLOCKING_TIME_NORMAL",
+ "PRCLOSE_TCP_BLOCKING_TIME_OFFLINE",
+ "PRCLOSE_TCP_BLOCKING_TIME_SHUTDOWN",
+ "PRCLOSE_UDP_BLOCKING_TIME_CONNECTIVITY_CHANGE",
+ "PRCLOSE_UDP_BLOCKING_TIME_LINK_CHANGE",
+ "PRCLOSE_UDP_BLOCKING_TIME_NORMAL",
+ "PRCLOSE_UDP_BLOCKING_TIME_OFFLINE",
+ "PRCLOSE_UDP_BLOCKING_TIME_SHUTDOWN",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_CONNECTIVITY_CHANGE",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_LINK_CHANGE",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_NORMAL",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_OFFLINE",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_SHUTDOWN",
+ "PRCONNECT_BLOCKING_TIME_CONNECTIVITY_CHANGE",
+ "PRCONNECT_BLOCKING_TIME_LINK_CHANGE",
+ "PRCONNECT_BLOCKING_TIME_NORMAL",
+ "PRCONNECT_BLOCKING_TIME_OFFLINE",
+ "PRCONNECT_BLOCKING_TIME_SHUTDOWN",
+ "PROCESS_CRASH_SUBMIT_ATTEMPT",
+ "PROCESS_CRASH_SUBMIT_SUCCESS",
+ "PWMGR_NUM_HTTPAUTH_PASSWORDS",
+ "PWMGR_USERNAME_PRESENT",
+ "REQUESTS_OF_ORIGINAL_CONTENT",
+ "SAFE_MODE_USAGE",
+ "SERVICE_WORKER_CONTROLLED_DOCUMENTS",
+ "SERVICE_WORKER_REGISTRATION_LOADING",
+ "SERVICE_WORKER_SPAWN_ATTEMPTS",
+ "SERVICE_WORKER_UPDATED",
+ "SERVICE_WORKER_WAS_SPAWNED",
+ "SHOULD_AUTO_DETECT_LANGUAGE",
+ "SHOULD_TRANSLATION_UI_APPEAR",
+ "STARTUP_CRASH_DETECTED",
+ "STS_POLL_AND_EVENTS_CYCLE",
+ "STS_POLL_AND_EVENT_THE_LAST_CYCLE",
+ "STS_POLL_BLOCK_TIME",
+ "STS_POLL_CYCLE",
+ "SUBPROCESS_ABNORMAL_ABORT",
+ "SUBPROCESS_CRASHES_WITH_DUMP",
+ "SYSTEM_FONT_FALLBACK",
+ "SYSTEM_FONT_FALLBACK_FIRST",
+ "SYSTEM_FONT_FALLBACK_SCRIPT",
+ "TAP_TO_LOAD_ENABLED",
+ "TAP_TO_LOAD_IMAGE_SIZE",
+ "THUNDERBIRD_GLODA_SIZE_MB",
+ "THUNDERBIRD_INDEXING_RATE_MSG_PER_S",
+ "TRANSLATED_CHARACTERS",
+ "TRANSLATED_PAGES",
+ "TRANSLATED_PAGES_BY_LANGUAGE",
+ "TRANSLATION_OPPORTUNITIES",
+ "TRANSLATION_OPPORTUNITIES_BY_LANGUAGE",
+ "WEAVE_COMPLETE_SUCCESS_COUNT",
+ "WEAVE_START_COUNT",
+ "WEBCRYPTO_ALG",
+ "WEBCRYPTO_EXTRACTABLE_ENC",
+ "WEBCRYPTO_EXTRACTABLE_GENERATE",
+ "WEBCRYPTO_EXTRACTABLE_IMPORT",
+ "WEBCRYPTO_EXTRACTABLE_SIG",
+ "WEBCRYPTO_METHOD",
+ "WEBCRYPTO_RESOLVED",
+ "WEBSOCKETS_HANDSHAKE_TYPE",
+ "XMLHTTPREQUEST_ASYNC_OR_SYNC"
+ ],
+ "bug_numbers": [
+ "A11Y_IATABLE_USAGE_FLAG",
+ "A11Y_ISIMPLEDOM_USAGE_FLAG",
+ "APPLICATION_REPUTATION_LOCAL",
+ "APPLICATION_REPUTATION_SERVER",
+ "APPLICATION_REPUTATION_SHOULD_BLOCK",
+ "AUTO_REJECTED_TRANSLATION_OFFERS",
+ "BACKGROUNDFILESAVER_THREAD_COUNT",
+ "BAD_FALLBACK_FONT",
+ "BROWSERPROVIDER_XUL_IMPORT_BOOKMARKS",
+ "BROWSER_IS_ASSIST_DEFAULT",
+ "BROWSER_IS_USER_DEFAULT_ERROR",
+ "BROWSER_SET_DEFAULT_ALWAYS_CHECK",
+ "BROWSER_SET_DEFAULT_DIALOG_PROMPT_RAWCOUNT",
+ "BROWSER_SET_DEFAULT_ERROR",
+ "BROWSER_SET_DEFAULT_RESULT",
+ "BUCKET_ORDER_ERRORS",
+ "CACHE_DEVICE_SEARCH_2",
+ "CACHE_LM_INCONSISTENT",
+ "CANVAS_2D_USED",
+ "CANVAS_WEBGL_USED",
+ "CERT_OCSP_ENABLED",
+ "CERT_OCSP_REQUIRED",
+ "CHANGES_OF_DETECTED_LANGUAGE",
+ "CHANGES_OF_TARGET_LANGUAGE",
+ "COMPONENTS_SHIM_ACCESSED_BY_CONTENT",
+ "COMPOSITE_FRAME_ROUNDTRIP_TIME",
+ "CRASH_STORE_COMPRESSED_BYTES",
+ "CYCLE_COLLECTOR",
+ "CYCLE_COLLECTOR_ASYNC_SNOW_WHITE_FREEING",
+ "CYCLE_COLLECTOR_COLLECTED",
+ "CYCLE_COLLECTOR_FINISH_IGC",
+ "CYCLE_COLLECTOR_FULL",
+ "CYCLE_COLLECTOR_NEED_GC",
+ "CYCLE_COLLECTOR_OOM",
+ "CYCLE_COLLECTOR_SYNC_SKIPPABLE",
+ "CYCLE_COLLECTOR_TIME_BETWEEN",
+ "CYCLE_COLLECTOR_VISITED_GCED",
+ "CYCLE_COLLECTOR_VISITED_REF_COUNTED",
+ "CYCLE_COLLECTOR_WORKER",
+ "CYCLE_COLLECTOR_WORKER_COLLECTED",
+ "CYCLE_COLLECTOR_WORKER_NEED_GC",
+ "CYCLE_COLLECTOR_WORKER_OOM",
+ "CYCLE_COLLECTOR_WORKER_VISITED_GCED",
+ "CYCLE_COLLECTOR_WORKER_VISITED_REF_COUNTED",
+ "D3D11_SYNC_HANDLE_FAILURE",
+ "DEFECTIVE_PERMISSIONS_SQL_REMOVED",
+ "DEFERRED_FINALIZE_ASYNC",
+ "DENIED_TRANSLATION_OFFERS",
+ "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE_LOCAL_MS",
+ "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE_REMOTE_MS",
+ "DEVTOOLS_HEAP_SNAPSHOT_EDGE_COUNT",
+ "DEVTOOLS_HEAP_SNAPSHOT_NODE_COUNT",
+ "DEVTOOLS_READ_HEAP_SNAPSHOT_MS",
+ "DEVTOOLS_SAVE_HEAP_SNAPSHOT_MS",
+ "DNS_BLACKLIST_COUNT",
+ "DNS_CLEANUP_AGE",
+ "DNS_FAILED_LOOKUP_TIME",
+ "DNS_LOOKUP_METHOD2",
+ "DNS_LOOKUP_TIME",
+ "DNS_RENEWAL_TIME",
+ "DNS_RENEWAL_TIME_FOR_TTL",
+ "DNT_USAGE",
+ "DWRITEFONT_DELAYEDINITFONTLIST_COLLECT",
+ "DWRITEFONT_DELAYEDINITFONTLIST_COUNT",
+ "DWRITEFONT_DELAYEDINITFONTLIST_TOTAL",
+ "DWRITEFONT_INIT_PROBLEM",
+ "FIND_PLUGINS",
+ "FONTLIST_INITFACENAMELISTS",
+ "FONTLIST_INITOTHERFAMILYNAMES",
+ "FONTLIST_INITOTHERFAMILYNAMES_NO_DEFERRING",
+ "FONT_CACHE_HIT",
+ "FORGET_SKIPPABLE_MAX",
+ "FX_SANITIZE_CACHE",
+ "FX_SANITIZE_COOKIES_2",
+ "FX_SANITIZE_DOWNLOADS",
+ "FX_SANITIZE_FORMDATA",
+ "FX_SANITIZE_HISTORY",
+ "FX_SANITIZE_OPENWINDOWS",
+ "FX_SANITIZE_SESSIONS",
+ "FX_SANITIZE_SITESETTINGS",
+ "FX_SANITIZE_TOTAL",
+ "FX_SESSION_RESTORE_ALL_FILES_CORRUPT",
+ "FX_SESSION_RESTORE_AUTO_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS",
+ "FX_SESSION_RESTORE_COLLECT_ALL_WINDOWS_DATA_MS",
+ "FX_SESSION_RESTORE_COLLECT_DATA_MS",
+ "FX_SESSION_RESTORE_CORRUPT_FILE",
+ "FX_SESSION_RESTORE_FILE_SIZE_BYTES",
+ "FX_SESSION_RESTORE_MANUAL_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS",
+ "FX_SESSION_RESTORE_NUMBER_OF_EAGER_TABS_RESTORED",
+ "FX_SESSION_RESTORE_NUMBER_OF_TABS_RESTORED",
+ "FX_SESSION_RESTORE_NUMBER_OF_WINDOWS_RESTORED",
+ "FX_SESSION_RESTORE_READ_FILE_MS",
+ "FX_SESSION_RESTORE_SEND_UPDATE_CAUSED_OOM",
+ "FX_SESSION_RESTORE_SERIALIZE_DATA_MS",
+ "FX_SESSION_RESTORE_WRITE_FILE_MS",
+ "FX_TABLETMODE_PAGE_LOAD",
+ "FX_TAB_CLICK_MS",
+ "FX_THUMBNAILS_BG_CAPTURE_CANVAS_DRAW_TIME_MS",
+ "FX_THUMBNAILS_BG_CAPTURE_DONE_REASON_2",
+ "FX_THUMBNAILS_BG_CAPTURE_PAGE_LOAD_TIME_MS",
+ "FX_THUMBNAILS_BG_CAPTURE_QUEUE_TIME_MS",
+ "FX_THUMBNAILS_BG_CAPTURE_SERVICE_TIME_MS",
+ "FX_THUMBNAILS_BG_QUEUE_SIZE_ON_CAPTURE",
+ "FX_THUMBNAILS_CAPTURE_TIME_MS",
+ "FX_THUMBNAILS_HIT_OR_MISS",
+ "FX_THUMBNAILS_STORE_TIME_MS",
+ "FX_TOTAL_TOP_VISITS",
+ "GDI_INITFONTLIST_TOTAL",
+ "GEOLOCATION_ERROR",
+ "GEOLOCATION_OSX_SOURCE_IS_MLS",
+ "GEOLOCATION_WIN8_SOURCE_IS_MLS",
+ "GFX_CONTENT_FAILED_TO_ACQUIRE_DEVICE",
+ "GFX_CRASH",
+ "GHOST_WINDOWS",
+ "GRAPHICS_DRIVER_STARTUP_TEST",
+ "GRAPHICS_SANITY_TEST",
+ "GRAPHICS_SANITY_TEST_REASON",
+ "HISTORY_LASTVISITED_TREE_QUERY_TIME_MS",
+ "HTTPCONNMGR_TOTAL_SPECULATIVE_CONN",
+ "HTTPCONNMGR_UNUSED_SPECULATIVE_CONN",
+ "HTTPCONNMGR_USED_SPECULATIVE_CONN",
+ "HTTP_CACHE_ENTRY_ALIVE_TIME",
+ "HTTP_CACHE_ENTRY_RELOAD_TIME",
+ "HTTP_CACHE_ENTRY_REUSE_COUNT",
+ "HTTP_CONNECTION_ENTRY_CACHE_HIT_1",
+ "HTTP_CONTENT_ENCODING",
+ "HTTP_PAGE_CACHE_READ_TIME_V2",
+ "HTTP_PAGE_COMPLETE_LOAD_CACHED_V2",
+ "HTTP_PAGE_COMPLETE_LOAD_NET_V2",
+ "HTTP_PAGE_COMPLETE_LOAD_V2",
+ "HTTP_PAGE_FIRST_SENT_TO_LAST_RECEIVED",
+ "HTTP_PAGE_OPEN_TO_FIRST_FROM_CACHE_V2",
+ "HTTP_PAGE_OPEN_TO_FIRST_RECEIVED",
+ "HTTP_PAGE_OPEN_TO_FIRST_SENT",
+ "HTTP_PAGE_REVALIDATION",
+ "HTTP_PROXY_TYPE",
+ "HTTP_REQUEST_PER_CONN",
+ "HTTP_REQUEST_PER_PAGE",
+ "HTTP_REQUEST_PER_PAGE_FROM_CACHE",
+ "HTTP_RESPONSE_VERSION",
+ "HTTP_SUBITEM_FIRST_BYTE_LATENCY_TIME",
+ "HTTP_SUBITEM_OPEN_LATENCY_TIME",
+ "HTTP_SUB_CACHE_READ_TIME_V2",
+ "HTTP_SUB_COMPLETE_LOAD_CACHED_V2",
+ "HTTP_SUB_COMPLETE_LOAD_NET_V2",
+ "HTTP_SUB_COMPLETE_LOAD_V2",
+ "HTTP_SUB_DNS_ISSUE_TIME",
+ "HTTP_SUB_DNS_LOOKUP_TIME",
+ "HTTP_SUB_FIRST_SENT_TO_LAST_RECEIVED",
+ "HTTP_SUB_OPEN_TO_FIRST_FROM_CACHE_V2",
+ "HTTP_SUB_OPEN_TO_FIRST_RECEIVED",
+ "HTTP_SUB_OPEN_TO_FIRST_SENT",
+ "HTTP_SUB_REVALIDATION",
+ "HTTP_TRANSACTION_USE_ALTSVC",
+ "HTTP_TRANSACTION_USE_ALTSVC_OE",
+ "IMAGE_DECODE_CHUNKS",
+ "IMAGE_DECODE_COUNT",
+ "IMAGE_DECODE_LATENCY_US",
+ "IMAGE_DECODE_ON_DRAW_LATENCY",
+ "IMAGE_DECODE_SPEED_GIF",
+ "IMAGE_DECODE_SPEED_JPEG",
+ "IMAGE_DECODE_SPEED_PNG",
+ "IMAGE_DECODE_TIME",
+ "INNERWINDOWS_WITH_MUTATION_LISTENERS",
+ "IPC_SAME_PROCESS_MESSAGE_COPY_OOM_KB",
+ "IPC_TRANSACTION_CANCEL",
+ "IPV4_AND_IPV6_ADDRESS_CONNECTIVITY",
+ "LOCALDOMSTORAGE_CLEAR_BLOCKING_MS",
+ "LOCALDOMSTORAGE_GETALLKEYS_BLOCKING_MS",
+ "LOCALDOMSTORAGE_GETKEY_BLOCKING_MS",
+ "LOCALDOMSTORAGE_GETLENGTH_BLOCKING_MS",
+ "LOCALDOMSTORAGE_GETVALUE_BLOCKING_MS",
+ "LOCALDOMSTORAGE_PRELOAD_PENDING_ON_FIRST_ACCESS",
+ "LOCALDOMSTORAGE_REMOVEKEY_BLOCKING_MS",
+ "LOCALDOMSTORAGE_SESSIONONLY_PRELOAD_BLOCKING_MS",
+ "LOCALDOMSTORAGE_SETVALUE_BLOCKING_MS",
+ "LOCALDOMSTORAGE_SHUTDOWN_DATABASE_MS",
+ "LOCALDOMSTORAGE_UNLOAD_BLOCKING_MS",
+ "LONG_REFLOW_INTERRUPTIBLE",
+ "MAC_INITFONTLIST_TOTAL",
+ "MASTER_PASSWORD_ENABLED",
+ "MEDIA_CODEC_USED",
+ "MEMORY_FREE_PURGED_PAGES_MS",
+ "MEMORY_HEAP_ALLOCATED",
+ "MEMORY_IMAGES_CONTENT_USED_UNCOMPRESSED",
+ "MEMORY_JS_COMPARTMENTS_SYSTEM",
+ "MEMORY_JS_COMPARTMENTS_USER",
+ "MEMORY_JS_GC_HEAP",
+ "MEMORY_STORAGE_SQLITE",
+ "MEMORY_VSIZE",
+ "MEMORY_VSIZE_MAX_CONTIGUOUS",
+ "MIXED_CONTENT_HSTS",
+ "MIXED_CONTENT_PAGE_LOAD",
+ "MIXED_CONTENT_UNBLOCK_COUNTER",
+ "MOZ_SQLITE_COOKIES_OPEN_READAHEAD_MS",
+ "NETWORK_CACHE_HIT_MISS_STAT_PER_CACHE_SIZE",
+ "NETWORK_CACHE_HIT_RATE_PER_CACHE_SIZE",
+ "NETWORK_CACHE_METADATA_FIRST_READ_TIME_MS",
+ "NETWORK_CACHE_METADATA_SECOND_READ_TIME_MS",
+ "NETWORK_CACHE_V1_HIT_TIME_MS",
+ "NETWORK_CACHE_V1_MISS_TIME_MS",
+ "NETWORK_CACHE_V1_TRUNCATE_TIME_MS",
+ "NETWORK_CACHE_V2_INPUT_STREAM_STATUS",
+ "NETWORK_CACHE_V2_OUTPUT_STREAM_STATUS",
+ "NETWORK_DISK_CACHE2_SHUTDOWN_CLEAR_PRIVATE",
+ "NETWORK_DISK_CACHE_DELETEDIR",
+ "NETWORK_DISK_CACHE_DELETEDIR_SHUTDOWN",
+ "NETWORK_DISK_CACHE_SHUTDOWN",
+ "NETWORK_DISK_CACHE_SHUTDOWN_CLEAR_PRIVATE",
+ "NETWORK_DISK_CACHE_SHUTDOWN_V2",
+ "NETWORK_DISK_CACHE_TRASHRENAME",
+ "NEWTAB_PAGE_BLOCKED_SITES_COUNT",
+ "NEWTAB_PAGE_ENABLED",
+ "NEWTAB_PAGE_PINNED_SITES_COUNT",
+ "NTLM_MODULE_USED_2",
+ "PAGE_FAULTS_HARD",
+ "PAINT_BUILD_DISPLAYLIST_TIME",
+ "PAINT_RASTERIZE_TIME",
+ "PERMISSIONS_SQL_CORRUPTED",
+ "PLACES_ANNOS_PAGES_COUNT",
+ "PLACES_AUTOCOMPLETE_1ST_RESULT_TIME_MS",
+ "PLACES_BACKUPS_BOOKMARKSTREE_MS",
+ "PLACES_BACKUPS_DAYSFROMLAST",
+ "PLACES_BACKUPS_TOJSON_MS",
+ "PLACES_BOOKMARKS_COUNT",
+ "PLACES_EXPIRATION_STEPS_TO_CLEAN2",
+ "PLACES_EXPORT_TOHTML_MS",
+ "PLACES_FAVICON_BMP_SIZES",
+ "PLACES_FAVICON_GIF_SIZES",
+ "PLACES_FAVICON_ICO_SIZES",
+ "PLACES_FAVICON_JPEG_SIZES",
+ "PLACES_FAVICON_OTHER_SIZES",
+ "PLACES_FAVICON_PNG_SIZES",
+ "PLACES_FAVICON_SVG_SIZES",
+ "PLACES_HISTORY_LIBRARY_SEARCH_TIME_MS",
+ "PLACES_IDLE_FRECENCY_DECAY_TIME_MS",
+ "PLACES_IDLE_MAINTENANCE_TIME_MS",
+ "PLACES_KEYWORDS_COUNT",
+ "PLACES_MAINTENANCE_DAYSFROMLAST",
+ "PLACES_PAGES_COUNT",
+ "PLACES_SORTED_BOOKMARKS_PERC",
+ "PLACES_TAGGED_BOOKMARKS_PERC",
+ "PLACES_TAGS_COUNT",
+ "PLUGIN_LOAD_METADATA",
+ "PLUGIN_SHUTDOWN_MS",
+ "PRCLOSE_TCP_BLOCKING_TIME_CONNECTIVITY_CHANGE",
+ "PRCLOSE_TCP_BLOCKING_TIME_LINK_CHANGE",
+ "PRCLOSE_TCP_BLOCKING_TIME_NORMAL",
+ "PRCLOSE_TCP_BLOCKING_TIME_OFFLINE",
+ "PRCLOSE_TCP_BLOCKING_TIME_SHUTDOWN",
+ "PRCLOSE_UDP_BLOCKING_TIME_CONNECTIVITY_CHANGE",
+ "PRCLOSE_UDP_BLOCKING_TIME_LINK_CHANGE",
+ "PRCLOSE_UDP_BLOCKING_TIME_NORMAL",
+ "PRCLOSE_UDP_BLOCKING_TIME_OFFLINE",
+ "PRCLOSE_UDP_BLOCKING_TIME_SHUTDOWN",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_CONNECTIVITY_CHANGE",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_LINK_CHANGE",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_NORMAL",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_OFFLINE",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_SHUTDOWN",
+ "PRCONNECT_BLOCKING_TIME_CONNECTIVITY_CHANGE",
+ "PRCONNECT_BLOCKING_TIME_LINK_CHANGE",
+ "PRCONNECT_BLOCKING_TIME_NORMAL",
+ "PRCONNECT_BLOCKING_TIME_OFFLINE",
+ "PRCONNECT_BLOCKING_TIME_SHUTDOWN",
+ "PROCESS_CRASH_SUBMIT_ATTEMPT",
+ "PROCESS_CRASH_SUBMIT_SUCCESS",
+ "PWMGR_NUM_HTTPAUTH_PASSWORDS",
+ "PWMGR_USERNAME_PRESENT",
+ "RANGE_CHECKSUM_ERRORS",
+ "READER_MODE_DOWNLOAD_RESULT",
+ "READER_MODE_PARSE_RESULT",
+ "REFRESH_DRIVER_TICK",
+ "REQUESTS_OF_ORIGINAL_CONTENT",
+ "SAFE_MODE_USAGE",
+ "SEARCH_SERVICE_COUNTRY_FETCH_RESULT",
+ "SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS",
+ "SEARCH_SERVICE_NONUS_COUNTRY_MISMATCHED_PLATFORM_OSX",
+ "SEARCH_SERVICE_NONUS_COUNTRY_MISMATCHED_PLATFORM_WIN",
+ "SEARCH_SERVICE_US_COUNTRY_MISMATCHED_PLATFORM_OSX",
+ "SEARCH_SERVICE_US_COUNTRY_MISMATCHED_PLATFORM_WIN",
+ "SEARCH_SERVICE_US_COUNTRY_MISMATCHED_TIMEZONE",
+ "SEARCH_SERVICE_US_TIMEZONE_MISMATCHED_COUNTRY",
+ "SERVICE_WORKER_CONTROLLED_DOCUMENTS",
+ "SERVICE_WORKER_REGISTRATION_LOADING",
+ "SERVICE_WORKER_SPAWN_ATTEMPTS",
+ "SERVICE_WORKER_UPDATED",
+ "SERVICE_WORKER_WAS_SPAWNED",
+ "SHOULD_AUTO_DETECT_LANGUAGE",
+ "SHOULD_TRANSLATION_UI_APPEAR",
+ "SLOW_SCRIPT_NOTICE_COUNT",
+ "SPDY_CHUNK_RECVD",
+ "SPDY_GOAWAY_LOCAL",
+ "SPDY_GOAWAY_PEER",
+ "SPDY_NPN_CONNECT",
+ "SPDY_NPN_JOIN",
+ "SPDY_PARALLEL_STREAMS",
+ "SPDY_SERVER_INITIATED_STREAMS",
+ "SPDY_SETTINGS_IW",
+ "SPDY_SETTINGS_MAX_STREAMS",
+ "SPDY_SYN_RATIO",
+ "SPDY_SYN_REPLY_RATIO",
+ "SPDY_SYN_REPLY_SIZE",
+ "SPDY_SYN_SIZE",
+ "SSL_NPN_TYPE",
+ "SSL_TLS10_INTOLERANCE_REASON_POST",
+ "SSL_TLS10_INTOLERANCE_REASON_PRE",
+ "SSL_TLS11_INTOLERANCE_REASON_POST",
+ "SSL_TLS11_INTOLERANCE_REASON_PRE",
+ "SSL_TLS12_INTOLERANCE_REASON_POST",
+ "SSL_TLS12_INTOLERANCE_REASON_PRE",
+ "SSL_VERSION_FALLBACK_INAPPROPRIATE",
+ "STARTUP_CRASH_DETECTED",
+ "STS_POLL_AND_EVENTS_CYCLE",
+ "STS_POLL_AND_EVENT_THE_LAST_CYCLE",
+ "STS_POLL_BLOCK_TIME",
+ "STS_POLL_CYCLE",
+ "SUBPROCESS_ABNORMAL_ABORT",
+ "SUBPROCESS_CRASHES_WITH_DUMP",
+ "SYSTEM_FONT_FALLBACK",
+ "SYSTEM_FONT_FALLBACK_FIRST",
+ "SYSTEM_FONT_FALLBACK_SCRIPT",
+ "TELEMETRY_COMPRESS",
+ "TELEMETRY_DISCARDED_ARCHIVED_PINGS_SIZE_MB",
+ "TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB",
+ "TELEMETRY_DISCARDED_SEND_PINGS_SIZE_MB",
+ "TELEMETRY_INVALID_PING_TYPE_SUBMITTED",
+ "TELEMETRY_PENDING_CHECKING_OVER_QUOTA_MS",
+ "TELEMETRY_PENDING_EVICTING_OVER_QUOTA_MS",
+ "TELEMETRY_PENDING_LOAD_FAILURE_PARSE",
+ "TELEMETRY_PENDING_LOAD_FAILURE_READ",
+ "TELEMETRY_PENDING_PINGS_AGE",
+ "TELEMETRY_PENDING_PINGS_EVICTED_OVER_QUOTA",
+ "TELEMETRY_PENDING_PINGS_SIZE_MB",
+ "TELEMETRY_PING_EVICTED_FOR_SERVER_ERRORS",
+ "TELEMETRY_PING_SIZE_EXCEEDED_ARCHIVED",
+ "TELEMETRY_PING_SIZE_EXCEEDED_PENDING",
+ "TELEMETRY_PING_SIZE_EXCEEDED_SEND",
+ "TELEMETRY_SESSIONDATA_FAILED_LOAD",
+ "TELEMETRY_SESSIONDATA_FAILED_PARSE",
+ "TELEMETRY_SESSIONDATA_FAILED_SAVE",
+ "TELEMETRY_SESSIONDATA_FAILED_VALIDATION",
+ "TELEMETRY_STRINGIFY",
+ "TELEMETRY_SUCCESS",
+ "TELEMETRY_TEST_COUNT",
+ "TELEMETRY_TEST_COUNT_INIT_NO_RECORD",
+ "TELEMETRY_TEST_EXPIRED",
+ "TELEMETRY_TEST_FLAG",
+ "TELEMETRY_TEST_KEYED_COUNT",
+ "TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD",
+ "TELEMETRY_TEST_KEYED_FLAG",
+ "TELEMETRY_TEST_KEYED_RELEASE_OPTIN",
+ "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT",
+ "TELEMETRY_TEST_RELEASE_OPTIN",
+ "TELEMETRY_TEST_RELEASE_OPTOUT",
+ "THUNDERBIRD_GLODA_SIZE_MB",
+ "THUNDERBIRD_INDEXING_RATE_MSG_PER_S",
+ "TOTAL_COUNT_HIGH_ERRORS",
+ "TOTAL_COUNT_LOW_ERRORS",
+ "TRANSLATED_CHARACTERS",
+ "TRANSLATED_PAGES",
+ "TRANSLATED_PAGES_BY_LANGUAGE",
+ "TRANSLATION_OPPORTUNITIES",
+ "TRANSLATION_OPPORTUNITIES_BY_LANGUAGE",
+ "URLCLASSIFIER_CL_CHECK_TIME",
+ "URLCLASSIFIER_LC_COMPLETIONS",
+ "URLCLASSIFIER_LC_PREFIXES",
+ "URLCLASSIFIER_PS_CONSTRUCT_TIME",
+ "URLCLASSIFIER_PS_FALLOCATE_TIME",
+ "URLCLASSIFIER_PS_FILELOAD_TIME",
+ "WEAVE_COMPLETE_SUCCESS_COUNT",
+ "WEAVE_START_COUNT",
+ "WEBCRYPTO_ALG",
+ "WEBCRYPTO_EXTRACTABLE_ENC",
+ "WEBCRYPTO_EXTRACTABLE_GENERATE",
+ "WEBCRYPTO_EXTRACTABLE_IMPORT",
+ "WEBCRYPTO_EXTRACTABLE_SIG",
+ "WEBCRYPTO_METHOD",
+ "WEBCRYPTO_RESOLVED",
+ "WEBFONT_COMPRESSION_WOFF",
+ "WEBFONT_COMPRESSION_WOFF2",
+ "WEBFONT_DOWNLOAD_TIME",
+ "WEBFONT_FONTTYPE",
+ "WEBFONT_PER_PAGE",
+ "WEBFONT_SIZE",
+ "WEBFONT_SIZE_PER_PAGE",
+ "WEBFONT_SRCTYPE",
+ "WEBRTC_AUDIO_QUALITY_INBOUND_BANDWIDTH_KBITS",
+ "WEBRTC_AUDIO_QUALITY_INBOUND_JITTER",
+ "WEBRTC_AUDIO_QUALITY_INBOUND_PACKETLOSS_RATE",
+ "WEBRTC_AUDIO_QUALITY_OUTBOUND_BANDWIDTH_KBITS",
+ "WEBRTC_AUDIO_QUALITY_OUTBOUND_JITTER",
+ "WEBRTC_AUDIO_QUALITY_OUTBOUND_PACKETLOSS_RATE",
+ "WEBRTC_AUDIO_QUALITY_OUTBOUND_RTT",
+ "WEBRTC_AVSYNC_WHEN_AUDIO_LAGS_VIDEO_MS",
+ "WEBRTC_AVSYNC_WHEN_VIDEO_LAGS_AUDIO_MS",
+ "WEBRTC_CALL_TYPE",
+ "WEBRTC_DATACHANNEL_NEGOTIATED",
+ "WEBRTC_GET_USER_MEDIA_TYPE",
+ "WEBRTC_LOAD_STATE_NORMAL",
+ "WEBRTC_LOAD_STATE_NORMAL_SHORT",
+ "WEBRTC_LOAD_STATE_RELAXED",
+ "WEBRTC_LOAD_STATE_RELAXED_SHORT",
+ "WEBRTC_LOAD_STATE_STRESSED",
+ "WEBRTC_LOAD_STATE_STRESSED_SHORT",
+ "WEBRTC_MAX_AUDIO_RECEIVE_TRACK",
+ "WEBRTC_MAX_AUDIO_SEND_TRACK",
+ "WEBRTC_MAX_VIDEO_RECEIVE_TRACK",
+ "WEBRTC_MAX_VIDEO_SEND_TRACK",
+ "WEBRTC_RENEGOTIATIONS",
+ "WEBRTC_VIDEO_DECODER_BITRATE_AVG_PER_CALL_KBPS",
+ "WEBRTC_VIDEO_DECODER_BITRATE_STD_DEV_PER_CALL_KBPS",
+ "WEBRTC_VIDEO_DECODER_DISCARDED_PACKETS_PER_CALL_PPM",
+ "WEBRTC_VIDEO_DECODER_FRAMERATE_10X_STD_DEV_PER_CALL",
+ "WEBRTC_VIDEO_DECODER_FRAMERATE_AVG_PER_CALL",
+ "WEBRTC_VIDEO_DECODE_ERROR_TIME_PERMILLE",
+ "WEBRTC_VIDEO_ENCODER_BITRATE_AVG_PER_CALL_KBPS",
+ "WEBRTC_VIDEO_ENCODER_BITRATE_STD_DEV_PER_CALL_KBPS",
+ "WEBRTC_VIDEO_ENCODER_DROPPED_FRAMES_PER_CALL_FPM",
+ "WEBRTC_VIDEO_ENCODER_FRAMERATE_10X_STD_DEV_PER_CALL",
+ "WEBRTC_VIDEO_ENCODER_FRAMERATE_AVG_PER_CALL",
+ "WEBRTC_VIDEO_ERROR_RECOVERY_MS",
+ "WEBRTC_VIDEO_QUALITY_INBOUND_BANDWIDTH_KBITS",
+ "WEBRTC_VIDEO_QUALITY_INBOUND_JITTER",
+ "WEBRTC_VIDEO_QUALITY_INBOUND_PACKETLOSS_RATE",
+ "WEBRTC_VIDEO_QUALITY_OUTBOUND_BANDWIDTH_KBITS",
+ "WEBRTC_VIDEO_QUALITY_OUTBOUND_JITTER",
+ "WEBRTC_VIDEO_QUALITY_OUTBOUND_PACKETLOSS_RATE",
+ "WEBRTC_VIDEO_QUALITY_OUTBOUND_RTT",
+ "WEBRTC_VIDEO_RECOVERY_AFTER_ERROR_PER_MIN",
+ "WEBRTC_VIDEO_RECOVERY_BEFORE_ERROR_PER_MIN",
+ "WEBSOCKETS_HANDSHAKE_TYPE",
+ "XMLHTTPREQUEST_ASYNC_OR_SYNC"
+ ],
+ "n_buckets": [
+ "MEMORY_JS_GC_HEAP",
+ "MEMORY_HEAP_ALLOCATED",
+ "SYSTEM_FONT_FALLBACK_SCRIPT",
+ "HTTP_REQUEST_PER_PAGE_FROM_CACHE",
+ "SSL_TIME_UNTIL_READY",
+ "SSL_TIME_UNTIL_READY_FIRST_TRY",
+ "SSL_TIME_UNTIL_READY_CONSERVATIVE",
+ "SSL_TIME_UNTIL_READY_ECH",
+ "SSL_TIME_UNTIL_READY_ECH_GREASE",
+ "SSL_TIME_UNTIL_HANDSHAKE_FINISHED_KEYED_BY_KA",
+ "CERT_VALIDATION_HTTP_REQUEST_CANCELED_TIME",
+ "CERT_VALIDATION_HTTP_REQUEST_SUCCEEDED_TIME",
+ "CERT_VALIDATION_HTTP_REQUEST_FAILED_TIME",
+ "SPDY_SERVER_INITIATED_STREAMS",
+ "STS_POLL_AND_EVENTS_CYCLE",
+ "STS_POLL_CYCLE",
+ "STS_POLL_AND_EVENT_THE_LAST_CYCLE",
+ "STS_POLL_BLOCK_TIME",
+ "PRCONNECT_BLOCKING_TIME_NORMAL",
+ "PRCONNECT_BLOCKING_TIME_SHUTDOWN",
+ "PRCONNECT_BLOCKING_TIME_CONNECTIVITY_CHANGE",
+ "PRCONNECT_BLOCKING_TIME_LINK_CHANGE",
+ "PRCONNECT_BLOCKING_TIME_OFFLINE",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_NORMAL",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_SHUTDOWN",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_CONNECTIVITY_CHANGE",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_LINK_CHANGE",
+ "PRCONNECTCONTINUE_BLOCKING_TIME_OFFLINE",
+ "PRCLOSE_TCP_BLOCKING_TIME_NORMAL",
+ "PRCLOSE_TCP_BLOCKING_TIME_SHUTDOWN",
+ "PRCLOSE_TCP_BLOCKING_TIME_CONNECTIVITY_CHANGE",
+ "PRCLOSE_TCP_BLOCKING_TIME_LINK_CHANGE",
+ "PRCLOSE_TCP_BLOCKING_TIME_OFFLINE",
+ "PRCLOSE_UDP_BLOCKING_TIME_NORMAL",
+ "PRCLOSE_UDP_BLOCKING_TIME_SHUTDOWN",
+ "PRCLOSE_UDP_BLOCKING_TIME_CONNECTIVITY_CHANGE",
+ "PRCLOSE_UDP_BLOCKING_TIME_LINK_CHANGE",
+ "PRCLOSE_UDP_BLOCKING_TIME_OFFLINE",
+ "UPDATE_PREF_UPDATE_CANCELATIONS_EXTERNAL",
+ "UPDATE_PREF_UPDATE_CANCELATIONS_NOTIFY",
+ "UPDATE_STATUS_ERROR_CODE_COMPLETE_STARTUP",
+ "UPDATE_STATUS_ERROR_CODE_PARTIAL_STARTUP",
+ "UPDATE_STATUS_ERROR_CODE_UNKNOWN_STARTUP",
+ "UPDATE_STATUS_ERROR_CODE_COMPLETE_STAGE",
+ "UPDATE_STATUS_ERROR_CODE_PARTIAL_STAGE",
+ "UPDATE_STATUS_ERROR_CODE_UNKNOWN_STAGE",
+ "SECURITY_UI",
+ "CRASH_STORE_COMPRESSED_BYTES",
+ "WEBRTC_AVSYNC_WHEN_AUDIO_LAGS_VIDEO_MS",
+ "WEBRTC_AVSYNC_WHEN_VIDEO_LAGS_AUDIO_MS",
+ "WEBRTC_VIDEO_QUALITY_INBOUND_BANDWIDTH_KBITS",
+ "WEBRTC_AUDIO_QUALITY_INBOUND_BANDWIDTH_KBITS",
+ "WEBRTC_VIDEO_QUALITY_OUTBOUND_BANDWIDTH_KBITS",
+ "WEBRTC_AUDIO_QUALITY_OUTBOUND_BANDWIDTH_KBITS",
+ "WEBRTC_AUDIO_QUALITY_INBOUND_JITTER",
+ "WEBRTC_VIDEO_QUALITY_OUTBOUND_JITTER",
+ "WEBRTC_AUDIO_QUALITY_OUTBOUND_JITTER",
+ "WEBRTC_VIDEO_ERROR_RECOVERY_MS",
+ "WEBRTC_VIDEO_RECOVERY_BEFORE_ERROR_PER_MIN",
+ "WEBRTC_VIDEO_RECOVERY_AFTER_ERROR_PER_MIN",
+ "WEBRTC_VIDEO_QUALITY_OUTBOUND_RTT",
+ "WEBRTC_AUDIO_QUALITY_OUTBOUND_RTT",
+ "WEBRTC_CALL_DURATION",
+ "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE_LOCAL_MS",
+ "DEVTOOLS_DEBUGGER_DISPLAY_SOURCE_REMOTE_MS",
+ "DEVTOOLS_SAVE_HEAP_SNAPSHOT_MS",
+ "DEVTOOLS_READ_HEAP_SNAPSHOT_MS",
+ "DEVTOOLS_HEAP_SNAPSHOT_NODE_COUNT",
+ "DEVTOOLS_HEAP_SNAPSHOT_EDGE_COUNT",
+ "NETWORK_CACHE_HIT_RATE_PER_CACHE_SIZE",
+ "SSL_HANDSHAKE_RESULT",
+ "SSL_HANDSHAKE_RESULT_FIRST_TRY",
+ "SSL_HANDSHAKE_RESULT_CONSERVATIVE",
+ "SSL_HANDSHAKE_RESULT_ECH",
+ "SSL_HANDSHAKE_RESULT_ECH_GREASE",
+ "SSL_REASONS_FOR_NOT_FALSE_STARTING",
+ "SSL_CERT_VERIFICATION_ERRORS",
+ "CERT_VALIDATION_SUCCESS_BY_CA",
+ "CERT_PINNING_FAILURES_BY_CA",
+ "CERT_PINNING_MOZ_RESULTS_BY_HOST",
+ "CERT_PINNING_MOZ_TEST_RESULTS_BY_HOST",
+ "GFX_CRASH",
+ "SSL_CT_POLICY_COMPLIANT_CONNECTIONS_BY_CA",
+ "SSL_CT_POLICY_NON_COMPLIANT_CONNECTIONS_BY_CA",
+ "FX_PAGE_LOAD_MS_2",
+ "FX_PAGE_RELOAD_NORMAL_MS",
+ "FX_PAGE_RELOAD_SKIP_CACHE_MS"
+ ],
+ "expiry_default": [
+ "IDLE_NOTIFY_IDLE_MS",
+ "GEOLOCATION_OSX_SOURCE_IS_MLS",
+ "TRANSLATION_OPPORTUNITIES_BY_LANGUAGE",
+ "FX_THUMBNAILS_BG_CAPTURE_DONE_REASON_2",
+ "TRANSLATED_PAGES",
+ "FX_SESSION_RESTORE_SEND_UPDATE_CAUSED_OOM",
+ "FX_THUMBNAILS_BG_QUEUE_SIZE_ON_CAPTURE",
+ "AUTO_REJECTED_TRANSLATION_OFFERS",
+ "TRANSLATED_CHARACTERS",
+ "NEWTAB_PAGE_ENABLED",
+ "SHOULD_TRANSLATION_UI_APPEAR",
+ "FX_TOTAL_TOP_VISITS",
+ "FX_SESSION_RESTORE_NUMBER_OF_EAGER_TABS_RESTORED",
+ "FX_THUMBNAILS_BG_CAPTURE_QUEUE_TIME_MS",
+ "NEWTAB_PAGE_PINNED_SITES_COUNT",
+ "WEAVE_COMPLETE_SUCCESS_COUNT",
+ "CHANGES_OF_TARGET_LANGUAGE",
+ "TRANSLATION_OPPORTUNITIES",
+ "NEWTAB_PAGE_BLOCKED_SITES_COUNT",
+ "FX_SESSION_RESTORE_NUMBER_OF_TABS_RESTORED",
+ "WEAVE_START_COUNT",
+ "FX_SESSION_RESTORE_CORRUPT_FILE",
+ "FX_TAB_CLICK_MS",
+ "LOCALDOMSTORAGE_GETVALUE_BLOCKING_MS",
+ "FX_SESSION_RESTORE_READ_FILE_MS",
+ "CHANGES_OF_DETECTED_LANGUAGE",
+ "FX_SESSION_RESTORE_AUTO_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS",
+ "FX_SESSION_RESTORE_COLLECT_DATA_MS",
+ "FX_SESSION_RESTORE_FILE_SIZE_BYTES",
+ "TRANSLATED_PAGES_BY_LANGUAGE",
+ "LOCALDOMSTORAGE_SHUTDOWN_DATABASE_MS",
+ "FX_SESSION_RESTORE_NUMBER_OF_WINDOWS_RESTORED",
+ "FX_THUMBNAILS_BG_CAPTURE_CANVAS_DRAW_TIME_MS",
+ "FX_SESSION_RESTORE_WRITE_FILE_MS",
+ "FX_THUMBNAILS_BG_CAPTURE_PAGE_LOAD_TIME_MS",
+ "REQUESTS_OF_ORIGINAL_CONTENT",
+ "FX_THUMBNAILS_BG_CAPTURE_SERVICE_TIME_MS",
+ "PLACES_BACKUPS_TOJSON_MS",
+ "A11Y_ISIMPLEDOM_USAGE_FLAG",
+ "FX_SESSION_RESTORE_MANUAL_RESTORE_DURATION_UNTIL_EAGER_TABS_RESTORED_MS",
+ "FX_SESSION_RESTORE_ALL_FILES_CORRUPT",
+ "SHOULD_AUTO_DETECT_LANGUAGE",
+ "A11Y_IATABLE_USAGE_FLAG",
+ "LOCALDOMSTORAGE_PRELOAD_PENDING_ON_FIRST_ACCESS",
+ "DENIED_TRANSLATION_OFFERS",
+ "PAGE_FAULTS_HARD",
+ "BROWSERPROVIDER_XUL_IMPORT_BOOKMARKS",
+ "GEOLOCATION_WIN8_SOURCE_IS_MLS"
+ ],
+ "kind": [
+ "A11Y_IATABLE_USAGE_FLAG",
+ "A11Y_INSTANTIATED_FLAG",
+ "A11Y_ISIMPLEDOM_USAGE_FLAG",
+ "AUTO_REJECTED_TRANSLATION_OFFERS",
+ "CANVAS_WEBGL_ACCL_FAILURE_ID",
+ "CANVAS_WEBGL_FAILURE_ID",
+ "CHANGES_OF_TARGET_LANGUAGE",
+ "COMPONENTS_SHIM_ACCESSED_BY_CONTENT",
+ "CONTENT_DOCUMENTS_DESTROYED",
+ "CYCLE_COLLECTOR_OOM",
+ "CYCLE_COLLECTOR_WORKER_OOM",
+ "D3D11_COMPOSITING_FAILURE_ID",
+ "D3D11_SYNC_HANDLE_FAILURE",
+ "DEDICATED_WORKER_SPAWN_GETS_QUEUED",
+ "DEDICATED_WORKER_DESTROYED",
+ "DEFECTIVE_PERMISSIONS_SQL_REMOVED",
+ "DENIED_TRANSLATION_OFFERS",
+ "DEVTOOLS_ABOUTDEBUGGING_OPENED_COUNT",
+ "DEVTOOLS_ANIMATIONINSPECTOR_OPENED_COUNT",
+ "DEVTOOLS_BROWSERCONSOLE_OPENED_COUNT",
+ "DEVTOOLS_COMPATIBILITYVIEW_OPENED_COUNT",
+ "DEVTOOLS_COMPUTEDVIEW_OPENED_COUNT",
+ "DEVTOOLS_CUSTOM_OPENED_COUNT",
+ "DEVTOOLS_DOM_OPENED_COUNT",
+ "DEVTOOLS_EYEDROPPER_OPENED_COUNT",
+ "DEVTOOLS_FONTINSPECTOR_OPENED_COUNT",
+ "DEVTOOLS_INSPECTOR_OPENED_COUNT",
+ "DEVTOOLS_JSBROWSERDEBUGGER_OPENED_COUNT",
+ "DEVTOOLS_JSDEBUGGER_OPENED_COUNT",
+ "DEVTOOLS_JSPROFILER_OPENED_COUNT",
+ "DEVTOOLS_LAYOUTVIEW_OPENED_COUNT",
+ "DEVTOOLS_MEMORY_OPENED_COUNT",
+ "DEVTOOLS_MENU_EYEDROPPER_OPENED_COUNT",
+ "DEVTOOLS_NETMONITOR_OPENED_COUNT",
+ "DEVTOOLS_OPTIONS_OPENED_COUNT",
+ "DEVTOOLS_PICKER_EYEDROPPER_OPENED_COUNT",
+ "DEVTOOLS_RESPONSIVE_OPENED_COUNT",
+ "DEVTOOLS_RULEVIEW_OPENED_COUNT",
+ "DEVTOOLS_STORAGE_OPENED_COUNT",
+ "DEVTOOLS_STYLEEDITOR_OPENED_COUNT",
+ "DEVTOOLS_TOOLBOX_OPENED_COUNT",
+ "DEVTOOLS_WEBCONSOLE_OPENED_COUNT",
+ "FX_CONTENT_CRASH_DUMP_UNAVAILABLE",
+ "FX_CONTENT_CRASH_NOT_SUBMITTED",
+ "FX_SESSION_RESTORE_SEND_UPDATE_CAUSED_OOM",
+ "GEOLOCATION_ERROR",
+ "MASTER_PASSWORD_ENABLED",
+ "MEDIA_CODEC_USED",
+ "NETWORK_CONNECTION_COUNT",
+ "OPENGL_COMPOSITING_FAILURE_ID",
+ "PERMISSIONS_SQL_CORRUPTED",
+ "PROCESS_CRASH_SUBMIT_ATTEMPT",
+ "PUSH_API_NOTIFY",
+ "REQUESTS_OF_ORIGINAL_CONTENT",
+ "SANDBOX_REJECTED_SYSCALLS",
+ "SEARCH_COUNTS",
+ "SEARCH_SERVICE_US_COUNTRY_MISMATCHED_TIMEZONE",
+ "SEARCH_SERVICE_US_TIMEZONE_MISMATCHED_COUNTRY",
+ "SERVICE_WORKER_CONTROLLED_DOCUMENTS",
+ "SERVICE_WORKER_SPAWN_ATTEMPTS",
+ "SERVICE_WORKER_SPAWN_GETS_QUEUED",
+ "SERVICE_WORKER_UPDATED",
+ "SERVICE_WORKER_WAS_SPAWNED",
+ "SERVICE_WORKER_DESTROYED",
+ "SHARED_WORKER_SPAWN_GETS_QUEUED",
+ "SHARED_WORKER_DESTROYED",
+ "SHOULD_AUTO_DETECT_LANGUAGE",
+ "SHOULD_TRANSLATION_UI_APPEAR",
+ "SLOW_SCRIPT_NOTICE_COUNT",
+ "SLOW_SCRIPT_PAGE_COUNT",
+ "STARTUP_CRASH_DETECTED",
+ "SUBPROCESS_ABNORMAL_ABORT",
+ "SUBPROCESS_CRASHES_WITH_DUMP",
+ "SUBPROCESS_KILL_HARD",
+ "SUBPROCESS_LAUNCH_FAILURE",
+ "TELEMETRY_ARCHIVE_SESSION_PING_COUNT",
+ "TELEMETRY_ASSEMBLE_PAYLOAD_EXCEPTION",
+ "TELEMETRY_INVALID_PAYLOAD_SUBMITTED",
+ "TELEMETRY_INVALID_PING_TYPE_SUBMITTED",
+ "TELEMETRY_PENDING_LOAD_FAILURE_PARSE",
+ "TELEMETRY_PENDING_LOAD_FAILURE_READ",
+ "TELEMETRY_PING_EVICTED_FOR_SERVER_ERRORS",
+ "TELEMETRY_PING_SIZE_EXCEEDED_ARCHIVED",
+ "TELEMETRY_PING_SIZE_EXCEEDED_PENDING",
+ "TELEMETRY_PING_SIZE_EXCEEDED_SEND",
+ "TELEMETRY_PING_SUBMISSION_WAITING_CLIENTID",
+ "TELEMETRY_SESSIONDATA_FAILED_LOAD",
+ "TELEMETRY_SESSIONDATA_FAILED_PARSE",
+ "TELEMETRY_SESSIONDATA_FAILED_SAVE",
+ "TELEMETRY_SESSIONDATA_FAILED_VALIDATION",
+ "TELEMETRY_TEST_COUNT",
+ "TELEMETRY_TEST_COUNT2",
+ "TELEMETRY_TEST_COUNT_INIT_NO_RECORD",
+ "TELEMETRY_TEST_EXPIRED",
+ "TELEMETRY_TEST_FLAG",
+ "TELEMETRY_TEST_FLAG_CONTENT_PROCESS",
+ "TELEMETRY_TEST_FLAG_MAIN_PROCESS",
+ "TELEMETRY_TEST_KEYED_COUNT",
+ "TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD",
+ "TELEMETRY_TEST_KEYED_FLAG",
+ "TELEMETRY_TEST_KEYED_RELEASE_OPTIN",
+ "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT",
+ "TELEMETRY_TEST_RELEASE_OPTIN",
+ "TELEMETRY_TEST_RELEASE_OPTOUT",
+ "TOP_LEVEL_CONTENT_DOCUMENTS_DESTROYED",
+ "TRANSLATED_PAGES",
+ "TRANSLATED_PAGES_BY_LANGUAGE",
+ "UPDATE_CANNOT_STAGE_EXTERNAL",
+ "UPDATE_CANNOT_STAGE_NOTIFY",
+ "UPDATE_CANNOT_STAGE_SUBSEQUENT",
+ "UPDATE_CHECK_EXTENDED_ERROR_EXTERNAL",
+ "UPDATE_CHECK_EXTENDED_ERROR_NOTIFY",
+ "UPDATE_CHECK_EXTENDED_ERROR_SUBSEQUENT",
+ "UPDATE_CHECK_NO_UPDATE_EXTERNAL",
+ "UPDATE_CHECK_NO_UPDATE_NOTIFY",
+ "UPDATE_CHECK_NO_UPDATE_SUBSEQUENT",
+ "UPDATE_INVALID_LASTUPDATETIME_EXTERNAL",
+ "UPDATE_INVALID_LASTUPDATETIME_NOTIFY",
+ "UPDATE_INVALID_LASTUPDATETIME_SUBSEQUENT",
+ "UPDATE_NOT_PREF_UPDATE_AUTO_EXTERNAL",
+ "UPDATE_NOT_PREF_UPDATE_AUTO_NOTIFY",
+ "UPDATE_NOT_PREF_UPDATE_AUTO_SUBSEQUENT",
+ "UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_EXTERNAL",
+ "UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_NOTIFY",
+ "UPDATE_NOT_PREF_UPDATE_SERVICE_ENABLED_SUBSEQUENT",
+ "UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_EXTERNAL",
+ "UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_NOTIFY",
+ "UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_SUBSEQUENT",
+ "UPDATE_PING_COUNT_EXTERNAL",
+ "UPDATE_PING_COUNT_NOTIFY",
+ "UPDATE_PING_COUNT_SUBSEQUENT",
+ "UPDATE_SERVICE_MANUALLY_UNINSTALLED_EXTERNAL",
+ "UPDATE_SERVICE_MANUALLY_UNINSTALLED_NOTIFY",
+ "UPDATE_SERVICE_MANUALLY_UNINSTALLED_SUBSEQUENT",
+ "UPDATE_UNABLE_TO_APPLY_EXTERNAL",
+ "UPDATE_UNABLE_TO_APPLY_NOTIFY",
+ "UPDATE_UNABLE_TO_APPLY_SUBSEQUENT",
+ "WEBFONT_PER_PAGE",
+ "WEBRTC_CALL_COUNT_3"
+ ]
+}
diff --git a/toolkit/components/telemetry/metrics.yaml b/toolkit/components/telemetry/metrics.yaml
new file mode 100644
index 0000000000..a03170e266
--- /dev/null
+++ b/toolkit/components/telemetry/metrics.yaml
@@ -0,0 +1,44 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# This file is for Internal Telemetry Use Only.
+# Please don't add anything here unless you have the permission of a
+# Telemetry Module Peer.
+
+---
+$schema: moz://mozilla.org/schemas/glean/metrics/2-0-0
+$tags:
+ - "Toolkit :: Telemetry"
+
+legacy.telemetry:
+ client_id:
+ type: uuid
+ lifetime: application
+ description: |
+ The client_id according to Telemetry.
+ Might not always have a value due to being too early for it to have
+ loaded.
+ Value may be the canary client id `c0ffeec0-ffee-c0ff-eec0-ffeec0ffeec0`
+ in pings near when the data upload pref is disabled (if Telemetry gets
+ to go first), or between when a client_id has been removed and when it
+ has been regenerated.
+ Does not need to be sent in the Glean "deletion-request" ping.
+ bugs:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1755549
+ data_reviews:
+ - https://bugzilla.mozilla.org/show_bug.cgi?id=1755549
+ data_sensitivity:
+ - technical
+ - highly_sensitive
+ notification_emails:
+ - chutten@mozilla.com
+ - glean-team@mozilla.com
+ expires: never
+ no_lint:
+ - BASELINE_PING
+ send_in_pings:
+ - 'metrics'
+ - 'events'
+ - 'newtab'
+ - 'baseline'
diff --git a/toolkit/components/telemetry/moz.build b/toolkit/components/telemetry/moz.build
new file mode 100644
index 0000000000..718120cf78
--- /dev/null
+++ b/toolkit/components/telemetry/moz.build
@@ -0,0 +1,275 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+include("/ipc/chromium/chromium-config.mozbuild")
+
+FINAL_LIBRARY = "xul"
+
+DIRS = [
+ "pingsender",
+]
+
+if CONFIG["COMPILE_ENVIRONMENT"]:
+ EXPORTS.mozilla += ["!dap_ffi_generated.h"]
+
+ CbindgenHeader("dap_ffi_generated.h", inputs=["dap/ffi"])
+
+DEFINES["MOZ_APP_VERSION"] = '"%s"' % CONFIG["MOZ_APP_VERSION"]
+
+LOCAL_INCLUDES += [
+ "/xpcom/build",
+ "/xpcom/threads",
+]
+
+SPHINX_TREES["/toolkit/components/telemetry"] = "docs"
+
+with Files("docs/**"):
+ SCHEDULES.exclusive = ["docs"]
+
+if CONFIG["ENABLE_TESTS"]:
+ # We used to need GeckoView tests as a separate directory. This
+ # is no longer true and we could probably move it to tests/gtest.
+ DIRS += ["geckoview/gtest", "tests/gtest"]
+
+TEST_DIRS += ["tests", "dap/ffi-gtest"]
+
+XPCSHELL_TESTS_MANIFESTS += [
+ "dap/tests/xpcshell/xpcshell.toml",
+ "tests/unit/xpcshell.toml",
+]
+BROWSER_CHROME_MANIFESTS += ["tests/browser/browser.toml"]
+
+XPIDL_SOURCES += [
+ "core/nsITelemetry.idl",
+ "dap/nsIDAPTelemetry.idl",
+]
+
+XPIDL_MODULE = "telemetry"
+
+EXPORTS.mozilla += [
+ "!TelemetryEventEnums.h",
+ "!TelemetryHistogramEnums.h",
+ "!TelemetryProcessEnums.h",
+ "!TelemetryScalarEnums.h",
+ "core/ipc/TelemetryComms.h",
+ "core/ipc/TelemetryIPC.h",
+ "core/Telemetry.h",
+ "dap/DAPTelemetry.h",
+ "dap/DAPTelemetryBindings.h",
+ "other/CombinedStacks.h",
+ "other/ProcessedStack.h",
+]
+
+EXPORTS.mozilla.telemetry += [
+ "core/Stopwatch.h",
+]
+
+SOURCES += [
+ "core/ipc/TelemetryIPC.cpp",
+ "core/ipc/TelemetryIPCAccumulator.cpp",
+ "core/Stopwatch.cpp",
+ "core/Telemetry.cpp",
+ "core/TelemetryCommon.cpp",
+ "core/TelemetryEvent.cpp",
+ "core/TelemetryHistogram.cpp",
+ "core/TelemetryScalar.cpp",
+ "core/TelemetryUserInteraction.cpp",
+ "dap/DAPTelemetry.cpp",
+ "geckoview/streaming/GeckoViewStreamingTelemetry.cpp",
+ "other/CombinedStacks.cpp",
+ "other/ProcessedStack.cpp",
+ "other/TelemetryIOInterposeObserver.cpp",
+]
+
+if CONFIG["OS_ARCH"] == "WINNT":
+ SOURCES += [
+ "other/UntrustedModules.cpp",
+ "other/UntrustedModulesBackupService.cpp",
+ "other/UntrustedModulesDataSerializer.cpp",
+ ]
+
+XPCOM_MANIFESTS += [
+ "components.conf",
+ "core/components.conf",
+ "dap/components.conf",
+]
+
+EXTRA_COMPONENTS += ["TelemetryStartup.manifest"]
+
+EXTRA_JS_MODULES += [
+ "app/ClientID.sys.mjs",
+ "app/TelemetryArchive.sys.mjs",
+ "app/TelemetryController.sys.mjs",
+ "app/TelemetryControllerBase.sys.mjs",
+ "app/TelemetryControllerContent.sys.mjs",
+ "app/TelemetryControllerParent.sys.mjs",
+ "app/TelemetryEnvironment.sys.mjs",
+ "app/TelemetryReportingPolicy.sys.mjs",
+ "app/TelemetryScheduler.sys.mjs",
+ "app/TelemetrySend.sys.mjs",
+ "app/TelemetryStorage.sys.mjs",
+ "app/TelemetryTimestamps.sys.mjs",
+ "app/TelemetryUtils.sys.mjs",
+ "dap/DAPTelemetrySender.sys.mjs",
+ "dap/DAPVisitCounter.sys.mjs",
+ "pings/CoveragePing.sys.mjs",
+ "pings/EventPing.sys.mjs",
+ "pings/HealthPing.sys.mjs",
+ "pings/ModulesPing.sys.mjs",
+ "pings/TelemetrySession.sys.mjs",
+ "pings/UntrustedModulesPing.sys.mjs",
+ "pings/UpdatePing.sys.mjs",
+ "TelemetryStartup.sys.mjs",
+]
+
+EXTRA_JS_MODULES.backgroundtasks += [
+ "pings/BackgroundTask_pingsender.sys.mjs",
+]
+
+if CONFIG["OS_ARCH"] == "WINNT":
+ EXTRA_JS_MODULES += [
+ "pings/UninstallPing.sys.mjs",
+ ]
+
+TESTING_JS_MODULES += [
+ "tests/unit/TelemetryArchiveTesting.sys.mjs",
+ "tests/unit/TelemetryEnvironmentTesting.sys.mjs",
+]
+
+PYTHON_UNITTEST_MANIFESTS += [
+ "tests/integration/tests/python.toml",
+ "tests/python/python.toml",
+]
+
+# Generate histogram files.
+histogram_files = [
+ "Histograms.json",
+]
+if CONFIG["MOZ_TELEMETRY_EXTRA_HISTOGRAM_FILES"]:
+ histogram_files.extend(CONFIG["MOZ_TELEMETRY_EXTRA_HISTOGRAM_FILES"])
+
+GeneratedFile(
+ "TelemetryHistogramData.inc",
+ script="build_scripts/gen_histogram_data.py",
+ inputs=histogram_files,
+)
+GeneratedFile(
+ "TelemetryHistogramEnums.h",
+ script="build_scripts/gen_histogram_enum.py",
+ inputs=histogram_files,
+)
+GeneratedFile(
+ "TelemetryHistogramNameMap.h",
+ script="build_scripts/gen_histogram_phf.py",
+ inputs=histogram_files,
+)
+
+# Generate scalar files.
+scalar_files = [
+ "Scalars.yaml",
+]
+if CONFIG["MOZ_TELEMETRY_EXTRA_SCALAR_FILES"]:
+ scalar_files.extend(CONFIG["MOZ_TELEMETRY_EXTRA_SCALAR_FILES"])
+
+GeneratedFile(
+ "TelemetryScalarData.h",
+ script="build_scripts/gen_scalar_data.py",
+ inputs=scalar_files,
+)
+GeneratedFile(
+ "TelemetryScalarEnums.h",
+ script="build_scripts/gen_scalar_enum.py",
+ inputs=scalar_files,
+)
+
+# Generate the JSON scalar definitions. They will only be
+# used in artifact or "build faster" builds.
+GeneratedFile(
+ "ScalarArtifactDefinitions.json",
+ script="build_scripts/gen_scalar_data.py",
+ entry_point="generate_JSON_definitions",
+ inputs=scalar_files,
+)
+
+# Move the scalars JSON file to the directory where the Firefox binary is.
+FINAL_TARGET_FILES += ["!ScalarArtifactDefinitions.json"]
+
+# Generate event files.
+event_files = [
+ "Events.yaml",
+]
+if CONFIG["MOZ_TELEMETRY_EXTRA_EVENT_FILES"]:
+ event_files.extend(CONFIG["MOZ_TELEMETRY_EXTRA_EVENT_FILES"])
+
+GeneratedFile(
+ "TelemetryEventData.h", script="build_scripts/gen_event_data.py", inputs=event_files
+)
+
+GeneratedFile(
+ "TelemetryEventEnums.h",
+ script="build_scripts/gen_event_enum.py",
+ inputs=event_files,
+)
+
+# Generate the JSON event definitions. They will only be
+# used in artifact or "build faster" builds.
+GeneratedFile(
+ "EventArtifactDefinitions.json",
+ script="build_scripts/gen_event_data.py",
+ entry_point="generate_JSON_definitions",
+ inputs=event_files,
+)
+
+# Move the events JSON file to the directory where the Firefox binary is.
+FINAL_TARGET_FILES += ["!EventArtifactDefinitions.json"]
+
+# Generate data from Processes.yaml
+processes_files = [
+ "Processes.yaml",
+]
+
+GeneratedFile(
+ "TelemetryProcessEnums.h",
+ script="build_scripts/gen_process_enum.py",
+ inputs=processes_files,
+)
+
+
+GeneratedFile(
+ "TelemetryProcessData.h",
+ script="build_scripts/gen_process_data.py",
+ inputs=processes_files,
+)
+
+# Ensure that the GeckoView metrics file parses correctly prior to it
+# being released in Android components. This triggers glean_parser by
+# pretending to generate a file named 'glean_checks'.
+# While this currently only applies to Android, in the medium-term it
+# is going to generate code for Firefox as well (project FOG).
+# Prior art for this was in bug 1063728, within SpiderMonkey tests.
+GeneratedFile(
+ "glean_checks",
+ script="build_scripts/run_glean_parser.py",
+ inputs=["geckoview/streaming/metrics.yaml"],
+)
+
+# Generate UserInteraction file
+userinteraction_files = [
+ "UserInteractions.yaml",
+]
+GeneratedFile(
+ "TelemetryUserInteractionData.h",
+ script="build_scripts/gen_userinteraction_data.py",
+ inputs=userinteraction_files,
+)
+GeneratedFile(
+ "TelemetryUserInteractionNameMap.h",
+ script="build_scripts/gen_userinteraction_phf.py",
+ inputs=userinteraction_files,
+)
+
+with Files("**"):
+ BUG_COMPONENT = ("Toolkit", "Telemetry")
diff --git a/toolkit/components/telemetry/other/CombinedStacks.cpp b/toolkit/components/telemetry/other/CombinedStacks.cpp
new file mode 100644
index 0000000000..61248f9b40
--- /dev/null
+++ b/toolkit/components/telemetry/other/CombinedStacks.cpp
@@ -0,0 +1,257 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CombinedStacks.h"
+
+#include "jsapi.h"
+#include "js/Array.h" // JS::NewArrayObject
+#include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineProperty
+#include "js/String.h"
+
+namespace mozilla::Telemetry {
+
+// The maximum number of chrome hangs stacks that we're keeping.
+const size_t kMaxChromeStacksKept = 50;
+
+CombinedStacks::CombinedStacks() : CombinedStacks(kMaxChromeStacksKept) {}
+
+CombinedStacks::CombinedStacks(size_t aMaxStacksCount)
+ : mNextIndex(0), mMaxStacksCount(aMaxStacksCount) {}
+
+size_t CombinedStacks::GetMaxStacksCount() const { return mMaxStacksCount; }
+size_t CombinedStacks::GetModuleCount() const { return mModules.size(); }
+
+const Telemetry::ProcessedStack::Module& CombinedStacks::GetModule(
+ unsigned aIndex) const {
+ return mModules[aIndex];
+}
+
+void CombinedStacks::AddFrame(
+ size_t aStackIndex, const ProcessedStack::Frame& aFrame,
+ const std::function<const ProcessedStack::Module&(int)>& aModuleGetter) {
+ uint16_t modIndex;
+ if (aFrame.mModIndex == std::numeric_limits<uint16_t>::max()) {
+ modIndex = aFrame.mModIndex;
+ } else {
+ const ProcessedStack::Module& module = aModuleGetter(aFrame.mModIndex);
+ auto modIterator = std::find(mModules.begin(), mModules.end(), module);
+ if (modIterator == mModules.end()) {
+ mModules.push_back(module);
+ modIndex = mModules.size() - 1;
+ } else {
+ modIndex = modIterator - mModules.begin();
+ }
+ }
+ mStacks[aStackIndex].push_back(
+ ProcessedStack::Frame{aFrame.mOffset, modIndex});
+}
+
+size_t CombinedStacks::AddStack(const Telemetry::ProcessedStack& aStack) {
+ size_t index = mNextIndex;
+ // Advance the indices of the circular queue holding the stacks.
+ mNextIndex = (mNextIndex + 1) % mMaxStacksCount;
+ // Grow the vector up to the maximum size, if needed.
+ if (mStacks.size() < mMaxStacksCount) {
+ mStacks.resize(mStacks.size() + 1);
+ }
+
+ // Clear the old stack before set.
+ mStacks[index].clear();
+
+ size_t stackSize = aStack.GetStackSize();
+ for (size_t i = 0; i < stackSize; ++i) {
+ // Need to specify a return type in the following lambda,
+ // otherwise it's incorrectly deduced to be a non-reference type.
+ AddFrame(index, aStack.GetFrame(i),
+ [&aStack](int aIdx) -> const ProcessedStack::Module& {
+ return aStack.GetModule(aIdx);
+ });
+ }
+ return index;
+}
+
+void CombinedStacks::AddStacks(const CombinedStacks& aStacks) {
+ mStacks.resize(
+ std::min(mStacks.size() + aStacks.GetStackCount(), mMaxStacksCount));
+
+ for (const auto& stack : aStacks.mStacks) {
+ size_t index = mNextIndex;
+ // Advance the indices of the circular queue holding the stacks.
+ mNextIndex = (mNextIndex + 1) % mMaxStacksCount;
+
+ // Clear the old stack before set.
+ mStacks[index].clear();
+
+ for (const auto& frame : stack) {
+ // Need to specify a return type in the following lambda,
+ // otherwise it's incorrectly deduced to be a non-reference type.
+ AddFrame(index, frame,
+ [&aStacks](int aIdx) -> const ProcessedStack::Module& {
+ return aStacks.mModules[aIdx];
+ });
+ }
+ }
+}
+
+const CombinedStacks::Stack& CombinedStacks::GetStack(unsigned aIndex) const {
+ return mStacks[aIndex];
+}
+
+size_t CombinedStacks::GetStackCount() const { return mStacks.size(); }
+
+size_t CombinedStacks::SizeOfExcludingThis() const {
+ // This is a crude approximation. We would like to do something like
+ // aMallocSizeOf(&mModules[0]), but on linux aMallocSizeOf will call
+ // malloc_usable_size which is only safe on the pointers returned by malloc.
+ // While it works on current libstdc++, it is better to be safe and not assume
+ // that &vec[0] points to one. We could use a custom allocator, but
+ // it doesn't seem worth it.
+ size_t n = 0;
+ n += mModules.capacity() * sizeof(Telemetry::ProcessedStack::Module);
+ n += mStacks.capacity() * sizeof(Stack);
+ for (const auto& s : mStacks) {
+ n += s.capacity() * sizeof(Telemetry::ProcessedStack::Frame);
+ }
+ return n;
+}
+
+void CombinedStacks::RemoveStack(unsigned aIndex) {
+ MOZ_ASSERT(aIndex < mStacks.size());
+
+ mStacks.erase(mStacks.begin() + aIndex);
+
+ if (aIndex < mNextIndex) {
+ if (mNextIndex == 0) {
+ mNextIndex = mStacks.size();
+ } else {
+ mNextIndex--;
+ }
+ }
+
+ if (mNextIndex > mStacks.size()) {
+ mNextIndex = mStacks.size();
+ }
+}
+
+void CombinedStacks::Swap(CombinedStacks& aOther) {
+ mModules.swap(aOther.mModules);
+ mStacks.swap(aOther.mStacks);
+
+ size_t nextIndex = aOther.mNextIndex;
+ aOther.mNextIndex = mNextIndex;
+ mNextIndex = nextIndex;
+
+ size_t maxStacksCount = aOther.mMaxStacksCount;
+ aOther.mMaxStacksCount = mMaxStacksCount;
+ mMaxStacksCount = maxStacksCount;
+}
+
+void CombinedStacks::Clear() {
+ mNextIndex = 0;
+ mStacks.clear();
+ mModules.clear();
+}
+
+JSObject* CreateJSStackObject(JSContext* cx, const CombinedStacks& stacks) {
+ JS::Rooted<JSObject*> ret(cx, JS_NewPlainObject(cx));
+ if (!ret) {
+ return nullptr;
+ }
+
+ JS::Rooted<JSObject*> moduleArray(cx, JS::NewArrayObject(cx, 0));
+ if (!moduleArray) {
+ return nullptr;
+ }
+ bool ok =
+ JS_DefineProperty(cx, ret, "memoryMap", moduleArray, JSPROP_ENUMERATE);
+ if (!ok) {
+ return nullptr;
+ }
+
+ const size_t moduleCount = stacks.GetModuleCount();
+ for (size_t moduleIndex = 0; moduleIndex < moduleCount; ++moduleIndex) {
+ // Current module
+ const Telemetry::ProcessedStack::Module& module =
+ stacks.GetModule(moduleIndex);
+
+ JS::Rooted<JSObject*> moduleInfoArray(cx, JS::NewArrayObject(cx, 0));
+ if (!moduleInfoArray) {
+ return nullptr;
+ }
+ if (!JS_DefineElement(cx, moduleArray, moduleIndex, moduleInfoArray,
+ JSPROP_ENUMERATE)) {
+ return nullptr;
+ }
+
+ unsigned index = 0;
+
+ // Module name
+ JS::Rooted<JSString*> str(cx, JS_NewUCStringCopyZ(cx, module.mName.get()));
+ if (!str || !JS_DefineElement(cx, moduleInfoArray, index++, str,
+ JSPROP_ENUMERATE)) {
+ return nullptr;
+ }
+
+ // Module breakpad identifier
+ JS::Rooted<JSString*> id(cx,
+ JS_NewStringCopyZ(cx, module.mBreakpadId.get()));
+ if (!id ||
+ !JS_DefineElement(cx, moduleInfoArray, index, id, JSPROP_ENUMERATE)) {
+ return nullptr;
+ }
+ }
+
+ JS::Rooted<JSObject*> reportArray(cx, JS::NewArrayObject(cx, 0));
+ if (!reportArray) {
+ return nullptr;
+ }
+ ok = JS_DefineProperty(cx, ret, "stacks", reportArray, JSPROP_ENUMERATE);
+ if (!ok) {
+ return nullptr;
+ }
+
+ const size_t length = stacks.GetStackCount();
+ for (size_t i = 0; i < length; ++i) {
+ // Represent call stack PCs as (module index, offset) pairs.
+ JS::Rooted<JSObject*> pcArray(cx, JS::NewArrayObject(cx, 0));
+ if (!pcArray) {
+ return nullptr;
+ }
+
+ if (!JS_DefineElement(cx, reportArray, i, pcArray, JSPROP_ENUMERATE)) {
+ return nullptr;
+ }
+
+ const CombinedStacks::Stack& stack = stacks.GetStack(i);
+ const uint32_t pcCount = stack.size();
+ for (size_t pcIndex = 0; pcIndex < pcCount; ++pcIndex) {
+ const Telemetry::ProcessedStack::Frame& frame = stack[pcIndex];
+ JS::Rooted<JSObject*> framePair(cx, JS::NewArrayObject(cx, 0));
+ if (!framePair) {
+ return nullptr;
+ }
+ int modIndex = (std::numeric_limits<uint16_t>::max() == frame.mModIndex)
+ ? -1
+ : frame.mModIndex;
+ if (!JS_DefineElement(cx, framePair, 0, modIndex, JSPROP_ENUMERATE)) {
+ return nullptr;
+ }
+ if (!JS_DefineElement(cx, framePair, 1,
+ static_cast<double>(frame.mOffset),
+ JSPROP_ENUMERATE)) {
+ return nullptr;
+ }
+ if (!JS_DefineElement(cx, pcArray, pcIndex, framePair,
+ JSPROP_ENUMERATE)) {
+ return nullptr;
+ }
+ }
+ }
+
+ return ret;
+}
+
+} // namespace mozilla::Telemetry
diff --git a/toolkit/components/telemetry/other/CombinedStacks.h b/toolkit/components/telemetry/other/CombinedStacks.h
new file mode 100644
index 0000000000..6bd46823db
--- /dev/null
+++ b/toolkit/components/telemetry/other/CombinedStacks.h
@@ -0,0 +1,108 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef CombinedStacks_h__
+#define CombinedStacks_h__
+
+#include <vector>
+
+#include "ipc/IPCMessageUtils.h"
+#include "ProcessedStack.h"
+
+class JSObject;
+struct JSContext;
+
+namespace mozilla {
+namespace Telemetry {
+
+/**
+ * This class is conceptually a list of ProcessedStack objects, but it
+ * represents them more efficiently by keeping a single global list of modules.
+ */
+class CombinedStacks {
+ public:
+ explicit CombinedStacks();
+ explicit CombinedStacks(size_t aMaxStacksCount);
+
+ CombinedStacks(CombinedStacks&&) = default;
+ CombinedStacks& operator=(CombinedStacks&&) = default;
+
+ void Swap(CombinedStacks& aOther);
+
+ typedef std::vector<Telemetry::ProcessedStack::Frame> Stack;
+ const Telemetry::ProcessedStack::Module& GetModule(unsigned aIndex) const;
+ size_t GetModuleCount() const;
+ const Stack& GetStack(unsigned aIndex) const;
+ size_t AddStack(const Telemetry::ProcessedStack& aStack);
+ void AddStacks(const CombinedStacks& aStacks);
+ size_t GetStackCount() const;
+ size_t SizeOfExcludingThis() const;
+ void RemoveStack(unsigned aIndex);
+ size_t GetMaxStacksCount() const;
+
+ /** Clears the contents of vectors and resets the index. */
+ void Clear();
+
+ private:
+ std::vector<Telemetry::ProcessedStack::Module> mModules;
+ // A circular buffer to hold the stacks.
+ std::vector<Stack> mStacks;
+ // The index of the next buffer element to write to in mStacks.
+ size_t mNextIndex;
+ // The maximum number of stacks to keep in the CombinedStacks object.
+ size_t mMaxStacksCount;
+
+ void AddFrame(
+ size_t aStackIndex, const ProcessedStack::Frame& aFrame,
+ const std::function<const ProcessedStack::Module&(int)>& aModuleGetter);
+
+ friend struct ::IPC::ParamTraits<CombinedStacks>;
+};
+
+/**
+ * Creates a JSON representation of given combined stacks object.
+ */
+JSObject* CreateJSStackObject(JSContext* cx, const CombinedStacks& stacks);
+
+} // namespace Telemetry
+} // namespace mozilla
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::Telemetry::CombinedStacks> {
+ typedef mozilla::Telemetry::CombinedStacks paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mModules);
+ WriteParam(aWriter, aParam.mStacks);
+ WriteParam(aWriter, aParam.mNextIndex);
+ WriteParam(aWriter, aParam.mMaxStacksCount);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ if (!ReadParam(aReader, &aResult->mModules)) {
+ return false;
+ }
+
+ if (!ReadParam(aReader, &aResult->mStacks)) {
+ return false;
+ }
+
+ if (!ReadParam(aReader, &aResult->mNextIndex)) {
+ return false;
+ }
+
+ if (!ReadParam(aReader, &aResult->mMaxStacksCount)) {
+ return false;
+ }
+
+ return true;
+ }
+};
+
+} // namespace IPC
+
+#endif // CombinedStacks_h__
diff --git a/toolkit/components/telemetry/other/ProcessedStack.cpp b/toolkit/components/telemetry/other/ProcessedStack.cpp
new file mode 100644
index 0000000000..ce62826a05
--- /dev/null
+++ b/toolkit/components/telemetry/other/ProcessedStack.cpp
@@ -0,0 +1,188 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ProcessedStack.h"
+
+namespace {
+
+struct StackFrame {
+ uintptr_t mPC; // The program counter at this position in the call stack.
+ uint16_t mIndex; // The number of this frame in the call stack.
+ uint16_t mModIndex; // The index of module that has this program counter.
+};
+
+#ifdef MOZ_GECKO_PROFILER
+static bool CompareByPC(const StackFrame& a, const StackFrame& b) {
+ return a.mPC < b.mPC;
+}
+
+static bool CompareByIndex(const StackFrame& a, const StackFrame& b) {
+ return a.mIndex < b.mIndex;
+}
+#endif
+
+} // namespace
+
+namespace mozilla::Telemetry {
+
+const size_t kMaxChromeStackDepth = 50;
+
+ProcessedStack::ProcessedStack() = default;
+
+size_t ProcessedStack::GetStackSize() const { return mStack.size(); }
+
+size_t ProcessedStack::GetNumModules() const { return mModules.size(); }
+
+bool ProcessedStack::Module::operator==(const Module& aOther) const {
+ return mName == aOther.mName && mBreakpadId == aOther.mBreakpadId;
+}
+
+const ProcessedStack::Frame& ProcessedStack::GetFrame(unsigned aIndex) const {
+ MOZ_ASSERT(aIndex < mStack.size());
+ return mStack[aIndex];
+}
+
+void ProcessedStack::AddFrame(const Frame& aFrame) { mStack.push_back(aFrame); }
+
+const ProcessedStack::Module& ProcessedStack::GetModule(unsigned aIndex) const {
+ MOZ_ASSERT(aIndex < mModules.size());
+ return mModules[aIndex];
+}
+
+void ProcessedStack::AddModule(const Module& aModule) {
+ mModules.push_back(aModule);
+}
+
+void ProcessedStack::Clear() {
+ mModules.clear();
+ mStack.clear();
+}
+
+ProcessedStack GetStackAndModules(const std::vector<uintptr_t>& aPCs) {
+ return BatchProcessedStackGenerator().GetStackAndModules(aPCs);
+}
+
+BatchProcessedStackGenerator::BatchProcessedStackGenerator()
+#ifdef MOZ_GECKO_PROFILER
+ : mSortedRawModules(SharedLibraryInfo::GetInfoForSelf())
+#endif
+{
+#ifdef MOZ_GECKO_PROFILER
+ mSortedRawModules.SortByAddress();
+#endif
+}
+
+#ifndef MOZ_GECKO_PROFILER
+static ProcessedStack GetStackAndModulesInternal(
+ std::vector<StackFrame>& aRawStack) {
+#else
+static ProcessedStack GetStackAndModulesInternal(
+ std::vector<StackFrame>& aRawStack, SharedLibraryInfo& aSortedRawModules) {
+ SharedLibraryInfo rawModules(aSortedRawModules);
+ // Remove all modules not referenced by a PC on the stack
+ std::sort(aRawStack.begin(), aRawStack.end(), CompareByPC);
+
+ size_t moduleIndex = 0;
+ size_t stackIndex = 0;
+ size_t stackSize = aRawStack.size();
+
+ while (moduleIndex < rawModules.GetSize()) {
+ const SharedLibrary& module = rawModules.GetEntry(moduleIndex);
+ uintptr_t moduleStart = module.GetStart();
+ uintptr_t moduleEnd = module.GetEnd() - 1;
+ // the interval is [moduleStart, moduleEnd)
+
+ bool moduleReferenced = false;
+ for (; stackIndex < stackSize; ++stackIndex) {
+ uintptr_t pc = aRawStack[stackIndex].mPC;
+ if (pc >= moduleEnd) break;
+
+ if (pc >= moduleStart) {
+ // If the current PC is within the current module, mark
+ // module as used
+ moduleReferenced = true;
+ aRawStack[stackIndex].mPC -= moduleStart;
+ aRawStack[stackIndex].mModIndex = moduleIndex;
+ } else {
+ // PC does not belong to any module. It is probably from
+ // the JIT. Use a fixed mPC so that we don't get different
+ // stacks on different runs.
+ aRawStack[stackIndex].mPC = std::numeric_limits<uintptr_t>::max();
+ }
+ }
+
+ if (moduleReferenced) {
+ ++moduleIndex;
+ } else {
+ // Remove module if no PCs within its address range
+ rawModules.RemoveEntries(moduleIndex, moduleIndex + 1);
+ }
+ }
+
+ for (; stackIndex < stackSize; ++stackIndex) {
+ // These PCs are past the last module.
+ aRawStack[stackIndex].mPC = std::numeric_limits<uintptr_t>::max();
+ }
+
+ std::sort(aRawStack.begin(), aRawStack.end(), CompareByIndex);
+#endif
+
+ // Copy the information to the return value.
+ ProcessedStack Ret;
+ for (auto& rawFrame : aRawStack) {
+ mozilla::Telemetry::ProcessedStack::Frame frame = {rawFrame.mPC,
+ rawFrame.mModIndex};
+ Ret.AddFrame(frame);
+ }
+
+#ifdef MOZ_GECKO_PROFILER
+ for (unsigned i = 0, n = rawModules.GetSize(); i != n; ++i) {
+ const SharedLibrary& info = rawModules.GetEntry(i);
+ mozilla::Telemetry::ProcessedStack::Module module = {info.GetDebugName(),
+ info.GetBreakpadId()};
+ Ret.AddModule(module);
+ }
+#endif
+
+ return Ret;
+}
+
+ProcessedStack BatchProcessedStackGenerator::GetStackAndModules(
+ const std::vector<uintptr_t>& aPCs) {
+ std::vector<StackFrame> rawStack;
+ auto stackEnd = aPCs.begin() + std::min(aPCs.size(), kMaxChromeStackDepth);
+ for (auto i = aPCs.begin(); i != stackEnd; ++i) {
+ uintptr_t aPC = *i;
+ StackFrame Frame = {aPC, static_cast<uint16_t>(rawStack.size()),
+ std::numeric_limits<uint16_t>::max()};
+ rawStack.push_back(Frame);
+ }
+
+#if defined(MOZ_GECKO_PROFILER)
+ return GetStackAndModulesInternal(rawStack, mSortedRawModules);
+#else
+ return GetStackAndModulesInternal(rawStack);
+#endif
+}
+
+ProcessedStack BatchProcessedStackGenerator::GetStackAndModules(
+ const uintptr_t* aBegin, const uintptr_t* aEnd) {
+ std::vector<StackFrame> rawStack;
+ for (auto i = aBegin; i != aEnd; ++i) {
+ uintptr_t aPC = *i;
+ StackFrame Frame = {aPC, static_cast<uint16_t>(rawStack.size()),
+ std::numeric_limits<uint16_t>::max()};
+ rawStack.push_back(Frame);
+ }
+
+#if defined(MOZ_GECKO_PROFILER)
+ return GetStackAndModulesInternal(rawStack, mSortedRawModules);
+#else
+ return GetStackAndModulesInternal(rawStack);
+#endif
+}
+
+} // namespace mozilla::Telemetry
diff --git a/toolkit/components/telemetry/other/ProcessedStack.h b/toolkit/components/telemetry/other/ProcessedStack.h
new file mode 100644
index 0000000000..375cab37a7
--- /dev/null
+++ b/toolkit/components/telemetry/other/ProcessedStack.h
@@ -0,0 +1,135 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef ProcessedStack_h__
+#define ProcessedStack_h__
+
+#include <vector>
+
+#include "ipc/IPCMessageUtils.h"
+#include "ipc/IPCMessageUtilsSpecializations.h"
+#include "mozilla/ipc/MessageChannel.h"
+#include "mozilla/Vector.h"
+#include "nsStringFwd.h"
+#include "shared-libraries.h"
+
+namespace mozilla {
+namespace Telemetry {
+
+// This class represents a stack trace and the modules referenced in that trace.
+// It is designed to be easy to read and write to disk or network and doesn't
+// include any logic on how to collect or read the information it stores.
+class ProcessedStack {
+ public:
+ ProcessedStack();
+ size_t GetStackSize() const;
+ size_t GetNumModules() const;
+
+ struct Frame {
+ // The offset of this program counter in its module or an absolute pc.
+ uintptr_t mOffset;
+ // The index to pass to GetModule to get the module this program counter
+ // was in.
+ uint16_t mModIndex;
+ };
+ struct Module {
+ // The file name, /foo/bar/libxul.so for example.
+ // It can contain unicode characters.
+ nsString mName;
+ nsCString mBreakpadId;
+
+ bool operator==(const Module& other) const;
+ };
+
+ const Frame& GetFrame(unsigned aIndex) const;
+ void AddFrame(const Frame& aFrame);
+ const Module& GetModule(unsigned aIndex) const;
+ void AddModule(const Module& aFrame);
+
+ void Clear();
+
+ private:
+ std::vector<Module> mModules;
+ std::vector<Frame> mStack;
+};
+
+// Get the current list of loaded modules, filter and pair it to the provided
+// stack. We let the caller collect the stack since different callers have
+// different needs (current thread X main thread, stopping the thread, etc).
+ProcessedStack GetStackAndModules(const std::vector<uintptr_t>& aPCs);
+
+// This class optimizes repeated calls to GetStackAndModules.
+class BatchProcessedStackGenerator {
+ public:
+ BatchProcessedStackGenerator();
+ ProcessedStack GetStackAndModules(const std::vector<uintptr_t>& aPCs);
+
+ template <typename AllocatorPolicy>
+ ProcessedStack GetStackAndModules(
+ const Vector<void*, 0, AllocatorPolicy>& aPCs) {
+ return GetStackAndModules(reinterpret_cast<const uintptr_t*>(aPCs.begin()),
+ reinterpret_cast<const uintptr_t*>(aPCs.end()));
+ }
+
+ private:
+ ProcessedStack GetStackAndModules(const uintptr_t* aBegin,
+ const uintptr_t* aEnd);
+#if defined(MOZ_GECKO_PROFILER)
+ SharedLibraryInfo mSortedRawModules;
+#endif
+};
+
+} // namespace Telemetry
+} // namespace mozilla
+
+namespace IPC {
+
+template <>
+struct ParamTraits<mozilla::Telemetry::ProcessedStack::Module> {
+ typedef mozilla::Telemetry::ProcessedStack::Module paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mName);
+ WriteParam(aWriter, aParam.mBreakpadId);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ if (!ReadParam(aReader, &aResult->mName)) {
+ return false;
+ }
+
+ if (!ReadParam(aReader, &aResult->mBreakpadId)) {
+ return false;
+ }
+
+ return true;
+ }
+};
+
+template <>
+struct ParamTraits<mozilla::Telemetry::ProcessedStack::Frame> {
+ typedef mozilla::Telemetry::ProcessedStack::Frame paramType;
+
+ static void Write(MessageWriter* aWriter, const paramType& aParam) {
+ WriteParam(aWriter, aParam.mOffset);
+ WriteParam(aWriter, aParam.mModIndex);
+ }
+
+ static bool Read(MessageReader* aReader, paramType* aResult) {
+ if (!ReadParam(aReader, &aResult->mOffset)) {
+ return false;
+ }
+
+ if (!ReadParam(aReader, &aResult->mModIndex)) {
+ return false;
+ }
+
+ return true;
+ }
+};
+
+} // namespace IPC
+
+#endif // ProcessedStack_h__
diff --git a/toolkit/components/telemetry/other/TelemetryIOInterposeObserver.cpp b/toolkit/components/telemetry/other/TelemetryIOInterposeObserver.cpp
new file mode 100644
index 0000000000..c0792bb48c
--- /dev/null
+++ b/toolkit/components/telemetry/other/TelemetryIOInterposeObserver.cpp
@@ -0,0 +1,182 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "TelemetryIOInterposeObserver.h"
+#include "core/TelemetryCommon.h"
+#include "js/Array.h" // JS::NewArrayObject
+#include "js/PropertyAndElement.h" // JS_DefineUCProperty
+#include "js/PropertyDescriptor.h" // JSPROP_ENUMERATE, JSPROP_READONLY
+#include "js/ValueArray.h"
+#include "nsIFile.h"
+
+namespace mozilla::Telemetry {
+
+TelemetryIOInterposeObserver::TelemetryIOInterposeObserver(nsIFile* aXreDir)
+ : mCurStage(STAGE_STARTUP) {
+ nsAutoString xreDirPath;
+ nsresult rv = aXreDir->GetPath(xreDirPath);
+ if (NS_SUCCEEDED(rv)) {
+ AddPath(xreDirPath, u"{xre}"_ns);
+ }
+}
+
+void TelemetryIOInterposeObserver::AddPath(const nsAString& aPath,
+ const nsAString& aSubstName) {
+ mSafeDirs.AppendElement(SafeDir(aPath, aSubstName));
+}
+
+// Threshold for reporting slow main-thread I/O (50 milliseconds).
+const TimeDuration kTelemetryReportThreshold =
+ TimeDuration::FromMilliseconds(50);
+
+void TelemetryIOInterposeObserver::Observe(Observation& aOb) {
+ // We only report main-thread I/O
+ if (!IsMainThread()) {
+ return;
+ }
+
+ if (aOb.ObservedOperation() == OpNextStage) {
+ mCurStage = NextStage(mCurStage);
+ MOZ_ASSERT(mCurStage < NUM_STAGES);
+ return;
+ }
+
+ if (aOb.Duration() < kTelemetryReportThreshold) {
+ return;
+ }
+
+ // Get the filename
+ nsAutoString filename;
+ aOb.Filename(filename);
+
+ // Discard observations without filename
+ if (filename.IsEmpty()) {
+ return;
+ }
+
+#if defined(XP_WIN)
+ auto comparator = nsCaseInsensitiveStringComparator;
+#else
+ auto comparator = nsTDefaultStringComparator<char16_t>;
+#endif
+ nsAutoString processedName;
+ uint32_t safeDirsLen = mSafeDirs.Length();
+ for (uint32_t i = 0; i < safeDirsLen; ++i) {
+ if (StringBeginsWith(filename, mSafeDirs[i].mPath, comparator)) {
+ processedName = mSafeDirs[i].mSubstName;
+ processedName += Substring(filename, mSafeDirs[i].mPath.Length());
+ break;
+ }
+ }
+
+ if (processedName.IsEmpty()) {
+ return;
+ }
+
+ // Create a new entry or retrieve the existing one
+ FileIOEntryType* entry = mFileStats.PutEntry(processedName);
+ if (entry) {
+ FileStats& stats = entry->GetModifiableData()->mStats[mCurStage];
+ // Update the statistics
+ stats.totalTime += (double)aOb.Duration().ToMilliseconds();
+ switch (aOb.ObservedOperation()) {
+ case OpCreateOrOpen:
+ stats.creates++;
+ break;
+ case OpRead:
+ stats.reads++;
+ break;
+ case OpWrite:
+ stats.writes++;
+ break;
+ case OpFSync:
+ stats.fsyncs++;
+ break;
+ case OpStat:
+ stats.stats++;
+ break;
+ default:
+ break;
+ }
+ }
+}
+
+bool TelemetryIOInterposeObserver::ReflectFileStats(FileIOEntryType* entry,
+ JSContext* cx,
+ JS::Handle<JSObject*> obj) {
+ JS::RootedValueArray<NUM_STAGES> stages(cx);
+
+ FileStatsByStage& statsByStage = *entry->GetModifiableData();
+ for (int s = STAGE_STARTUP; s < NUM_STAGES; ++s) {
+ FileStats& fileStats = statsByStage.mStats[s];
+
+ if (fileStats.totalTime == 0 && fileStats.creates == 0 &&
+ fileStats.reads == 0 && fileStats.writes == 0 &&
+ fileStats.fsyncs == 0 && fileStats.stats == 0) {
+ // Don't add an array that contains no information
+ stages[s].setNull();
+ continue;
+ }
+
+ // Array we want to report
+ JS::RootedValueArray<6> stats(cx);
+ stats[0].setNumber(fileStats.totalTime);
+ stats[1].setNumber(fileStats.creates);
+ stats[2].setNumber(fileStats.reads);
+ stats[3].setNumber(fileStats.writes);
+ stats[4].setNumber(fileStats.fsyncs);
+ stats[5].setNumber(fileStats.stats);
+
+ // Create jsStats as array of elements above
+ JS::Rooted<JSObject*> jsStats(cx, JS::NewArrayObject(cx, stats));
+ if (!jsStats) {
+ continue;
+ }
+
+ stages[s].setObject(*jsStats);
+ }
+
+ JS::Rooted<JSObject*> jsEntry(cx, JS::NewArrayObject(cx, stages));
+ if (!jsEntry) {
+ return false;
+ }
+
+ // Add jsEntry to top-level dictionary
+ const nsAString& key = entry->GetKey();
+ return JS_DefineUCProperty(cx, obj, key.Data(), key.Length(), jsEntry,
+ JSPROP_ENUMERATE | JSPROP_READONLY);
+}
+
+bool TelemetryIOInterposeObserver::ReflectIntoJS(
+ JSContext* cx, JS::Handle<JSObject*> rootObj) {
+ return mFileStats.ReflectIntoJS(ReflectFileStats, cx, rootObj);
+}
+
+/**
+ * Get size of hash table with file stats
+ */
+
+size_t TelemetryIOInterposeObserver::SizeOfIncludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ return aMallocSizeOf(this) + SizeOfExcludingThis(aMallocSizeOf);
+}
+
+size_t TelemetryIOInterposeObserver::SizeOfExcludingThis(
+ mozilla::MallocSizeOf aMallocSizeOf) const {
+ size_t size = 0;
+ size += mFileStats.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ for (auto iter = mFileStats.ConstIter(); !iter.Done(); iter.Next()) {
+ size += iter.Get()->GetKey().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+ size += mSafeDirs.ShallowSizeOfExcludingThis(aMallocSizeOf);
+ uint32_t safeDirsLen = mSafeDirs.Length();
+ for (uint32_t i = 0; i < safeDirsLen; ++i) {
+ size += mSafeDirs[i].SizeOfExcludingThis(aMallocSizeOf);
+ }
+ return size;
+}
+
+} // namespace mozilla::Telemetry
diff --git a/toolkit/components/telemetry/other/TelemetryIOInterposeObserver.h b/toolkit/components/telemetry/other/TelemetryIOInterposeObserver.h
new file mode 100644
index 0000000000..54e3caf9b4
--- /dev/null
+++ b/toolkit/components/telemetry/other/TelemetryIOInterposeObserver.h
@@ -0,0 +1,116 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * IOInterposeObserver recording statistics of main-thread I/O during execution,
+ * aimed at consumption by TelemetryImpl
+ */
+
+#ifndef TelemetryIOInterposeObserver_h__
+#define TelemetryIOInterposeObserver_h__
+
+#include "core/TelemetryCommon.h"
+#include "js/RootingAPI.h"
+#include "js/TypeDecls.h"
+#include "mozilla/IOInterposer.h"
+#include "nsBaseHashtable.h"
+#include "nsHashKeys.h"
+#include "nsTArray.h"
+
+namespace mozilla {
+namespace Telemetry {
+
+class TelemetryIOInterposeObserver : public IOInterposeObserver {
+ /** File-level statistics structure */
+ struct FileStats {
+ FileStats()
+ : creates(0), reads(0), writes(0), fsyncs(0), stats(0), totalTime(0) {}
+ uint32_t creates; /** Number of create/open operations */
+ uint32_t reads; /** Number of read operations */
+ uint32_t writes; /** Number of write operations */
+ uint32_t fsyncs; /** Number of fsync operations */
+ uint32_t stats; /** Number of stat operations */
+ double totalTime; /** Accumulated duration of all operations */
+ };
+
+ struct SafeDir {
+ SafeDir(const nsAString& aPath, const nsAString& aSubstName)
+ : mPath(aPath), mSubstName(aSubstName) {}
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const {
+ return mPath.SizeOfExcludingThisIfUnshared(aMallocSizeOf) +
+ mSubstName.SizeOfExcludingThisIfUnshared(aMallocSizeOf);
+ }
+ nsString mPath; /** Path to the directory */
+ nsString mSubstName; /** Name to substitute with */
+ };
+
+ public:
+ explicit TelemetryIOInterposeObserver(nsIFile* aXreDir);
+
+ /**
+ * An implementation of Observe that records statistics of all
+ * file IO operations.
+ */
+ void Observe(Observation& aOb) override;
+
+ /**
+ * Reflect recorded file IO statistics into Javascript
+ */
+ bool ReflectIntoJS(JSContext* cx, JS::Handle<JSObject*> rootObj);
+
+ /**
+ * Adds a path for inclusion in main thread I/O report.
+ * @param aPath Directory path
+ * @param aSubstName Name to substitute for aPath for privacy reasons
+ */
+ void AddPath(const nsAString& aPath, const nsAString& aSubstName);
+
+ /**
+ * Get size of hash table with file stats
+ */
+ size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ size_t SizeOfExcludingThis(mozilla::MallocSizeOf aMallocSizeOf) const;
+
+ private:
+ enum Stage { STAGE_STARTUP = 0, STAGE_NORMAL, STAGE_SHUTDOWN, NUM_STAGES };
+ static inline Stage NextStage(Stage aStage) {
+ switch (aStage) {
+ case STAGE_STARTUP:
+ return STAGE_NORMAL;
+ case STAGE_NORMAL:
+ return STAGE_SHUTDOWN;
+ case STAGE_SHUTDOWN:
+ return STAGE_SHUTDOWN;
+ default:
+ return NUM_STAGES;
+ }
+ }
+
+ struct FileStatsByStage {
+ FileStats mStats[NUM_STAGES];
+ };
+ typedef nsBaseHashtableET<nsStringHashKey, FileStatsByStage> FileIOEntryType;
+
+ // Statistics for each filename
+ Common::AutoHashtable<FileIOEntryType> mFileStats;
+ // Container for allowed directories
+ nsTArray<SafeDir> mSafeDirs;
+ Stage mCurStage;
+
+ /**
+ * Reflect a FileIOEntryType object to a Javascript property on obj with
+ * filename as key containing array:
+ * [totalTime, creates, reads, writes, fsyncs, stats]
+ */
+ static bool ReflectFileStats(FileIOEntryType* entry, JSContext* cx,
+ JS::Handle<JSObject*> obj);
+};
+
+} // namespace Telemetry
+} // namespace mozilla
+
+#endif // TelemetryIOInterposeObserver_h__
diff --git a/toolkit/components/telemetry/other/UntrustedModules.cpp b/toolkit/components/telemetry/other/UntrustedModules.cpp
new file mode 100644
index 0000000000..7a80cd9c8d
--- /dev/null
+++ b/toolkit/components/telemetry/other/UntrustedModules.cpp
@@ -0,0 +1,305 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "UntrustedModules.h"
+
+#include "GMPServiceParent.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/MozPromise.h"
+#include "mozilla/net/SocketProcessParent.h"
+#include "mozilla/ipc/UtilityProcessParent.h"
+#include "mozilla/ipc/UtilityProcessManager.h"
+#include "mozilla/RDDChild.h"
+#include "mozilla/RDDProcessManager.h"
+#include "mozilla/WinDllServices.h"
+#include "nsISupportsImpl.h"
+#include "nsProxyRelease.h"
+#include "nsXULAppAPI.h"
+#include "UntrustedModulesDataSerializer.h"
+
+namespace mozilla {
+namespace Telemetry {
+
+static const uint32_t kMaxModulesArrayLen = 100;
+
+using UntrustedModulesIpcPromise =
+ MozPromise<Maybe<UntrustedModulesData>, ipc::ResponseRejectReason, true>;
+
+using MultiGetUntrustedModulesPromise =
+ MozPromise<bool /*aIgnored*/, nsresult, true>;
+
+class MOZ_HEAP_CLASS MultiGetUntrustedModulesData final {
+ public:
+ /**
+ * @param aFlags [in] Combinations of the flags defined under nsITelemetry.
+ * (See "Flags for getUntrustedModuleLoadEvents"
+ * in nsITelemetry.idl)
+ */
+ explicit MultiGetUntrustedModulesData(uint32_t aFlags)
+ : mFlags(aFlags),
+ mBackupSvc(UntrustedModulesBackupService::Get()),
+ mPromise(new MultiGetUntrustedModulesPromise::Private(__func__)),
+ mNumPending(0) {}
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MultiGetUntrustedModulesData)
+
+ RefPtr<MultiGetUntrustedModulesPromise> GetUntrustedModuleLoadEvents();
+ void Serialize(RefPtr<dom::Promise>&& aPromise);
+
+ MultiGetUntrustedModulesData(const MultiGetUntrustedModulesData&) = delete;
+ MultiGetUntrustedModulesData(MultiGetUntrustedModulesData&&) = delete;
+ MultiGetUntrustedModulesData& operator=(const MultiGetUntrustedModulesData&) =
+ delete;
+ MultiGetUntrustedModulesData& operator=(MultiGetUntrustedModulesData&&) =
+ delete;
+
+ private:
+ ~MultiGetUntrustedModulesData() = default;
+
+ void AddPending(RefPtr<UntrustedModulesPromise>&& aNewPending) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ ++mNumPending;
+
+ RefPtr<MultiGetUntrustedModulesData> self(this);
+ aNewPending->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self](Maybe<UntrustedModulesData>&& aResult) {
+ self->OnCompletion(std::move(aResult));
+ },
+ [self](nsresult aReason) { self->OnCompletion(); });
+ }
+
+ void AddPending(RefPtr<UntrustedModulesIpcPromise>&& aNewPending) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ ++mNumPending;
+
+ RefPtr<MultiGetUntrustedModulesData> self(this);
+ aNewPending->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [self](Maybe<UntrustedModulesData>&& aResult) {
+ self->OnCompletion(std::move(aResult));
+ },
+ [self](ipc::ResponseRejectReason&& aReason) { self->OnCompletion(); });
+ }
+
+ void OnCompletion() {
+ MOZ_ASSERT(NS_IsMainThread() && mNumPending > 0);
+
+ --mNumPending;
+ if (mNumPending) {
+ return;
+ }
+
+ mPromise->Resolve(true, __func__);
+ }
+
+ void OnCompletion(Maybe<UntrustedModulesData>&& aResult) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ if (aResult.isSome()) {
+ mBackupSvc->Backup(std::move(aResult.ref()));
+ }
+
+ OnCompletion();
+ }
+
+ private:
+ // Combinations of the flags defined under nsITelemetry.
+ // (See "Flags for getUntrustedModuleLoadEvents" in nsITelemetry.idl)
+ uint32_t mFlags;
+
+ RefPtr<UntrustedModulesBackupService> mBackupSvc;
+ RefPtr<MultiGetUntrustedModulesPromise::Private> mPromise;
+ size_t mNumPending;
+};
+
+RefPtr<MultiGetUntrustedModulesPromise>
+MultiGetUntrustedModulesData::GetUntrustedModuleLoadEvents() {
+ MOZ_ASSERT(XRE_IsParentProcess() && NS_IsMainThread());
+
+ // Parent process
+ RefPtr<DllServices> dllSvc(DllServices::Get());
+ AddPending(dllSvc->GetUntrustedModulesData());
+
+ // Child processes
+ nsTArray<dom::ContentParent*> contentParents;
+ dom::ContentParent::GetAll(contentParents);
+ for (auto&& contentParent : contentParents) {
+ AddPending(contentParent->SendGetUntrustedModulesData());
+ }
+
+ if (auto* socketActor = net::SocketProcessParent::GetSingleton()) {
+ AddPending(socketActor->SendGetUntrustedModulesData());
+ }
+
+ if (RDDProcessManager* rddMgr = RDDProcessManager::Get()) {
+ if (RDDChild* rddChild = rddMgr->GetRDDChild()) {
+ AddPending(rddChild->SendGetUntrustedModulesData());
+ }
+ }
+
+ if (RefPtr<ipc::UtilityProcessManager> utilityManager =
+ ipc::UtilityProcessManager::GetIfExists()) {
+ for (RefPtr<ipc::UtilityProcessParent>& parent :
+ utilityManager->GetAllProcessesProcessParent()) {
+ AddPending(parent->SendGetUntrustedModulesData());
+ }
+ }
+
+ if (RefPtr<gmp::GeckoMediaPluginServiceParent> gmps =
+ gmp::GeckoMediaPluginServiceParent::GetSingleton()) {
+ nsTArray<RefPtr<
+ gmp::GeckoMediaPluginServiceParent::GetUntrustedModulesDataPromise>>
+ promises;
+ gmps->SendGetUntrustedModulesData(promises);
+ for (auto& promise : promises) {
+ AddPending(std::move(promise));
+ }
+ }
+
+ return mPromise;
+}
+
+void MultiGetUntrustedModulesData::Serialize(RefPtr<dom::Promise>&& aPromise) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ dom::AutoJSAPI jsapi;
+ if (NS_WARN_IF(!jsapi.Init(aPromise->GetGlobalObject()))) {
+ aPromise->MaybeReject(NS_ERROR_FAILURE);
+ return;
+ }
+
+ JSContext* cx = jsapi.cx();
+ UntrustedModulesDataSerializer serializer(cx, kMaxModulesArrayLen, mFlags);
+ if (!serializer) {
+ aPromise->MaybeReject(NS_ERROR_FAILURE);
+ return;
+ }
+
+ nsresult rv;
+ if (mFlags & nsITelemetry::INCLUDE_OLD_LOADEVENTS) {
+ // When INCLUDE_OLD_LOADEVENTS is set, we need to return instances
+ // from both "Staging" and "Settled" backup.
+ if (mFlags & nsITelemetry::KEEP_LOADEVENTS_NEW) {
+ // When INCLUDE_OLD_LOADEVENTS and KEEP_LOADEVENTS_NEW are set, we need to
+ // return a JS object consisting of all instances from both "Staging" and
+ // "Settled" backups, keeping instances in those backups as is.
+ if (mFlags & nsITelemetry::EXCLUDE_STACKINFO_FROM_LOADEVENTS) {
+ // Without the stack info, we can add multiple UntrustedModulesData to
+ // the serializer directly.
+ rv = serializer.Add(mBackupSvc->Staging());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aPromise->MaybeReject(rv);
+ return;
+ }
+ rv = serializer.Add(mBackupSvc->Settled());
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aPromise->MaybeReject(rv);
+ return;
+ }
+ } else {
+ // Currently we don't have a method to merge UntrustedModulesData into
+ // a serialized JS object because merging CombinedStack will be tricky.
+ // Thus we return an error on this flag combination.
+ aPromise->MaybeReject(NS_ERROR_INVALID_ARG);
+ return;
+ }
+ } else {
+ // When KEEP_LOADEVENTS_NEW is not set, we can move data from "Staging"
+ // to "Settled" first, then add "Settled" to the serializer.
+ mBackupSvc->SettleAllStagingData();
+
+ const UntrustedModulesBackupData& settledRef = mBackupSvc->Settled();
+ if (settledRef.IsEmpty()) {
+ aPromise->MaybeReject(NS_ERROR_NOT_AVAILABLE);
+ return;
+ }
+
+ rv = serializer.Add(settledRef);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aPromise->MaybeReject(rv);
+ return;
+ }
+ }
+ } else {
+ // When INCLUDE_OLD_LOADEVENTS is not set, we serialize only the "Staging"
+ // into a JS object.
+ const UntrustedModulesBackupData& stagingRef = mBackupSvc->Staging();
+
+ if (stagingRef.IsEmpty()) {
+ aPromise->MaybeReject(NS_ERROR_NOT_AVAILABLE);
+ return;
+ }
+
+ rv = serializer.Add(stagingRef);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aPromise->MaybeReject(rv);
+ return;
+ }
+
+ // When KEEP_LOADEVENTS_NEW is not set, we move all "Staging" instances
+ // to the "Settled".
+ if (!(mFlags & nsITelemetry::KEEP_LOADEVENTS_NEW)) {
+ mBackupSvc->SettleAllStagingData();
+ }
+ }
+
+#if defined(XP_WIN)
+ RefPtr<DllServices> dllSvc(DllServices::Get());
+ nt::SharedSection* sharedSection = dllSvc->GetSharedSection();
+ if (sharedSection) {
+ auto dynamicBlocklist = sharedSection->GetDynamicBlocklist();
+
+ nsTArray<nsDependentSubstring> blockedModules;
+ for (const auto& blockedEntry : dynamicBlocklist) {
+ if (!blockedEntry.IsValidDynamicBlocklistEntry()) {
+ break;
+ }
+ blockedModules.AppendElement(
+ nsDependentSubstring(blockedEntry.mName.Buffer,
+ blockedEntry.mName.Length / sizeof(wchar_t)));
+ }
+ rv = serializer.AddBlockedModules(blockedModules);
+ if (NS_WARN_IF(NS_FAILED(rv))) {
+ aPromise->MaybeReject(rv);
+ return;
+ }
+ }
+#endif
+
+ JS::Rooted<JS::Value> jsval(cx);
+ serializer.GetObject(&jsval);
+ aPromise->MaybeResolve(jsval);
+}
+
+nsresult GetUntrustedModuleLoadEvents(uint32_t aFlags, JSContext* cx,
+ dom::Promise** aPromise) {
+ // Create a promise using global context.
+ nsIGlobalObject* global = xpc::CurrentNativeGlobal(cx);
+ if (NS_WARN_IF(!global)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ ErrorResult result;
+ RefPtr<dom::Promise> promise(dom::Promise::Create(global, result));
+ if (NS_WARN_IF(result.Failed())) {
+ return result.StealNSResult();
+ }
+
+ auto multi = MakeRefPtr<MultiGetUntrustedModulesData>(aFlags);
+ multi->GetUntrustedModuleLoadEvents()->Then(
+ GetMainThreadSerialEventTarget(), __func__,
+ [promise, multi](bool) mutable { multi->Serialize(std::move(promise)); },
+ [promise](nsresult aRv) { promise->MaybeReject(aRv); });
+
+ promise.forget(aPromise);
+ return NS_OK;
+}
+
+} // namespace Telemetry
+} // namespace mozilla
diff --git a/toolkit/components/telemetry/other/UntrustedModules.h b/toolkit/components/telemetry/other/UntrustedModules.h
new file mode 100644
index 0000000000..3bb53c1f9a
--- /dev/null
+++ b/toolkit/components/telemetry/other/UntrustedModules.h
@@ -0,0 +1,31 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef telemetry_UntrustedModules_h__
+#define telemetry_UntrustedModules_h__
+
+#include "jsapi.h"
+#include "mozilla/dom/Promise.h"
+
+namespace mozilla {
+namespace Telemetry {
+
+/**
+ * This function returns a promise that asynchronously processes and gathers
+ * untrusted module data. The promise is either resolved with the JS object
+ * ping payload, or is rejected upon failure.
+ *
+ * @param aFlags [in] Combinations of the flags defined under nsITelemetry.
+ * (See "Flags for getUntrustedModuleLoadEvents"
+ * in nsITelemetry.idl)
+ */
+nsresult GetUntrustedModuleLoadEvents(uint32_t aFlags, JSContext* cx,
+ dom::Promise** aPromise);
+
+} // namespace Telemetry
+} // namespace mozilla
+
+#endif // telemetry_UntrustedModules_h__
diff --git a/toolkit/components/telemetry/other/UntrustedModulesBackupService.cpp b/toolkit/components/telemetry/other/UntrustedModulesBackupService.cpp
new file mode 100644
index 0000000000..1cbe35e8c0
--- /dev/null
+++ b/toolkit/components/telemetry/other/UntrustedModulesBackupService.cpp
@@ -0,0 +1,95 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#include "UntrustedModulesBackupService.h"
+
+#include "mozilla/ClearOnShutdown.h"
+#include "mozilla/HashFunctions.h"
+#include "mozilla/SchedulerGroup.h"
+#include "mozilla/StaticLocalPtr.h"
+
+namespace mozilla {
+
+ProcessHashKey::ProcessHashKey(GeckoProcessType aType, DWORD aPid)
+ : mType(aType), mPid(aPid) {}
+
+bool ProcessHashKey::operator==(const ProcessHashKey& aOther) const {
+ return mPid == aOther.mPid && mType == aOther.mType;
+}
+
+PLDHashNumber ProcessHashKey::Hash() const { return HashGeneric(mPid, mType); }
+
+void UntrustedModulesBackupData::Add(UntrustedModulesData&& aData) {
+ WithEntryHandle(
+ ProcessHashKey(aData.mProcessType, aData.mPid), [&](auto&& p) {
+ if (p) {
+ p.Data()->mData.Merge(std::move(aData));
+ } else {
+ p.Insert(MakeRefPtr<UntrustedModulesDataContainer>(std::move(aData)));
+ }
+ });
+}
+
+void UntrustedModulesBackupData::AddWithoutStacks(
+ UntrustedModulesData&& aData) {
+ WithEntryHandle(
+ ProcessHashKey(aData.mProcessType, aData.mPid), [&](auto&& p) {
+ if (p) {
+ p.Data()->mData.MergeWithoutStacks(std::move(aData));
+ } else {
+ aData.Truncate(true);
+ p.Insert(MakeRefPtr<UntrustedModulesDataContainer>(std::move(aData)));
+ }
+ });
+}
+
+/* static */
+UntrustedModulesBackupService* UntrustedModulesBackupService::Get() {
+ if (!XRE_IsParentProcess()) {
+ return nullptr;
+ }
+
+ static StaticLocalRefPtr<UntrustedModulesBackupService> sInstance(
+ []() -> already_AddRefed<UntrustedModulesBackupService> {
+ RefPtr<UntrustedModulesBackupService> instance(
+ new UntrustedModulesBackupService());
+
+ auto setClearOnShutdown = [ptr = &sInstance]() -> void {
+ ClearOnShutdown(ptr);
+ };
+
+ if (NS_IsMainThread()) {
+ setClearOnShutdown();
+ return instance.forget();
+ }
+
+ SchedulerGroup::Dispatch(NS_NewRunnableFunction(
+ "mozilla::UntrustedModulesBackupService::Get",
+ std::move(setClearOnShutdown)));
+
+ return instance.forget();
+ }());
+
+ return sInstance;
+}
+
+void UntrustedModulesBackupService::Backup(UntrustedModulesData&& aData) {
+ mStaging.Add(std::move(aData));
+}
+
+void UntrustedModulesBackupService::SettleAllStagingData() {
+ UntrustedModulesBackupData staging;
+ staging.SwapElements(mStaging);
+
+ for (auto&& iter = staging.Iter(); !iter.Done(); iter.Next()) {
+ if (!iter.Data()) {
+ continue;
+ }
+ mSettled.AddWithoutStacks(std::move(iter.Data()->mData));
+ }
+}
+
+} // namespace mozilla
diff --git a/toolkit/components/telemetry/other/UntrustedModulesBackupService.h b/toolkit/components/telemetry/other/UntrustedModulesBackupService.h
new file mode 100644
index 0000000000..36e4cc7b1c
--- /dev/null
+++ b/toolkit/components/telemetry/other/UntrustedModulesBackupService.h
@@ -0,0 +1,73 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_UntrustedModulesBackupService_h
+#define mozilla_UntrustedModulesBackupService_h
+
+#include "mozilla/UntrustedModulesData.h"
+#include "nsRefPtrHashtable.h"
+
+namespace mozilla {
+
+struct ProcessHashKey {
+ GeckoProcessType mType;
+ DWORD mPid;
+ ProcessHashKey(GeckoProcessType aType, DWORD aPid);
+ bool operator==(const ProcessHashKey& aOther) const;
+ PLDHashNumber Hash() const;
+};
+
+// UntrustedModulesData should not be refcounted as it's exchanged via IPC.
+// Instead, we define this container class owning UntrustedModulesData along
+// with a refcount.
+class MOZ_HEAP_CLASS UntrustedModulesDataContainer final {
+ ~UntrustedModulesDataContainer() = default;
+
+ public:
+ UntrustedModulesData mData;
+
+ explicit UntrustedModulesDataContainer(UntrustedModulesData&& aData)
+ : mData(std::move(aData)) {}
+
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(UntrustedModulesDataContainer)
+};
+
+class UntrustedModulesBackupData
+ : public nsRefPtrHashtable<nsGenericHashKey<ProcessHashKey>,
+ UntrustedModulesDataContainer> {
+ public:
+ void Add(UntrustedModulesData&& aData);
+ void AddWithoutStacks(UntrustedModulesData&& aData);
+};
+
+class MOZ_HEAP_CLASS UntrustedModulesBackupService final {
+ public:
+ NS_INLINE_DECL_THREADSAFE_REFCOUNTING(UntrustedModulesBackupService)
+
+ static UntrustedModulesBackupService* Get();
+
+ // Back up data to mStaging
+ void Backup(UntrustedModulesData&& aData);
+
+ void SettleAllStagingData();
+
+ const UntrustedModulesBackupData& Staging() const { return mStaging; }
+ const UntrustedModulesBackupData& Settled() const { return mSettled; }
+
+ private:
+ // Data not yet submitted as telemetry
+ UntrustedModulesBackupData mStaging;
+
+ // Data already submitted as telemetry
+ // (This does not have stack information)
+ UntrustedModulesBackupData mSettled;
+
+ ~UntrustedModulesBackupService() = default;
+};
+
+} // namespace mozilla
+
+#endif // mozilla_UntrustedModulesBackupService_h
diff --git a/toolkit/components/telemetry/other/UntrustedModulesDataSerializer.cpp b/toolkit/components/telemetry/other/UntrustedModulesDataSerializer.cpp
new file mode 100644
index 0000000000..f352485e2b
--- /dev/null
+++ b/toolkit/components/telemetry/other/UntrustedModulesDataSerializer.cpp
@@ -0,0 +1,606 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "UntrustedModulesDataSerializer.h"
+
+#include "core/TelemetryCommon.h"
+#include "js/Array.h" // JS::NewArrayObject
+#include "js/PropertyAndElement.h" // JS_DefineElement, JS_DefineProperty, JS_GetProperty
+#include "jsapi.h"
+#include "mozilla/dom/ToJSValue.h"
+#include "nsITelemetry.h"
+#include "nsUnicharUtils.h"
+#include "nsXULAppAPI.h"
+#include "shared-libraries.h"
+
+namespace mozilla {
+namespace Telemetry {
+
+static const uint32_t kThirdPartyModulesPingVersion = 1;
+
+/**
+ * Limits the length of a string by removing the middle of the string, replacing
+ * with ellipsis.
+ * e.g. LimitStringLength("hello world", 6) would result in "he...d"
+ *
+ * @param aStr [in,out] The string to transform
+ * @param aMaxFieldLength [in] The maximum length of the resulting string.
+ */
+static void LimitStringLength(nsAString& aStr, size_t aMaxFieldLength) {
+ if (aStr.Length() <= aMaxFieldLength) {
+ return;
+ }
+
+ constexpr auto kEllipsis = u"..."_ns;
+
+ if (aMaxFieldLength <= (kEllipsis.Length() + 3)) {
+ // An ellipsis is useless in this case, as it would obscure the string to
+ // the point that we cannot even determine the string's contents. We might
+ // as well just truncate.
+ aStr.Truncate(aMaxFieldLength);
+ return;
+ }
+
+ size_t cutPos = (aMaxFieldLength - kEllipsis.Length()) / 2;
+ size_t rightLen = aMaxFieldLength - kEllipsis.Length() - cutPos;
+ size_t cutLen = aStr.Length() - (cutPos + rightLen);
+
+ aStr.Replace(cutPos, cutLen, kEllipsis);
+}
+
+/**
+ * Adds a string property to a JS object, that's limited in length using
+ * LimitStringLength().
+ *
+ * @param cx [in] The JS context
+ * @param aObj [in] The object to add the property to
+ * @param aName [in] The name of the property to add
+ * @param aVal [in] The JS value of the resulting property.
+ * @param aMaxFieldLength [in] The maximum length of the value
+ * (see LimitStringLength())
+ * @return true upon success
+ */
+static bool AddLengthLimitedStringProp(JSContext* cx,
+ JS::Handle<JSObject*> aObj,
+ const char* aName, const nsAString& aVal,
+ size_t aMaxFieldLength = MAX_PATH) {
+ JS::Rooted<JS::Value> jsval(cx);
+ nsAutoString shortVal(aVal);
+ LimitStringLength(shortVal, aMaxFieldLength);
+ jsval.setString(Common::ToJSString(cx, shortVal));
+ return JS_DefineProperty(cx, aObj, aName, jsval, JSPROP_ENUMERATE);
+};
+
+static JSString* ModuleVersionToJSString(JSContext* aCx,
+ const ModuleVersion& aVersion) {
+ auto [major, minor, patch, build] = aVersion.AsTuple();
+
+ constexpr auto dot = u"."_ns;
+
+ nsAutoString strVer;
+ strVer.AppendInt(major);
+ strVer.Append(dot);
+ strVer.AppendInt(minor);
+ strVer.Append(dot);
+ strVer.AppendInt(patch);
+ strVer.Append(dot);
+ strVer.AppendInt(build);
+
+ return Common::ToJSString(aCx, strVer);
+}
+
+/**
+ * Convert the given container object to a JavaScript array.
+ *
+ * @param cx [in] The JS context.
+ * @param aRet [out] This gets assigned to the newly created
+ * array object.
+ * @param aContainer [in] The source container to convert.
+ * @param aElementConverter [in] A callable used to convert each element
+ * to a JS element. The form of this function is:
+ * bool(JSContext *cx,
+ * JS::MutableHandleValue aRet,
+ * const ElementT& aElement)
+ * @return true if aRet was successfully assigned to the new array object.
+ */
+template <typename T, typename Converter, typename... Args>
+static bool ContainerToJSArray(JSContext* cx, JS::MutableHandle<JSObject*> aRet,
+ const T& aContainer,
+ Converter&& aElementConverter, Args&&... aArgs) {
+ JS::Rooted<JSObject*> arr(cx, JS::NewArrayObject(cx, 0));
+ if (!arr) {
+ return false;
+ }
+
+ size_t i = 0;
+ for (auto&& item : aContainer) {
+ JS::Rooted<JS::Value> jsel(cx);
+ if (!aElementConverter(cx, &jsel, *item, std::forward<Args>(aArgs)...)) {
+ return false;
+ }
+ if (!JS_DefineElement(cx, arr, i, jsel, JSPROP_ENUMERATE)) {
+ return false;
+ }
+ ++i;
+ }
+
+ aRet.set(arr);
+ return true;
+}
+
+static bool SerializeModule(JSContext* aCx,
+ JS::MutableHandle<JS::Value> aElement,
+ const RefPtr<ModuleRecord>& aModule,
+ uint32_t aFlags) {
+ if (!aModule) {
+ return false;
+ }
+
+ JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx));
+ if (!obj) {
+ return false;
+ }
+
+ if (aFlags & nsITelemetry::INCLUDE_PRIVATE_FIELDS_IN_LOADEVENTS) {
+ JS::Rooted<JS::Value> jsFileObj(aCx);
+ if (!dom::ToJSValue(aCx, aModule->mResolvedDosName, &jsFileObj) ||
+ !JS_DefineProperty(aCx, obj, "dllFile", jsFileObj, JSPROP_ENUMERATE)) {
+ return false;
+ }
+ } else {
+ if (!AddLengthLimitedStringProp(aCx, obj, "resolvedDllName",
+ aModule->mSanitizedDllName)) {
+ return false;
+ }
+ }
+
+ if (aModule->mVersion.isSome()) {
+ JS::Rooted<JS::Value> jsModuleVersion(aCx);
+ jsModuleVersion.setString(
+ ModuleVersionToJSString(aCx, aModule->mVersion.ref()));
+ if (!JS_DefineProperty(aCx, obj, "fileVersion", jsModuleVersion,
+ JSPROP_ENUMERATE)) {
+ return false;
+ }
+ }
+
+#if defined(MOZ_GECKO_PROFILER)
+ if (aModule->mResolvedDosName) {
+ nsAutoString path;
+ if (aModule->mResolvedDosName->GetPath(path) == NS_OK) {
+ SharedLibraryInfo info = SharedLibraryInfo::GetInfoFromPath(path.Data());
+ if (info.GetSize() > 0) {
+ nsCString breakpadId = info.GetEntry(0).GetBreakpadId();
+ if (!AddLengthLimitedStringProp(aCx, obj, "debugID",
+ NS_ConvertASCIItoUTF16(breakpadId))) {
+ return false;
+ }
+ }
+ }
+ }
+#endif // MOZ_GECKO_PROFILER
+
+ if (aModule->mVendorInfo.isSome()) {
+ const char* propName;
+
+ const VendorInfo& vendorInfo = aModule->mVendorInfo.ref();
+ switch (vendorInfo.mSource) {
+ case VendorInfo::Source::Signature:
+ propName = "signedBy";
+ break;
+ case VendorInfo::Source::VersionInfo:
+ propName = "companyName";
+ break;
+ default:
+ MOZ_ASSERT_UNREACHABLE("Unknown VendorInfo Source!");
+ return false;
+ }
+
+ MOZ_ASSERT(!vendorInfo.mVendor.IsEmpty());
+ if (vendorInfo.mVendor.IsEmpty()) {
+ return false;
+ }
+
+ if (!AddLengthLimitedStringProp(aCx, obj, propName, vendorInfo.mVendor)) {
+ return false;
+ }
+ }
+
+ JS::Rooted<JS::Value> jsTrustFlags(aCx);
+ jsTrustFlags.setNumber(static_cast<uint32_t>(aModule->mTrustFlags));
+ if (!JS_DefineProperty(aCx, obj, "trustFlags", jsTrustFlags,
+ JSPROP_ENUMERATE)) {
+ return false;
+ }
+
+ aElement.setObject(*obj);
+ return true;
+}
+
+/* static */
+bool UntrustedModulesDataSerializer::SerializeEvent(
+ JSContext* aCx, JS::MutableHandle<JS::Value> aElement,
+ const ProcessedModuleLoadEventContainer& aEventContainer,
+ const IndexMap& aModuleIndices) {
+ MOZ_ASSERT(NS_IsMainThread());
+
+ const ProcessedModuleLoadEvent& event = aEventContainer.mEvent;
+ if (!event) {
+ return false;
+ }
+
+ JS::Rooted<JSObject*> obj(aCx, JS_NewPlainObject(aCx));
+ if (!obj) {
+ return false;
+ }
+
+ JS::Rooted<JS::Value> jsProcessUptimeMS(aCx);
+ // Javascript doesn't like 64-bit integers; convert to double.
+ jsProcessUptimeMS.setNumber(static_cast<double>(event.mProcessUptimeMS));
+ if (!JS_DefineProperty(aCx, obj, "processUptimeMS", jsProcessUptimeMS,
+ JSPROP_ENUMERATE)) {
+ return false;
+ }
+
+ if (event.mLoadDurationMS) {
+ JS::Rooted<JS::Value> jsLoadDurationMS(aCx);
+ jsLoadDurationMS.setNumber(event.mLoadDurationMS.value());
+ if (!JS_DefineProperty(aCx, obj, "loadDurationMS", jsLoadDurationMS,
+ JSPROP_ENUMERATE)) {
+ return false;
+ }
+ }
+
+ JS::Rooted<JS::Value> jsThreadId(aCx);
+ jsThreadId.setNumber(static_cast<uint32_t>(event.mThreadId));
+ if (!JS_DefineProperty(aCx, obj, "threadID", jsThreadId, JSPROP_ENUMERATE)) {
+ return false;
+ }
+
+ nsDependentCString effectiveThreadName;
+ if (event.mThreadId == ::GetCurrentThreadId()) {
+ effectiveThreadName.Rebind("Main Thread"_ns, 0);
+ } else {
+ effectiveThreadName.Rebind(event.mThreadName, 0);
+ }
+
+ if (!effectiveThreadName.IsEmpty()) {
+ JS::Rooted<JS::Value> jsThreadName(aCx);
+ jsThreadName.setString(Common::ToJSString(aCx, effectiveThreadName));
+ if (!JS_DefineProperty(aCx, obj, "threadName", jsThreadName,
+ JSPROP_ENUMERATE)) {
+ return false;
+ }
+ }
+
+ // Don't add this property unless mRequestedDllName differs from
+ // the associated module's mSanitizedDllName
+ if (!event.mRequestedDllName.IsEmpty() &&
+ !event.mRequestedDllName.Equals(event.mModule->mSanitizedDllName,
+ nsCaseInsensitiveStringComparator)) {
+ if (!AddLengthLimitedStringProp(aCx, obj, "requestedDllName",
+ event.mRequestedDllName)) {
+ return false;
+ }
+ }
+
+ nsAutoString strBaseAddress;
+ strBaseAddress.AppendLiteral(u"0x");
+ strBaseAddress.AppendInt(event.mBaseAddress, 16);
+
+ JS::Rooted<JS::Value> jsBaseAddress(aCx);
+ jsBaseAddress.setString(Common::ToJSString(aCx, strBaseAddress));
+ if (!JS_DefineProperty(aCx, obj, "baseAddress", jsBaseAddress,
+ JSPROP_ENUMERATE)) {
+ return false;
+ }
+
+ uint32_t index;
+ if (!aModuleIndices.Get(event.mModule->mResolvedNtName, &index)) {
+ return false;
+ }
+
+ JS::Rooted<JS::Value> jsModuleIndex(aCx);
+ jsModuleIndex.setNumber(index);
+ if (!JS_DefineProperty(aCx, obj, "moduleIndex", jsModuleIndex,
+ JSPROP_ENUMERATE)) {
+ return false;
+ }
+
+ JS::Rooted<JS::Value> jsIsDependent(aCx);
+ jsIsDependent.setBoolean(event.mIsDependent);
+ if (!JS_DefineProperty(aCx, obj, "isDependent", jsIsDependent,
+ JSPROP_ENUMERATE)) {
+ return false;
+ }
+
+ JS::Rooted<JS::Value> jsLoadStatus(aCx);
+ jsLoadStatus.setNumber(event.mLoadStatus);
+ if (!JS_DefineProperty(aCx, obj, "loadStatus", jsLoadStatus,
+ JSPROP_ENUMERATE)) {
+ return false;
+ }
+
+ aElement.setObject(*obj);
+
+ return true;
+}
+
+static nsDependentCString GetProcessTypeString(GeckoProcessType aType) {
+ nsDependentCString strProcType;
+ if (aType == GeckoProcessType_Default) {
+ strProcType.Rebind("browser"_ns, 0);
+ } else {
+ strProcType.Rebind(XRE_GeckoProcessTypeToString(aType));
+ }
+ return strProcType;
+}
+
+nsresult UntrustedModulesDataSerializer::GetPerProcObject(
+ const UntrustedModulesData& aData, JS::MutableHandle<JSObject*> aObj) {
+ JS::Rooted<JS::Value> jsProcType(mCx);
+ jsProcType.setString(
+ Common::ToJSString(mCx, GetProcessTypeString(aData.mProcessType)));
+ if (!JS_DefineProperty(mCx, aObj, "processType", jsProcType,
+ JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ JS::Rooted<JS::Value> jsElapsed(mCx);
+ jsElapsed.setNumber(aData.mElapsed.ToSecondsSigDigits());
+ if (!JS_DefineProperty(mCx, aObj, "elapsed", jsElapsed, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (aData.mXULLoadDurationMS.isSome()) {
+ JS::Rooted<JS::Value> jsXulLoadDurationMS(mCx);
+ jsXulLoadDurationMS.setNumber(aData.mXULLoadDurationMS.value());
+ if (!JS_DefineProperty(mCx, aObj, "xulLoadDurationMS", jsXulLoadDurationMS,
+ JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ JS::Rooted<JS::Value> jsSanitizationFailures(mCx);
+ jsSanitizationFailures.setNumber(aData.mSanitizationFailures);
+ if (!JS_DefineProperty(mCx, aObj, "sanitizationFailures",
+ jsSanitizationFailures, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ JS::Rooted<JS::Value> jsTrustTestFailures(mCx);
+ jsTrustTestFailures.setNumber(aData.mTrustTestFailures);
+ if (!JS_DefineProperty(mCx, aObj, "trustTestFailures", jsTrustTestFailures,
+ JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ JS::Rooted<JSObject*> eventsArray(mCx);
+ if (!ContainerToJSArray(mCx, &eventsArray, aData.mEvents, &SerializeEvent,
+ mIndexMap)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!JS_DefineProperty(mCx, aObj, "events", eventsArray, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!(mFlags & nsITelemetry::EXCLUDE_STACKINFO_FROM_LOADEVENTS)) {
+ JS::Rooted<JSObject*> combinedStacksObj(
+ mCx, CreateJSStackObject(mCx, aData.mStacks));
+ if (!combinedStacksObj) {
+ return NS_ERROR_FAILURE;
+ }
+
+ if (!JS_DefineProperty(mCx, aObj, "combinedStacks", combinedStacksObj,
+ JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult UntrustedModulesDataSerializer::AddLoadEvents(
+ const UntrustedModuleLoadingEvents& aEvents,
+ JS::MutableHandle<JSObject*> aPerProcObj) {
+ JS::Rooted<JS::Value> eventsArrayVal(mCx);
+ if (!JS_GetProperty(mCx, aPerProcObj, "events", &eventsArrayVal) ||
+ !eventsArrayVal.isObject()) {
+ return NS_ERROR_FAILURE;
+ }
+
+ JS::Rooted<JSObject*> eventsArray(mCx, &eventsArrayVal.toObject());
+ bool isArray;
+ if (!JS::IsArrayObject(mCx, eventsArray, &isArray) && !isArray) {
+ return NS_ERROR_FAILURE;
+ }
+
+ uint32_t currentPos;
+ if (!GetArrayLength(mCx, eventsArray, &currentPos)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ for (auto item : aEvents) {
+ JS::Rooted<JS::Value> jsel(mCx);
+ if (!SerializeEvent(mCx, &jsel, *item, mIndexMap) ||
+ !JS_DefineElement(mCx, eventsArray, currentPos++, jsel,
+ JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult UntrustedModulesDataSerializer::AddSingleData(
+ const UntrustedModulesData& aData) {
+ // Serialize each entry in the modules hashtable out to the "modules" array
+ // and store the indices in |mIndexMap|
+ for (const auto& entry : aData.mModules) {
+ if (!mIndexMap.WithEntryHandle(entry.GetKey(), [&](auto&& addPtr) {
+ if (!addPtr) {
+ addPtr.Insert(mCurModulesArrayIdx);
+
+ JS::Rooted<JS::Value> jsModule(mCx);
+ if (!SerializeModule(mCx, &jsModule, entry.GetData(), mFlags) ||
+ !JS_DefineElement(mCx, mModulesArray, mCurModulesArrayIdx,
+ jsModule, JSPROP_ENUMERATE)) {
+ return false;
+ }
+
+ ++mCurModulesArrayIdx;
+ }
+ return true;
+ })) {
+ return NS_ERROR_FAILURE;
+ }
+ }
+
+ if (mCurModulesArrayIdx >= mMaxModulesArrayLen) {
+ return NS_ERROR_CANNOT_CONVERT_DATA;
+ }
+
+ nsAutoCString strPid;
+ strPid.Append(GetProcessTypeString(aData.mProcessType));
+ strPid.AppendLiteral(".0x");
+ strPid.AppendInt(static_cast<uint32_t>(aData.mPid), 16);
+
+ if (mFlags & nsITelemetry::EXCLUDE_STACKINFO_FROM_LOADEVENTS) {
+ JS::Rooted<JS::Value> perProcVal(mCx);
+ if (JS_GetProperty(mCx, mPerProcObjContainer, strPid.get(), &perProcVal) &&
+ perProcVal.isObject()) {
+ // If a corresponding per-proc object already exists in the dictionary,
+ // and we skip to serialize CombinedStacks, we can add loading events
+ // into the JS object directly.
+ JS::Rooted<JSObject*> perProcObj(mCx, &perProcVal.toObject());
+ return AddLoadEvents(aData.mEvents, &perProcObj);
+ }
+ }
+
+ JS::Rooted<JSObject*> perProcObj(mCx, JS_NewPlainObject(mCx));
+ if (!perProcObj) {
+ return NS_ERROR_FAILURE;
+ }
+
+ nsresult rv = GetPerProcObject(aData, &perProcObj);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+
+ JS::Rooted<JS::Value> jsPerProcObjValue(mCx);
+ jsPerProcObjValue.setObject(*perProcObj);
+ if (!JS_DefineProperty(mCx, mPerProcObjContainer, strPid.get(),
+ jsPerProcObjValue, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return NS_OK;
+}
+
+UntrustedModulesDataSerializer::UntrustedModulesDataSerializer(
+ JSContext* aCx, uint32_t aMaxModulesArrayLen, uint32_t aFlags)
+ : mCtorResult(NS_ERROR_FAILURE),
+ mCx(aCx),
+ mMainObj(mCx, JS_NewPlainObject(mCx)),
+ mModulesArray(mCx, JS::NewArrayObject(mCx, 0)),
+ mBlockedModulesArray(mCx, JS::NewArrayObject(mCx, 0)),
+ mPerProcObjContainer(mCx, JS_NewPlainObject(mCx)),
+ mMaxModulesArrayLen(aMaxModulesArrayLen),
+ mCurModulesArrayIdx(0),
+ mCurBlockedModulesArrayIdx(0),
+ mFlags(aFlags) {
+ if (!mMainObj || !mModulesArray || !mBlockedModulesArray ||
+ !mPerProcObjContainer) {
+ return;
+ }
+
+ JS::Rooted<JS::Value> jsVersion(mCx);
+ jsVersion.setNumber(kThirdPartyModulesPingVersion);
+ if (!JS_DefineProperty(mCx, mMainObj, "structVersion", jsVersion,
+ JSPROP_ENUMERATE)) {
+ return;
+ }
+
+ JS::Rooted<JS::Value> jsModulesArrayValue(mCx);
+ jsModulesArrayValue.setObject(*mModulesArray);
+ if (!JS_DefineProperty(mCx, mMainObj, "modules", jsModulesArrayValue,
+ JSPROP_ENUMERATE)) {
+ return;
+ }
+
+ JS::Rooted<JS::Value> jsBlockedModulesArrayValue(mCx);
+ jsBlockedModulesArrayValue.setObject(*mBlockedModulesArray);
+ if (!JS_DefineProperty(mCx, mMainObj, "blockedModules",
+ jsBlockedModulesArrayValue, JSPROP_ENUMERATE)) {
+ return;
+ }
+
+ JS::Rooted<JS::Value> jsPerProcObjContainerValue(mCx);
+ jsPerProcObjContainerValue.setObject(*mPerProcObjContainer);
+ if (!JS_DefineProperty(mCx, mMainObj, "processes", jsPerProcObjContainerValue,
+ JSPROP_ENUMERATE)) {
+ return;
+ }
+
+ mCtorResult = NS_OK;
+}
+
+UntrustedModulesDataSerializer::operator bool() const {
+ return NS_SUCCEEDED(mCtorResult);
+}
+
+void UntrustedModulesDataSerializer::GetObject(
+ JS::MutableHandle<JS::Value> aRet) {
+ aRet.setObject(*mMainObj);
+}
+
+nsresult UntrustedModulesDataSerializer::Add(
+ const UntrustedModulesBackupData& aData) {
+ if (NS_FAILED(mCtorResult)) {
+ return mCtorResult;
+ }
+
+ for (const RefPtr<UntrustedModulesDataContainer>& container :
+ aData.Values()) {
+ if (!container) {
+ continue;
+ }
+
+ nsresult rv = AddSingleData(container->mData);
+ if (NS_FAILED(rv)) {
+ return rv;
+ }
+ }
+
+ return NS_OK;
+}
+
+nsresult UntrustedModulesDataSerializer::AddBlockedModules(
+ const nsTArray<nsDependentSubstring>& blockedModules) {
+ if (NS_FAILED(mCtorResult)) {
+ return mCtorResult;
+ }
+
+ if (blockedModules.Length() >= mMaxModulesArrayLen) {
+ return NS_ERROR_CANNOT_CONVERT_DATA;
+ }
+
+ for (const auto& blockedModule : blockedModules) {
+ JS::Rooted<JS::Value> jsBlockedModule(mCx);
+ jsBlockedModule.setString(Common::ToJSString(mCx, blockedModule));
+ if (!JS_DefineElement(mCx, mBlockedModulesArray, mCurBlockedModulesArrayIdx,
+ jsBlockedModule, JSPROP_ENUMERATE)) {
+ return NS_ERROR_FAILURE;
+ }
+ ++mCurBlockedModulesArrayIdx;
+ }
+
+ return NS_OK;
+}
+
+} // namespace Telemetry
+} // namespace mozilla
diff --git a/toolkit/components/telemetry/other/UntrustedModulesDataSerializer.h b/toolkit/components/telemetry/other/UntrustedModulesDataSerializer.h
new file mode 100644
index 0000000000..cb0bce18ee
--- /dev/null
+++ b/toolkit/components/telemetry/other/UntrustedModulesDataSerializer.h
@@ -0,0 +1,84 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef UntrustedModulesDataSerializer_h__
+#define UntrustedModulesDataSerializer_h__
+
+#include "js/TypeDecls.h"
+#include "mozilla/UntrustedModulesData.h"
+#include "mozilla/Vector.h"
+#include "nsTHashMap.h"
+#include "UntrustedModulesBackupService.h"
+
+namespace mozilla {
+namespace Telemetry {
+
+// This class owns a JS object and serializes a given UntrustedModulesData
+// into it. Because this class uses JSAPI, an AutoJSAPI instance must be
+// on the stack before instantiating the class.
+class MOZ_RAII UntrustedModulesDataSerializer final {
+ using IndexMap = nsTHashMap<nsStringHashKey, uint32_t>;
+
+ nsresult mCtorResult;
+ JSContext* mCx;
+ JS::Rooted<JSObject*> mMainObj;
+ JS::Rooted<JSObject*> mModulesArray;
+ JS::Rooted<JSObject*> mBlockedModulesArray;
+ JS::Rooted<JSObject*> mPerProcObjContainer;
+
+ IndexMap mIndexMap;
+ const uint32_t mMaxModulesArrayLen;
+ uint32_t mCurModulesArrayIdx;
+ uint32_t mCurBlockedModulesArrayIdx;
+
+ // Combinations of the flags defined under nsITelemetry.
+ // (See "Flags for getUntrustedModuleLoadEvents" in nsITelemetry.idl)
+ const uint32_t mFlags;
+
+ static bool SerializeEvent(
+ JSContext* aCx, JS::MutableHandle<JS::Value> aElement,
+ const ProcessedModuleLoadEventContainer& aEventContainer,
+ const IndexMap& aModuleIndices);
+ nsresult GetPerProcObject(const UntrustedModulesData& aData,
+ JS::MutableHandle<JSObject*> aObj);
+ nsresult AddLoadEvents(const UntrustedModuleLoadingEvents& aEvents,
+ JS::MutableHandle<JSObject*> aPerProcObj);
+ nsresult AddSingleData(const UntrustedModulesData& aData);
+
+ public:
+ UntrustedModulesDataSerializer(JSContext* aCx, uint32_t aMaxModulesArrayLen,
+ uint32_t aFlags = 0);
+ explicit operator bool() const;
+
+ /**
+ * Retrieves the JS object.
+ *
+ * @param aRet [out] This gets assigned to the newly created object.
+ */
+ void GetObject(JS::MutableHandle<JS::Value> aRet);
+
+ /**
+ * Adds data to the JS object.
+ *
+ * When the process of a given UntrustedModulesData collides with a key in
+ * the JS object, the entire UntrustedModulesData instance in the JS object
+ * will be replaced unless EXCLUDE_STACKINFO_FROM_LOADEVENTS is set.
+ * When EXCLUDE_STACKINFO_FROM_LOADEVENTS is set, the given loading events
+ * are appended into the JS object, keeping the existing data as is.
+ *
+ * @param aData [in] The source objects to add.
+ * @return nsresult
+ */
+ nsresult Add(const UntrustedModulesBackupData& aData);
+
+ nsresult AddBlockedModules(
+ const nsTArray<nsDependentSubstring>& blockedModules);
+};
+
+} // namespace Telemetry
+} // namespace mozilla
+
+#endif // UntrustedModulesDataSerializer_h__
diff --git a/toolkit/components/telemetry/pings.yaml b/toolkit/components/telemetry/pings.yaml
new file mode 100644
index 0000000000..d99e19d66b
--- /dev/null
+++ b/toolkit/components/telemetry/pings.yaml
@@ -0,0 +1,6 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+---
+$schema: moz://mozilla.org/schemas/glean/pings/2-0-0
diff --git a/toolkit/components/telemetry/pings/BackgroundTask_pingsender.sys.mjs b/toolkit/components/telemetry/pings/BackgroundTask_pingsender.sys.mjs
new file mode 100644
index 0000000000..2297e6f94f
--- /dev/null
+++ b/toolkit/components/telemetry/pings/BackgroundTask_pingsender.sys.mjs
@@ -0,0 +1,41 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { sendStandalonePing } from "resource://gre/modules/TelemetrySend.sys.mjs";
+
+export async function runBackgroundTask(commandLine) {
+ let sends = [];
+ for (let i = 0; i < commandLine.length; i += 2) {
+ sends.push(
+ sendPing(commandLine.getArgument(i), commandLine.getArgument(i + 1))
+ );
+ }
+
+ return Promise.all(sends);
+}
+
+// The standalone pingsender utility had an allowlist of endpoints, which was
+// added to prevent it from being used as a generic exfiltration utility by
+// unrelated malware running on the same system. It's unclear whether a gecko-
+// based pingsender would be similarly desirable for that use-case, but it's
+// easy enough to implement an allowlist here as well.
+const ALLOWED_ENDPOINTS = ["localhost", "incoming.telemetry.mozilla.org"];
+
+async function sendPing(endpoint, path) {
+ console.log(`pingsender: sending ${path} to ${endpoint}`);
+
+ let hostname = new URL(endpoint).hostname;
+ if (!ALLOWED_ENDPOINTS.includes(hostname)) {
+ throw new Error(`pingsender: Endpoint ${endpoint} is not allowed`);
+ }
+
+ let json = await IOUtils.readUTF8(path);
+ await sendStandalonePing(endpoint, json, {
+ "User-Agent": "pingsender/2.0",
+ "X-PingSender-Version": "2.0",
+ });
+
+ return IOUtils.remove(path);
+}
diff --git a/toolkit/components/telemetry/pings/CoveragePing.sys.mjs b/toolkit/components/telemetry/pings/CoveragePing.sys.mjs
new file mode 100644
index 0000000000..a41d7dcc7b
--- /dev/null
+++ b/toolkit/components/telemetry/pings/CoveragePing.sys.mjs
@@ -0,0 +1,154 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { Log } from "resource://gre/modules/Log.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ CommonUtils: "resource://services-common/utils.sys.mjs",
+ ServiceRequest: "resource://gre/modules/ServiceRequest.sys.mjs",
+ UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
+});
+
+const COVERAGE_VERSION = "2";
+
+const COVERAGE_ENABLED_PREF = "toolkit.coverage.enabled";
+const LOG_LEVEL_PREF = "toolkit.coverage.log-level";
+const OPT_OUT_PREF = "toolkit.coverage.opt-out";
+const ALREADY_RUN_PREF = `toolkit.coverage.already-run.v${COVERAGE_VERSION}`;
+const COVERAGE_UUID_PREF = `toolkit.coverage.uuid.v${COVERAGE_VERSION}`;
+const TELEMETRY_ENABLED_PREF = "datareporting.healthreport.uploadEnabled";
+const REPORTING_ENDPOINT_BASE_PREF = `toolkit.coverage.endpoint.base`;
+const REPORTING_ENDPOINT = "submit/coverage/coverage";
+const PING_SUBMISSION_TIMEOUT = 30 * 1000; // 30 seconds
+
+const log = Log.repository.getLogger("Telemetry::CoveragePing");
+log.level = Services.prefs.getIntPref(LOG_LEVEL_PREF, Log.Level.Error);
+log.addAppender(new Log.ConsoleAppender(new Log.BasicFormatter()));
+
+export var CoveragePing = Object.freeze({
+ async startup() {
+ if (!Services.prefs.getBoolPref(COVERAGE_ENABLED_PREF, false)) {
+ log.debug("coverage not enabled");
+ return;
+ }
+
+ if (Services.prefs.getBoolPref(OPT_OUT_PREF, false)) {
+ log.debug("user has set opt-out pref");
+ return;
+ }
+
+ if (Services.prefs.getBoolPref(ALREADY_RUN_PREF, false)) {
+ log.debug("already run on this profile");
+ return;
+ }
+
+ if (!Services.prefs.getCharPref(REPORTING_ENDPOINT_BASE_PREF, null)) {
+ log.error("no endpoint base set");
+ return;
+ }
+
+ try {
+ await this.reportTelemetrySetting();
+ } catch (e) {
+ log.error("unable to upload payload", e);
+ }
+ },
+
+ // NOTE - this does not use existing Telemetry code or honor Telemetry opt-out prefs,
+ // by design. It also sends no identifying data like the client ID. See the "coverage ping"
+ // documentation for details.
+ reportTelemetrySetting() {
+ const enabled = Services.prefs.getBoolPref(TELEMETRY_ENABLED_PREF, false);
+
+ const payload = {
+ appVersion: Services.appinfo.version,
+ appUpdateChannel: lazy.UpdateUtils.getUpdateChannel(false),
+ osName: Services.sysinfo.getProperty("name"),
+ osVersion: Services.sysinfo.getProperty("version"),
+ telemetryEnabled: enabled,
+ };
+
+ let cachedUuid = Services.prefs.getCharPref(COVERAGE_UUID_PREF, null);
+ if (!cachedUuid) {
+ // Totally random UUID, just for detecting duplicates.
+ cachedUuid = lazy.CommonUtils.generateUUID();
+ Services.prefs.setCharPref(COVERAGE_UUID_PREF, cachedUuid);
+ }
+
+ let reportingEndpointBase = Services.prefs.getCharPref(
+ REPORTING_ENDPOINT_BASE_PREF,
+ null
+ );
+
+ let endpoint = `${reportingEndpointBase}/${REPORTING_ENDPOINT}/${COVERAGE_VERSION}/${cachedUuid}`;
+
+ log.debug(`putting to endpoint ${endpoint} with payload:`, payload);
+
+ let deferred = Promise.withResolvers();
+
+ let request = new lazy.ServiceRequest({ mozAnon: true });
+ request.mozBackgroundRequest = true;
+ request.timeout = PING_SUBMISSION_TIMEOUT;
+
+ request.open("PUT", endpoint, true);
+ request.overrideMimeType("text/plain");
+ request.setRequestHeader("Content-Type", "application/json; charset=UTF-8");
+ request.setRequestHeader("Date", new Date().toUTCString());
+
+ let errorhandler = event => {
+ let failure = event.type;
+ log.error(`error making request to ${endpoint}: ${failure}`);
+ deferred.reject(event);
+ };
+
+ request.onerror = errorhandler;
+ request.ontimeout = errorhandler;
+ request.onabort = errorhandler;
+
+ request.onloadend = event => {
+ let status = request.status;
+ let statusClass = status - (status % 100);
+ let success = false;
+
+ if (statusClass === 200) {
+ // We can treat all 2XX as success.
+ log.info(`successfully submitted, status: ${status}`);
+ success = true;
+ } else if (statusClass === 400) {
+ // 4XX means that something with the request was broken.
+
+ // TODO: we should handle this better, but for now we should avoid resubmitting
+ // broken requests by pretending success.
+ success = true;
+ log.error(
+ `error submitting to ${endpoint}, status: ${status} - ping request broken?`
+ );
+ } else if (statusClass === 500) {
+ // 5XX means there was a server-side error and we should try again later.
+ log.error(
+ `error submitting to ${endpoint}, status: ${status} - server error, should retry later`
+ );
+ } else {
+ // We received an unexpected status code.
+ log.error(
+ `error submitting to ${endpoint}, status: ${status}, type: ${event.type}`
+ );
+ }
+
+ if (success) {
+ Services.prefs.setBoolPref(ALREADY_RUN_PREF, true);
+ log.debug(`result from PUT: ${request.responseText}`);
+ deferred.resolve();
+ } else {
+ deferred.reject(event);
+ }
+ };
+
+ request.send(JSON.stringify(payload));
+
+ return deferred.promise;
+ },
+});
diff --git a/toolkit/components/telemetry/pings/EventPing.sys.mjs b/toolkit/components/telemetry/pings/EventPing.sys.mjs
new file mode 100644
index 0000000000..36e8489e37
--- /dev/null
+++ b/toolkit/components/telemetry/pings/EventPing.sys.mjs
@@ -0,0 +1,241 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * This module sends Telemetry Events periodically:
+ * https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/data/event-ping.html
+ */
+
+import { TelemetryUtils } from "resource://gre/modules/TelemetryUtils.sys.mjs";
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ Log: "resource://gre/modules/Log.sys.mjs",
+ TelemetryController: "resource://gre/modules/TelemetryController.sys.mjs",
+ TelemetrySession: "resource://gre/modules/TelemetrySession.sys.mjs",
+ clearTimeout: "resource://gre/modules/Timer.sys.mjs",
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+const Utils = TelemetryUtils;
+
+const MS_IN_A_MINUTE = 60 * 1000;
+
+const DEFAULT_EVENT_LIMIT = 1000;
+const DEFAULT_MIN_FREQUENCY_MS = 60 * MS_IN_A_MINUTE;
+const DEFAULT_MAX_FREQUENCY_MS = 10 * MS_IN_A_MINUTE;
+
+const LOGGER_NAME = "Toolkit.Telemetry";
+const LOGGER_PREFIX = "TelemetryEventPing::";
+
+const EVENT_LIMIT_REACHED_TOPIC = "event-telemetry-storage-limit-reached";
+
+export var Policy = {
+ setTimeout: (callback, delayMs) => lazy.setTimeout(callback, delayMs),
+ clearTimeout: id => lazy.clearTimeout(id),
+ sendPing: (type, payload, options) =>
+ lazy.TelemetryController.submitExternalPing(type, payload, options),
+};
+
+export var TelemetryEventPing = {
+ Reason: Object.freeze({
+ PERIODIC: "periodic", // Sent the ping containing events from the past periodic interval (default one hour).
+ MAX: "max", // Sent the ping containing the maximum number (default 1000) of event records, earlier than the periodic interval.
+ SHUTDOWN: "shutdown", // Recorded data was sent on shutdown.
+ }),
+
+ EVENT_PING_TYPE: "event",
+
+ _logger: null,
+
+ _testing: false,
+
+ // So that if we quickly reach the max limit we can immediately send.
+ _lastSendTime: -DEFAULT_MIN_FREQUENCY_MS,
+
+ _processStartTimestamp: 0,
+
+ get dataset() {
+ return Services.telemetry.canRecordPrereleaseData
+ ? Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS
+ : Ci.nsITelemetry.DATASET_ALL_CHANNELS;
+ },
+
+ startup() {
+ this._log.trace("Starting up.");
+
+ // Calculate process creation once.
+ this._processStartTimestamp =
+ Math.round(
+ (Date.now() - TelemetryUtils.monotonicNow()) / MS_IN_A_MINUTE
+ ) * MS_IN_A_MINUTE;
+
+ Services.obs.addObserver(this, EVENT_LIMIT_REACHED_TOPIC);
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "maxFrequency",
+ Utils.Preferences.EventPingMaximumFrequency,
+ DEFAULT_MAX_FREQUENCY_MS
+ );
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ "minFrequency",
+ Utils.Preferences.EventPingMinimumFrequency,
+ DEFAULT_MIN_FREQUENCY_MS
+ );
+
+ this._startTimer();
+ },
+
+ shutdown() {
+ this._log.trace("Shutting down.");
+ // removeObserver may throw, which could interrupt shutdown.
+ try {
+ Services.obs.removeObserver(this, EVENT_LIMIT_REACHED_TOPIC);
+ } catch (ex) {}
+
+ this._submitPing(this.Reason.SHUTDOWN, true /* discardLeftovers */);
+ this._clearTimer();
+ },
+
+ observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case EVENT_LIMIT_REACHED_TOPIC:
+ this._log.trace("event limit reached");
+ let now = Utils.monotonicNow();
+ if (now - this._lastSendTime < this.maxFrequency) {
+ this._log.trace("can't submit ping immediately as it's too soon");
+ this._startTimer(
+ this.maxFrequency - this._lastSendTime,
+ this.Reason.MAX,
+ true /* discardLeftovers*/
+ );
+ } else {
+ this._log.trace("submitting ping immediately");
+ this._submitPing(this.Reason.MAX);
+ }
+ break;
+ }
+ },
+
+ _startTimer(
+ delay = this.minFrequency,
+ reason = this.Reason.PERIODIC,
+ discardLeftovers = false
+ ) {
+ this._clearTimer();
+ this._timeoutId = Policy.setTimeout(
+ () => TelemetryEventPing._submitPing(reason, discardLeftovers),
+ delay
+ );
+ },
+
+ _clearTimer() {
+ if (this._timeoutId) {
+ Policy.clearTimeout(this._timeoutId);
+ this._timeoutId = null;
+ }
+ },
+
+ /**
+ * Submits an "event" ping and restarts the timer for the next interval.
+ *
+ * @param {String} reason The reason we're sending the ping. One of TelemetryEventPing.Reason.
+ * @param {bool} discardLeftovers Whether to discard event records left over from a previous ping.
+ */
+ _submitPing(reason, discardLeftovers = false) {
+ this._log.trace("_submitPing");
+
+ if (reason !== this.Reason.SHUTDOWN) {
+ this._startTimer();
+ }
+
+ let snapshot = Services.telemetry.snapshotEvents(
+ this.dataset,
+ true /* clear */,
+ DEFAULT_EVENT_LIMIT
+ );
+
+ if (!this._testing) {
+ for (let process of Object.keys(snapshot)) {
+ snapshot[process] = snapshot[process].filter(
+ ([, category]) => !category.startsWith("telemetry.test")
+ );
+ }
+ }
+
+ let eventCount = Object.values(snapshot).reduce(
+ (acc, val) => acc + val.length,
+ 0
+ );
+ if (eventCount === 0) {
+ // Don't send a ping if we haven't any events.
+ this._log.trace("not sending event ping due to lack of events");
+ return;
+ }
+
+ // The reason doesn't matter as it will just be echo'd back.
+ let sessionMeta = lazy.TelemetrySession.getMetadata(reason);
+
+ let payload = {
+ reason,
+ processStartTimestamp: this._processStartTimestamp,
+ sessionId: sessionMeta.sessionId,
+ subsessionId: sessionMeta.subsessionId,
+ lostEventsCount: 0,
+ events: snapshot,
+ };
+
+ if (discardLeftovers) {
+ // Any leftovers must be discarded, the count submitted in the ping.
+ // This can happen on shutdown or if our max was reached before faster
+ // than our maxFrequency.
+ let leftovers = Services.telemetry.snapshotEvents(
+ this.dataset,
+ true /* clear */
+ );
+ let leftoverCount = Object.values(leftovers).reduce(
+ (acc, val) => acc + val.length,
+ 0
+ );
+ payload.lostEventsCount = leftoverCount;
+ }
+
+ const options = {
+ addClientId: true,
+ addEnvironment: true,
+ usePingSender: reason == this.Reason.SHUTDOWN,
+ };
+
+ this._lastSendTime = Utils.monotonicNow();
+ Services.telemetry
+ .getHistogramById("TELEMETRY_EVENT_PING_SENT")
+ .add(reason);
+ Policy.sendPing(this.EVENT_PING_TYPE, payload, options);
+ },
+
+ /**
+ * Test-only, restore to initial state.
+ */
+ testReset() {
+ this._lastSendTime = -DEFAULT_MIN_FREQUENCY_MS;
+ this._clearTimer();
+ this._testing = true;
+ },
+
+ get _log() {
+ if (!this._logger) {
+ this._logger = lazy.Log.repository.getLoggerWithMessagePrefix(
+ LOGGER_NAME,
+ LOGGER_PREFIX + "::"
+ );
+ }
+
+ return this._logger;
+ },
+};
diff --git a/toolkit/components/telemetry/pings/HealthPing.sys.mjs b/toolkit/components/telemetry/pings/HealthPing.sys.mjs
new file mode 100644
index 0000000000..3450f65a3e
--- /dev/null
+++ b/toolkit/components/telemetry/pings/HealthPing.sys.mjs
@@ -0,0 +1,271 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * This module collects data on send failures and other critical issues with Telemetry submissions.
+ */
+
+import { TelemetryUtils } from "resource://gre/modules/TelemetryUtils.sys.mjs";
+
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ Log: "resource://gre/modules/Log.sys.mjs",
+ TelemetryController: "resource://gre/modules/TelemetryController.sys.mjs",
+ clearTimeout: "resource://gre/modules/Timer.sys.mjs",
+ setTimeout: "resource://gre/modules/Timer.sys.mjs",
+});
+
+const Utils = TelemetryUtils;
+
+const MS_IN_A_MINUTE = 60 * 1000;
+// Send health ping every hour
+const SEND_TICK_DELAY = 60 * MS_IN_A_MINUTE;
+
+// Send top 10 discarded pings only to minimize health ping size
+const MAX_SEND_DISCARDED_PINGS = 10;
+
+const LOGGER_NAME = "Toolkit.Telemetry";
+const LOGGER_PREFIX = "TelemetryHealthPing::";
+
+export var Policy = {
+ setSchedulerTickTimeout: (callback, delayMs) =>
+ lazy.setTimeout(callback, delayMs),
+ clearSchedulerTickTimeout: id => lazy.clearTimeout(id),
+};
+
+export var TelemetryHealthPing = {
+ Reason: Object.freeze({
+ IMMEDIATE: "immediate", // Ping was sent immediately after recording with no delay.
+ DELAYED: "delayed", // Recorded data was sent after a delay.
+ SHUT_DOWN: "shutdown", // Recorded data was sent on shutdown.
+ }),
+
+ FailureType: Object.freeze({
+ DISCARDED_FOR_SIZE: "pingDiscardedForSize",
+ SEND_FAILURE: "sendFailure",
+ }),
+
+ OsInfo: Object.freeze({
+ name: Services.appinfo.OS,
+ version:
+ Services.sysinfo.get("kernel_version") || Services.sysinfo.get("version"),
+ }),
+
+ HEALTH_PING_TYPE: "health",
+
+ _logger: null,
+
+ // The health ping is sent every every SEND_TICK_DELAY.
+ // Initialize this so that first failures are sent immediately.
+ _lastSendTime: -SEND_TICK_DELAY,
+
+ /**
+ * This stores reported send failures with the following structure:
+ * {
+ * type1: {
+ * subtype1: value,
+ * ...
+ * subtypeN: value
+ * },
+ * ...
+ * }
+ */
+ _failures: {},
+ _timeoutId: null,
+
+ /**
+ * Record a failure to send a ping out.
+ * @param {String} failureType The type of failure (e.g. "timeout", ...).
+ * @returns {Promise} Test-only, resolved when the ping is stored or sent.
+ */
+ recordSendFailure(failureType) {
+ return this._addToFailure(this.FailureType.SEND_FAILURE, failureType);
+ },
+
+ /**
+ * Record that a ping was discarded and its type.
+ * @param {String} pingType The type of discarded ping (e.g. "main", ...).
+ * @returns {Promise} Test-only, resolved when the ping is stored or sent.
+ */
+ recordDiscardedPing(pingType) {
+ return this._addToFailure(this.FailureType.DISCARDED_FOR_SIZE, pingType);
+ },
+
+ /**
+ * Assemble payload.
+ * @param {String} reason A string indicating the triggering reason (e.g. "immediate", "delayed", "shutdown").
+ * @returns {Object} The assembled payload.
+ */
+ _assemblePayload(reason) {
+ this._log.trace("_assemblePayload()");
+ let payload = {
+ os: this.OsInfo,
+ reason,
+ };
+
+ for (let key of Object.keys(this._failures)) {
+ if (key === this.FailureType.DISCARDED_FOR_SIZE) {
+ payload[key] = this._getTopDiscardFailures(this._failures[key]);
+ } else {
+ payload[key] = this._failures[key];
+ }
+ }
+
+ return payload;
+ },
+
+ /**
+ * Sort input dictionary descending by value.
+ * @param {Object} failures A dictionary of failures subtype and count.
+ * @returns {Object} Sorted failures by value.
+ */
+ _getTopDiscardFailures(failures) {
+ this._log.trace("_getTopDiscardFailures()");
+ let sortedItems = Object.entries(failures).sort((first, second) => {
+ return second[1] - first[1];
+ });
+
+ let result = {};
+ sortedItems.slice(0, MAX_SEND_DISCARDED_PINGS).forEach(([key, value]) => {
+ result[key] = value;
+ });
+
+ return result;
+ },
+
+ /**
+ * Assemble the failure information and submit it.
+ * @param {String} reason A string indicating the triggering reason (e.g. "immediate", "delayed", "shutdown").
+ * @returns {Promise} Test-only promise that resolves when the ping was stored or sent (if any).
+ */
+ _submitPing(reason) {
+ if (!TelemetryHealthPing.enabled || !this._hasDataToSend()) {
+ return Promise.resolve();
+ }
+
+ this._log.trace("_submitPing(" + reason + ")");
+ let payload = this._assemblePayload(reason);
+ this._clearData();
+ this._lastSendTime = Utils.monotonicNow();
+
+ let options = {
+ addClientId: true,
+ usePingSender: reason === this.Reason.SHUT_DOWN,
+ };
+
+ return new Promise(r =>
+ // If we submit the health ping immediately, the send task would be triggered again
+ // before discarding oversized pings from the queue.
+ // To work around this, we send the ping on the next tick.
+ Services.tm.dispatchToMainThread(() =>
+ r(
+ lazy.TelemetryController.submitExternalPing(
+ this.HEALTH_PING_TYPE,
+ payload,
+ options
+ )
+ )
+ )
+ );
+ },
+
+ /**
+ * Accumulate failure information and trigger a ping immediately or on timeout.
+ * @param {String} failureType The type of failure (e.g. "timeout", ...).
+ * @param {String} failureSubType The subtype of failure (e.g. ping type, ...).
+ * @returns {Promise} Test-only, resolved when the ping is stored or sent.
+ */
+ _addToFailure(failureType, failureSubType) {
+ this._log.trace(
+ "_addToFailure() - with type and subtype: " +
+ failureType +
+ " : " +
+ failureSubType
+ );
+
+ if (!(failureType in this._failures)) {
+ this._failures[failureType] = {};
+ }
+
+ let current = this._failures[failureType][failureSubType] || 0;
+ this._failures[failureType][failureSubType] = current + 1;
+
+ const now = Utils.monotonicNow();
+ if (now - this._lastSendTime >= SEND_TICK_DELAY) {
+ return this._submitPing(this.Reason.IMMEDIATE);
+ }
+
+ let submissionDelay = SEND_TICK_DELAY - now - this._lastSendTime;
+ this._timeoutId = Policy.setSchedulerTickTimeout(
+ () => TelemetryHealthPing._submitPing(this.Reason.DELAYED),
+ submissionDelay
+ );
+ return Promise.resolve();
+ },
+
+ /**
+ * @returns {boolean} Check the availability of recorded failures data.
+ */
+ _hasDataToSend() {
+ return Object.keys(this._failures).length !== 0;
+ },
+
+ /**
+ * Clear recorded failures data.
+ */
+ _clearData() {
+ this._log.trace("_clearData()");
+ this._failures = {};
+ },
+
+ /**
+ * Clear and reset timeout.
+ */
+ _resetTimeout() {
+ if (this._timeoutId) {
+ Policy.clearSchedulerTickTimeout(this._timeoutId);
+ this._timeoutId = null;
+ }
+ },
+
+ /**
+ * Submit latest ping on shutdown.
+ * @returns {Promise} Test-only, resolved when the ping is stored or sent.
+ */
+ shutdown() {
+ this._log.trace("shutdown()");
+ this._resetTimeout();
+ return this._submitPing(this.Reason.SHUT_DOWN);
+ },
+
+ /**
+ * Test-only, restore to initial state.
+ */
+ testReset() {
+ this._lastSendTime = -SEND_TICK_DELAY;
+ this._clearData();
+ this._resetTimeout();
+ },
+
+ get _log() {
+ if (!this._logger) {
+ this._logger = lazy.Log.repository.getLoggerWithMessagePrefix(
+ LOGGER_NAME,
+ LOGGER_PREFIX + "::"
+ );
+ }
+
+ return this._logger;
+ },
+};
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ TelemetryHealthPing,
+ "enabled",
+ TelemetryUtils.Preferences.HealthPingEnabled,
+ true
+);
diff --git a/toolkit/components/telemetry/pings/ModulesPing.sys.mjs b/toolkit/components/telemetry/pings/ModulesPing.sys.mjs
new file mode 100644
index 0000000000..222fb7a47e
--- /dev/null
+++ b/toolkit/components/telemetry/pings/ModulesPing.sys.mjs
@@ -0,0 +1,116 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+import { Log } from "resource://gre/modules/Log.sys.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ TelemetryController: "resource://gre/modules/TelemetryController.sys.mjs",
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ lazy,
+ "gUpdateTimerManager",
+ "@mozilla.org/updates/timer-manager;1",
+ "nsIUpdateTimerManager"
+);
+
+const LOGGER_NAME = "Toolkit.Telemetry";
+const LOGGER_PREFIX = "TelemetryModules::";
+
+// The default is 1 week.
+const MODULES_PING_INTERVAL_SECONDS = 7 * 24 * 60 * 60;
+const MODULES_PING_INTERVAL_PREFERENCE =
+ "toolkit.telemetry.modulesPing.interval";
+
+const MAX_MODULES_NUM = 512;
+const MAX_NAME_LENGTH = 64;
+const TRUNCATION_DELIMITER = "\u2026";
+
+export var TelemetryModules = Object.freeze({
+ _log: Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX),
+
+ start() {
+ // The list of loaded modules is obtainable only when the profiler is enabled.
+ // If it isn't, we don't want to send the ping at all.
+ if (!AppConstants.MOZ_GECKO_PROFILER) {
+ return;
+ }
+
+ // Use nsIUpdateTimerManager for a long-duration timer that survives across sessions.
+ let interval = Services.prefs.getIntPref(
+ MODULES_PING_INTERVAL_PREFERENCE,
+ MODULES_PING_INTERVAL_SECONDS
+ );
+ lazy.gUpdateTimerManager.registerTimer(
+ "telemetry_modules_ping",
+ this,
+ interval,
+ interval != 0 // only skip the first interval if the interval is non-0
+ );
+ },
+
+ /**
+ * Called when the 'telemetry_modules_ping' timer fires.
+ */
+ notify() {
+ try {
+ Services.telemetry.getLoadedModules().then(
+ modules => {
+ modules = modules.filter(module => !!module.name.length);
+
+ // Cut the list of modules to MAX_MODULES_NUM entries.
+ if (modules.length > MAX_MODULES_NUM) {
+ modules = modules.slice(0, MAX_MODULES_NUM);
+ }
+
+ // Cut the file names of the modules to MAX_NAME_LENGTH characters.
+ for (let module of modules) {
+ if (module.name.length > MAX_NAME_LENGTH) {
+ module.name =
+ module.name.substr(0, MAX_NAME_LENGTH - 1) +
+ TRUNCATION_DELIMITER;
+ }
+
+ if (
+ module.debugName !== null &&
+ module.debugName.length > MAX_NAME_LENGTH
+ ) {
+ module.debugName =
+ module.debugName.substr(0, MAX_NAME_LENGTH - 1) +
+ TRUNCATION_DELIMITER;
+ }
+
+ if (
+ module.certSubject !== undefined &&
+ module.certSubject.length > MAX_NAME_LENGTH
+ ) {
+ module.certSubject =
+ module.certSubject.substr(0, MAX_NAME_LENGTH - 1) +
+ TRUNCATION_DELIMITER;
+ }
+ }
+
+ lazy.TelemetryController.submitExternalPing(
+ "modules",
+ {
+ version: 1,
+ modules,
+ },
+ {
+ addClientId: true,
+ addEnvironment: true,
+ }
+ );
+ },
+ err => this._log.error("notify - promise failed", err)
+ );
+ } catch (ex) {
+ this._log.error("notify - caught exception", ex);
+ }
+ },
+});
diff --git a/toolkit/components/telemetry/pings/TelemetrySession.sys.mjs b/toolkit/components/telemetry/pings/TelemetrySession.sys.mjs
new file mode 100644
index 0000000000..f564cd41dd
--- /dev/null
+++ b/toolkit/components/telemetry/pings/TelemetrySession.sys.mjs
@@ -0,0 +1,1411 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { Log } from "resource://gre/modules/Log.sys.mjs";
+import { TelemetryUtils } from "resource://gre/modules/TelemetryUtils.sys.mjs";
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ AddonManagerPrivate: "resource://gre/modules/AddonManager.sys.mjs",
+ TelemetryController: "resource://gre/modules/TelemetryController.sys.mjs",
+ TelemetryEnvironment: "resource://gre/modules/TelemetryEnvironment.sys.mjs",
+ TelemetryReportingPolicy:
+ "resource://gre/modules/TelemetryReportingPolicy.sys.mjs",
+ TelemetryScheduler: "resource://gre/modules/TelemetryScheduler.sys.mjs",
+ TelemetryStorage: "resource://gre/modules/TelemetryStorage.sys.mjs",
+});
+
+const Utils = TelemetryUtils;
+
+// When modifying the payload in incompatible ways, please bump this version number
+const PAYLOAD_VERSION = 4;
+const PING_TYPE_MAIN = "main";
+const PING_TYPE_SAVED_SESSION = "saved-session";
+
+const REASON_ABORTED_SESSION = "aborted-session";
+const REASON_DAILY = "daily";
+const REASON_SAVED_SESSION = "saved-session";
+const REASON_GATHER_PAYLOAD = "gather-payload";
+const REASON_TEST_PING = "test-ping";
+const REASON_ENVIRONMENT_CHANGE = "environment-change";
+const REASON_SHUTDOWN = "shutdown";
+
+const ENVIRONMENT_CHANGE_LISTENER = "TelemetrySession::onEnvironmentChange";
+
+const MIN_SUBSESSION_LENGTH_MS =
+ Services.prefs.getIntPref("toolkit.telemetry.minSubsessionLength", 5 * 60) *
+ 1000;
+
+const LOGGER_NAME = "Toolkit.Telemetry";
+const LOGGER_PREFIX =
+ "TelemetrySession" + (Utils.isContentProcess ? "#content::" : "::");
+
+// Whether the FHR/Telemetry unification features are enabled.
+// Changing this pref requires a restart.
+const IS_UNIFIED_TELEMETRY = Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.Unified,
+ false
+);
+
+var gWasDebuggerAttached = false;
+
+function generateUUID() {
+ let str = Services.uuid.generateUUID().toString();
+ // strip {}
+ return str.substring(1, str.length - 1);
+}
+
+/**
+ * This is a policy object used to override behavior for testing.
+ */
+export var Policy = {
+ now: () => new Date(),
+ monotonicNow: Utils.monotonicNow,
+ generateSessionUUID: () => generateUUID(),
+ generateSubsessionUUID: () => generateUUID(),
+};
+
+/**
+ * Get the ping type based on the payload.
+ * @param {Object} aPayload The ping payload.
+ * @return {String} A string representing the ping type.
+ */
+function getPingType(aPayload) {
+ // To remain consistent with server-side ping handling, set "saved-session" as the ping
+ // type for "saved-session" payload reasons.
+ if (aPayload.info.reason == REASON_SAVED_SESSION) {
+ return PING_TYPE_SAVED_SESSION;
+ }
+
+ return PING_TYPE_MAIN;
+}
+
+/**
+ * Annotate the current session ID with the crash reporter to map potential
+ * crash pings with the related main ping.
+ */
+function annotateCrashReport(sessionId) {
+ try {
+ Services.appinfo.annotateCrashReport("TelemetrySessionId", sessionId);
+ } catch (e) {
+ // Ignore errors when crash reporting is disabled
+ }
+}
+
+/**
+ * Read current process I/O counters.
+ */
+var processInfo = {
+ _initialized: false,
+ _IO_COUNTERS: null,
+ _kernel32: null,
+ _GetProcessIoCounters: null,
+ _GetCurrentProcess: null,
+ getCounters() {
+ let isWindows = "@mozilla.org/windows-registry-key;1" in Cc;
+ if (isWindows) {
+ return this.getCounters_Windows();
+ }
+ return null;
+ },
+ getCounters_Windows() {
+ if (!this._initialized) {
+ var { ctypes } = ChromeUtils.importESModule(
+ "resource://gre/modules/ctypes.sys.mjs"
+ );
+ this._IO_COUNTERS = new ctypes.StructType("IO_COUNTERS", [
+ { readOps: ctypes.unsigned_long_long },
+ { writeOps: ctypes.unsigned_long_long },
+ { otherOps: ctypes.unsigned_long_long },
+ { readBytes: ctypes.unsigned_long_long },
+ { writeBytes: ctypes.unsigned_long_long },
+ { otherBytes: ctypes.unsigned_long_long },
+ ]);
+ try {
+ this._kernel32 = ctypes.open("Kernel32.dll");
+ this._GetProcessIoCounters = this._kernel32.declare(
+ "GetProcessIoCounters",
+ ctypes.winapi_abi,
+ ctypes.bool, // return
+ ctypes.voidptr_t, // hProcess
+ this._IO_COUNTERS.ptr
+ ); // lpIoCounters
+ this._GetCurrentProcess = this._kernel32.declare(
+ "GetCurrentProcess",
+ ctypes.winapi_abi,
+ ctypes.voidptr_t
+ ); // return
+ this._initialized = true;
+ } catch (err) {
+ return null;
+ }
+ }
+ let io = new this._IO_COUNTERS();
+ if (!this._GetProcessIoCounters(this._GetCurrentProcess(), io.address())) {
+ return null;
+ }
+ return [parseInt(io.readBytes), parseInt(io.writeBytes)];
+ },
+};
+
+export var TelemetrySession = Object.freeze({
+ /**
+ * Send a ping to a test server. Used only for testing.
+ */
+ testPing() {
+ return Impl.testPing();
+ },
+ /**
+ * Returns the current telemetry payload.
+ * @param reason Optional, the reason to trigger the payload.
+ * @param clearSubsession Optional, whether to clear subsession specific data.
+ * @returns Object
+ */
+ getPayload(reason, clearSubsession = false) {
+ return Impl.getPayload(reason, clearSubsession);
+ },
+ /**
+ * Save the session state to a pending file.
+ * Used only for testing purposes.
+ */
+ testSavePendingPing() {
+ return Impl.testSavePendingPing();
+ },
+ /**
+ * Collect and store information about startup.
+ */
+ gatherStartup() {
+ return Impl.gatherStartup();
+ },
+ /**
+ * Inform the ping which AddOns are installed.
+ *
+ * @param aAddOns - The AddOns.
+ */
+ setAddOns(aAddOns) {
+ return Impl.setAddOns(aAddOns);
+ },
+ /**
+ * Descriptive metadata
+ *
+ * @param reason
+ * The reason for the telemetry ping, this will be included in the
+ * returned metadata,
+ * @return The metadata as a JS object
+ */
+ getMetadata(reason) {
+ return Impl.getMetadata(reason);
+ },
+
+ /**
+ * Reset the subsession and profile subsession counter.
+ * This should only be called when the profile should be considered completely new,
+ * e.g. after opting out of sending Telemetry
+ */
+ resetSubsessionCounter() {
+ Impl._subsessionCounter = 0;
+ Impl._profileSubsessionCounter = 0;
+ },
+
+ /**
+ * Used only for testing purposes.
+ */
+ testReset() {
+ Impl._newProfilePingSent = false;
+ Impl._sessionId = null;
+ Impl._subsessionId = null;
+ Impl._previousSessionId = null;
+ Impl._previousSubsessionId = null;
+ Impl._subsessionCounter = 0;
+ Impl._profileSubsessionCounter = 0;
+ Impl._subsessionStartActiveTicks = 0;
+ Impl._sessionActiveTicks = 0;
+ Impl._isUserActive = true;
+ Impl._subsessionStartTimeMonotonic = 0;
+ Impl._lastEnvironmentChangeDate = Policy.monotonicNow();
+ this.testUninstall();
+ },
+ /**
+ * Triggers shutdown of the module.
+ */
+ shutdown() {
+ return Impl.shutdownChromeProcess();
+ },
+ /**
+ * Used only for testing purposes.
+ */
+ testUninstall() {
+ try {
+ Impl.uninstall();
+ } catch (ex) {
+ // Ignore errors
+ }
+ },
+ /**
+ * Lightweight init function, called as soon as Firefox starts.
+ */
+ earlyInit(aTesting = false) {
+ return Impl.earlyInit(aTesting);
+ },
+ /**
+ * Does the "heavy" Telemetry initialization later on, so we
+ * don't impact startup performance.
+ * @return {Promise} Resolved when the initialization completes.
+ */
+ delayedInit() {
+ return Impl.delayedInit();
+ },
+ /**
+ * Send a notification.
+ */
+ observe(aSubject, aTopic, aData) {
+ return Impl.observe(aSubject, aTopic, aData);
+ },
+ /**
+ * Marks the "new-profile" ping as sent in the telemetry state file.
+ * @return {Promise} A promise resolved when the new telemetry state is saved to disk.
+ */
+ markNewProfilePingSent() {
+ return Impl.markNewProfilePingSent();
+ },
+ /**
+ * Returns if the "new-profile" ping has ever been sent for this profile.
+ * Please note that the returned value is trustworthy only after the delayed setup.
+ *
+ * @return {Boolean} True if the new profile ping was sent on this profile,
+ * false otherwise.
+ */
+ get newProfilePingSent() {
+ return Impl._newProfilePingSent;
+ },
+
+ saveAbortedSessionPing(aProvidedPayload) {
+ return Impl._saveAbortedSessionPing(aProvidedPayload);
+ },
+
+ sendDailyPing() {
+ return Impl._sendDailyPing();
+ },
+
+ testOnEnvironmentChange(...args) {
+ return Impl._onEnvironmentChange(...args);
+ },
+});
+
+var Impl = {
+ _initialized: false,
+ _logger: null,
+ _slowSQLStartup: {},
+ // The activity state for the user. If false, don't count the next
+ // active tick. Otherwise, increment the active ticks as usual.
+ _isUserActive: true,
+ _startupIO: {},
+ // The previous build ID, if this is the first run with a new build.
+ // Null if this is the first run, or the previous build ID is unknown.
+ _previousBuildId: null,
+ // Unique id that identifies this session so the server can cope with duplicate
+ // submissions, orphaning and other oddities. The id is shared across subsessions.
+ _sessionId: null,
+ // Random subsession id.
+ _subsessionId: null,
+ // Session id of the previous session, null on first run.
+ _previousSessionId: null,
+ // Subsession id of the previous subsession (even if it was in a different session),
+ // null on first run.
+ _previousSubsessionId: null,
+ // The running no. of subsessions since the start of the browser session
+ _subsessionCounter: 0,
+ // The running no. of all subsessions for the whole profile life time
+ _profileSubsessionCounter: 0,
+ // Date of the last session split
+ _subsessionStartDate: null,
+ // Start time of the current subsession using a monotonic clock for the subsession
+ // length measurements.
+ _subsessionStartTimeMonotonic: 0,
+ // The active ticks counted when the subsession starts
+ _subsessionStartActiveTicks: 0,
+ // Active ticks in the whole session.
+ _sessionActiveTicks: 0,
+ // A task performing delayed initialization of the chrome process
+ _delayedInitTask: null,
+ _testing: false,
+ // An accumulator of total memory across all processes. Only valid once the final child reports.
+ _lastEnvironmentChangeDate: 0,
+ // We save whether the "new-profile" ping was sent yet, to
+ // survive profile refresh and migrations.
+ _newProfilePingSent: false,
+ // Keep track of the active observers
+ _observedTopics: new Set(),
+
+ addObserver(aTopic) {
+ Services.obs.addObserver(this, aTopic);
+ this._observedTopics.add(aTopic);
+ },
+
+ removeObserver(aTopic) {
+ Services.obs.removeObserver(this, aTopic);
+ this._observedTopics.delete(aTopic);
+ },
+
+ get _log() {
+ if (!this._logger) {
+ this._logger = Log.repository.getLoggerWithMessagePrefix(
+ LOGGER_NAME,
+ LOGGER_PREFIX
+ );
+ }
+ return this._logger;
+ },
+
+ /**
+ * Gets a series of simple measurements (counters). At the moment, this
+ * only returns startup data from nsIAppStartup.getStartupInfo().
+ * @param {Boolean} isSubsession True if this is a subsession, false otherwise.
+ * @param {Boolean} clearSubsession True if a new subsession is being started, false otherwise.
+ *
+ * @return simple measurements as a dictionary.
+ */
+ getSimpleMeasurements: function getSimpleMeasurements(
+ forSavedSession,
+ isSubsession,
+ clearSubsession
+ ) {
+ let si = Services.startup.getStartupInfo();
+
+ // Measurements common to chrome and content processes.
+ let elapsedTime = Date.now() - si.process;
+ var ret = {
+ totalTime: Math.round(elapsedTime / 1000), // totalTime, in seconds
+ };
+
+ // Look for app-specific timestamps
+ var appTimestamps = {};
+ try {
+ let { TelemetryTimestamps } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryTimestamps.sys.mjs"
+ );
+ appTimestamps = TelemetryTimestamps.get();
+ } catch (ex) {}
+
+ // Only submit this if the extended set is enabled.
+ if (!Utils.isContentProcess && Services.telemetry.canRecordExtended) {
+ try {
+ ret.addonManager = lazy.AddonManagerPrivate.getSimpleMeasures();
+ } catch (ex) {}
+ }
+
+ if (si.process) {
+ for (let field of Object.keys(si)) {
+ if (field == "process") {
+ continue;
+ }
+ ret[field] = si[field] - si.process;
+ }
+
+ for (let p in appTimestamps) {
+ if (!(p in ret) && appTimestamps[p]) {
+ ret[p] = appTimestamps[p] - si.process;
+ }
+ }
+ }
+
+ ret.startupInterrupted = Number(Services.startup.interrupted);
+
+ if (Utils.isContentProcess) {
+ return ret;
+ }
+
+ // Measurements specific to chrome process
+
+ // Update debuggerAttached flag
+ let debugService = Cc["@mozilla.org/xpcom/debug;1"].getService(
+ Ci.nsIDebug2
+ );
+ let isDebuggerAttached = debugService.isDebuggerAttached;
+ gWasDebuggerAttached = gWasDebuggerAttached || isDebuggerAttached;
+ ret.debuggerAttached = Number(gWasDebuggerAttached);
+
+ let shutdownDuration = Services.telemetry.lastShutdownDuration;
+ if (shutdownDuration) {
+ ret.shutdownDuration = shutdownDuration;
+ }
+
+ let failedProfileLockCount = Services.telemetry.failedProfileLockCount;
+ if (failedProfileLockCount) {
+ ret.failedProfileLockCount = failedProfileLockCount;
+ }
+
+ for (let ioCounter in this._startupIO) {
+ ret[ioCounter] = this._startupIO[ioCounter];
+ }
+
+ let activeTicks = this._sessionActiveTicks;
+ if (isSubsession) {
+ activeTicks = this._sessionActiveTicks - this._subsessionStartActiveTicks;
+ }
+
+ if (clearSubsession) {
+ this._subsessionStartActiveTicks = this._sessionActiveTicks;
+ }
+
+ ret.activeTicks = activeTicks;
+
+ return ret;
+ },
+
+ getHistograms: function getHistograms(clearSubsession) {
+ return Services.telemetry.getSnapshotForHistograms(
+ "main",
+ clearSubsession,
+ !this._testing
+ );
+ },
+
+ getKeyedHistograms(clearSubsession) {
+ return Services.telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ clearSubsession,
+ !this._testing
+ );
+ },
+
+ /**
+ * Get a snapshot of the scalars and clear them.
+ * @param {subsession} If true, then we collect the data for a subsession.
+ * @param {clearSubsession} If true, we need to clear the subsession.
+ * @param {keyed} Take a snapshot of keyed or non keyed scalars.
+ * @return {Object} The scalar data as a Javascript object, including the
+ * data from child processes, in the following format:
+ * {'content': { 'scalarName': ... }, 'gpu': { ... } }
+ */
+ getScalars(subsession, clearSubsession, keyed) {
+ if (!subsession) {
+ // We only support scalars for subsessions.
+ this._log.trace("getScalars - We only support scalars in subsessions.");
+ return {};
+ }
+
+ let scalarsSnapshot = keyed
+ ? Services.telemetry.getSnapshotForKeyedScalars(
+ "main",
+ clearSubsession,
+ !this._testing
+ )
+ : Services.telemetry.getSnapshotForScalars(
+ "main",
+ clearSubsession,
+ !this._testing
+ );
+
+ return scalarsSnapshot;
+ },
+
+ /**
+ * Descriptive metadata
+ *
+ * @param reason
+ * The reason for the telemetry ping, this will be included in the
+ * returned metadata,
+ * @return The metadata as a JS object
+ */
+ getMetadata: function getMetadata(reason) {
+ const sessionStartDate = Utils.toLocalTimeISOString(
+ Utils.truncateToHours(this._sessionStartDate)
+ );
+ const subsessionStartDate = Utils.toLocalTimeISOString(
+ Utils.truncateToHours(this._subsessionStartDate)
+ );
+ const monotonicNow = Policy.monotonicNow();
+
+ let ret = {
+ reason,
+ revision: AppConstants.SOURCE_REVISION_URL,
+
+ // Date.getTimezoneOffset() unintuitively returns negative values if we are ahead of
+ // UTC and vice versa (e.g. -60 for UTC+1). We invert the sign here.
+ timezoneOffset: -this._subsessionStartDate.getTimezoneOffset(),
+ previousBuildId: this._previousBuildId,
+
+ sessionId: this._sessionId,
+ subsessionId: this._subsessionId,
+ previousSessionId: this._previousSessionId,
+ previousSubsessionId: this._previousSubsessionId,
+
+ subsessionCounter: this._subsessionCounter,
+ profileSubsessionCounter: this._profileSubsessionCounter,
+
+ sessionStartDate,
+ subsessionStartDate,
+
+ // Compute the session and subsession length in seconds.
+ // We use monotonic clocks as Date() is affected by jumping clocks (leading
+ // to negative lengths and other issues).
+ sessionLength: Math.floor(monotonicNow / 1000),
+ subsessionLength: Math.floor(
+ (monotonicNow - this._subsessionStartTimeMonotonic) / 1000
+ ),
+ };
+
+ // TODO: Remove this when bug 1201837 lands.
+ if (this._addons) {
+ ret.addons = this._addons;
+ }
+
+ return ret;
+ },
+
+ /**
+ * Get the current session's payload using the provided
+ * simpleMeasurements and info, which are typically obtained by a call
+ * to |this.getSimpleMeasurements| and |this.getMetadata|,
+ * respectively.
+ */
+ assemblePayloadWithMeasurements(
+ simpleMeasurements,
+ info,
+ reason,
+ clearSubsession
+ ) {
+ const isSubsession = IS_UNIFIED_TELEMETRY && !this._isClassicReason(reason);
+ clearSubsession = IS_UNIFIED_TELEMETRY && clearSubsession;
+ this._log.trace(
+ "assemblePayloadWithMeasurements - reason: " +
+ reason +
+ ", submitting subsession data: " +
+ isSubsession
+ );
+
+ // This allows wrapping data retrieval calls in a try-catch block so that
+ // failures don't break the rest of the ping assembly.
+ const protect = (fn, defaultReturn = null) => {
+ try {
+ return fn();
+ } catch (ex) {
+ this._log.error(
+ "assemblePayloadWithMeasurements - caught exception",
+ ex
+ );
+ return defaultReturn;
+ }
+ };
+
+ // Payload common to chrome and content processes.
+ let payloadObj = {
+ ver: PAYLOAD_VERSION,
+ simpleMeasurements,
+ };
+
+ // Add extended set measurements common to chrome & content processes
+ if (Services.telemetry.canRecordExtended) {
+ payloadObj.log = [];
+ }
+
+ if (Utils.isContentProcess) {
+ return payloadObj;
+ }
+
+ // Additional payload for chrome process.
+ let measurements = {
+ histograms: protect(() => this.getHistograms(clearSubsession), {}),
+ keyedHistograms: protect(
+ () => this.getKeyedHistograms(clearSubsession),
+ {}
+ ),
+ scalars: protect(
+ () => this.getScalars(isSubsession, clearSubsession),
+ {}
+ ),
+ keyedScalars: protect(
+ () => this.getScalars(isSubsession, clearSubsession, true),
+ {}
+ ),
+ };
+
+ let measurementsContainGPU = Object.keys(measurements).some(
+ key => "gpu" in measurements[key]
+ );
+
+ let measurementsContainSocket = Object.keys(measurements).some(
+ key => "socket" in measurements[key]
+ );
+
+ let measurementsContainUtility = Object.keys(measurements).some(
+ key => "utility" in measurements[key]
+ );
+
+ payloadObj.processes = {};
+ let processTypes = ["parent", "content", "extension", "dynamic"];
+ // Only include the GPU process if we've accumulated data for it.
+ if (measurementsContainGPU) {
+ processTypes.push("gpu");
+ }
+ if (measurementsContainSocket) {
+ processTypes.push("socket");
+ }
+ if (measurementsContainUtility) {
+ processTypes.push("utility");
+ }
+
+ // Collect per-process measurements.
+ for (const processType of processTypes) {
+ let processPayload = {};
+
+ for (const key in measurements) {
+ let payloadLoc = processPayload;
+ // Parent histograms are added to the top-level payload object instead of the process payload.
+ if (
+ processType == "parent" &&
+ (key == "histograms" || key == "keyedHistograms")
+ ) {
+ payloadLoc = payloadObj;
+ }
+ // The Dynamic process only collects scalars and keyed scalars.
+ if (
+ processType == "dynamic" &&
+ key !== "scalars" &&
+ key !== "keyedScalars"
+ ) {
+ continue;
+ }
+
+ // Process measurements can be empty, set a default value.
+ payloadLoc[key] = measurements[key][processType] || {};
+ }
+
+ // Add process measurements to payload.
+ payloadObj.processes[processType] = processPayload;
+ }
+
+ payloadObj.info = info;
+
+ // Add extended set measurements for chrome process.
+ if (Services.telemetry.canRecordExtended) {
+ payloadObj.slowSQL = protect(() => Services.telemetry.slowSQL);
+ payloadObj.fileIOReports = protect(
+ () => Services.telemetry.fileIOReports
+ );
+ payloadObj.lateWrites = protect(() => Services.telemetry.lateWrites);
+
+ payloadObj.addonDetails = protect(() =>
+ lazy.AddonManagerPrivate.getTelemetryDetails()
+ );
+
+ if (
+ this._slowSQLStartup &&
+ !!Object.keys(this._slowSQLStartup).length &&
+ (Object.keys(this._slowSQLStartup.mainThread).length ||
+ Object.keys(this._slowSQLStartup.otherThreads).length)
+ ) {
+ payloadObj.slowSQLStartup = this._slowSQLStartup;
+ }
+ }
+
+ return payloadObj;
+ },
+
+ /**
+ * Start a new subsession.
+ */
+ startNewSubsession() {
+ this._subsessionStartDate = Policy.now();
+ this._subsessionStartTimeMonotonic = Policy.monotonicNow();
+ this._previousSubsessionId = this._subsessionId;
+ this._subsessionId = Policy.generateSubsessionUUID();
+ this._subsessionCounter++;
+ this._profileSubsessionCounter++;
+ },
+
+ getSessionPayload: function getSessionPayload(reason, clearSubsession) {
+ this._log.trace(
+ "getSessionPayload - reason: " +
+ reason +
+ ", clearSubsession: " +
+ clearSubsession
+ );
+
+ let payload;
+ try {
+ const isMobile = AppConstants.platform == "android";
+ const isSubsession = isMobile ? false : !this._isClassicReason(reason);
+
+ // The order of the next two msSinceProcessStart* calls is somewhat
+ // important. In theory, `session_time_including_suspend` is supposed to
+ // ALWAYS be lower or equal than `session_time_excluding_suspend` (because
+ // the former is a temporal superset of the latter). When a device has not
+ // been suspended since boot, we want the previous property to hold,
+ // regardless of the delay during or between the two
+ // `msSinceProcessStart*` calls.
+ Services.telemetry.scalarSet(
+ "browser.engagement.session_time_excluding_suspend",
+ Services.telemetry.msSinceProcessStartExcludingSuspend()
+ );
+ Services.telemetry.scalarSet(
+ "browser.engagement.session_time_including_suspend",
+ Services.telemetry.msSinceProcessStartIncludingSuspend()
+ );
+
+ if (isMobile) {
+ clearSubsession = false;
+ }
+
+ let measurements = this.getSimpleMeasurements(
+ reason == REASON_SAVED_SESSION,
+ isSubsession,
+ clearSubsession
+ );
+ let info = !Utils.isContentProcess ? this.getMetadata(reason) : null;
+ payload = this.assemblePayloadWithMeasurements(
+ measurements,
+ info,
+ reason,
+ clearSubsession
+ );
+ } catch (ex) {
+ Services.telemetry
+ .getHistogramById("TELEMETRY_ASSEMBLE_PAYLOAD_EXCEPTION")
+ .add(1);
+ throw ex;
+ } finally {
+ if (!Utils.isContentProcess && clearSubsession) {
+ this.startNewSubsession();
+ // Persist session data to disk (don't wait until it completes).
+ let sessionData = this._getSessionDataObject();
+ lazy.TelemetryStorage.saveSessionData(sessionData);
+
+ // Notify that there was a subsession split in the parent process. This is an
+ // internal topic and is only meant for internal Telemetry usage.
+ Services.obs.notifyObservers(
+ null,
+ "internal-telemetry-after-subsession-split"
+ );
+ }
+ }
+
+ return payload;
+ },
+
+ /**
+ * Send data to the server. Record success/send-time in histograms
+ */
+ send: async function send(reason) {
+ this._log.trace("send - Reason " + reason);
+ // populate histograms one last time
+ await Services.telemetry.gatherMemory();
+
+ const isSubsession = !this._isClassicReason(reason);
+ let payload = this.getSessionPayload(reason, isSubsession);
+ let options = {
+ addClientId: true,
+ addEnvironment: true,
+ };
+ return lazy.TelemetryController.submitExternalPing(
+ getPingType(payload),
+ payload,
+ options
+ );
+ },
+
+ /**
+ * Attaches the needed observers during Telemetry early init, in the
+ * chrome process.
+ */
+ attachEarlyObservers() {
+ this.addObserver("sessionstore-windows-restored");
+ if (AppConstants.platform === "android") {
+ this.addObserver("application-background");
+ }
+ this.addObserver("xul-window-visible");
+
+ // Attach the active-ticks related observers.
+ this.addObserver("user-interaction-active");
+ this.addObserver("user-interaction-inactive");
+ },
+
+ /**
+ * Lightweight init function, called as soon as Firefox starts.
+ */
+ earlyInit(testing) {
+ this._log.trace("earlyInit");
+
+ this._initStarted = true;
+ this._testing = testing;
+
+ if (this._initialized && !testing) {
+ this._log.error("earlyInit - already initialized");
+ return;
+ }
+
+ if (!Services.telemetry.canRecordBase && !testing) {
+ this._log.config(
+ "earlyInit - Telemetry recording is disabled, skipping Chrome process setup."
+ );
+ return;
+ }
+
+ // Generate a unique id once per session so the server can cope with duplicate
+ // submissions, orphaning and other oddities. The id is shared across subsessions.
+ this._sessionId = Policy.generateSessionUUID();
+ this.startNewSubsession();
+ // startNewSubsession sets |_subsessionStartDate| to the current date/time. Use
+ // the very same value for |_sessionStartDate|.
+ this._sessionStartDate = this._subsessionStartDate;
+
+ annotateCrashReport(this._sessionId);
+
+ // Record old value and update build ID preference if this is the first
+ // run with a new build ID.
+ let previousBuildId = Services.prefs.getStringPref(
+ TelemetryUtils.Preferences.PreviousBuildID,
+ null
+ );
+ let thisBuildID = Services.appinfo.appBuildID;
+ // If there is no previousBuildId preference, we send null to the server.
+ if (previousBuildId != thisBuildID) {
+ this._previousBuildId = previousBuildId;
+ Services.prefs.setStringPref(
+ TelemetryUtils.Preferences.PreviousBuildID,
+ thisBuildID
+ );
+ }
+
+ this.attachEarlyObservers();
+ },
+
+ /**
+ * Does the "heavy" Telemetry initialization later on, so we
+ * don't impact startup performance.
+ * @return {Promise} Resolved when the initialization completes.
+ */
+ delayedInit() {
+ this._log.trace("delayedInit");
+
+ this._delayedInitTask = (async () => {
+ try {
+ this._initialized = true;
+
+ await this._loadSessionData();
+ // Update the session data to keep track of new subsessions created before
+ // the initialization.
+ await lazy.TelemetryStorage.saveSessionData(
+ this._getSessionDataObject()
+ );
+
+ this.addObserver("idle-daily");
+ await Services.telemetry.gatherMemory();
+
+ Services.telemetry.asyncFetchTelemetryData(function () {});
+
+ if (IS_UNIFIED_TELEMETRY) {
+ // Check for a previously written aborted session ping.
+ await lazy.TelemetryController.checkAbortedSessionPing();
+
+ // Write the first aborted-session ping as early as possible. Just do that
+ // if we are not testing, since calling Telemetry.reset() will make a previous
+ // aborted ping a pending ping.
+ if (!this._testing) {
+ await this._saveAbortedSessionPing();
+ }
+
+ // The last change date for the environment, used to throttle environment changes.
+ this._lastEnvironmentChangeDate = Policy.monotonicNow();
+ lazy.TelemetryEnvironment.registerChangeListener(
+ ENVIRONMENT_CHANGE_LISTENER,
+ (reason, data) => this._onEnvironmentChange(reason, data)
+ );
+
+ // Start the scheduler.
+ // We skip this if unified telemetry is off, so we don't
+ // trigger the new unified ping types.
+ lazy.TelemetryScheduler.init();
+ }
+
+ this._delayedInitTask = null;
+ } catch (e) {
+ this._delayedInitTask = null;
+ throw e;
+ }
+ })();
+
+ return this._delayedInitTask;
+ },
+
+ /**
+ * On Desktop: Save the "shutdown" ping to disk.
+ * On Android: Save the "saved-session" ping to disk.
+ * This needs to be called after TelemetrySend shuts down otherwise pings
+ * would be sent instead of getting persisted to disk.
+ */
+ saveShutdownPings() {
+ this._log.trace("saveShutdownPings");
+
+ // We append the promises to this list and wait
+ // on all pings to be saved after kicking off their collection.
+ let p = [];
+
+ if (IS_UNIFIED_TELEMETRY) {
+ let shutdownPayload = this.getSessionPayload(REASON_SHUTDOWN, false);
+
+ // Only send the shutdown ping using the pingsender from the second
+ // browsing session on, to mitigate issues with "bot" profiles (see bug 1354482).
+ const sendOnThisSession =
+ Services.prefs.getBoolPref(
+ Utils.Preferences.ShutdownPingSenderFirstSession,
+ false
+ ) || !lazy.TelemetryReportingPolicy.isFirstRun();
+ let sendWithPingsender =
+ Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.ShutdownPingSender,
+ false
+ ) && sendOnThisSession;
+
+ let options = {
+ addClientId: true,
+ addEnvironment: true,
+ usePingSender: sendWithPingsender,
+ };
+ p.push(
+ lazy.TelemetryController.submitExternalPing(
+ getPingType(shutdownPayload),
+ shutdownPayload,
+ options
+ ).catch(e =>
+ this._log.error(
+ "saveShutdownPings - failed to submit shutdown ping",
+ e
+ )
+ )
+ );
+
+ // Send a duplicate of first-shutdown pings as a new ping type, in order to properly
+ // evaluate first session profiles (see bug 1390095).
+ const sendFirstShutdownPing =
+ Services.prefs.getBoolPref(
+ Utils.Preferences.ShutdownPingSender,
+ false
+ ) &&
+ Services.prefs.getBoolPref(
+ Utils.Preferences.FirstShutdownPingEnabled,
+ false
+ ) &&
+ lazy.TelemetryReportingPolicy.isFirstRun();
+
+ if (sendFirstShutdownPing) {
+ let options = {
+ addClientId: true,
+ addEnvironment: true,
+ usePingSender: true,
+ };
+ p.push(
+ lazy.TelemetryController.submitExternalPing(
+ "first-shutdown",
+ shutdownPayload,
+ options
+ ).catch(e =>
+ this._log.error(
+ "saveShutdownPings - failed to submit first shutdown ping",
+ e
+ )
+ )
+ );
+ }
+ }
+
+ if (
+ AppConstants.platform == "android" &&
+ Services.telemetry.canRecordExtended
+ ) {
+ let payload = this.getSessionPayload(REASON_SAVED_SESSION, false);
+
+ let options = {
+ addClientId: true,
+ addEnvironment: true,
+ };
+ p.push(
+ lazy.TelemetryController.submitExternalPing(
+ getPingType(payload),
+ payload,
+ options
+ ).catch(e =>
+ this._log.error(
+ "saveShutdownPings - failed to submit saved-session ping",
+ e
+ )
+ )
+ );
+ }
+
+ // Wait on pings to be saved.
+ return Promise.all(p);
+ },
+
+ testSavePendingPing() {
+ let payload = this.getSessionPayload(REASON_SAVED_SESSION, false);
+ let options = {
+ addClientId: true,
+ addEnvironment: true,
+ overwrite: true,
+ };
+ return lazy.TelemetryController.addPendingPing(
+ getPingType(payload),
+ payload,
+ options
+ );
+ },
+
+ /**
+ * Do some shutdown work that is common to all process types.
+ */
+ uninstall() {
+ for (let topic of this._observedTopics) {
+ try {
+ // Tests may flip Telemetry.canRecordExtended on and off. It can be the case
+ // that the observer TOPIC_CYCLE_COLLECTOR_BEGIN was not added.
+ this.removeObserver(topic);
+ } catch (e) {
+ this._log.warn("uninstall - Failed to remove " + topic, e);
+ }
+ }
+ },
+
+ getPayload: function getPayload(reason, clearSubsession) {
+ this._log.trace("getPayload - clearSubsession: " + clearSubsession);
+ reason = reason || REASON_GATHER_PAYLOAD;
+ // This function returns the current Telemetry payload to the caller.
+ // We only gather startup info once.
+ if (!Object.keys(this._slowSQLStartup).length) {
+ this._slowSQLStartup = Services.telemetry.slowSQL;
+ }
+ Services.telemetry.gatherMemory();
+ return this.getSessionPayload(reason, clearSubsession);
+ },
+
+ gatherStartup: function gatherStartup() {
+ this._log.trace("gatherStartup");
+ let counters = processInfo.getCounters();
+ if (counters) {
+ [
+ this._startupIO.startupSessionRestoreReadBytes,
+ this._startupIO.startupSessionRestoreWriteBytes,
+ ] = counters;
+ }
+ this._slowSQLStartup = Services.telemetry.slowSQL;
+ },
+
+ setAddOns: function setAddOns(aAddOns) {
+ this._addons = aAddOns;
+ },
+
+ testPing: function testPing() {
+ return this.send(REASON_TEST_PING);
+ },
+
+ /**
+ * Tracks the number of "ticks" the user was active in.
+ */
+ _onActiveTick(aUserActive) {
+ const needsUpdate = aUserActive && this._isUserActive;
+ this._isUserActive = aUserActive;
+
+ // Don't count the first active tick after we get out of
+ // inactivity, because it is just the start of this active tick.
+ if (needsUpdate) {
+ this._sessionActiveTicks++;
+ Services.telemetry.scalarAdd("browser.engagement.active_ticks", 1);
+ Glean.browserEngagement.activeTicks.add(1);
+ }
+ },
+
+ /**
+ * This observer drives telemetry.
+ */
+ observe(aSubject, aTopic, aData) {
+ this._log.trace("observe - " + aTopic + " notified.");
+
+ switch (aTopic) {
+ case "xul-window-visible":
+ this.removeObserver("xul-window-visible");
+ var counters = processInfo.getCounters();
+ if (counters) {
+ [
+ this._startupIO.startupWindowVisibleReadBytes,
+ this._startupIO.startupWindowVisibleWriteBytes,
+ ] = counters;
+ }
+ break;
+ case "sessionstore-windows-restored":
+ this.removeObserver("sessionstore-windows-restored");
+ // Check whether debugger was attached during startup
+ let debugService = Cc["@mozilla.org/xpcom/debug;1"].getService(
+ Ci.nsIDebug2
+ );
+ gWasDebuggerAttached = debugService.isDebuggerAttached;
+ this.gatherStartup();
+ break;
+ case "idle-daily":
+ // Enqueue to main-thread, otherwise components may be inited by the
+ // idle-daily category and miss the gather-telemetry notification.
+ Services.tm.dispatchToMainThread(function () {
+ // Notify that data should be gathered now.
+ // TODO: We are keeping this behaviour for now but it will be removed as soon as
+ // bug 1127907 lands.
+ Services.obs.notifyObservers(null, "gather-telemetry");
+ });
+ break;
+
+ case "application-background":
+ if (AppConstants.platform !== "android") {
+ break;
+ }
+ // On Android, we can get killed without warning once we are in the background,
+ // but we may also submit data and/or come back into the foreground without getting
+ // killed. To deal with this, we save the current session data to file when we are
+ // put into the background. This handles the following post-backgrounding scenarios:
+ // 1) We are killed immediately. In this case the current session data (which we
+ // save to a file) will be loaded and submitted on a future run.
+ // 2) We submit the data while in the background, and then are killed. In this case
+ // the file that we saved will be deleted by the usual process in
+ // finishPingRequest after it is submitted.
+ // 3) We submit the data, and then come back into the foreground. Same as case (2).
+ // 4) We do not submit the data, but come back into the foreground. In this case
+ // we have the option of either deleting the file that we saved (since we will either
+ // send the live data while in the foreground, or create the file again on the next
+ // backgrounding), or not (in which case we will delete it on submit, or overwrite
+ // it on the next backgrounding). Not deleting it is faster, so that's what we do.
+ let payload = this.getSessionPayload(REASON_SAVED_SESSION, false);
+ let options = {
+ addClientId: true,
+ addEnvironment: true,
+ overwrite: true,
+ };
+ lazy.TelemetryController.addPendingPing(
+ getPingType(payload),
+ payload,
+ options
+ );
+ break;
+ case "user-interaction-active":
+ this._onActiveTick(true);
+ break;
+ case "user-interaction-inactive":
+ this._onActiveTick(false);
+ break;
+ }
+ return undefined;
+ },
+
+ /**
+ * This tells TelemetrySession to uninitialize and save any pending pings.
+ */
+ shutdownChromeProcess() {
+ this._log.trace("shutdownChromeProcess");
+
+ let cleanup = () => {
+ if (IS_UNIFIED_TELEMETRY) {
+ lazy.TelemetryEnvironment.unregisterChangeListener(
+ ENVIRONMENT_CHANGE_LISTENER
+ );
+ lazy.TelemetryScheduler.shutdown();
+ }
+ this.uninstall();
+
+ let reset = () => {
+ this._initStarted = false;
+ this._initialized = false;
+ };
+
+ return (async () => {
+ await this.saveShutdownPings();
+
+ if (IS_UNIFIED_TELEMETRY) {
+ await lazy.TelemetryController.removeAbortedSessionPing();
+ }
+
+ reset();
+ })();
+ };
+
+ // We can be in one the following states here:
+ // 1) delayedInit was never called
+ // or it was called and
+ // 2) _delayedInitTask is running now.
+ // 3) _delayedInitTask finished running already.
+
+ // This handles 1).
+ if (!this._initStarted) {
+ return Promise.resolve();
+ }
+
+ // This handles 3).
+ if (!this._delayedInitTask) {
+ // We already ran the delayed initialization.
+ return cleanup();
+ }
+
+ // This handles 2).
+ return this._delayedInitTask.then(cleanup);
+ },
+
+ /**
+ * Gather and send a daily ping.
+ * @return {Promise} Resolved when the ping is sent.
+ */
+ _sendDailyPing() {
+ this._log.trace("_sendDailyPing");
+ let payload = this.getSessionPayload(REASON_DAILY, true);
+
+ let options = {
+ addClientId: true,
+ addEnvironment: true,
+ };
+
+ let promise = lazy.TelemetryController.submitExternalPing(
+ getPingType(payload),
+ payload,
+ options
+ );
+
+ // Also save the payload as an aborted session. If we delay this, aborted-session can
+ // lag behind for the profileSubsessionCounter and other state, complicating analysis.
+ if (IS_UNIFIED_TELEMETRY) {
+ this._saveAbortedSessionPing(payload).catch(e =>
+ this._log.error(
+ "_sendDailyPing - Failed to save the aborted session ping",
+ e
+ )
+ );
+ }
+
+ return promise;
+ },
+
+ /** Loads session data from the session data file.
+ * @return {Promise<object>} A promise which is resolved with an object when
+ * loading has completed, with null otherwise.
+ */
+ async _loadSessionData() {
+ let data = await lazy.TelemetryStorage.loadSessionData();
+
+ if (!data) {
+ return null;
+ }
+
+ if (
+ !("profileSubsessionCounter" in data) ||
+ !(typeof data.profileSubsessionCounter == "number") ||
+ !("subsessionId" in data) ||
+ !("sessionId" in data)
+ ) {
+ this._log.error("_loadSessionData - session data is invalid");
+ Services.telemetry
+ .getHistogramById("TELEMETRY_SESSIONDATA_FAILED_VALIDATION")
+ .add(1);
+ return null;
+ }
+
+ this._previousSessionId = data.sessionId;
+ this._previousSubsessionId = data.subsessionId;
+ // Add |_subsessionCounter| to the |_profileSubsessionCounter| to account for
+ // new subsession while loading still takes place. This will always be exactly
+ // 1 - the current subsessions.
+ this._profileSubsessionCounter =
+ data.profileSubsessionCounter + this._subsessionCounter;
+ // If we don't have this flag in the state file, it means that this is an old profile.
+ // We don't want to send the "new-profile" ping on new profile, so se this to true.
+ this._newProfilePingSent =
+ "newProfilePingSent" in data ? data.newProfilePingSent : true;
+ return data;
+ },
+
+ /**
+ * Get the session data object to serialise to disk.
+ */
+ _getSessionDataObject() {
+ return {
+ sessionId: this._sessionId,
+ subsessionId: this._subsessionId,
+ profileSubsessionCounter: this._profileSubsessionCounter,
+ newProfilePingSent: this._newProfilePingSent,
+ };
+ },
+
+ _onEnvironmentChange(reason, oldEnvironment) {
+ this._log.trace("_onEnvironmentChange", reason);
+
+ let now = Policy.monotonicNow();
+ let timeDelta = now - this._lastEnvironmentChangeDate;
+ if (timeDelta <= MIN_SUBSESSION_LENGTH_MS) {
+ this._log.trace(
+ `_onEnvironmentChange - throttling; last change was ${Math.round(
+ timeDelta / 1000
+ )}s ago.`
+ );
+ return;
+ }
+
+ this._lastEnvironmentChangeDate = now;
+ let payload = this.getSessionPayload(REASON_ENVIRONMENT_CHANGE, true);
+ lazy.TelemetryScheduler.rescheduleDailyPing(payload);
+
+ let options = {
+ addClientId: true,
+ addEnvironment: true,
+ overrideEnvironment: oldEnvironment,
+ };
+ lazy.TelemetryController.submitExternalPing(
+ getPingType(payload),
+ payload,
+ options
+ );
+ },
+
+ _isClassicReason(reason) {
+ const classicReasons = [
+ REASON_SAVED_SESSION,
+ REASON_GATHER_PAYLOAD,
+ REASON_TEST_PING,
+ ];
+ return classicReasons.includes(reason);
+ },
+
+ /**
+ * Get an object describing the current state of this module for AsyncShutdown diagnostics.
+ */
+ _getState() {
+ return {
+ initialized: this._initialized,
+ initStarted: this._initStarted,
+ haveDelayedInitTask: !!this._delayedInitTask,
+ };
+ },
+
+ /**
+ * Saves the aborted session ping to disk.
+ * @param {Object} [aProvidedPayload=null] A payload object to be used as an aborted
+ * session ping. The reason of this payload is changed to aborted-session.
+ * If not provided, a new payload is gathered.
+ */
+ _saveAbortedSessionPing(aProvidedPayload = null) {
+ this._log.trace("_saveAbortedSessionPing");
+
+ let payload = null;
+ if (aProvidedPayload) {
+ payload = Cu.cloneInto(aProvidedPayload, {});
+ // Overwrite the original reason.
+ payload.info.reason = REASON_ABORTED_SESSION;
+ } else {
+ payload = this.getSessionPayload(REASON_ABORTED_SESSION, false);
+ }
+
+ return lazy.TelemetryController.saveAbortedSessionPing(payload);
+ },
+
+ async markNewProfilePingSent() {
+ this._log.trace("markNewProfilePingSent");
+ this._newProfilePingSent = true;
+ return lazy.TelemetryStorage.saveSessionData(this._getSessionDataObject());
+ },
+};
diff --git a/toolkit/components/telemetry/pings/UninstallPing.sys.mjs b/toolkit/components/telemetry/pings/UninstallPing.sys.mjs
new file mode 100644
index 0000000000..00c1e80a40
--- /dev/null
+++ b/toolkit/components/telemetry/pings/UninstallPing.sys.mjs
@@ -0,0 +1,34 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ WindowsInstallsInfo:
+ "resource://gre/modules/components-utils/WindowsInstallsInfo.sys.mjs",
+});
+
+/**
+ * The Windows-only "uninstall" ping, which is saved to disk for the uninstaller to find.
+ * The ping is actually assembled by TelemetryControllerParent.saveUninstallPing().
+ */
+export var UninstallPing = {
+ /**
+ * Maximum number of other installs to count (see
+ * toolkit/components/telemetry/docs/data/uninstall-ping.rst for motivation)
+ */
+ MAX_OTHER_INSTALLS: 11,
+
+ /**
+ * Count other installs of this app, based on the values in the TaskBarIDs registry key.
+ *
+ */
+ getOtherInstallsCount() {
+ return lazy.WindowsInstallsInfo.getInstallPaths(
+ this.MAX_OTHER_INSTALLS,
+ new Set([Services.dirsvc.get("GreBinD", Ci.nsIFile).path])
+ ).size;
+ },
+};
diff --git a/toolkit/components/telemetry/pings/UntrustedModulesPing.sys.mjs b/toolkit/components/telemetry/pings/UntrustedModulesPing.sys.mjs
new file mode 100644
index 0000000000..85f565775d
--- /dev/null
+++ b/toolkit/components/telemetry/pings/UntrustedModulesPing.sys.mjs
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * This module periodically sends a Telemetry ping containing information
+ * about untrusted module loads on Windows.
+ *
+ * https://firefox-source-docs.mozilla.org/toolkit/components/telemetry/telemetry/data/third-party-modules-ping.html
+ */
+
+import { Log } from "resource://gre/modules/Log.sys.mjs";
+import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ TelemetryController: "resource://gre/modules/TelemetryController.sys.mjs",
+ TelemetryUtils: "resource://gre/modules/TelemetryUtils.sys.mjs",
+});
+
+XPCOMUtils.defineLazyServiceGetters(lazy, {
+ UpdateTimerManager: [
+ "@mozilla.org/updates/timer-manager;1",
+ "nsIUpdateTimerManager",
+ ],
+});
+
+const DEFAULT_INTERVAL_SECONDS = 24 * 60 * 60; // 1 day
+
+const LOGGER_NAME = "Toolkit.Telemetry";
+const LOGGER_PREFIX = "TelemetryUntrustedModulesPing::";
+const TIMER_NAME = "telemetry_untrustedmodules_ping";
+const PING_SUBMISSION_NAME = "third-party-modules";
+
+export var TelemetryUntrustedModulesPing = Object.freeze({
+ _log: Log.repository.getLoggerWithMessagePrefix(LOGGER_NAME, LOGGER_PREFIX),
+
+ start() {
+ lazy.UpdateTimerManager.registerTimer(
+ TIMER_NAME,
+ this,
+ Services.prefs.getIntPref(
+ lazy.TelemetryUtils.Preferences.UntrustedModulesPingFrequency,
+ DEFAULT_INTERVAL_SECONDS
+ )
+ );
+ },
+
+ notify() {
+ try {
+ Services.telemetry.getUntrustedModuleLoadEvents().then(payload => {
+ try {
+ if (payload) {
+ lazy.TelemetryController.submitExternalPing(
+ PING_SUBMISSION_NAME,
+ payload,
+ {
+ addClientId: true,
+ addEnvironment: true,
+ }
+ );
+ }
+ } catch (ex) {
+ this._log.error("payload handler caught an exception", ex);
+ }
+ });
+ } catch (ex) {
+ this._log.error("notify() caught an exception", ex);
+ }
+ },
+});
diff --git a/toolkit/components/telemetry/pings/UpdatePing.sys.mjs b/toolkit/components/telemetry/pings/UpdatePing.sys.mjs
new file mode 100644
index 0000000000..0334d8d1b4
--- /dev/null
+++ b/toolkit/components/telemetry/pings/UpdatePing.sys.mjs
@@ -0,0 +1,181 @@
+/* -*- js-indent-level: 2; indent-tabs-mode: nil -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+import { Log } from "resource://gre/modules/Log.sys.mjs";
+
+import { TelemetryUtils } from "resource://gre/modules/TelemetryUtils.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ TelemetryController: "resource://gre/modules/TelemetryController.sys.mjs",
+});
+
+const LOGGER_NAME = "Toolkit.Telemetry";
+const PING_TYPE = "update";
+const UPDATE_DOWNLOADED_TOPIC = "update-downloaded";
+const UPDATE_STAGED_TOPIC = "update-staged";
+
+/**
+ * This module is responsible for listening to all the relevant update
+ * signals, gathering the needed information and assembling the "update"
+ * ping.
+ */
+export var UpdatePing = {
+ _enabled: false,
+
+ earlyInit() {
+ this._log = Log.repository.getLoggerWithMessagePrefix(
+ LOGGER_NAME,
+ "UpdatePing::"
+ );
+ this._enabled = Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.UpdatePing,
+ false
+ );
+
+ this._log.trace("init - enabled: " + this._enabled);
+
+ if (!this._enabled) {
+ return;
+ }
+
+ Services.obs.addObserver(this, UPDATE_DOWNLOADED_TOPIC);
+ Services.obs.addObserver(this, UPDATE_STAGED_TOPIC);
+ },
+
+ /**
+ * Get the information about the update we're going to apply/was just applied
+ * from the update manager.
+ *
+ * @return {nsIUpdate} The information about the update, if available, or null.
+ */
+ _getActiveUpdate() {
+ let updateManager = Cc["@mozilla.org/updates/update-manager;1"].getService(
+ Ci.nsIUpdateManager
+ );
+ if (!updateManager || !updateManager.readyUpdate) {
+ return null;
+ }
+
+ return updateManager.readyUpdate;
+ },
+
+ /**
+ * Generate an "update" ping with reason "success" and dispatch it
+ * to the Telemetry system.
+ *
+ * @param {String} aPreviousVersion The browser version we updated from.
+ * @param {String} aPreviousBuildId The browser build id we updated from.
+ */
+ handleUpdateSuccess(aPreviousVersion, aPreviousBuildId) {
+ if (!this._enabled) {
+ return;
+ }
+
+ this._log.trace("handleUpdateSuccess");
+
+ // An update could potentially change the update channel. Moreover,
+ // updates can only be applied if the update's channel matches with the build channel.
+ // There's no way to pass this information from the caller nor the environment as,
+ // in that case, the environment would report the "new" channel. However, the
+ // update manager should still have information about the active update: given the
+ // previous assumptions, we can simply get the channel from the update and assume
+ // it matches with the state previous to the update.
+ let update = this._getActiveUpdate();
+
+ const payload = {
+ reason: "success",
+ previousChannel: update ? update.channel : null,
+ previousVersion: aPreviousVersion,
+ previousBuildId: aPreviousBuildId,
+ };
+
+ const options = {
+ addClientId: true,
+ addEnvironment: true,
+ usePingSender: false,
+ };
+
+ lazy.TelemetryController.submitExternalPing(
+ PING_TYPE,
+ payload,
+ options
+ ).catch(e =>
+ this._log.error("handleUpdateSuccess - failed to submit update ping", e)
+ );
+ },
+
+ /**
+ * Generate an "update" ping with reason "ready" and dispatch it
+ * to the Telemetry system.
+ *
+ * @param {String} aUpdateState The state of the downloaded patch. See
+ * nsIUpdateService.idl for a list of possible values.
+ */
+ _handleUpdateReady(aUpdateState) {
+ const ALLOWED_STATES = [
+ "applied",
+ "applied-service",
+ "pending",
+ "pending-service",
+ "pending-elevate",
+ ];
+ if (!ALLOWED_STATES.includes(aUpdateState)) {
+ this._log.trace("Unexpected update state: " + aUpdateState);
+ return;
+ }
+
+ // Get the information about the update we're going to apply from the
+ // update manager.
+ let update = this._getActiveUpdate();
+ if (!update) {
+ this._log.trace(
+ "Cannot get the update manager or no update is currently active."
+ );
+ return;
+ }
+
+ const payload = {
+ reason: "ready",
+ targetChannel: update.channel,
+ targetVersion: update.appVersion,
+ targetBuildId: update.buildID,
+ targetDisplayVersion: update.displayVersion,
+ };
+
+ const options = {
+ addClientId: true,
+ addEnvironment: true,
+ usePingSender: true,
+ };
+
+ lazy.TelemetryController.submitExternalPing(
+ PING_TYPE,
+ payload,
+ options
+ ).catch(e =>
+ this._log.error("_handleUpdateReady - failed to submit update ping", e)
+ );
+ },
+
+ /**
+ * The notifications handler.
+ */
+ observe(aSubject, aTopic, aData) {
+ this._log.trace("observe - aTopic: " + aTopic);
+ if (aTopic == UPDATE_DOWNLOADED_TOPIC || aTopic == UPDATE_STAGED_TOPIC) {
+ this._handleUpdateReady(aData);
+ }
+ },
+
+ shutdown() {
+ if (!this._enabled) {
+ return;
+ }
+ Services.obs.removeObserver(this, UPDATE_DOWNLOADED_TOPIC);
+ Services.obs.removeObserver(this, UPDATE_STAGED_TOPIC);
+ },
+};
diff --git a/toolkit/components/telemetry/pingsender/moz.build b/toolkit/components/telemetry/pingsender/moz.build
new file mode 100644
index 0000000000..e3526f4c34
--- /dev/null
+++ b/toolkit/components/telemetry/pingsender/moz.build
@@ -0,0 +1,38 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+if CONFIG["OS_TARGET"] != "Android":
+ GeckoProgram("pingsender", linkage=None)
+
+ UNIFIED_SOURCES += [
+ "pingsender.cpp",
+ ]
+
+ LOCAL_INCLUDES += [
+ "/toolkit/crashreporter/google-breakpad/src",
+ ]
+
+ USE_LIBS += [
+ "zlib",
+ ]
+
+if CONFIG["OS_TARGET"] == "WINNT":
+ UNIFIED_SOURCES += [
+ "pingsender_win.cpp",
+ ]
+
+ OS_LIBS += [
+ "wininet",
+ ]
+else:
+ UNIFIED_SOURCES += [
+ "pingsender_unix_common.cpp",
+ ]
+
+
+# Don't use the STL wrappers; we don't link with -lmozalloc, and it really
+# doesn't matter here anyway.
+DisableStlWrapping()
diff --git a/toolkit/components/telemetry/pingsender/pingsender.cpp b/toolkit/components/telemetry/pingsender/pingsender.cpp
new file mode 100644
index 0000000000..30f2907c72
--- /dev/null
+++ b/toolkit/components/telemetry/pingsender/pingsender.cpp
@@ -0,0 +1,228 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <cstdlib>
+#include <cstdint>
+#include <cstring>
+#include <ctime>
+#include <fstream>
+#include <iomanip>
+#include <string>
+#include <vector>
+
+#include <zlib.h>
+
+#include "pingsender.h"
+
+using std::ifstream;
+using std::ios;
+using std::string;
+using std::vector;
+
+namespace PingSender {
+
+// Operate in std::string because nul bytes will be preserved
+bool IsValidDestination(std::string aHost) {
+ static const std::string kValidDestinations[] = {
+ "localhost",
+ "incoming.telemetry.mozilla.org",
+ };
+ for (auto destination : kValidDestinations) {
+ if (aHost == destination) {
+ return true;
+ }
+ }
+ return false;
+}
+
+bool IsValidDestination(char* aHost) {
+ return IsValidDestination(std::string(aHost));
+}
+
+/**
+ * This shared function returns a Date header string for use in HTTP requests.
+ * See "RFC 7231, section 7.1.1.2: Date" for its specifications.
+ */
+std::string GenerateDateHeader() {
+ char buffer[128];
+ std::time_t t = std::time(nullptr);
+ strftime(buffer, sizeof(buffer), "Date: %a, %d %b %Y %H:%M:%S GMT",
+ std::gmtime(&t));
+ return string(buffer);
+}
+
+std::string GzipCompress(const std::string& rawData) {
+ z_stream deflater = {};
+
+ // Use the maximum window size when compressing: this also tells zlib to
+ // generate a gzip header.
+ const int32_t kWindowSize = MAX_WBITS + 16;
+ if (deflateInit2(&deflater, Z_DEFAULT_COMPRESSION, Z_DEFLATED, kWindowSize, 8,
+ Z_DEFAULT_STRATEGY) != Z_OK) {
+ PINGSENDER_LOG("ERROR: Could not initialize zlib deflating\n");
+ return "";
+ }
+
+ // Initialize the output buffer. The size of the buffer is the same
+ // as defined by the ZIP_BUFLEN macro in Gecko.
+ const uint32_t kBufferSize = 4 * 1024 - 1;
+ unsigned char outputBuffer[kBufferSize];
+ deflater.next_out = outputBuffer;
+ deflater.avail_out = kBufferSize;
+
+ // Let zlib know about the input data.
+ deflater.avail_in = rawData.size();
+ deflater.next_in =
+ reinterpret_cast<Bytef*>(const_cast<char*>(rawData.c_str()));
+
+ // Compress and append chunk by chunk.
+ std::string gzipData;
+ int err = Z_OK;
+
+ while (deflater.avail_in > 0 && err == Z_OK) {
+ err = deflate(&deflater, Z_NO_FLUSH);
+
+ // Since we're using the Z_NO_FLUSH policy, zlib can decide how
+ // much data to compress. When the buffer is full, we repeadetly
+ // flush out.
+ while (deflater.avail_out == 0) {
+ gzipData.append(reinterpret_cast<const char*>(outputBuffer), kBufferSize);
+
+ // Update the state and let the deflater know about it.
+ deflater.next_out = outputBuffer;
+ deflater.avail_out = kBufferSize;
+ err = deflate(&deflater, Z_NO_FLUSH);
+ }
+ }
+
+ // Flush the deflater buffers.
+ while (err == Z_OK) {
+ err = deflate(&deflater, Z_FINISH);
+ size_t bytesToWrite = kBufferSize - deflater.avail_out;
+ if (bytesToWrite == 0) {
+ break;
+ }
+ gzipData.append(reinterpret_cast<const char*>(outputBuffer), bytesToWrite);
+ deflater.next_out = outputBuffer;
+ deflater.avail_out = kBufferSize;
+ }
+
+ // Clean up.
+ deflateEnd(&deflater);
+
+ if (err != Z_STREAM_END) {
+ PINGSENDER_LOG("ERROR: There was a problem while compressing the ping\n");
+ return "";
+ }
+
+ return gzipData;
+}
+
+class Ping {
+ public:
+ Ping(const string& aUrl, const string& aPath) : mUrl(aUrl), mPath(aPath) {}
+ bool Send() const;
+ bool Delete() const;
+
+ private:
+ string Read() const;
+
+ const string mUrl;
+ const string mPath;
+};
+
+bool Ping::Send() const {
+ string ping(Read());
+
+ if (ping.empty()) {
+ PINGSENDER_LOG("ERROR: Ping payload is empty\n");
+ return false;
+ }
+
+ // Compress the ping using gzip.
+ string gzipPing(GzipCompress(ping));
+
+ // In the unlikely event of failure to gzip-compress the ping, don't
+ // attempt to send it uncompressed: Telemetry will pick it up and send
+ // it compressed.
+ if (gzipPing.empty()) {
+ PINGSENDER_LOG("ERROR: Ping compression failed\n");
+ return false;
+ }
+
+ if (!Post(mUrl, gzipPing)) {
+ return false;
+ }
+
+ return true;
+}
+
+bool Ping::Delete() const {
+ return !mPath.empty() && !std::remove(mPath.c_str());
+}
+
+string Ping::Read() const {
+ string ping;
+ ifstream file;
+
+ file.open(mPath.c_str(), ios::in | ios::binary);
+
+ if (!file.is_open()) {
+ PINGSENDER_LOG("ERROR: Could not open ping file\n");
+ return "";
+ }
+
+ do {
+ char buff[4096];
+
+ file.read(buff, sizeof(buff));
+
+ if (file.bad()) {
+ PINGSENDER_LOG("ERROR: Could not read ping contents\n");
+ return "";
+ }
+
+ ping.append(buff, file.gcount());
+ } while (!file.eof());
+
+ return ping;
+}
+
+} // namespace PingSender
+
+using namespace PingSender;
+
+int main(int argc, char* argv[]) {
+ vector<Ping> pings;
+
+ if ((argc >= 3) && ((argc - 1) % 2 == 0)) {
+ for (int i = 1; i < argc; i += 2) {
+ Ping ping(argv[i], argv[i + 1]);
+ pings.push_back(ping);
+ }
+ } else {
+ PINGSENDER_LOG(
+ "Usage: pingsender URL1 PATH1 URL2 PATH2 ...\n"
+ "Send the payloads stored in PATH<n> to the specified URL<n> using an "
+ "HTTP POST\nmessage for each payload then delete the file after a "
+ "successful send.\n");
+ return EXIT_FAILURE;
+ }
+
+ ChangeCurrentWorkingDirectory(argv[2]);
+
+ for (const auto& ping : pings) {
+ if (!ping.Send()) {
+ return EXIT_FAILURE;
+ }
+
+ if (!ping.Delete()) {
+ PINGSENDER_LOG("ERROR: Could not delete the ping file\n");
+ return EXIT_FAILURE;
+ }
+ }
+
+ return EXIT_SUCCESS;
+}
diff --git a/toolkit/components/telemetry/pingsender/pingsender.exe.manifest b/toolkit/components/telemetry/pingsender/pingsender.exe.manifest
new file mode 100644
index 0000000000..8e4bb8749b
--- /dev/null
+++ b/toolkit/components/telemetry/pingsender/pingsender.exe.manifest
@@ -0,0 +1,19 @@
+<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
+<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
+<assemblyIdentity
+ version="1.0.0.0"
+ processorArchitecture="*"
+ name="pingsender"
+ type="win32"
+/>
+<dependency>
+ <dependentAssembly>
+ <assemblyIdentity
+ type="win32"
+ name="mozglue"
+ version="1.0.0.0"
+ language="*"
+ />
+ </dependentAssembly>
+</dependency>
+</assembly>
diff --git a/toolkit/components/telemetry/pingsender/pingsender.h b/toolkit/components/telemetry/pingsender/pingsender.h
new file mode 100644
index 0000000000..2359738abf
--- /dev/null
+++ b/toolkit/components/telemetry/pingsender/pingsender.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_telemetry_pingsender_h
+#define mozilla_telemetry_pingsender_h
+
+#include <string>
+
+#ifdef DEBUG
+# define PINGSENDER_LOG(s, ...) printf(s, ##__VA_ARGS__)
+#else
+# define PINGSENDER_LOG(s, ...)
+#endif // DEBUG
+
+namespace PingSender {
+
+// The maximum time, in milliseconds, we allow for the connection phase
+// to the server.
+constexpr uint32_t kConnectionTimeoutMs = 30 * 1000;
+constexpr char kUserAgent[] = "pingsender/1.0";
+constexpr char kCustomVersionHeader[] = "X-PingSender-Version: 1.0";
+constexpr char kContentEncodingHeader[] = "Content-Encoding: gzip";
+
+// System-specific function that changes the current working directory to be
+// the same as the one containing the ping file. This is currently required on
+// Windows to release the Firefox installation folder (see bug 1597803 for more
+// details) and is a no-op on other platforms.
+void ChangeCurrentWorkingDirectory(const std::string& pingPath);
+
+// System-specific function to make an HTTP POST operation
+bool Post(const std::string& url, const std::string& payload);
+
+bool IsValidDestination(char* aUriEndingInHost);
+bool IsValidDestination(std::string aUriEndingInHost);
+std::string GenerateDateHeader();
+
+} // namespace PingSender
+
+#endif
diff --git a/toolkit/components/telemetry/pingsender/pingsender_unix_common.cpp b/toolkit/components/telemetry/pingsender/pingsender_unix_common.cpp
new file mode 100644
index 0000000000..1f201d177e
--- /dev/null
+++ b/toolkit/components/telemetry/pingsender/pingsender_unix_common.cpp
@@ -0,0 +1,301 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <algorithm>
+#include <cerrno>
+#include <cstring>
+#include <dlfcn.h>
+#include <string>
+#include <unistd.h>
+#include "mozilla/Unused.h"
+#include "third_party/curl/curl.h"
+
+#include "pingsender.h"
+
+namespace PingSender {
+
+using std::string;
+
+using mozilla::Unused;
+
+/**
+ * A simple wrapper around libcurl "easy" functions. Provides RAII opening
+ * and initialization of the curl library
+ */
+class CurlWrapper {
+ public:
+ CurlWrapper();
+ ~CurlWrapper();
+ bool Init();
+ bool IsValidDestination(const string& url);
+ bool Post(const string& url, const string& payload);
+
+ // libcurl functions
+ CURL* (*easy_init)(void);
+ CURLcode (*easy_setopt)(CURL*, CURLoption, ...);
+ CURLcode (*easy_perform)(CURL*);
+ CURLcode (*easy_getinfo)(CURL*, CURLINFO, ...);
+ curl_slist* (*slist_append)(curl_slist*, const char*);
+ void (*slist_free_all)(curl_slist*);
+ const char* (*easy_strerror)(CURLcode);
+ void (*easy_cleanup)(CURL*);
+ void (*global_cleanup)(void);
+
+ CURLU* (*curl_url)();
+ CURLUcode (*curl_url_get)(CURLU*, CURLUPart, char**, unsigned int);
+ CURLUcode (*curl_url_set)(CURLU*, CURLUPart, const char*, unsigned int);
+ void (*curl_free)(char*);
+ void (*curl_url_cleanup)(CURLU*);
+
+ private:
+ void* mLib;
+ void* mCurl;
+ bool mCanParseUrl;
+};
+
+CurlWrapper::CurlWrapper()
+ : easy_init(nullptr),
+ easy_setopt(nullptr),
+ easy_perform(nullptr),
+ easy_getinfo(nullptr),
+ slist_append(nullptr),
+ slist_free_all(nullptr),
+ easy_strerror(nullptr),
+ easy_cleanup(nullptr),
+ global_cleanup(nullptr),
+ curl_url(nullptr),
+ curl_url_get(nullptr),
+ curl_url_set(nullptr),
+ curl_free(nullptr),
+ curl_url_cleanup(nullptr),
+ mLib(nullptr),
+ mCurl(nullptr) {}
+
+CurlWrapper::~CurlWrapper() {
+ if (mLib) {
+ if (mCurl && easy_cleanup) {
+ easy_cleanup(mCurl);
+ }
+
+ if (global_cleanup) {
+ global_cleanup();
+ }
+
+ dlclose(mLib);
+ }
+}
+
+bool CurlWrapper::Init() {
+ const char* libcurlPaths[] = {
+#if defined(XP_MACOSX)
+ // macOS
+ "/usr/lib/libcurl.dylib",
+ "/usr/lib/libcurl.4.dylib",
+ "/usr/lib/libcurl.3.dylib",
+#else // Linux, *BSD, ...
+ "libcurl.so",
+ "libcurl.so.4",
+ // Debian gives libcurl a different name when it is built against GnuTLS
+ "libcurl-gnutls.so",
+ "libcurl-gnutls.so.4",
+ // Older versions in case we find nothing better
+ "libcurl.so.3",
+ "libcurl-gnutls.so.3", // See above for Debian
+#endif
+ };
+
+ // libcurl might show up under different names & paths, try them all until
+ // we find it
+ for (const char* libname : libcurlPaths) {
+ mLib = dlopen(libname, RTLD_NOW);
+
+ if (mLib) {
+ break;
+ }
+ }
+
+ if (!mLib) {
+ PINGSENDER_LOG("ERROR: Could not find libcurl\n");
+ return false;
+ }
+
+ *(void**)(&easy_init) = dlsym(mLib, "curl_easy_init");
+ *(void**)(&easy_setopt) = dlsym(mLib, "curl_easy_setopt");
+ *(void**)(&easy_perform) = dlsym(mLib, "curl_easy_perform");
+ *(void**)(&easy_getinfo) = dlsym(mLib, "curl_easy_getinfo");
+ *(void**)(&slist_append) = dlsym(mLib, "curl_slist_append");
+ *(void**)(&slist_free_all) = dlsym(mLib, "curl_slist_free_all");
+ *(void**)(&easy_strerror) = dlsym(mLib, "curl_easy_strerror");
+ *(void**)(&easy_cleanup) = dlsym(mLib, "curl_easy_cleanup");
+ *(void**)(&global_cleanup) = dlsym(mLib, "curl_global_cleanup");
+
+ *(void**)(&curl_url) = dlsym(mLib, "curl_url");
+ *(void**)(&curl_url_set) = dlsym(mLib, "curl_url_set");
+ *(void**)(&curl_url_get) = dlsym(mLib, "curl_url_get");
+ *(void**)(&curl_free) = dlsym(mLib, "curl_free");
+ *(void**)(&curl_url_cleanup) = dlsym(mLib, "curl_url_cleanup");
+
+ if (!easy_init || !easy_setopt || !easy_perform || !easy_getinfo ||
+ !slist_append || !slist_free_all || !easy_strerror || !easy_cleanup ||
+ !global_cleanup) {
+ PINGSENDER_LOG("ERROR: libcurl is missing one of the required symbols\n");
+ return false;
+ }
+
+ mCanParseUrl = true;
+ if (!curl_url || !curl_url_get || !curl_url_set || !curl_free ||
+ !curl_url_cleanup) {
+ mCanParseUrl = false;
+ PINGSENDER_LOG("WARNING: Do not have url parsing functions in libcurl\n");
+ }
+
+ mCurl = easy_init();
+
+ if (!mCurl) {
+ PINGSENDER_LOG("ERROR: Could not initialize libcurl\n");
+ return false;
+ }
+
+ return true;
+}
+
+static size_t DummyWriteCallback(char* ptr, size_t size, size_t nmemb,
+ void* userdata) {
+ Unused << ptr;
+ Unused << size;
+ Unused << nmemb;
+ Unused << userdata;
+
+ return size * nmemb;
+}
+
+// If we can't use curl's URL parsing (which is safer) we have to fallback
+// to this handwritten one (which is only as safe as we are clever.)
+bool FallbackIsValidDestination(const string& aUrl) {
+ // Lowercase the url
+ string url = aUrl;
+ std::transform(url.begin(), url.end(), url.begin(),
+ [](unsigned char c) { return std::tolower(c); });
+ // Strip off the scheme in the beginning
+ if (url.find("http://") == 0) {
+ url = url.substr(7);
+ } else if (url.find("https://") == 0) {
+ url = url.substr(8);
+ }
+
+ // Remove any user information. If a @ appeared in the userinformation,
+ // it would need to be encoded.
+ unsigned long atStart = url.find_first_of("@");
+ url = (atStart == std::string::npos) ? url : url.substr(atStart + 1);
+
+ // Remove any path or fragment information, leaving us with a url that may
+ // contain host, and port.
+ unsigned long fragStart = url.find_first_of("#");
+ url = (fragStart == std::string::npos) ? url : url.substr(0, fragStart);
+ unsigned long pathStart = url.find_first_of("/");
+ url = (pathStart == std::string::npos) ? url : url.substr(0, pathStart);
+
+ // Remove the port, because we run tests targeting localhost:port
+ unsigned long portStart = url.find_last_of(":");
+ url = (portStart == std::string::npos) ? url : url.substr(0, portStart);
+
+ return PingSender::IsValidDestination(url);
+}
+
+bool CurlWrapper::IsValidDestination(const string& aUrl) {
+ if (!mCanParseUrl) {
+ return FallbackIsValidDestination(aUrl);
+ }
+
+ bool ret = false;
+ CURLU* h = curl_url();
+ if (!h) {
+ return FallbackIsValidDestination(aUrl);
+ }
+
+ if (CURLUE_OK != curl_url_set(h, CURLUPART_URL, aUrl.c_str(), 0)) {
+ goto cleanup;
+ }
+
+ char* host;
+ if (CURLUE_OK != curl_url_get(h, CURLUPART_HOST, &host, 0)) {
+ goto cleanup;
+ }
+
+ ret = PingSender::IsValidDestination(host);
+ curl_free(host);
+
+cleanup:
+ curl_url_cleanup(h);
+ return ret;
+}
+
+bool CurlWrapper::Post(const string& url, const string& payload) {
+ easy_setopt(mCurl, CURLOPT_URL, url.c_str());
+ easy_setopt(mCurl, CURLOPT_USERAGENT, kUserAgent);
+ easy_setopt(mCurl, CURLOPT_WRITEFUNCTION, DummyWriteCallback);
+
+ // Build the date header.
+ std::string dateHeader = GenerateDateHeader();
+
+ // Set the custom headers.
+ curl_slist* headerChunk = nullptr;
+ headerChunk = slist_append(headerChunk, kCustomVersionHeader);
+ headerChunk = slist_append(headerChunk, kContentEncodingHeader);
+ headerChunk = slist_append(headerChunk, dateHeader.c_str());
+ CURLcode err = easy_setopt(mCurl, CURLOPT_HTTPHEADER, headerChunk);
+ if (err != CURLE_OK) {
+ PINGSENDER_LOG("ERROR: Failed to set HTTP headers, %s\n",
+ easy_strerror(err));
+ slist_free_all(headerChunk);
+ return false;
+ }
+
+ // Set the size of the POST data
+ easy_setopt(mCurl, CURLOPT_POSTFIELDSIZE, payload.length());
+
+ // Set the contents of the POST data
+ easy_setopt(mCurl, CURLOPT_POSTFIELDS, payload.c_str());
+
+ // Fail if the server returns a 4xx code
+ easy_setopt(mCurl, CURLOPT_FAILONERROR, 1);
+
+ // Override the default connection timeout, which is 5 minutes.
+ easy_setopt(mCurl, CURLOPT_CONNECTTIMEOUT_MS, kConnectionTimeoutMs);
+
+ // Block until the operation is performend. Ignore the response, if the POST
+ // fails we can't do anything about it.
+ err = easy_perform(mCurl);
+ // Whatever happens, we want to clean up the header memory.
+ slist_free_all(headerChunk);
+
+ if (err != CURLE_OK) {
+ PINGSENDER_LOG("ERROR: Failed to send HTTP request, %s\n",
+ easy_strerror(err));
+ return false;
+ }
+
+ return true;
+}
+
+bool Post(const string& url, const string& payload) {
+ CurlWrapper curl;
+
+ if (!curl.Init()) {
+ return false;
+ }
+ if (!curl.IsValidDestination(url)) {
+ PINGSENDER_LOG("ERROR: Invalid destination host\n");
+ return false;
+ }
+
+ return curl.Post(url, payload);
+}
+
+void ChangeCurrentWorkingDirectory(const string& pingPath) {
+ // This is not needed under Linux/macOS
+}
+
+} // namespace PingSender
diff --git a/toolkit/components/telemetry/pingsender/pingsender_win.cpp b/toolkit/components/telemetry/pingsender/pingsender_win.cpp
new file mode 100644
index 0000000000..c0c250d7c5
--- /dev/null
+++ b/toolkit/components/telemetry/pingsender/pingsender_win.cpp
@@ -0,0 +1,180 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include <string>
+
+#include <direct.h>
+#include <windows.h>
+#include <wininet.h>
+
+#include "pingsender.h"
+
+namespace PingSender {
+
+using std::string;
+
+/**
+ * A helper to automatically close internet handles when they go out of scope
+ */
+class ScopedHInternet {
+ public:
+ explicit ScopedHInternet(HINTERNET handle) : mHandle(handle) {}
+
+ ~ScopedHInternet() {
+ if (mHandle) {
+ InternetCloseHandle(mHandle);
+ }
+ }
+
+ bool empty() { return (mHandle == nullptr); }
+ HINTERNET get() { return mHandle; }
+
+ private:
+ HINTERNET mHandle;
+};
+
+const size_t kUrlComponentsSchemeLength = 256;
+const size_t kUrlComponentsHostLength = 256;
+const size_t kUrlComponentsPathLength = 256;
+
+/**
+ * Post the specified payload to a telemetry server
+ *
+ * @param url The URL of the telemetry server
+ * @param payload The ping payload
+ */
+bool Post(const string& url, const string& payload) {
+ char scheme[kUrlComponentsSchemeLength];
+ char host[kUrlComponentsHostLength];
+ char path[kUrlComponentsPathLength];
+
+ URL_COMPONENTS components = {};
+ components.dwStructSize = sizeof(components);
+ components.lpszScheme = scheme;
+ components.dwSchemeLength = kUrlComponentsSchemeLength;
+ components.lpszHostName = host;
+ components.dwHostNameLength = kUrlComponentsHostLength;
+ components.lpszUrlPath = path;
+ components.dwUrlPathLength = kUrlComponentsPathLength;
+
+ if (!InternetCrackUrl(url.c_str(), url.size(), 0, &components)) {
+ PINGSENDER_LOG("ERROR: Could not separate the URL components\n");
+ return false;
+ }
+
+ if (!IsValidDestination(host)) {
+ PINGSENDER_LOG("ERROR: Invalid destination host '%s'\n", host);
+ return false;
+ }
+
+ ScopedHInternet internet(InternetOpen(kUserAgent,
+ INTERNET_OPEN_TYPE_PRECONFIG,
+ /* lpszProxyName */ NULL,
+ /* lpszProxyBypass */ NULL,
+ /* dwFlags */ 0));
+
+ if (internet.empty()) {
+ PINGSENDER_LOG("ERROR: Could not open wininet internet handle\n");
+ return false;
+ }
+
+ DWORD timeout = static_cast<DWORD>(kConnectionTimeoutMs);
+ bool rv = InternetSetOption(internet.get(), INTERNET_OPTION_CONNECT_TIMEOUT,
+ &timeout, sizeof(timeout));
+ if (!rv) {
+ PINGSENDER_LOG("ERROR: Could not set the connection timeout\n");
+ return false;
+ }
+
+ ScopedHInternet connection(
+ InternetConnect(internet.get(), host, components.nPort,
+ /* lpszUsername */ NULL,
+ /* lpszPassword */ NULL, INTERNET_SERVICE_HTTP,
+ /* dwFlags */ 0,
+ /* dwContext */ NULL));
+
+ if (connection.empty()) {
+ PINGSENDER_LOG("ERROR: Could not connect\n");
+ return false;
+ }
+
+ DWORD flags = ((strcmp(scheme, "https") == 0) ? INTERNET_FLAG_SECURE : 0) |
+ INTERNET_FLAG_NO_COOKIES;
+ ScopedHInternet request(HttpOpenRequest(connection.get(), "POST", path,
+ /* lpszVersion */ NULL,
+ /* lpszReferer */ NULL,
+ /* lplpszAcceptTypes */ NULL, flags,
+ /* dwContext */ NULL));
+
+ if (request.empty()) {
+ PINGSENDER_LOG("ERROR: Could not open HTTP POST request\n");
+ return false;
+ }
+
+ // Build a string containing all the headers.
+ std::string headers = GenerateDateHeader() + "\r\n";
+ headers += kCustomVersionHeader;
+ headers += "\r\n";
+ headers += kContentEncodingHeader;
+
+ rv = HttpSendRequest(request.get(), headers.c_str(), -1L,
+ (LPVOID)payload.c_str(), payload.size());
+ if (!rv) {
+ PINGSENDER_LOG("ERROR: Could not execute HTTP POST request\n");
+ return false;
+ }
+
+ // HttpSendRequest doesn't fail if we hit an HTTP error, so manually check
+ // for errors. Please note that this is not needed on the Linux/MacOS version
+ // of the pingsender, as libcurl already automatically fails on HTTP errors.
+ DWORD statusCode = 0;
+ DWORD bufferLength = sizeof(DWORD);
+ rv = HttpQueryInfo(
+ request.get(),
+ /* dwInfoLevel */ HTTP_QUERY_STATUS_CODE | HTTP_QUERY_FLAG_NUMBER,
+ /* lpvBuffer */ &statusCode,
+ /* lpdwBufferLength */ &bufferLength,
+ /* lpdwIndex */ NULL);
+ if (!rv) {
+ PINGSENDER_LOG("ERROR: Could not get the HTTP status code\n");
+ return false;
+ }
+
+ if (statusCode != 200) {
+ PINGSENDER_LOG("ERROR: Error submitting the HTTP request: code %lu\n",
+ statusCode);
+ return false;
+ }
+
+ return rv;
+}
+
+void ChangeCurrentWorkingDirectory(const string& pingPath) {
+ char fullPath[MAX_PATH + 1] = {};
+ if (!_fullpath(fullPath, pingPath.c_str(), sizeof(fullPath))) {
+ PINGSENDER_LOG("Could not build the full path to the ping\n");
+ return;
+ }
+
+ char drive[_MAX_DRIVE] = {};
+ char dir[_MAX_DIR] = {};
+ if (_splitpath_s(fullPath, drive, sizeof(drive), dir, sizeof(dir), nullptr, 0,
+ nullptr, 0)) {
+ PINGSENDER_LOG("Could not split the current path\n");
+ return;
+ }
+
+ char cwd[MAX_PATH + 1] = {};
+ if (_makepath_s(cwd, sizeof(cwd), drive, dir, nullptr, nullptr)) {
+ PINGSENDER_LOG("Could not assemble the path for the new cwd\n");
+ return;
+ }
+
+ if (_chdir(cwd) == -1) {
+ PINGSENDER_LOG("Could not change the current working directory\n");
+ }
+}
+
+} // namespace PingSender
diff --git a/toolkit/components/telemetry/telemetry-constants.mozbuild b/toolkit/components/telemetry/telemetry-constants.mozbuild
new file mode 100644
index 0000000000..5d1e84a23b
--- /dev/null
+++ b/toolkit/components/telemetry/telemetry-constants.mozbuild
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEFINES["TELEMETRY_PING_FORMAT_VERSION"] = 4
diff --git a/toolkit/components/telemetry/tests/addons/long-fields/manifest.json b/toolkit/components/telemetry/tests/addons/long-fields/manifest.json
new file mode 100644
index 0000000000..e18169169f
--- /dev/null
+++ b/toolkit/components/telemetry/tests/addons/long-fields/manifest.json
@@ -0,0 +1,12 @@
+{
+ "manifest_version": 2,
+ "name": "This is a really long addon name, that will get limited to 100 characters. We're much longer, we're at about 219. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus nullam sodales. Yeah, Latin placeholder.",
+ "description": "This is a really long addon description, that will get limited to 100 characters. We're much longer, we're at about 200. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vivamus nullam sodales.",
+ "version": "1.0",
+
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "tel-longfields-webext@tests.mozilla.org"
+ }
+ }
+}
diff --git a/toolkit/components/telemetry/tests/addons/restartless/manifest.json b/toolkit/components/telemetry/tests/addons/restartless/manifest.json
new file mode 100644
index 0000000000..a70a99d469
--- /dev/null
+++ b/toolkit/components/telemetry/tests/addons/restartless/manifest.json
@@ -0,0 +1,12 @@
+{
+ "manifest_version": 2,
+ "name": "XPI Telemetry Restartless Test",
+ "description": "A restartless addon which gets enabled without a reboot.",
+ "version": "1.0",
+
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "tel-restartless-webext@tests.mozilla.org"
+ }
+ }
+}
diff --git a/toolkit/components/telemetry/tests/addons/signed-webext/.web-extension-id b/toolkit/components/telemetry/tests/addons/signed-webext/.web-extension-id
new file mode 100644
index 0000000000..e78cecf6d6
--- /dev/null
+++ b/toolkit/components/telemetry/tests/addons/signed-webext/.web-extension-id
@@ -0,0 +1,3 @@
+# This file was created by https://github.com/mozilla/web-ext
+# Your auto-generated extension ID for addons.mozilla.org is:
+tel-signed-webext@tests.mozilla.org \ No newline at end of file
diff --git a/toolkit/components/telemetry/tests/addons/signed-webext/META-INF/manifest.mf b/toolkit/components/telemetry/tests/addons/signed-webext/META-INF/manifest.mf
new file mode 100644
index 0000000000..6be3a6e32f
--- /dev/null
+++ b/toolkit/components/telemetry/tests/addons/signed-webext/META-INF/manifest.mf
@@ -0,0 +1,7 @@
+Manifest-Version: 1.0
+
+Name: manifest.json
+Digest-Algorithms: MD5 SHA1
+MD5-Digest: vh0VF5quc9YIhMhIsZgKcg==
+SHA1-Digest: DNXAbrHJ4ncET5W+qtJl4+45D6s=
+
diff --git a/toolkit/components/telemetry/tests/addons/signed-webext/META-INF/mozilla.rsa b/toolkit/components/telemetry/tests/addons/signed-webext/META-INF/mozilla.rsa
new file mode 100644
index 0000000000..b202c515a8
--- /dev/null
+++ b/toolkit/components/telemetry/tests/addons/signed-webext/META-INF/mozilla.rsa
Binary files differ
diff --git a/toolkit/components/telemetry/tests/addons/signed-webext/META-INF/mozilla.sf b/toolkit/components/telemetry/tests/addons/signed-webext/META-INF/mozilla.sf
new file mode 100644
index 0000000000..72af14d816
--- /dev/null
+++ b/toolkit/components/telemetry/tests/addons/signed-webext/META-INF/mozilla.sf
@@ -0,0 +1,4 @@
+Signature-Version: 1.0
+MD5-Digest-Manifest: tbeA48G5pe86kvUbd4rsXA==
+SHA1-Digest-Manifest: jyvWt2v9XUnYHrvrlSi4BdyZV/0=
+
diff --git a/toolkit/components/telemetry/tests/addons/signed-webext/manifest.json b/toolkit/components/telemetry/tests/addons/signed-webext/manifest.json
new file mode 100644
index 0000000000..437b415fef
--- /dev/null
+++ b/toolkit/components/telemetry/tests/addons/signed-webext/manifest.json
@@ -0,0 +1,12 @@
+{
+ "manifest_version": 2,
+ "name": "XPI Telemetry Signed Test",
+ "description": "A signed webextension",
+ "version": "1.0",
+
+ "applications": {
+ "gecko": {
+ "id": "tel-signed-webext@tests.mozilla.org"
+ }
+ }
+}
diff --git a/toolkit/components/telemetry/tests/addons/system/manifest.json b/toolkit/components/telemetry/tests/addons/system/manifest.json
new file mode 100644
index 0000000000..db2072b8a5
--- /dev/null
+++ b/toolkit/components/telemetry/tests/addons/system/manifest.json
@@ -0,0 +1,13 @@
+{
+ "manifest_version": 2,
+
+ "name": "XPI Telemetry System Add-on Test",
+ "description": "A system addon which is shipped with Firefox.",
+ "version": "1.0",
+
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "tel-system-xpi@tests.mozilla.org"
+ }
+ }
+}
diff --git a/toolkit/components/telemetry/tests/browser/browser.toml b/toolkit/components/telemetry/tests/browser/browser.toml
new file mode 100644
index 0000000000..c0df7ceb0f
--- /dev/null
+++ b/toolkit/components/telemetry/tests/browser/browser.toml
@@ -0,0 +1,13 @@
+[DEFAULT]
+support-files = [
+ "file_iframe.html",
+ "file_media.html",
+ "gizmo.mp4",
+]
+
+["browser_DynamicScalars.js"]
+skip-if = ["verify"]
+
+["browser_UpdatePingSuccess.js"]
+
+["browser_media_element_in_page_scalar.js"]
diff --git a/toolkit/components/telemetry/tests/browser/browser_DynamicScalars.js b/toolkit/components/telemetry/tests/browser/browser_DynamicScalars.js
new file mode 100644
index 0000000000..966c7c1be3
--- /dev/null
+++ b/toolkit/components/telemetry/tests/browser/browser_DynamicScalars.js
@@ -0,0 +1,247 @@
+"use strict";
+
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { TelemetryUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryUtils.sys.mjs"
+);
+
+const CONTENT_CREATED = "ipc:content-created";
+
+async function waitForProcessesScalars(
+ aProcesses,
+ aKeyed,
+ aAdditionalCondition = data => true
+) {
+ await TestUtils.waitForCondition(() => {
+ const scalars = aKeyed
+ ? Services.telemetry.getSnapshotForKeyedScalars("main", false)
+ : Services.telemetry.getSnapshotForScalars("main", false);
+ return (
+ aProcesses.every(p => Object.keys(scalars).includes(p)) &&
+ aAdditionalCondition(scalars)
+ );
+ });
+}
+
+add_task(async function test_setup() {
+ // Make sure the newly spawned content processes will have extended Telemetry enabled.
+ // Since Telemetry reads the prefs only at process startup, flush all cached
+ // and preallocated processes so they pick up the setting.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [TelemetryUtils.Preferences.OverridePreRelease, true],
+ ["dom.ipc.processPrelaunch.enabled", false],
+ ],
+ });
+ Services.ppmm.releaseCachedProcesses();
+ await SpecialPowers.pushPrefEnv({
+ set: [["dom.ipc.processPrelaunch.enabled", true]],
+ });
+
+ // And take care of the already initialized one as well.
+ let canRecordExtended = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+ registerCleanupFunction(
+ () => (Services.telemetry.canRecordExtended = canRecordExtended)
+ );
+});
+
+add_task(async function test_recording() {
+ let currentPid = gBrowser.selectedBrowser.frameLoader.remoteTab.osPid;
+
+ // Register test scalars before spawning the content process: the scalar
+ // definitions will propagate to it.
+ Services.telemetry.registerScalars("telemetry.test.dynamic", {
+ pre_content_spawn: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ keyed: false,
+ record_on_release: true,
+ },
+ pre_content_spawn_expiration: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ keyed: false,
+ record_on_release: true,
+ },
+ });
+
+ Services.telemetry.scalarSet(
+ "telemetry.test.dynamic.pre_content_spawn_expiration",
+ 3
+ );
+
+ let processCreated = TestUtils.topicObserved(CONTENT_CREATED);
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank", forceNewProcess: true },
+ async function (browser) {
+ // Make sure our new browser is in its own process. The processCreated
+ // promise should have already resolved by this point.
+ await processCreated;
+ let newPid = browser.frameLoader.remoteTab.osPid;
+ Assert.notEqual(
+ currentPid,
+ newPid,
+ "The new tab must spawn its own process"
+ );
+
+ // Register test scalars after spawning the content process: the scalar
+ // definitions will propagate to it.
+ // Also attempt to register again "pre_content_spawn_expiration" and set
+ // it to expired.
+ Services.telemetry.registerScalars("telemetry.test.dynamic", {
+ post_content_spawn: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_BOOLEAN,
+ keyed: false,
+ record_on_release: false,
+ },
+ post_content_spawn_keyed: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ keyed: true,
+ record_on_release: true,
+ },
+ pre_content_spawn_expiration: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ keyed: false,
+ record_on_release: true,
+ expired: true,
+ },
+ });
+
+ // Accumulate from the content process into both dynamic scalars.
+ await SpecialPowers.spawn(browser, [], async function () {
+ Services.telemetry.scalarAdd(
+ "telemetry.test.dynamic.pre_content_spawn_expiration",
+ 1
+ );
+ Services.telemetry.scalarSet(
+ "telemetry.test.dynamic.pre_content_spawn",
+ 3
+ );
+ Services.telemetry.scalarSet(
+ "telemetry.test.dynamic.post_content_spawn",
+ true
+ );
+ Services.telemetry.keyedScalarSet(
+ "telemetry.test.dynamic.post_content_spawn_keyed",
+ "testKey",
+ 3
+ );
+ });
+ }
+ );
+
+ // Wait for the dynamic scalars to appear non-keyed snapshots.
+ await waitForProcessesScalars(["dynamic"], true, scalars => {
+ // Wait for the scalars set in the content process to be available.
+ return "telemetry.test.dynamic.post_content_spawn_keyed" in scalars.dynamic;
+ });
+
+ // Verify the content of the snapshots.
+ const scalars = Services.telemetry.getSnapshotForScalars("main", false);
+ ok(
+ "dynamic" in scalars,
+ "The scalars must contain the 'dynamic' process section"
+ );
+ ok(
+ "telemetry.test.dynamic.pre_content_spawn" in scalars.dynamic,
+ "Dynamic scalars registered before a process spawns must be present."
+ );
+ is(
+ scalars.dynamic["telemetry.test.dynamic.pre_content_spawn"],
+ 3,
+ "The dynamic scalar must contain the expected value."
+ );
+ is(
+ scalars.dynamic["telemetry.test.dynamic.pre_content_spawn_expiration"],
+ 3,
+ "The dynamic scalar must not be updated after being expired."
+ );
+ ok(
+ "telemetry.test.dynamic.post_content_spawn" in scalars.dynamic,
+ "Dynamic scalars registered after a process spawns must be present."
+ );
+ is(
+ scalars.dynamic["telemetry.test.dynamic.post_content_spawn"],
+ true,
+ "The dynamic scalar must contain the expected value."
+ );
+
+ // Wait for the dynamic scalars to appear in the keyed snapshots.
+ await waitForProcessesScalars(["dynamic"], true);
+
+ const keyedScalars = Services.telemetry.getSnapshotForKeyedScalars(
+ "main",
+ false
+ );
+ ok(
+ "dynamic" in keyedScalars,
+ "The keyed scalars must contain the 'dynamic' process section"
+ );
+ ok(
+ "telemetry.test.dynamic.post_content_spawn_keyed" in keyedScalars.dynamic,
+ "Dynamic keyed scalars registered after a process spawns must be present."
+ );
+ is(
+ keyedScalars.dynamic["telemetry.test.dynamic.post_content_spawn_keyed"]
+ .testKey,
+ 3,
+ "The dynamic keyed scalar must contain the expected value."
+ );
+});
+
+add_task(async function test_aggregation() {
+ Services.telemetry.clearScalars();
+
+ // Register test scalars before spawning the content process: the scalar
+ // definitions will propagate to it. Also cheat TelemetrySession to put
+ // the test scalar in the payload by using "cheattest" instead of "test" in
+ // the scalar category name.
+ Services.telemetry.registerScalars("telemetry.cheattest.dynamic", {
+ test_aggregation: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ keyed: false,
+ record_on_release: true,
+ },
+ });
+
+ const SCALAR_FULL_NAME = "telemetry.cheattest.dynamic.test_aggregation";
+ Services.telemetry.scalarAdd(SCALAR_FULL_NAME, 1);
+
+ await BrowserTestUtils.withNewTab(
+ { gBrowser, url: "about:blank", forceNewProcess: true },
+ async function (browser) {
+ // Accumulate from the content process into both dynamic scalars.
+ await SpecialPowers.spawn(
+ browser,
+ [SCALAR_FULL_NAME],
+ async function (aName) {
+ Services.telemetry.scalarAdd(aName, 3);
+ }
+ );
+ }
+ );
+
+ // Wait for the dynamic scalars to appear. Since we're testing that children
+ // and parent data get aggregated, we might need to wait a bit more:
+ // TelemetryIPCAccumulator.cpp sends batches to the parent process every 2 seconds.
+ await waitForProcessesScalars(["dynamic"], false, scalarData => {
+ return (
+ "dynamic" in scalarData &&
+ SCALAR_FULL_NAME in scalarData.dynamic &&
+ scalarData.dynamic[SCALAR_FULL_NAME] == 4
+ );
+ });
+
+ // Check that the definitions made it to the ping payload.
+ const pingData = TelemetryController.getCurrentPingData(true);
+ ok(
+ "dynamic" in pingData.payload.processes,
+ "The ping payload must contain the 'dynamic' process section"
+ );
+ is(
+ pingData.payload.processes.dynamic.scalars[SCALAR_FULL_NAME],
+ 4,
+ "The dynamic scalar must contain the aggregated parent and children data."
+ );
+});
diff --git a/toolkit/components/telemetry/tests/browser/browser_UpdatePingSuccess.js b/toolkit/components/telemetry/tests/browser/browser_UpdatePingSuccess.js
new file mode 100644
index 0000000000..f747419cfc
--- /dev/null
+++ b/toolkit/components/telemetry/tests/browser/browser_UpdatePingSuccess.js
@@ -0,0 +1,166 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+"use strict";
+
+const { TelemetryUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryUtils.sys.mjs"
+);
+const { TelemetryArchiveTesting } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryArchiveTesting.sys.mjs"
+);
+
+add_task(async function test_updatePing() {
+ const TEST_VERSION = "37.85";
+ const TEST_BUILDID = "20150711123724";
+ const XML_UPDATE = `<?xml version="1.0"?>
+ <updates xmlns="http://www.mozilla.org/2005/app-update">
+ <update appVersion="${Services.appinfo.version}" buildID="20080811053724"
+ channel="nightly" displayVersion="Version 1.0"
+ installDate="1238441400314" isCompleteUpdate="true" type="minor"
+ name="Update Test 1.0" detailsURL="http://example.com/"
+ previousAppVersion="${TEST_VERSION}"
+ serviceURL="https://example.com/" foregroundDownload="true"
+ statusText="The Update was successfully installed">
+ <patch type="complete" URL="http://example.com/" size="775"
+ selected="true" state="succeeded"/>
+ </update>
+ </updates>`;
+
+ // Set the preferences needed for the test: they will be cleared up
+ // after it runs.
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ [TelemetryUtils.Preferences.UpdatePing, true],
+ ["browser.startup.homepage_override.mstone", TEST_VERSION],
+ ["browser.startup.homepage_override.buildID", TEST_BUILDID],
+ ["toolkit.telemetry.log.level", "Trace"],
+ ],
+ });
+
+ registerCleanupFunction(async () => {
+ let activeUpdateFile = getActiveUpdateFile();
+ activeUpdateFile.remove(false);
+ reloadUpdateManagerData(true);
+ });
+ writeUpdatesToXMLFile(XML_UPDATE);
+ reloadUpdateManagerData(false);
+
+ // Start monitoring the ping archive.
+ let archiveChecker = new TelemetryArchiveTesting.Checker();
+ await archiveChecker.promiseInit();
+
+ // Manually call the BrowserContentHandler: this automatically gets called when
+ // the browser is started and an update was applied successfully in order to
+ // display the "update" info page.
+ Cc["@mozilla.org/browser/clh;1"].getService(Ci.nsIBrowserHandler).defaultArgs;
+
+ // We cannot control when the ping will be generated/archived after we trigger
+ // an update, so let's make sure to have one before moving on with validation.
+ let updatePing;
+ await BrowserTestUtils.waitForCondition(
+ async function () {
+ // Check that the ping made it into the Telemetry archive.
+ // The test data is defined in ../data/sharedUpdateXML.js
+ updatePing = await archiveChecker.promiseFindPing("update", [
+ [["payload", "reason"], "success"],
+ [["payload", "previousBuildId"], TEST_BUILDID],
+ [["payload", "previousVersion"], TEST_VERSION],
+ ]);
+ return !!updatePing;
+ },
+ "Make sure the ping is generated before trying to validate it.",
+ 500,
+ 100
+ );
+
+ ok(updatePing, "The 'update' ping must be correctly sent.");
+
+ // We have no easy way to simulate a previously applied update from toolkit/telemetry.
+ // Instead of moving this test to mozapps/update as well, just test that the
+ // "previousChannel" field is present and either a string or null.
+ ok(
+ "previousChannel" in updatePing.payload,
+ "The payload must contain the 'previousChannel' field"
+ );
+ const channelField = updatePing.payload.previousChannel;
+ if (channelField != null) {
+ Assert.equal(
+ typeof channelField,
+ "string",
+ "'previousChannel' must be a string, if available."
+ );
+ }
+
+ // Also make sure that the ping contains both a client id and an
+ // environment section.
+ ok("clientId" in updatePing, "The update ping must report a client id.");
+ ok(
+ "environment" in updatePing,
+ "The update ping must report the environment."
+ );
+});
+
+/**
+ * Removes the updates.xml file and returns the nsIFile for the
+ * active-update.xml file.
+ *
+ * @return The nsIFile for the active-update.xml file.
+ */
+function getActiveUpdateFile() {
+ let updateRootDir = Services.dirsvc.get("UpdRootD", Ci.nsIFile);
+ let updatesFile = updateRootDir.clone();
+ updatesFile.append("updates.xml");
+ if (updatesFile.exists()) {
+ // The following is non-fatal.
+ try {
+ updatesFile.remove(false);
+ } catch (e) {}
+ }
+ let activeUpdateFile = updateRootDir.clone();
+ activeUpdateFile.append("active-update.xml");
+ return activeUpdateFile;
+}
+
+/**
+ * Reloads the update xml files.
+ *
+ * @param skipFiles (optional)
+ * If true, the update xml files will not be read and the metadata will
+ * be reset. If false (the default), the update xml files will be read
+ * to populate the update metadata.
+ */
+function reloadUpdateManagerData(skipFiles = false) {
+ Cc["@mozilla.org/updates/update-manager;1"]
+ .getService(Ci.nsIUpdateManager)
+ .QueryInterface(Ci.nsIObserver)
+ .observe(null, "um-reload-update-data", skipFiles ? "skip-files" : "");
+}
+
+/**
+ * Writes the updates specified to the active-update.xml file.
+ *
+ * @param aText
+ * The updates represented as a string to write to the active-update.xml
+ * file.
+ */
+function writeUpdatesToXMLFile(aText) {
+ const PERMS_FILE = 0o644;
+
+ const MODE_WRONLY = 0x02;
+ const MODE_CREATE = 0x08;
+ const MODE_TRUNCATE = 0x20;
+
+ let activeUpdateFile = getActiveUpdateFile();
+ if (!activeUpdateFile.exists()) {
+ activeUpdateFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
+ }
+ let fos = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(
+ Ci.nsIFileOutputStream
+ );
+ let flags = MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE;
+ fos.init(activeUpdateFile, flags, PERMS_FILE, 0);
+ fos.write(aText, aText.length);
+ fos.close();
+}
diff --git a/toolkit/components/telemetry/tests/browser/browser_media_element_in_page_scalar.js b/toolkit/components/telemetry/tests/browser/browser_media_element_in_page_scalar.js
new file mode 100644
index 0000000000..3c9dd8f8ee
--- /dev/null
+++ b/toolkit/components/telemetry/tests/browser/browser_media_element_in_page_scalar.js
@@ -0,0 +1,128 @@
+"use strict";
+
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+const LOCATION =
+ "https://example.com/browser/toolkit/components/telemetry/tests/browser/";
+const CORS_LOCATION =
+ "https://example.org/browser/toolkit/components/telemetry/tests/browser/";
+const MEDIA_SCALAR_NAME = "media.element_in_page_count";
+
+/**
+ * 'media.element_in_page_count' is a permanant scalar, this test is used to
+ * check if that scalar can be accumulated correctly under different situations.
+ */
+add_task(async function start_tests() {
+ // Clean all scalars first to prevent being interfered by former test.
+ TelemetryTestUtils.getProcessScalars("parent", false, true /* clear */);
+
+ await testMediaInPageScalar({
+ description: "load a page with one media element",
+ url: "file_media.html",
+ expectedScalarCount: 1,
+ });
+ await testMediaInPageScalar({
+ description: "load a page with multiple media elements",
+ url: "file_media.html",
+ options: {
+ createSecondMedia: true,
+ },
+ expectedScalarCount: 1,
+ });
+ await testMediaInPageScalar({
+ description: "load a page with media element created from iframe",
+ url: "file_iframe.html",
+ options: {
+ iframeUrl: "file_media.html",
+ },
+ expectedScalarCount: 1,
+ });
+ await testMediaInPageScalar({
+ description: "load a page with media element created from CORS iframe",
+ url: "file_iframe.html",
+ options: {
+ iframeUrl: "file_media.html",
+ CORSIframe: true,
+ },
+ expectedScalarCount: 1,
+ });
+ await testMediaInPageScalar({
+ description: "run multiple tabs, all loading media page",
+ url: "file_media.html",
+ options: {
+ tabNums: 2,
+ },
+ expectedScalarCount: 2,
+ });
+});
+
+async function testMediaInPageScalar({
+ description,
+ url,
+ options,
+ expectedScalarCount,
+} = {}) {
+ info(`media scalar should be undefined in the start`);
+ let scalars = TelemetryTestUtils.getProcessScalars("parent");
+ is(scalars[MEDIA_SCALAR_NAME], undefined, "has not created media scalar yet");
+
+ info(`run test '${description}'`);
+ url = LOCATION + url;
+ await runMediaPage(url, options);
+
+ info(`media scalar should be increased to ${expectedScalarCount}`);
+ scalars = TelemetryTestUtils.getProcessScalars(
+ "parent",
+ false,
+ true /* clear */
+ );
+ is(
+ scalars[MEDIA_SCALAR_NAME],
+ expectedScalarCount,
+ "media scalar count is correct"
+ );
+ info("============= Next Testcase =============");
+}
+
+/**
+ * The following are helper functions.
+ */
+async function runMediaPage(url, options = {}) {
+ const tabNums = options.tabNums ? options.tabNums : 1;
+ for (let idx = 0; idx < tabNums; idx++) {
+ info(`open a tab loading media page`);
+ const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+ if (options.iframeUrl) {
+ let iframeURL = options.CORSIframe ? CORS_LOCATION : LOCATION;
+ iframeURL += options.iframeUrl;
+ await loadPageForIframe(tab, iframeURL);
+ }
+
+ if (options.createSecondMedia) {
+ info(`create second media in the page`);
+ await createMedia(tab);
+ }
+
+ info(`remove tab`);
+ await BrowserTestUtils.removeTab(tab);
+ await BrowserUtils.promiseObserved("window-global-destroyed");
+ }
+}
+
+function createMedia(tab) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [], _ => {
+ const video = content.document.createElement("VIDEO");
+ video.src = "gizmo.mp4";
+ video.loop = true;
+ content.document.body.appendChild(video);
+ });
+}
+
+function loadPageForIframe(tab, url) {
+ return SpecialPowers.spawn(tab.linkedBrowser, [url], async url => {
+ const iframe = content.document.getElementById("iframe");
+ iframe.src = url;
+ await new Promise(r => (iframe.onload = r));
+ });
+}
diff --git a/toolkit/components/telemetry/tests/browser/file_iframe.html b/toolkit/components/telemetry/tests/browser/file_iframe.html
new file mode 100644
index 0000000000..271c179eb2
--- /dev/null
+++ b/toolkit/components/telemetry/tests/browser/file_iframe.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>Media loaded in iframe</title>
+</head>
+<body>
+<iframe id="iframe"></iframe>
+</body>
+</html>
diff --git a/toolkit/components/telemetry/tests/browser/file_media.html b/toolkit/components/telemetry/tests/browser/file_media.html
new file mode 100644
index 0000000000..e2109d18f5
--- /dev/null
+++ b/toolkit/components/telemetry/tests/browser/file_media.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+<title>media page</title>
+</head>
+<body>
+<video id="video" src="gizmo.mp4" loop></video>
+</body>
+</html>
diff --git a/toolkit/components/telemetry/tests/browser/gizmo.mp4 b/toolkit/components/telemetry/tests/browser/gizmo.mp4
new file mode 100644
index 0000000000..87efad5ade
--- /dev/null
+++ b/toolkit/components/telemetry/tests/browser/gizmo.mp4
Binary files differ
diff --git a/toolkit/components/telemetry/tests/gtest/TelemetryFixture.cpp b/toolkit/components/telemetry/tests/gtest/TelemetryFixture.cpp
new file mode 100644
index 0000000000..b329a3c08d
--- /dev/null
+++ b/toolkit/components/telemetry/tests/gtest/TelemetryFixture.cpp
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+#include "TelemetryFixture.h"
+#include "mozilla/dom/SimpleGlobalObject.h"
+
+using namespace mozilla;
+
+void TelemetryTestFixture::SetUp() {
+ mTelemetry = do_GetService("@mozilla.org/base/telemetry;1");
+
+ mCleanGlobal = dom::SimpleGlobalObject::Create(
+ dom::SimpleGlobalObject::GlobalType::BindingDetail);
+
+ // The test must fail if we failed getting the global.
+ ASSERT_NE(mCleanGlobal, nullptr)
+ << "SimpleGlobalObject must return a valid global object.";
+}
+
+AutoJSContextWithGlobal::AutoJSContextWithGlobal(JSObject* aGlobalObject)
+ : mCx(nullptr) {
+ // The JS API must initialize correctly.
+ JS::Rooted<JSObject*> globalObject(dom::RootingCx(), aGlobalObject);
+ mJsAPI.emplace();
+ MOZ_ALWAYS_TRUE(mJsAPI->Init(globalObject));
+}
+
+JSContext* AutoJSContextWithGlobal::GetJSContext() const {
+ return mJsAPI->cx();
+}
diff --git a/toolkit/components/telemetry/tests/gtest/TelemetryFixture.h b/toolkit/components/telemetry/tests/gtest/TelemetryFixture.h
new file mode 100644
index 0000000000..c7f2ca8977
--- /dev/null
+++ b/toolkit/components/telemetry/tests/gtest/TelemetryFixture.h
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+#ifndef TelemetryFixture_h_
+#define TelemetryFixture_h_
+
+#include "gtest/gtest.h"
+#include "mozilla/CycleCollectedJSContext.h"
+#include "mozilla/dom/ScriptSettings.h"
+#include "mozilla/Maybe.h"
+#include "nsITelemetry.h"
+
+class TelemetryTestFixture : public ::testing::Test {
+ protected:
+ TelemetryTestFixture() : mCleanGlobal(nullptr) {}
+ virtual void SetUp();
+
+ JSObject* mCleanGlobal;
+
+ nsCOMPtr<nsITelemetry> mTelemetry;
+};
+
+// AutoJSAPI is annotated with MOZ_STACK_CLASS and thus cannot be
+// used as a member of TelemetryTestFixture, since gtest instantiates
+// that on the heap. To work around the problem, use the following class
+// at the beginning of each Telemetry test.
+// Note: this is very similar to AutoJSContext, but it allows to pass a
+// global JS object in.
+class MOZ_RAII AutoJSContextWithGlobal {
+ public:
+ explicit AutoJSContextWithGlobal(JSObject* aGlobalObject);
+ JSContext* GetJSContext() const;
+
+ protected:
+ mozilla::Maybe<mozilla::dom::AutoJSAPI> mJsAPI;
+ JSContext* mCx;
+};
+
+#endif // TelemetryFixture_h_
diff --git a/toolkit/components/telemetry/tests/gtest/TelemetryTestHelpers.cpp b/toolkit/components/telemetry/tests/gtest/TelemetryTestHelpers.cpp
new file mode 100644
index 0000000000..1848052c2e
--- /dev/null
+++ b/toolkit/components/telemetry/tests/gtest/TelemetryTestHelpers.cpp
@@ -0,0 +1,372 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+#include "TelemetryTestHelpers.h"
+
+#include "core/TelemetryCommon.h"
+#include "gtest/gtest.h"
+#include "js/Array.h" // JS::GetArrayLength, JS::IsArrayObject
+#include "js/CallAndConstruct.h" // JS_CallFunctionName
+#include "js/PropertyAndElement.h" // JS_Enumerate, JS_GetElement, JS_GetProperty
+#include "mozilla/CycleCollectedJSContext.h"
+#include "mozilla/gtest/MozAssertions.h"
+#include "mozilla/Unused.h"
+#include "nsTArray.h"
+#include "nsPrintfCString.h"
+
+using namespace mozilla;
+
+// Helper methods provided to simplify writing tests and meant to be used in C++
+// Gtests.
+namespace TelemetryTestHelpers {
+
+void CheckUintScalar(const char* aName, JSContext* aCx,
+ JS::Handle<JS::Value> aSnapshot, uint32_t expectedValue) {
+ // Validate the value of the test scalar.
+ JS::Rooted<JS::Value> value(aCx);
+ JS::Rooted<JSObject*> scalarObj(aCx, &aSnapshot.toObject());
+ ASSERT_TRUE(JS_GetProperty(aCx, scalarObj, aName, &value))
+ << "The test scalar must be reported.";
+ JS_GetProperty(aCx, scalarObj, aName, &value);
+
+ ASSERT_TRUE(value.isInt32())
+ << "The scalar value must be of the correct type.";
+ ASSERT_TRUE(value.toInt32() >= 0)
+ << "The uint scalar type must contain a value >= 0.";
+ ASSERT_EQ(static_cast<uint32_t>(value.toInt32()), expectedValue)
+ << "The scalar value must match the expected value.";
+}
+
+void CheckBoolScalar(const char* aName, JSContext* aCx,
+ JS::Handle<JS::Value> aSnapshot, bool expectedValue) {
+ // Validate the value of the test scalar.
+ JS::Rooted<JS::Value> value(aCx);
+ JS::Rooted<JSObject*> scalarObj(aCx, &aSnapshot.toObject());
+ ASSERT_TRUE(JS_GetProperty(aCx, scalarObj, aName, &value))
+ << "The test scalar must be reported.";
+ ASSERT_TRUE(value.isBoolean())
+ << "The scalar value must be of the correct type.";
+ ASSERT_EQ(static_cast<bool>(value.toBoolean()), expectedValue)
+ << "The scalar value must match the expected value.";
+}
+
+void CheckStringScalar(const char* aName, JSContext* aCx,
+ JS::Handle<JS::Value> aSnapshot,
+ const char* expectedValue) {
+ // Validate the value of the test scalar.
+ JS::Rooted<JS::Value> value(aCx);
+ JS::Rooted<JSObject*> scalarObj(aCx, &aSnapshot.toObject());
+ ASSERT_TRUE(JS_GetProperty(aCx, scalarObj, aName, &value))
+ << "The test scalar must be reported.";
+ ASSERT_TRUE(value.isString())
+ << "The scalar value must be of the correct type.";
+
+ bool sameString;
+ ASSERT_TRUE(
+ JS_StringEqualsAscii(aCx, value.toString(), expectedValue, &sameString))
+ << "JS String comparison failed";
+ ASSERT_TRUE(sameString)
+ << "The scalar value must match the expected string";
+}
+
+void CheckKeyedUintScalar(const char* aName, const char* aKey, JSContext* aCx,
+ JS::Handle<JS::Value> aSnapshot,
+ uint32_t expectedValue) {
+ JS::Rooted<JS::Value> keyedScalar(aCx);
+ JS::Rooted<JSObject*> scalarObj(aCx, &aSnapshot.toObject());
+ // Get the aName keyed scalar object from the scalars snapshot.
+ ASSERT_TRUE(JS_GetProperty(aCx, scalarObj, aName, &keyedScalar))
+ << "The keyed scalar must be reported.";
+
+ CheckUintScalar(aKey, aCx, keyedScalar, expectedValue);
+}
+
+void CheckKeyedBoolScalar(const char* aName, const char* aKey, JSContext* aCx,
+ JS::Handle<JS::Value> aSnapshot, bool expectedValue) {
+ JS::Rooted<JS::Value> keyedScalar(aCx);
+ JS::Rooted<JSObject*> scalarObj(aCx, &aSnapshot.toObject());
+ // Get the aName keyed scalar object from the scalars snapshot.
+ ASSERT_TRUE(JS_GetProperty(aCx, scalarObj, aName, &keyedScalar))
+ << "The keyed scalar must be reported.";
+
+ CheckBoolScalar(aKey, aCx, keyedScalar, expectedValue);
+}
+
+void CheckNumberOfProperties(const char* aName, JSContext* aCx,
+ JS::Handle<JS::Value> aSnapshot,
+ uint32_t expectedNumProperties) {
+ JS::Rooted<JS::Value> keyedScalar(aCx);
+ JS::Rooted<JSObject*> scalarObj(aCx, &aSnapshot.toObject());
+ // Get the aName keyed scalar object from the scalars snapshot.
+ ASSERT_TRUE(JS_GetProperty(aCx, scalarObj, aName, &keyedScalar))
+ << "The keyed scalar must be reported.";
+
+ JS::Rooted<JSObject*> keyedScalarObj(aCx, &keyedScalar.toObject());
+ JS::Rooted<JS::IdVector> ids(aCx, JS::IdVector(aCx));
+ ASSERT_TRUE(JS_Enumerate(aCx, keyedScalarObj, &ids))
+ << "We must be able to get keyed scalar members.";
+
+ ASSERT_EQ(expectedNumProperties, ids.length())
+ << "The scalar must report the expected number of properties.";
+}
+
+bool EventPresent(JSContext* aCx, const JS::RootedValue& aSnapshot,
+ const nsACString& aCategory, const nsACString& aMethod,
+ const nsACString& aObject) {
+ EXPECT_FALSE(aSnapshot.isNullOrUndefined())
+ << "Event snapshot must not be null/undefined.";
+ bool isArray = false;
+ EXPECT_TRUE(JS::IsArrayObject(aCx, aSnapshot, &isArray) && isArray)
+ << "The snapshot must be an array.";
+ JS::Rooted<JSObject*> arrayObj(aCx, &aSnapshot.toObject());
+ uint32_t arrayLength = 0;
+ EXPECT_TRUE(JS::GetArrayLength(aCx, arrayObj, &arrayLength))
+ << "Array must have a length.";
+ EXPECT_TRUE(arrayLength > 0) << "Array must have at least one element.";
+
+ for (uint32_t arrayIdx = 0; arrayIdx < arrayLength; ++arrayIdx) {
+ JS::Rooted<JS::Value> element(aCx);
+ EXPECT_TRUE(JS_GetElement(aCx, arrayObj, arrayIdx, &element))
+ << "Must be able to get element.";
+ EXPECT_TRUE(JS::IsArrayObject(aCx, element, &isArray) && isArray)
+ << "Element must be an array.";
+ JS::Rooted<JSObject*> eventArray(aCx, &element.toObject());
+ uint32_t eventLength;
+ EXPECT_TRUE(JS::GetArrayLength(aCx, eventArray, &eventLength))
+ << "Event array must have a length.";
+ EXPECT_TRUE(eventLength >= 4)
+ << "Event array must have at least 4 elements (timestamp, category, "
+ "method, object).";
+
+ JS::Rooted<JS::Value> str(aCx);
+ nsAutoJSString jsStr;
+ EXPECT_TRUE(JS_GetElement(aCx, eventArray, 1, &str))
+ << "Must be able to get category.";
+ EXPECT_TRUE(str.isString()) << "Category must be a string.";
+ EXPECT_TRUE(jsStr.init(aCx, str))
+ << "Category must be able to be init'd to a jsstring.";
+ if (NS_ConvertUTF16toUTF8(jsStr) != aCategory) {
+ continue;
+ }
+
+ EXPECT_TRUE(JS_GetElement(aCx, eventArray, 2, &str))
+ << "Must be able to get method.";
+ EXPECT_TRUE(str.isString()) << "Method must be a string.";
+ EXPECT_TRUE(jsStr.init(aCx, str))
+ << "Method must be able to be init'd to a jsstring.";
+ if (NS_ConvertUTF16toUTF8(jsStr) != aMethod) {
+ continue;
+ }
+
+ EXPECT_TRUE(JS_GetElement(aCx, eventArray, 3, &str))
+ << "Must be able to get object.";
+ EXPECT_TRUE(str.isString()) << "Object must be a string.";
+ EXPECT_TRUE(jsStr.init(aCx, str))
+ << "Object must be able to be init'd to a jsstring.";
+ if (NS_ConvertUTF16toUTF8(jsStr) != aObject) {
+ continue;
+ }
+
+ // We found it!
+ return true;
+ }
+
+ // We didn't find it!
+ return false;
+}
+
+nsTArray<nsString> EventValuesToArray(JSContext* aCx,
+ const JS::RootedValue& aSnapshot,
+ const nsAString& aCategory,
+ const nsAString& aMethod,
+ const nsAString& aObject) {
+ constexpr int kIndexOfCategory = 1;
+ constexpr int kIndexOfMethod = 2;
+ constexpr int kIndexOfObject = 3;
+ constexpr int kIndexOfValueString = 4;
+
+ nsTArray<nsString> valueArray;
+ if (aSnapshot.isNullOrUndefined()) {
+ return valueArray;
+ }
+
+ bool isArray = false;
+ EXPECT_TRUE(JS::IsArrayObject(aCx, aSnapshot, &isArray) && isArray)
+ << "The snapshot must be an array.";
+
+ JS::Rooted<JSObject*> arrayObj(aCx, &aSnapshot.toObject());
+
+ uint32_t arrayLength = 0;
+ EXPECT_TRUE(JS::GetArrayLength(aCx, arrayObj, &arrayLength))
+ << "Array must have a length.";
+
+ JS::Rooted<JS::Value> jsVal(aCx);
+ nsAutoJSString jsStr;
+
+ for (uint32_t arrayIdx = 0; arrayIdx < arrayLength; ++arrayIdx) {
+ JS::Rooted<JS::Value> element(aCx);
+ EXPECT_TRUE(JS_GetElement(aCx, arrayObj, arrayIdx, &element))
+ << "Must be able to get element.";
+
+ EXPECT_TRUE(JS::IsArrayObject(aCx, element, &isArray) && isArray)
+ << "Element must be an array.";
+
+ JS::Rooted<JSObject*> eventArray(aCx, &element.toObject());
+ uint32_t eventLength;
+ EXPECT_TRUE(JS::GetArrayLength(aCx, eventArray, &eventLength))
+ << "Event array must have a length.";
+ EXPECT_TRUE(eventLength >= kIndexOfValueString)
+ << "Event array must have at least 4 elements (timestamp, category, "
+ "method, object).";
+
+ EXPECT_TRUE(JS_GetElement(aCx, eventArray, kIndexOfCategory, &jsVal))
+ << "Must be able to get category.";
+ EXPECT_TRUE(jsVal.isString()) << "Category must be a string.";
+ EXPECT_TRUE(jsStr.init(aCx, jsVal))
+ << "Category must be able to be init'd to a jsstring.";
+ if (jsStr != aCategory) {
+ continue;
+ }
+
+ EXPECT_TRUE(JS_GetElement(aCx, eventArray, kIndexOfMethod, &jsVal))
+ << "Must be able to get method.";
+ EXPECT_TRUE(jsVal.isString()) << "Method must be a string.";
+ EXPECT_TRUE(jsStr.init(aCx, jsVal))
+ << "Method must be able to be init'd to a jsstring.";
+ if (jsStr != aMethod) {
+ continue;
+ }
+
+ EXPECT_TRUE(JS_GetElement(aCx, eventArray, kIndexOfObject, &jsVal))
+ << "Must be able to get object.";
+ EXPECT_TRUE(jsVal.isString()) << "Object must be a string.";
+ EXPECT_TRUE(jsStr.init(aCx, jsVal))
+ << "Object must be able to be init'd to a jsstring.";
+ if (jsStr != aObject) {
+ continue;
+ }
+
+ if (!JS_GetElement(aCx, eventArray, kIndexOfValueString, &jsVal)) {
+ continue;
+ }
+
+ nsString str;
+ EXPECT_TRUE(AssignJSString(aCx, str, jsVal.toString()))
+ << "Value must be able to be init'd to a string.";
+ Unused << valueArray.EmplaceBack(std::move(str));
+ }
+
+ return valueArray;
+}
+
+void GetEventSnapshot(JSContext* aCx, JS::MutableHandle<JS::Value> aResult,
+ ProcessID aProcessType) {
+ nsCOMPtr<nsITelemetry> telemetry =
+ do_GetService("@mozilla.org/base/telemetry;1");
+
+ JS::Rooted<JS::Value> eventSnapshot(aCx);
+ nsresult rv;
+ rv = telemetry->SnapshotEvents(1 /* PRERELEASE_CHANNELS */, false /* clear */,
+ 0 /* eventLimit */, aCx, 1 /* argc */,
+ &eventSnapshot);
+ ASSERT_EQ(rv, NS_OK) << "Snapshotting events must not fail.";
+ ASSERT_TRUE(eventSnapshot.isObject())
+ << "The snapshot must be an object.";
+
+ JS::Rooted<JS::Value> processEvents(aCx);
+ JS::Rooted<JSObject*> eventObj(aCx, &eventSnapshot.toObject());
+ Unused << JS_GetProperty(aCx, eventObj,
+ Telemetry::Common::GetNameForProcessID(aProcessType),
+ &processEvents);
+
+ aResult.set(processEvents);
+}
+
+void GetScalarsSnapshot(bool aKeyed, JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult,
+ ProcessID aProcessType) {
+ nsCOMPtr<nsITelemetry> telemetry =
+ do_GetService("@mozilla.org/base/telemetry;1");
+
+ // Get a snapshot of the scalars.
+ JS::Rooted<JS::Value> scalarsSnapshot(aCx);
+ nsresult rv;
+
+ if (aKeyed) {
+ rv = telemetry->GetSnapshotForKeyedScalars(
+ "main"_ns, false, false /* filter */, aCx, &scalarsSnapshot);
+ } else {
+ rv = telemetry->GetSnapshotForScalars("main"_ns, false, false /* filter */,
+ aCx, &scalarsSnapshot);
+ }
+
+ // Validate the snapshot.
+ ASSERT_EQ(rv, NS_OK) << "Creating a snapshot of the data must not fail.";
+ ASSERT_TRUE(scalarsSnapshot.isObject())
+ << "The snapshot must be an object.";
+
+ JS::Rooted<JS::Value> processScalars(aCx);
+ JS::Rooted<JSObject*> scalarObj(aCx, &scalarsSnapshot.toObject());
+ // Don't complain if no scalars for the process can be found. Just
+ // return an empty object.
+ Unused << JS_GetProperty(aCx, scalarObj,
+ Telemetry::Common::GetNameForProcessID(aProcessType),
+ &processScalars);
+
+ aResult.set(processScalars);
+}
+
+void GetAndClearHistogram(JSContext* cx, nsCOMPtr<nsITelemetry> mTelemetry,
+ const nsACString& name, bool is_keyed) {
+ JS::Rooted<JS::Value> testHistogram(cx);
+ nsresult rv =
+ is_keyed ? mTelemetry->GetKeyedHistogramById(name, cx, &testHistogram)
+ : mTelemetry->GetHistogramById(name, cx, &testHistogram);
+
+ ASSERT_EQ(rv, NS_OK) << "Cannot fetch histogram";
+
+ // Clear the stored value
+ JS::Rooted<JSObject*> testHistogramObj(cx, &testHistogram.toObject());
+ JS::Rooted<JS::Value> rval(cx);
+ ASSERT_TRUE(JS_CallFunctionName(cx, testHistogramObj, "clear",
+ JS::HandleValueArray::empty(), &rval))
+ << "Cannot clear histogram";
+}
+
+void GetProperty(JSContext* cx, const char* name, JS::Handle<JS::Value> valueIn,
+ JS::MutableHandle<JS::Value> valueOut) {
+ JS::Rooted<JS::Value> property(cx);
+ JS::Rooted<JSObject*> valueInObj(cx, &valueIn.toObject());
+ ASSERT_TRUE(JS_GetProperty(cx, valueInObj, name, &property))
+ << "Cannot get property '" << name << "'";
+ valueOut.set(property);
+}
+
+void GetElement(JSContext* cx, uint32_t index, JS::Handle<JS::Value> valueIn,
+ JS::MutableHandle<JS::Value> valueOut) {
+ JS::Rooted<JS::Value> element(cx);
+ JS::Rooted<JSObject*> valueInObj(cx, &valueIn.toObject());
+ ASSERT_TRUE(JS_GetElement(cx, valueInObj, index, &element))
+ << "Cannot get element at index '" << index << "'";
+ valueOut.set(element);
+}
+
+void GetSnapshots(JSContext* cx, nsCOMPtr<nsITelemetry> mTelemetry,
+ const char* name, JS::MutableHandle<JS::Value> valueOut,
+ bool is_keyed) {
+ JS::Rooted<JS::Value> snapshots(cx);
+ nsresult rv = is_keyed
+ ? mTelemetry->GetSnapshotForKeyedHistograms(
+ "main"_ns, false, false /* filter */, cx, &snapshots)
+ : mTelemetry->GetSnapshotForHistograms(
+ "main"_ns, false, false /* filter */, cx, &snapshots);
+
+ JS::Rooted<JS::Value> snapshot(cx);
+ GetProperty(cx, "parent", snapshots, &snapshot);
+
+ ASSERT_EQ(rv, NS_OK) << "Cannot call histogram snapshots";
+ valueOut.set(snapshot);
+}
+
+} // namespace TelemetryTestHelpers
diff --git a/toolkit/components/telemetry/tests/gtest/TelemetryTestHelpers.h b/toolkit/components/telemetry/tests/gtest/TelemetryTestHelpers.h
new file mode 100644
index 0000000000..b2eab48c73
--- /dev/null
+++ b/toolkit/components/telemetry/tests/gtest/TelemetryTestHelpers.h
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+#ifndef TelemetryTestHelpers_h_
+#define TelemetryTestHelpers_h_
+
+#include "js/TypeDecls.h"
+#include "mozilla/TelemetryProcessEnums.h"
+#include "nsITelemetry.h"
+
+using mozilla::Telemetry::ProcessID;
+
+namespace TelemetryTestHelpers {
+
+void CheckUintScalar(const char* aName, JSContext* aCx,
+ JS::Handle<JS::Value> aSnapshot, uint32_t expectedValue);
+
+void CheckBoolScalar(const char* aName, JSContext* aCx,
+ JS::Handle<JS::Value> aSnapshot, bool expectedValue);
+
+void CheckStringScalar(const char* aName, JSContext* aCx,
+ JS::Handle<JS::Value> aSnapshot,
+ const char* expectedValue);
+
+void CheckKeyedUintScalar(const char* aName, const char* aKey, JSContext* aCx,
+ JS::Handle<JS::Value> aSnapshot,
+ uint32_t expectedValue);
+
+void CheckKeyedBoolScalar(const char* aName, const char* aKey, JSContext* aCx,
+ JS::Handle<JS::Value> aSnapshot, bool expectedValue);
+
+void CheckNumberOfProperties(const char* aName, JSContext* aCx,
+ JS::Handle<JS::Value> aSnapshot,
+ uint32_t expectedNumProperties);
+
+bool EventPresent(JSContext* aCx, const JS::RootedValue& aSnapshot,
+ const nsACString& aCategory, const nsACString& aMethod,
+ const nsACString& aObject);
+
+nsTArray<nsString> EventValuesToArray(JSContext* aCx,
+ const JS::RootedValue& aSnapshot,
+ const nsAString& aCategory,
+ const nsAString& aMethod,
+ const nsAString& aObject);
+
+void GetEventSnapshot(JSContext* aCx, JS::MutableHandle<JS::Value> aResult,
+ ProcessID aProcessType = ProcessID::Parent);
+
+void GetScalarsSnapshot(bool aKeyed, JSContext* aCx,
+ JS::MutableHandle<JS::Value> aResult,
+ ProcessID aProcessType = ProcessID::Parent);
+
+void GetAndClearHistogram(JSContext* cx, nsCOMPtr<nsITelemetry> mTelemetry,
+ const nsACString& name, bool is_keyed);
+
+void GetProperty(JSContext* cx, const char* name, JS::Handle<JS::Value> valueIn,
+ JS::MutableHandle<JS::Value> valueOut);
+
+void GetElement(JSContext* cx, uint32_t index, JS::Handle<JS::Value> valueIn,
+ JS::MutableHandle<JS::Value> valueOut);
+
+void GetSnapshots(JSContext* cx, nsCOMPtr<nsITelemetry> mTelemetry,
+ const char* name, JS::MutableHandle<JS::Value> valueOut,
+ bool is_keyed);
+} // namespace TelemetryTestHelpers
+
+#endif
diff --git a/toolkit/components/telemetry/tests/gtest/TestCombinedStacks.cpp b/toolkit/components/telemetry/tests/gtest/TestCombinedStacks.cpp
new file mode 100644
index 0000000000..3e21c7378c
--- /dev/null
+++ b/toolkit/components/telemetry/tests/gtest/TestCombinedStacks.cpp
@@ -0,0 +1,158 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+#include "TelemetryFixture.h"
+#include "TelemetryTestHelpers.h"
+#include "other/CombinedStacks.h"
+#include "other/ProcessedStack.h"
+#include "nsPrintfCString.h"
+
+using namespace mozilla::Telemetry;
+using namespace TelemetryTestHelpers;
+
+TEST_F(TelemetryTestFixture, CombinedStacks) {
+ const size_t kMaxStacksKept = 10;
+ CombinedStacks stacks(kMaxStacksKept);
+
+ size_t iterations = kMaxStacksKept * 2;
+ for (size_t i = 0; i < iterations; ++i) {
+ ProcessedStack stack;
+ ProcessedStack::Frame frame = {static_cast<uint16_t>(i)};
+ const nsAutoString& name =
+ NS_ConvertUTF8toUTF16(nsPrintfCString("test%zu", i));
+ ProcessedStack::Module module = {name};
+
+ stack.AddFrame(frame);
+ stack.AddModule(module);
+ stacks.AddStack(stack);
+ }
+
+ ASSERT_EQ(stacks.GetStackCount(), kMaxStacksKept) << "Wrong number of stacks";
+ ASSERT_EQ(stacks.GetModuleCount(), kMaxStacksKept * 2)
+ << "Wrong number of modules";
+
+ for (size_t i = 0; i < kMaxStacksKept; ++i) {
+ ProcessedStack::Frame frame = stacks.GetStack(i)[0];
+ ASSERT_EQ(frame.mOffset, kMaxStacksKept + i)
+ << "Frame is not returning expected value";
+
+ ProcessedStack::Module module = stacks.GetModule(frame.mModIndex);
+ nsPrintfCString moduleName("test%hu", frame.mModIndex);
+ ASSERT_TRUE(module.mName.Equals(NS_ConvertUTF8toUTF16(moduleName)))
+ << "Module should have expected name";
+ }
+
+ for (size_t i = 0; i < kMaxStacksKept; ++i) {
+ stacks.RemoveStack(kMaxStacksKept - i - 1);
+ ASSERT_EQ(stacks.GetStackCount(), kMaxStacksKept - i - 1)
+ << "Stack should be removed";
+ }
+}
+
+template <int N>
+ProcessedStack MakeStack(const nsLiteralString (&aModules)[N],
+ const uintptr_t (&aOffsets)[N]) {
+ ProcessedStack stack;
+ for (int i = 0; i < N; ++i) {
+ ProcessedStack::Frame frame = {aOffsets[i]};
+ if (aModules[i].IsEmpty()) {
+ frame.mModIndex = std::numeric_limits<uint16_t>::max();
+ } else {
+ frame.mModIndex = stack.GetNumModules();
+ stack.AddModule(ProcessedStack::Module{aModules[i]});
+ }
+ stack.AddFrame(frame);
+ }
+ return stack;
+}
+
+TEST(CombinedStacks, Combine)
+{
+ const nsLiteralString moduleSet1[] = {u"mod1"_ns, u"mod2"_ns, u"base"_ns};
+ const nsLiteralString moduleSet2[] = {u"modX"_ns, u""_ns, u"modZ"_ns,
+ u"base"_ns};
+ // [0] 00 mod1+100
+ // 01 mod2+200
+ // 02 base+300
+ // [1] 00 mod1+1000
+ // 01 mod2+2000
+ // 02 base+3000
+ // [2] 00 modX+100
+ // 01 <no module>+200
+ // 02 modZ+300
+ // 03 base+400
+ // [3] 00 modX+1000
+ // 01 <no module>+3000
+ // 02 modZ+2000
+ // 03 base+4000
+ const ProcessedStack testStacks[] = {
+ MakeStack(moduleSet1, {100ul, 200ul, 300ul}),
+ MakeStack(moduleSet1, {1000ul, 2000ul, 3000ul}),
+ MakeStack(moduleSet2, {100ul, 200ul, 300ul, 400ul}),
+ MakeStack(moduleSet2, {1000ul, 2000ul, 3000ul, 4000ul}),
+ };
+
+ // combined1 <-- testStacks[0] + testStacks[1]
+ // combined2 <-- testStacks[2] + testStacks[3]
+ CombinedStacks combined1, combined2;
+ combined1.AddStack(testStacks[0]);
+ combined1.AddStack(testStacks[1]);
+ combined2.AddStack(testStacks[2]);
+ combined2.AddStack(testStacks[3]);
+
+ EXPECT_EQ(combined1.GetModuleCount(), mozilla::ArrayLength(moduleSet1));
+ EXPECT_EQ(combined1.GetStackCount(), 2u);
+ EXPECT_EQ(combined2.GetModuleCount(), mozilla::ArrayLength(moduleSet2) - 1);
+ EXPECT_EQ(combined2.GetStackCount(), 2u);
+
+ // combined1 <-- combined1 + combined2
+ combined1.AddStacks(combined2);
+
+ EXPECT_EQ(combined1.GetModuleCount(), 5u); // {mod1, mod2, modX, modZ, base}
+ EXPECT_EQ(combined1.GetStackCount(), mozilla::ArrayLength(testStacks));
+
+ for (size_t i = 0; i < combined1.GetStackCount(); ++i) {
+ const auto& expectedStack = testStacks[i];
+ const auto& actualStack = combined1.GetStack(i);
+ EXPECT_EQ(actualStack.size(), expectedStack.GetStackSize());
+ if (actualStack.size() != expectedStack.GetStackSize()) {
+ continue;
+ }
+
+ for (size_t j = 0; j < actualStack.size(); ++j) {
+ const auto& expectedFrame = expectedStack.GetFrame(j);
+ const auto& actualFrame = actualStack[j];
+
+ EXPECT_EQ(actualFrame.mOffset, expectedFrame.mOffset);
+
+ if (expectedFrame.mModIndex == std::numeric_limits<uint16_t>::max()) {
+ EXPECT_EQ(actualFrame.mModIndex, std::numeric_limits<uint16_t>::max());
+ } else {
+ EXPECT_EQ(combined1.GetModule(actualFrame.mModIndex),
+ expectedStack.GetModule(expectedFrame.mModIndex));
+ }
+ }
+ }
+
+ // Only testStacks[3] will be stored into oneStack
+ CombinedStacks oneStack(1);
+ oneStack.AddStacks(combined1);
+
+ EXPECT_EQ(oneStack.GetStackCount(), 1u);
+ EXPECT_EQ(oneStack.GetStack(0).size(), testStacks[3].GetStackSize());
+
+ for (size_t i = 0; i < oneStack.GetStack(0).size(); ++i) {
+ const auto& expectedFrame = testStacks[3].GetFrame(i);
+ const auto& actualFrame = oneStack.GetStack(0)[i];
+
+ EXPECT_EQ(actualFrame.mOffset, expectedFrame.mOffset);
+
+ if (expectedFrame.mModIndex == std::numeric_limits<uint16_t>::max()) {
+ EXPECT_EQ(actualFrame.mModIndex, std::numeric_limits<uint16_t>::max());
+ } else {
+ EXPECT_EQ(oneStack.GetModule(actualFrame.mModIndex),
+ testStacks[3].GetModule(expectedFrame.mModIndex));
+ }
+ }
+}
diff --git a/toolkit/components/telemetry/tests/gtest/TestCounters.cpp b/toolkit/components/telemetry/tests/gtest/TestCounters.cpp
new file mode 100644
index 0000000000..79b4c66bb9
--- /dev/null
+++ b/toolkit/components/telemetry/tests/gtest/TestCounters.cpp
@@ -0,0 +1,173 @@
+/* vim:set ts=2 sw=2 sts=0 et: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+#include "gtest/gtest.h"
+#include "js/Conversions.h"
+#include "mozilla/Telemetry.h"
+#include "TelemetryFixture.h"
+#include "TelemetryTestHelpers.h"
+
+using namespace mozilla;
+using namespace TelemetryTestHelpers;
+
+TEST_F(TelemetryTestFixture, AutoCounter) {
+ const uint32_t kExpectedValue = 100;
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ const char* telemetryTestCountName =
+ Telemetry::GetHistogramName(Telemetry::TELEMETRY_TEST_COUNT);
+
+ GetAndClearHistogram(cx.GetJSContext(), mTelemetry, "TELEMETRY_TEST_COUNT"_ns,
+ false);
+
+ // Accumulate in the histogram
+ {
+ Telemetry::AutoCounter<Telemetry::TELEMETRY_TEST_COUNT> autoCounter;
+ autoCounter += kExpectedValue / 2;
+ }
+ // This counter should not accumulate since it does not go out of scope
+ Telemetry::AutoCounter<Telemetry::TELEMETRY_TEST_COUNT> autoCounter;
+ autoCounter += kExpectedValue;
+ // Accumulate a second time in the histogram
+ {
+ Telemetry::AutoCounter<Telemetry::TELEMETRY_TEST_COUNT> autoCounter;
+ autoCounter += kExpectedValue / 2;
+ }
+
+ // Get a snapshot for all the histograms
+ JS::Rooted<JS::Value> snapshot(cx.GetJSContext());
+ GetSnapshots(cx.GetJSContext(), mTelemetry, telemetryTestCountName, &snapshot,
+ false);
+
+ // Get the histogram from the snapshot
+ JS::Rooted<JS::Value> histogram(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), telemetryTestCountName, snapshot, &histogram);
+
+ // Get "sum" property from histogram
+ JS::Rooted<JS::Value> sum(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "sum", histogram, &sum);
+
+ // Check that the "sum" stored in the histogram matches with |kExpectedValue|
+ uint32_t uSum = 0;
+ JS::ToUint32(cx.GetJSContext(), sum, &uSum);
+ ASSERT_EQ(uSum, kExpectedValue)
+ << "The histogram is not returning expected value";
+}
+
+TEST_F(TelemetryTestFixture, AutoCounterUnderflow) {
+ const uint32_t kExpectedValue = 0;
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ const char* telemetryTestCountName =
+ Telemetry::GetHistogramName(Telemetry::TELEMETRY_TEST_COUNT);
+
+ GetAndClearHistogram(cx.GetJSContext(), mTelemetry, "TELEMETRY_TEST_COUNT"_ns,
+ false);
+
+ // Accumulate in the histogram
+ {
+ Telemetry::AutoCounter<Telemetry::TELEMETRY_TEST_COUNT> autoCounter;
+ autoCounter += -1;
+ }
+
+ // Get a snapshot for all the histograms
+ JS::Rooted<JS::Value> snapshot(cx.GetJSContext());
+ GetSnapshots(cx.GetJSContext(), mTelemetry, telemetryTestCountName, &snapshot,
+ false);
+
+ // Get the histogram from the snapshot
+ JS::Rooted<JS::Value> histogram(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), telemetryTestCountName, snapshot, &histogram);
+
+ // Get "sum" property from histogram
+ JS::Rooted<JS::Value> sum(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "sum", histogram, &sum);
+
+ // Check that the "sum" stored in the histogram matches with |kExpectedValue|
+ uint32_t uSum = 42;
+ JS::ToUint32(cx.GetJSContext(), sum, &uSum);
+ ASSERT_EQ(uSum, kExpectedValue)
+ << "The histogram is supposed to return 0 when an underflow occurs.";
+}
+
+TEST_F(TelemetryTestFixture, RuntimeAutoCounter) {
+ const uint32_t kExpectedValue = 100;
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ const char* telemetryTestCountName =
+ Telemetry::GetHistogramName(Telemetry::TELEMETRY_TEST_COUNT);
+
+ GetAndClearHistogram(cx.GetJSContext(), mTelemetry, "TELEMETRY_TEST_COUNT"_ns,
+ false);
+
+ // Accumulate in the histogram
+ {
+ Telemetry::RuntimeAutoCounter autoCounter(Telemetry::TELEMETRY_TEST_COUNT);
+ autoCounter += kExpectedValue / 2;
+ }
+ // This counter should not accumulate since it does not go out of scope
+ Telemetry::RuntimeAutoCounter autoCounter(Telemetry::TELEMETRY_TEST_COUNT);
+ autoCounter += kExpectedValue;
+ // Accumulate a second time in the histogram
+ {
+ Telemetry::RuntimeAutoCounter autoCounter(Telemetry::TELEMETRY_TEST_COUNT);
+ autoCounter += kExpectedValue / 2;
+ }
+ // Get a snapshot for all the histograms
+ JS::Rooted<JS::Value> snapshot(cx.GetJSContext());
+ GetSnapshots(cx.GetJSContext(), mTelemetry, telemetryTestCountName, &snapshot,
+ false);
+
+ // Get the histogram from the snapshot
+ JS::Rooted<JS::Value> histogram(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), telemetryTestCountName, snapshot, &histogram);
+
+ // Get "sum" property from histogram
+ JS::Rooted<JS::Value> sum(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "sum", histogram, &sum);
+
+ // Check that the "sum" stored in the histogram matches with |kExpectedValue|
+ uint32_t uSum = 0;
+ JS::ToUint32(cx.GetJSContext(), sum, &uSum);
+ ASSERT_EQ(uSum, kExpectedValue)
+ << "The histogram is not returning expected value";
+}
+
+TEST_F(TelemetryTestFixture, RuntimeAutoCounterUnderflow) {
+ const uint32_t kExpectedValue = 0;
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ const char* telemetryTestCountName =
+ Telemetry::GetHistogramName(Telemetry::TELEMETRY_TEST_COUNT);
+
+ GetAndClearHistogram(cx.GetJSContext(), mTelemetry, "TELEMETRY_TEST_COUNT"_ns,
+ false);
+
+ // Accumulate in the histogram
+ {
+ Telemetry::RuntimeAutoCounter autoCounter(Telemetry::TELEMETRY_TEST_COUNT,
+ kExpectedValue);
+ autoCounter += -1;
+ }
+
+ // Get a snapshot for all the histograms
+ JS::Rooted<JS::Value> snapshot(cx.GetJSContext());
+ GetSnapshots(cx.GetJSContext(), mTelemetry, telemetryTestCountName, &snapshot,
+ false);
+
+ // Get the histogram from the snapshot
+ JS::Rooted<JS::Value> histogram(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), telemetryTestCountName, snapshot, &histogram);
+
+ // Get "sum" property from histogram
+ JS::Rooted<JS::Value> sum(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "sum", histogram, &sum);
+
+ // Check that the "sum" stored in the histogram matches with |kExpectedValue|
+ uint32_t uSum = 42;
+ JS::ToUint32(cx.GetJSContext(), sum, &uSum);
+ ASSERT_EQ(uSum, kExpectedValue)
+ << "The histogram is supposed to return 0 when an underflow occurs.";
+}
diff --git a/toolkit/components/telemetry/tests/gtest/TestEvents.cpp b/toolkit/components/telemetry/tests/gtest/TestEvents.cpp
new file mode 100644
index 0000000000..be08f234a9
--- /dev/null
+++ b/toolkit/components/telemetry/tests/gtest/TestEvents.cpp
@@ -0,0 +1,115 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+#include "core/TelemetryEvent.h"
+#include "gtest/gtest.h"
+#include "js/Array.h" // JS::GetArrayLength
+#include "js/PropertyAndElement.h" // JS_GetElement, JS_GetProperty
+#include "mozilla/Maybe.h"
+#include "mozilla/Telemetry.h"
+#include "mozilla/Unused.h"
+#include "TelemetryFixture.h"
+#include "TelemetryTestHelpers.h"
+
+using namespace mozilla;
+using namespace TelemetryTestHelpers;
+
+// Test that we can properly record events using the C++ API.
+TEST_F(TelemetryTestFixture, RecordEventNative) {
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ // Make sure we don't get events from other tests.
+ Unused << mTelemetry->ClearEvents();
+
+ const nsLiteralCString category("telemetry.test");
+ const nsLiteralCString method("test1");
+ const nsLiteralCString method2("test2");
+ const nsLiteralCString object("object1");
+ const nsLiteralCString object2("object2");
+ const nsLiteralCString value("value");
+ const nsLiteralCString valueLong(
+ "this value is much too long and must be truncated to fit in the limit "
+ "which at time of writing was 80 bytes.");
+ const nsLiteralCString extraKey("key1");
+ const nsLiteralCString extraValue("extra value");
+ const nsLiteralCString extraValueLong(
+ "this extra value is much too long and must be truncated to fit in the "
+ "limit which at time of writing was 80 bytes.");
+
+ // Try recording before category's enabled.
+ Telemetry::RecordEvent(Telemetry::EventID::TelemetryTest_Test1_Object1,
+ Nothing(), Nothing());
+
+ // Ensure "telemetry.test" is enabled
+ Telemetry::SetEventRecordingEnabled(category, true);
+
+ // Try recording after it's enabled.
+ Telemetry::RecordEvent(Telemetry::EventID::TelemetryTest_Test2_Object1,
+ Nothing(), Nothing());
+
+ // Try recording with normal value, extra
+ CopyableTArray<EventExtraEntry> extra(
+ {EventExtraEntry{extraKey, extraValue}});
+ Telemetry::RecordEvent(Telemetry::EventID::TelemetryTest_Test1_Object2,
+ mozilla::Some(value), mozilla::Some(extra));
+
+ // Try recording with too-long value, extra
+ CopyableTArray<EventExtraEntry> longish(
+ {EventExtraEntry{extraKey, extraValueLong}});
+ Telemetry::RecordEvent(Telemetry::EventID::TelemetryTest_Test2_Object2,
+ mozilla::Some(valueLong), mozilla::Some(longish));
+
+ JS::Rooted<JS::Value> eventsSnapshot(cx.GetJSContext());
+ GetEventSnapshot(cx.GetJSContext(), &eventsSnapshot);
+
+ ASSERT_TRUE(!EventPresent(cx.GetJSContext(), eventsSnapshot, category, method,
+ object))
+ << "Test event must not be present when recorded before enabled.";
+ ASSERT_TRUE(EventPresent(cx.GetJSContext(), eventsSnapshot, category, method2,
+ object))
+ << "Test event must be present.";
+ ASSERT_TRUE(EventPresent(cx.GetJSContext(), eventsSnapshot, category, method,
+ object2))
+ << "Test event with value and extra must be present.";
+ ASSERT_TRUE(EventPresent(cx.GetJSContext(), eventsSnapshot, category, method2,
+ object2))
+ << "Test event with truncated value and extra must be present.";
+
+ // Ensure that the truncations happened appropriately.
+ JSContext* aCx = cx.GetJSContext();
+ JS::Rooted<JSObject*> arrayObj(aCx, &eventsSnapshot.toObject());
+ JS::Rooted<JS::Value> eventRecord(aCx);
+ ASSERT_TRUE(JS_GetElement(aCx, arrayObj, 2, &eventRecord))
+ << "Must be able to get record.";
+ JS::Rooted<JSObject*> recordArray(aCx, &eventRecord.toObject());
+ uint32_t recordLength;
+ ASSERT_TRUE(JS::GetArrayLength(aCx, recordArray, &recordLength))
+ << "Event record array must have length.";
+ ASSERT_TRUE(recordLength == 6)
+ << "Event record must have 6 elements.";
+
+ JS::Rooted<JS::Value> str(aCx);
+ nsAutoJSString jsStr;
+ // The value string is at index 4
+ ASSERT_TRUE(JS_GetElement(aCx, recordArray, 4, &str))
+ << "Must be able to get value.";
+ ASSERT_TRUE(jsStr.init(aCx, str))
+ << "Value must be able to be init'd to a jsstring.";
+ ASSERT_EQ(NS_ConvertUTF16toUTF8(jsStr).Length(), (uint32_t)80)
+ << "Value must have been truncated to 80 bytes.";
+
+ // Extra is at index 5
+ JS::Rooted<JS::Value> obj(aCx);
+ ASSERT_TRUE(JS_GetElement(aCx, recordArray, 5, &obj))
+ << "Must be able to get extra.";
+ JS::Rooted<JSObject*> extraObj(aCx, &obj.toObject());
+ JS::Rooted<JS::Value> extraVal(aCx);
+ ASSERT_TRUE(JS_GetProperty(aCx, extraObj, extraKey.get(), &extraVal))
+ << "Must be able to get the extra key's value.";
+ ASSERT_TRUE(jsStr.init(aCx, extraVal))
+ << "Extra must be able to be init'd to a jsstring.";
+ ASSERT_EQ(NS_ConvertUTF16toUTF8(jsStr).Length(), (uint32_t)80)
+ << "Extra must have been truncated to 80 bytes.";
+}
diff --git a/toolkit/components/telemetry/tests/gtest/TestHistograms.cpp b/toolkit/components/telemetry/tests/gtest/TestHistograms.cpp
new file mode 100644
index 0000000000..28b6cce751
--- /dev/null
+++ b/toolkit/components/telemetry/tests/gtest/TestHistograms.cpp
@@ -0,0 +1,891 @@
+/* vim:set ts=2 sw=2 sts=0 et: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+#include "gtest/gtest.h"
+#include "js/Conversions.h"
+#include "mozilla/Telemetry.h"
+#include "TelemetryFixture.h"
+#include "TelemetryTestHelpers.h"
+
+using namespace mozilla;
+using namespace TelemetryTestHelpers;
+
+TEST_F(TelemetryTestFixture, AccumulateCountHistogram) {
+ const uint32_t kExpectedValue = 200;
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ const char* telemetryTestCountName =
+ Telemetry::GetHistogramName(Telemetry::TELEMETRY_TEST_COUNT);
+ ASSERT_STREQ(telemetryTestCountName, "TELEMETRY_TEST_COUNT")
+ << "The histogram name is wrong";
+
+ GetAndClearHistogram(cx.GetJSContext(), mTelemetry, "TELEMETRY_TEST_COUNT"_ns,
+ false);
+
+ // Accumulate in the histogram
+ Telemetry::Accumulate(Telemetry::TELEMETRY_TEST_COUNT, kExpectedValue / 2);
+ Telemetry::Accumulate("TELEMETRY_TEST_COUNT", kExpectedValue / 2);
+
+ // Get a snapshot for all the histograms
+ JS::Rooted<JS::Value> snapshot(cx.GetJSContext());
+ GetSnapshots(cx.GetJSContext(), mTelemetry, telemetryTestCountName, &snapshot,
+ false);
+
+ // Get the histogram from the snapshot
+ JS::Rooted<JS::Value> histogram(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), telemetryTestCountName, snapshot, &histogram);
+
+ // Get "sum" property from histogram
+ JS::Rooted<JS::Value> sum(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "sum", histogram, &sum);
+
+ // Check that the "sum" stored in the histogram matches with |kExpectedValue|
+ uint32_t uSum = 0;
+ JS::ToUint32(cx.GetJSContext(), sum, &uSum);
+ ASSERT_EQ(uSum, kExpectedValue)
+ << "The histogram is not returning expected value";
+}
+
+TEST_F(TelemetryTestFixture, AccumulateKeyedCountHistogram) {
+ const uint32_t kExpectedValue = 100;
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ GetAndClearHistogram(cx.GetJSContext(), mTelemetry,
+ "TELEMETRY_TEST_KEYED_COUNT"_ns, true);
+
+ // Accumulate data in the provided key within the histogram
+ Telemetry::Accumulate(Telemetry::TELEMETRY_TEST_KEYED_COUNT, "sample"_ns,
+ kExpectedValue);
+
+ // Get a snapshot for all the histograms
+ JS::Rooted<JS::Value> snapshot(cx.GetJSContext());
+ GetSnapshots(cx.GetJSContext(), mTelemetry, "TELEMETRY_TEST_KEYED_COUNT",
+ &snapshot, true);
+
+ // Get the histogram from the snapshot
+ JS::Rooted<JS::Value> histogram(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "TELEMETRY_TEST_KEYED_COUNT", snapshot,
+ &histogram);
+
+ // Get "sample" property from histogram
+ JS::Rooted<JS::Value> expectedKeyData(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "sample", histogram, &expectedKeyData);
+
+ // Get "sum" property from keyed data
+ JS::Rooted<JS::Value> sum(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "sum", expectedKeyData, &sum);
+
+ // Check that the sum stored in the histogram matches with |kExpectedValue|
+ uint32_t uSum = 0;
+ JS::ToUint32(cx.GetJSContext(), sum, &uSum);
+ ASSERT_EQ(uSum, kExpectedValue)
+ << "The histogram is not returning expected sum";
+}
+
+TEST_F(TelemetryTestFixture, TestKeyedKeysHistogram) {
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ JS::Rooted<JS::Value> testHistogram(cx.GetJSContext());
+ JS::Rooted<JS::Value> rval(cx.GetJSContext());
+
+ GetAndClearHistogram(cx.GetJSContext(), mTelemetry,
+ "TELEMETRY_TEST_KEYED_KEYS"_ns, true);
+
+ // Test the accumulation on both the allowed and unallowed keys, using
+ // the API that accepts histogram IDs.
+ Telemetry::Accumulate(Telemetry::TELEMETRY_TEST_KEYED_KEYS, "not-allowed"_ns,
+ 1);
+ Telemetry::Accumulate(Telemetry::TELEMETRY_TEST_KEYED_KEYS, "testkey"_ns, 0);
+ // Do the same, using the API that accepts the histogram name as a string.
+ Telemetry::Accumulate("TELEMETRY_TEST_KEYED_KEYS", "not-allowed"_ns, 1);
+ Telemetry::Accumulate("TELEMETRY_TEST_KEYED_KEYS", "CommonKey"_ns, 1);
+
+ // Get a snapshot for all the histograms
+ JS::Rooted<JS::Value> snapshot(cx.GetJSContext());
+ GetSnapshots(cx.GetJSContext(), mTelemetry, "TELEMETRY_TEST_KEYED_KEYS",
+ &snapshot, true);
+
+ // Get the histogram from the snapshot
+ JS::Rooted<JS::Value> histogram(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "TELEMETRY_TEST_KEYED_KEYS", snapshot,
+ &histogram);
+
+ // Get "testkey" property from histogram and check that it stores the correct
+ // data.
+ JS::Rooted<JS::Value> expectedKeyData(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "testkey", histogram, &expectedKeyData);
+ ASSERT_TRUE(!expectedKeyData.isUndefined())
+ << "Cannot find the expected key in the histogram data";
+ JS::Rooted<JS::Value> sum(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "sum", expectedKeyData, &sum);
+ uint32_t uSum = 0;
+ JS::ToUint32(cx.GetJSContext(), sum, &uSum);
+ ASSERT_EQ(uSum, 0U)
+ << "The histogram is not returning expected sum for 'testkey'";
+
+ // Do the same for the "CommonKey" property.
+ GetProperty(cx.GetJSContext(), "CommonKey", histogram, &expectedKeyData);
+ ASSERT_TRUE(!expectedKeyData.isUndefined())
+ << "Cannot find the expected key in the histogram data";
+ GetProperty(cx.GetJSContext(), "sum", expectedKeyData, &sum);
+ JS::ToUint32(cx.GetJSContext(), sum, &uSum);
+ ASSERT_EQ(uSum, 1U)
+ << "The histogram is not returning expected sum for 'CommonKey'";
+
+ GetProperty(cx.GetJSContext(), "not-allowed", histogram, &expectedKeyData);
+ ASSERT_TRUE(expectedKeyData.isUndefined())
+ << "Unallowed keys must not be recorded in the histogram data";
+
+ // The 'not-allowed' key accumulation for 'TELEMETRY_TESTED_KEYED_KEYS' was
+ // attemtped twice, so we expect the count of
+ // 'telemetry.accumulate_unknown_histogram_keys' to be 2
+ const uint32_t expectedAccumulateUnknownCount = 2;
+ JS::Rooted<JS::Value> scalarsSnapshot(cx.GetJSContext());
+ GetScalarsSnapshot(true, cx.GetJSContext(), &scalarsSnapshot);
+ CheckKeyedUintScalar("telemetry.accumulate_unknown_histogram_keys",
+ "TELEMETRY_TEST_KEYED_KEYS", cx.GetJSContext(),
+ scalarsSnapshot, expectedAccumulateUnknownCount);
+}
+
+TEST_F(TelemetryTestFixture, AccumulateCategoricalHistogram) {
+ const uint32_t kExpectedValue = 2;
+
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ GetAndClearHistogram(cx.GetJSContext(), mTelemetry,
+ "TELEMETRY_TEST_CATEGORICAL"_ns, false);
+
+ // Accumulate one unit into the categorical histogram with label
+ // Telemetry::LABELS_TELEMETRY_TEST_CATEGORICAL::CommonLabel
+ Telemetry::AccumulateCategorical(
+ Telemetry::LABELS_TELEMETRY_TEST_CATEGORICAL::CommonLabel);
+
+ // Accumulate another unit into the same categorical histogram using a string
+ // label
+ Telemetry::AccumulateCategorical(Telemetry::TELEMETRY_TEST_CATEGORICAL,
+ "CommonLabel"_ns);
+
+ // Get a snapshot for all the histograms
+ JS::Rooted<JS::Value> snapshot(cx.GetJSContext());
+ GetSnapshots(cx.GetJSContext(), mTelemetry, "TELEMETRY_TEST_CATEGORICAL",
+ &snapshot, false);
+
+ // Get our histogram from the snapshot
+ JS::Rooted<JS::Value> histogram(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "TELEMETRY_TEST_CATEGORICAL", snapshot,
+ &histogram);
+
+ // Get values object from histogram. Each entry in the object maps to a label
+ // in the histogram.
+ JS::Rooted<JS::Value> values(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "values", histogram, &values);
+
+ // Get the value for the label we care about
+ JS::Rooted<JS::Value> value(cx.GetJSContext());
+ GetElement(cx.GetJSContext(),
+ static_cast<uint32_t>(
+ Telemetry::LABELS_TELEMETRY_TEST_CATEGORICAL::CommonLabel),
+ values, &value);
+
+ // Check that the value stored in the histogram matches with |kExpectedValue|
+ uint32_t uValue = 0;
+ JS::ToUint32(cx.GetJSContext(), value, &uValue);
+ ASSERT_EQ(uValue, kExpectedValue)
+ << "The histogram is not returning expected value";
+}
+
+TEST_F(TelemetryTestFixture, AccumulateKeyedCategoricalHistogram) {
+ const uint32_t kSampleExpectedValue = 2;
+ const uint32_t kOtherSampleExpectedValue = 1;
+
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ GetAndClearHistogram(cx.GetJSContext(), mTelemetry,
+ "TELEMETRY_TEST_KEYED_CATEGORICAL"_ns, true);
+
+ // Accumulate one unit into the categorical histogram with label
+ // Telemetry::LABELS_TELEMETRY_TEST_KEYED_CATEGORICAL::CommonLabel
+ Telemetry::AccumulateCategoricalKeyed(
+ "sample"_ns,
+ Telemetry::LABELS_TELEMETRY_TEST_KEYED_CATEGORICAL::CommonLabel);
+ // Accumulate another unit into the same categorical histogram
+ Telemetry::AccumulateCategoricalKeyed(
+ "sample"_ns,
+ Telemetry::LABELS_TELEMETRY_TEST_KEYED_CATEGORICAL::CommonLabel);
+ // Accumulate another unit into a different categorical histogram
+ Telemetry::AccumulateCategoricalKeyed(
+ "other-sample"_ns,
+ Telemetry::LABELS_TELEMETRY_TEST_KEYED_CATEGORICAL::CommonLabel);
+
+ // Get a snapshot for all the histograms
+ JS::Rooted<JS::Value> snapshot(cx.GetJSContext());
+ GetSnapshots(cx.GetJSContext(), mTelemetry,
+ "TELEMETRY_TEST_KEYED_CATEGORICAL", &snapshot, true);
+ // Get the histogram from the snapshot
+ JS::Rooted<JS::Value> histogram(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "TELEMETRY_TEST_KEYED_CATEGORICAL", snapshot,
+ &histogram);
+
+ // Check that the sample histogram contains the values we expect
+ JS::Rooted<JS::Value> sample(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "sample", histogram, &sample);
+ // Get values object from sample. Each entry in the object maps to a label in
+ // the histogram.
+ JS::Rooted<JS::Value> sampleValues(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "values", sample, &sampleValues);
+ // Get the value for the label we care about
+ JS::Rooted<JS::Value> sampleValue(cx.GetJSContext());
+ GetElement(
+ cx.GetJSContext(),
+ static_cast<uint32_t>(
+ Telemetry::LABELS_TELEMETRY_TEST_KEYED_CATEGORICAL::CommonLabel),
+ sampleValues, &sampleValue);
+ // Check that the value stored in the histogram matches with
+ // |kSampleExpectedValue|
+ uint32_t uSampleValue = 0;
+ JS::ToUint32(cx.GetJSContext(), sampleValue, &uSampleValue);
+ ASSERT_EQ(uSampleValue, kSampleExpectedValue)
+ << "The sample histogram is not returning expected value";
+
+ // Check that the other-sample histogram contains the values we expect
+ JS::Rooted<JS::Value> otherSample(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "other-sample", histogram, &otherSample);
+ // Get values object from the other-sample. Each entry in the object maps to a
+ // label in the histogram.
+ JS::Rooted<JS::Value> otherValues(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "values", otherSample, &otherValues);
+ // Get the value for the label we care about
+ JS::Rooted<JS::Value> otherValue(cx.GetJSContext());
+ GetElement(
+ cx.GetJSContext(),
+ static_cast<uint32_t>(
+ Telemetry::LABELS_TELEMETRY_TEST_KEYED_CATEGORICAL::CommonLabel),
+ otherValues, &otherValue);
+ // Check that the value stored in the histogram matches with
+ // |kOtherSampleExpectedValue|
+ uint32_t uOtherValue = 0;
+ JS::ToUint32(cx.GetJSContext(), otherValue, &uOtherValue);
+ ASSERT_EQ(uOtherValue, kOtherSampleExpectedValue)
+ << "The other-sample histogram is not returning expected value";
+}
+
+TEST_F(TelemetryTestFixture, AccumulateCountHistogram_MultipleSamples) {
+ nsTArray<uint32_t> samples({4, 4, 4});
+ const uint32_t kExpectedSum = 12;
+
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ GetAndClearHistogram(cx.GetJSContext(), mTelemetry, "TELEMETRY_TEST_COUNT"_ns,
+ false);
+
+ // Accumulate in histogram
+ Telemetry::Accumulate(Telemetry::TELEMETRY_TEST_COUNT, samples);
+
+ // Get a snapshot of all the histograms
+ JS::Rooted<JS::Value> snapshot(cx.GetJSContext());
+ GetSnapshots(cx.GetJSContext(), mTelemetry, "TELEMETRY_TEST_COUNT", &snapshot,
+ false);
+
+ // Get histogram from snapshot
+ JS::Rooted<JS::Value> histogram(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "TELEMETRY_TEST_COUNT", snapshot, &histogram);
+
+ // Get "sum" from histogram
+ JS::Rooted<JS::Value> sum(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "sum", histogram, &sum);
+
+ // Check that sum matches with aValue
+ uint32_t uSum = 0;
+ JS::ToUint32(cx.GetJSContext(), sum, &uSum);
+ ASSERT_EQ(uSum, kExpectedSum)
+ << "This histogram is not returning expected value";
+}
+
+TEST_F(TelemetryTestFixture, AccumulateLinearHistogram_MultipleSamples) {
+ nsTArray<uint32_t> samples({4, 4, 4});
+ const uint32_t kExpectedCount = 3;
+
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ GetAndClearHistogram(cx.GetJSContext(), mTelemetry,
+ "TELEMETRY_TEST_LINEAR"_ns, false);
+
+ // Accumulate in the histogram
+ Telemetry::Accumulate(Telemetry::TELEMETRY_TEST_LINEAR, samples);
+
+ // Get a snapshot of all the histograms
+ JS::Rooted<JS::Value> snapshot(cx.GetJSContext());
+ GetSnapshots(cx.GetJSContext(), mTelemetry, "TELEMETRY_TEST_LINEAR",
+ &snapshot, false);
+
+ // Get histogram from snapshot
+ JS::Rooted<JS::Value> histogram(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "TELEMETRY_TEST_LINEAR", snapshot, &histogram);
+
+ // Get "values" object from histogram
+ JS::Rooted<JS::Value> values(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "values", histogram, &values);
+
+ // Index 0 is only for values less than 'low'. Values within range start at
+ // index 1
+ JS::Rooted<JS::Value> count(cx.GetJSContext());
+ const uint32_t index = 1;
+ GetElement(cx.GetJSContext(), index, values, &count);
+
+ // Check that this count matches with nSamples
+ uint32_t uCount = 0;
+ JS::ToUint32(cx.GetJSContext(), count, &uCount);
+ ASSERT_EQ(uCount, kExpectedCount)
+ << "The histogram did not accumulate the correct number of values";
+}
+
+TEST_F(TelemetryTestFixture, AccumulateLinearHistogram_DifferentSamples) {
+ nsTArray<uint32_t> samples(
+ {4, 8, 2147483646, uint32_t(INT_MAX) + 1, UINT32_MAX});
+
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ mTelemetry->ClearScalars();
+ GetAndClearHistogram(cx.GetJSContext(), mTelemetry,
+ "TELEMETRY_TEST_LINEAR"_ns, false);
+
+ // Accumulate in histogram
+ Telemetry::Accumulate(Telemetry::TELEMETRY_TEST_LINEAR, samples);
+
+ // Get a snapshot of all histograms
+ JS::Rooted<JS::Value> snapshot(cx.GetJSContext());
+ GetSnapshots(cx.GetJSContext(), mTelemetry, "TELEMETRY_TEST_LINEAR",
+ &snapshot, false);
+
+ // Get histogram from snapshot
+ JS::Rooted<JS::Value> histogram(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "TELEMETRY_TEST_LINEAR", snapshot, &histogram);
+
+ // Get values object from histogram
+ JS::Rooted<JS::Value> values(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "values", histogram, &values);
+
+ // Get values in first and last buckets
+ JS::Rooted<JS::Value> countFirst(cx.GetJSContext());
+ JS::Rooted<JS::Value> countLast(cx.GetJSContext());
+ const uint32_t firstIndex = 1;
+ // Buckets are indexed by their start value
+ const uint32_t lastIndex = INT32_MAX - 1;
+ GetElement(cx.GetJSContext(), firstIndex, values, &countFirst);
+ GetElement(cx.GetJSContext(), lastIndex, values, &countLast);
+
+ // Check that the values match
+ uint32_t uCountFirst = 0;
+ uint32_t uCountLast = 0;
+ JS::ToUint32(cx.GetJSContext(), countFirst, &uCountFirst);
+ JS::ToUint32(cx.GetJSContext(), countLast, &uCountLast);
+
+ const uint32_t kExpectedCountFirst = 2;
+ // We expect 2147483646 to be in the last bucket, as well the two samples
+ // above 2^31 (prior to bug 1438335, values between INT_MAX and UINT32_MAX
+ // would end up as 0s)
+ const uint32_t kExpectedCountLast = 3;
+ ASSERT_EQ(uCountFirst, kExpectedCountFirst)
+ << "The first bucket did not accumulate the correct number of values";
+ ASSERT_EQ(uCountLast, kExpectedCountLast)
+ << "The last bucket did not accumulate the correct number of values";
+
+ // We accumulated two values that had to be clamped. We expect the count in
+ // 'telemetry.accumulate_clamped_values' to be 2 (only one storage).
+ const uint32_t expectedAccumulateClampedCount = 2;
+ JS::Rooted<JS::Value> scalarsSnapshot(cx.GetJSContext());
+ GetScalarsSnapshot(true, cx.GetJSContext(), &scalarsSnapshot);
+ CheckKeyedUintScalar("telemetry.accumulate_clamped_values",
+ "TELEMETRY_TEST_LINEAR", cx.GetJSContext(),
+ scalarsSnapshot, expectedAccumulateClampedCount);
+}
+
+TEST_F(TelemetryTestFixture, AccumulateKeyedCountHistogram_MultipleSamples) {
+ const nsTArray<uint32_t> samples({5, 10, 15});
+ const uint32_t kExpectedSum = 5 + 10 + 15;
+
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ GetAndClearHistogram(cx.GetJSContext(), mTelemetry,
+ "TELEMETRY_TEST_KEYED_COUNT"_ns, true);
+
+ // Accumulate data in the provided key within the histogram
+ Telemetry::Accumulate(Telemetry::TELEMETRY_TEST_KEYED_COUNT, "sample"_ns,
+ samples);
+
+ // Get a snapshot for all the histograms
+ JS::Rooted<JS::Value> snapshot(cx.GetJSContext());
+ GetSnapshots(cx.GetJSContext(), mTelemetry, "TELEMETRY_TEST_KEYED_COUNT",
+ &snapshot, true);
+
+ // Get the histogram from the snapshot
+ JS::Rooted<JS::Value> histogram(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "TELEMETRY_TEST_KEYED_COUNT", snapshot,
+ &histogram);
+
+ // Get "sample" property from histogram
+ JS::Rooted<JS::Value> expectedKeyData(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "sample", histogram, &expectedKeyData);
+
+ // Get "sum" property from keyed data
+ JS::Rooted<JS::Value> sum(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "sum", expectedKeyData, &sum);
+
+ // Check that the sum stored in the histogram matches with |kExpectedSum|
+ uint32_t uSum = 0;
+ JS::ToUint32(cx.GetJSContext(), sum, &uSum);
+ ASSERT_EQ(uSum, kExpectedSum)
+ << "The histogram is not returning expected sum";
+}
+
+TEST_F(TelemetryTestFixture, TestKeyedLinearHistogram_MultipleSamples) {
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ mTelemetry->ClearScalars();
+ GetAndClearHistogram(cx.GetJSContext(), mTelemetry,
+ "TELEMETRY_TEST_KEYED_LINEAR"_ns, true);
+
+ const nsTArray<uint32_t> samples({1, 5, 250000, UINT_MAX});
+ // Test the accumulation on the key 'testkey', using
+ // the API that accepts histogram IDs.
+ Telemetry::Accumulate(Telemetry::TELEMETRY_TEST_KEYED_LINEAR, "testkey"_ns,
+ samples);
+
+ // Get a snapshot for all the histograms
+ JS::Rooted<JS::Value> snapshot(cx.GetJSContext());
+ GetSnapshots(cx.GetJSContext(), mTelemetry, "TELEMETRY_TEST_KEYED_LINEAR",
+ &snapshot, true);
+
+ // Get the histogram from the snapshot
+ JS::Rooted<JS::Value> histogram(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "TELEMETRY_TEST_KEYED_LINEAR", snapshot,
+ &histogram);
+
+ // Get "testkey" property from histogram.
+ JS::Rooted<JS::Value> expectedKeyData(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "testkey", histogram, &expectedKeyData);
+ ASSERT_TRUE(!expectedKeyData.isUndefined())
+ << "Cannot find the expected key in the histogram data";
+
+ // Get values object from 'testkey' histogram.
+ JS::Rooted<JS::Value> values(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "values", expectedKeyData, &values);
+
+ // Get values in first and last buckets.
+ JS::Rooted<JS::Value> countFirst(cx.GetJSContext());
+ JS::Rooted<JS::Value> countLast(cx.GetJSContext());
+ const uint32_t firstIndex = 1;
+ // Buckets are indexed by their start value
+ const uint32_t lastIndex = 250000;
+ GetElement(cx.GetJSContext(), firstIndex, values, &countFirst);
+ GetElement(cx.GetJSContext(), lastIndex, values, &countLast);
+
+ // Check that the values match.
+ uint32_t uCountFirst = 0;
+ uint32_t uCountLast = 0;
+ JS::ToUint32(cx.GetJSContext(), countFirst, &uCountFirst);
+ JS::ToUint32(cx.GetJSContext(), countLast, &uCountLast);
+
+ const uint32_t kExpectedCountFirst = 2;
+ const uint32_t kExpectedCountLast = 2;
+ ASSERT_EQ(uCountFirst, kExpectedCountFirst)
+ << "The first bucket did not accumulate the correct number of values for "
+ "key 'testkey'";
+ ASSERT_EQ(uCountLast, kExpectedCountLast)
+ << "The last bucket did not accumulate the correct number of values for "
+ "key 'testkey'";
+
+ // We accumulated one keyed values that had to be clamped. We expect the
+ // count in 'telemetry.accumulate_clamped_values' to be 1
+ const uint32_t expectedAccumulateClampedCount = 1;
+ JS::Rooted<JS::Value> scalarsSnapshot(cx.GetJSContext());
+ GetScalarsSnapshot(true, cx.GetJSContext(), &scalarsSnapshot);
+ CheckKeyedUintScalar("telemetry.accumulate_clamped_values",
+ "TELEMETRY_TEST_KEYED_LINEAR", cx.GetJSContext(),
+ scalarsSnapshot, expectedAccumulateClampedCount);
+}
+
+TEST_F(TelemetryTestFixture, TestKeyedKeysHistogram_MultipleSamples) {
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+ mTelemetry->ClearScalars();
+ const nsTArray<uint32_t> samples({false, false, true, 32, true});
+
+ GetAndClearHistogram(cx.GetJSContext(), mTelemetry,
+ "TELEMETRY_TEST_KEYED_KEYS"_ns, true);
+
+ // Test the accumulation on both the allowed and unallowed keys, using
+ // the API that accepts histogram IDs.
+ Telemetry::Accumulate(Telemetry::TELEMETRY_TEST_KEYED_KEYS, "not-allowed"_ns,
+ samples);
+ Telemetry::Accumulate(Telemetry::TELEMETRY_TEST_KEYED_KEYS, "testkey"_ns,
+ samples);
+
+ // Get a snapshot for all the histograms
+ JS::Rooted<JS::Value> snapshot(cx.GetJSContext());
+ GetSnapshots(cx.GetJSContext(), mTelemetry, "TELEMETRY_TEST_KEYED_KEYS",
+ &snapshot, true);
+
+ // Get the histogram from the snapshot
+ JS::Rooted<JS::Value> histogram(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "TELEMETRY_TEST_KEYED_KEYS", snapshot,
+ &histogram);
+
+ // Get "testkey" property from histogram and check that it stores the correct
+ // data.
+ JS::Rooted<JS::Value> testKeyData(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "testkey", histogram, &testKeyData);
+ ASSERT_TRUE(!testKeyData.isUndefined())
+ << "Cannot find the key 'testkey' in the histogram data";
+
+ JS::Rooted<JS::Value> values(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "values", testKeyData, &values);
+
+ // Get values in buckets 0,1,2
+ const uint32_t falseIndex = 0;
+ const uint32_t trueIndex = 1;
+ const uint32_t otherIndex = 2;
+
+ JS::Rooted<JS::Value> countFalse(cx.GetJSContext());
+ JS::Rooted<JS::Value> countTrue(cx.GetJSContext());
+ JS::Rooted<JS::Value> countOther(cx.GetJSContext());
+
+ GetElement(cx.GetJSContext(), falseIndex, values, &countFalse);
+ GetElement(cx.GetJSContext(), trueIndex, values, &countTrue);
+ GetElement(cx.GetJSContext(), otherIndex, values, &countOther);
+
+ uint32_t uCountFalse = 0;
+ uint32_t uCountTrue = 0;
+ uint32_t uCountOther = 0;
+ JS::ToUint32(cx.GetJSContext(), countFalse, &uCountFalse);
+ JS::ToUint32(cx.GetJSContext(), countTrue, &uCountTrue);
+ JS::ToUint32(cx.GetJSContext(), countOther, &uCountOther);
+
+ const uint32_t kExpectedCountFalse = 2;
+ const uint32_t kExpectedCountTrue = 3;
+ const uint32_t kExpectedCountOther = 0;
+
+ ASSERT_EQ(uCountFalse, kExpectedCountFalse)
+ << "The histogram did not accumulate the correct number of 'false' "
+ "booleans for key 'testkey'";
+ ASSERT_EQ(uCountTrue, kExpectedCountTrue)
+ << "The histogram did not accumulate the correct number of 'true' "
+ "booleans for key 'testkey'";
+ ASSERT_EQ(uCountOther, kExpectedCountOther)
+ << "The histogram did not accumulate the correct number of undefined "
+ "values for key 'testkey'";
+
+ // Here we check that we are not accumulating to a different (but still
+ // 'allowed') key. Get "CommonKey" property from histogram and check that it
+ // has no data. Since we accumulated no data to it, commonKeyData should be
+ // undefined.
+ JS::Rooted<JS::Value> commonKeyData(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "CommonKey", histogram, &commonKeyData);
+ ASSERT_TRUE(commonKeyData.isUndefined())
+ << "Found data in key 'CommonKey' even though we accumulated no data to "
+ "it";
+
+ // Here we check that our function does not allow accumulation into unallowed
+ // keys. Get 'not-allowed' property from histogram and check that this also
+ // has no data. This should contain no data because this key is not allowed.
+ JS::Rooted<JS::Value> notAllowedKeyData(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "not-allowed", histogram, &notAllowedKeyData);
+ ASSERT_TRUE(notAllowedKeyData.isUndefined())
+ << "Found data in key 'not-allowed' even though accumuling data to it is "
+ "not allowed";
+
+ // The 'not-allowed' key accumulation for 'TELEMETRY_TESTED_KEYED_KEYS' was
+ // attemtped once, so we expect the count of
+ // 'telemetry.accumulate_unknown_histogram_keys' to be 1
+ const uint32_t expectedAccumulateUnknownCount = 1;
+ JS::Rooted<JS::Value> scalarsSnapshot(cx.GetJSContext());
+ GetScalarsSnapshot(true, cx.GetJSContext(), &scalarsSnapshot);
+ CheckKeyedUintScalar("telemetry.accumulate_unknown_histogram_keys",
+ "TELEMETRY_TEST_KEYED_KEYS", cx.GetJSContext(),
+ scalarsSnapshot, expectedAccumulateUnknownCount);
+}
+
+TEST_F(TelemetryTestFixture,
+ AccumulateCategoricalHistogram_MultipleStringLabels) {
+ const uint32_t kExpectedValue = 2;
+ const nsTArray<nsCString> labels({"CommonLabel"_ns, "CommonLabel"_ns});
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ GetAndClearHistogram(cx.GetJSContext(), mTelemetry,
+ "TELEMETRY_TEST_CATEGORICAL"_ns, false);
+
+ // Accumulate the units into a categorical histogram using a string label
+ Telemetry::AccumulateCategorical(Telemetry::TELEMETRY_TEST_CATEGORICAL,
+ labels);
+
+ // Get a snapshot for all the histograms
+ JS::Rooted<JS::Value> snapshot(cx.GetJSContext());
+ GetSnapshots(cx.GetJSContext(), mTelemetry, "TELEMETRY_TEST_CATEGORICAL",
+ &snapshot, false);
+
+ // Get our histogram from the snapshot
+ JS::Rooted<JS::Value> histogram(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "TELEMETRY_TEST_CATEGORICAL", snapshot,
+ &histogram);
+
+ // Get values object from histogram. Each entry in the object maps to a label
+ // in the histogram.
+ JS::Rooted<JS::Value> values(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "values", histogram, &values);
+
+ // Get the value for the label we care about
+ JS::Rooted<JS::Value> value(cx.GetJSContext());
+ GetElement(cx.GetJSContext(),
+ static_cast<uint32_t>(
+ Telemetry::LABELS_TELEMETRY_TEST_CATEGORICAL::CommonLabel),
+ values, &value);
+
+ // Check that the value stored in the histogram matches with |kExpectedValue|
+ uint32_t uValue = 0;
+ JS::ToUint32(cx.GetJSContext(), value, &uValue);
+ ASSERT_EQ(uValue, kExpectedValue)
+ << "The histogram is not returning expected value";
+
+ // Now we check for no accumulation when a bad label is present in the array.
+ //
+ // The 'values' property is not initialized unless data is accumulated so
+ // keeping another test to check for this case alone is wasteful as we will
+ // have to accumulate some data anyway.
+
+ const nsTArray<nsCString> badLabelArray({"CommonLabel"_ns, "BadLabel"_ns});
+
+ // Try to accumulate the array into the histogram.
+ Telemetry::AccumulateCategorical(Telemetry::TELEMETRY_TEST_CATEGORICAL,
+ badLabelArray);
+
+ // Get snapshot of all the histograms
+ GetSnapshots(cx.GetJSContext(), mTelemetry, "TELEMETRY_TEST_CATEGORICAL",
+ &snapshot, false);
+
+ // Get our histogram from the snapshot
+ GetProperty(cx.GetJSContext(), "TELEMETRY_TEST_CATEGORICAL", snapshot,
+ &histogram);
+
+ // Get values array from histogram
+ GetProperty(cx.GetJSContext(), "values", histogram, &values);
+
+ // Get the value for the label we care about
+ GetElement(cx.GetJSContext(),
+ static_cast<uint32_t>(
+ Telemetry::LABELS_TELEMETRY_TEST_CATEGORICAL::CommonLabel),
+ values, &value);
+
+ // Check that the value stored in the histogram matches with |kExpectedValue|
+ uValue = 0;
+ JS::ToUint32(cx.GetJSContext(), value, &uValue);
+ ASSERT_EQ(uValue, kExpectedValue)
+ << "The histogram accumulated data when it should not have";
+}
+
+TEST_F(TelemetryTestFixture,
+ AccumulateCategoricalHistogram_MultipleEnumValues) {
+ const uint32_t kExpectedValue = 2;
+ const nsTArray<Telemetry::LABELS_TELEMETRY_TEST_CATEGORICAL> enumLabels(
+ {Telemetry::LABELS_TELEMETRY_TEST_CATEGORICAL::CommonLabel,
+ Telemetry::LABELS_TELEMETRY_TEST_CATEGORICAL::CommonLabel});
+
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ GetAndClearHistogram(cx.GetJSContext(), mTelemetry,
+ "TELEMETRY_TEST_CATEGORICAL"_ns, false);
+
+ // Accumulate the units into a categorical histogram using the enumLabels
+ // array
+ Telemetry::AccumulateCategorical<
+ Telemetry::LABELS_TELEMETRY_TEST_CATEGORICAL>(enumLabels);
+
+ // Get a snapshot for all the histograms
+ JS::Rooted<JS::Value> snapshot(cx.GetJSContext());
+ GetSnapshots(cx.GetJSContext(), mTelemetry, "TELEMETRY_TEST_CATEGORICAL",
+ &snapshot, false);
+
+ // Get our histogram from the snapshot
+ JS::Rooted<JS::Value> histogram(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "TELEMETRY_TEST_CATEGORICAL", snapshot,
+ &histogram);
+
+ // Get values object from histogram. Each entry in the object maps to a label
+ // in the histogram.
+ JS::Rooted<JS::Value> values(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "values", histogram, &values);
+
+ // Get the value for the label we care about
+ JS::Rooted<JS::Value> value(cx.GetJSContext());
+ GetElement(cx.GetJSContext(),
+ static_cast<uint32_t>(
+ Telemetry::LABELS_TELEMETRY_TEST_CATEGORICAL::CommonLabel),
+ values, &value);
+
+ // Check that the value stored in the histogram matches with |kExpectedValue|
+ uint32_t uValue = 0;
+ JS::ToUint32(cx.GetJSContext(), value, &uValue);
+ ASSERT_EQ(uValue, kExpectedValue)
+ << "The histogram is not returning expected value";
+}
+
+TEST_F(TelemetryTestFixture,
+ AccumulateKeyedCategoricalHistogram_MultipleEnumValues) {
+ const uint32_t kExpectedCommonLabel = 2;
+ const uint32_t kExpectedLabel2 = 1;
+ const nsTArray<Telemetry::LABELS_TELEMETRY_TEST_KEYED_CATEGORICAL> enumLabels(
+ {Telemetry::LABELS_TELEMETRY_TEST_KEYED_CATEGORICAL::CommonLabel,
+ Telemetry::LABELS_TELEMETRY_TEST_KEYED_CATEGORICAL::CommonLabel,
+ Telemetry::LABELS_TELEMETRY_TEST_KEYED_CATEGORICAL::Label2});
+
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ GetAndClearHistogram(cx.GetJSContext(), mTelemetry,
+ "TELEMETRY_TEST_KEYED_CATEGORICAL"_ns, true);
+
+ // Accumulate the array into the categorical keyed histogram
+ Telemetry::AccumulateCategoricalKeyed("sampleKey"_ns, enumLabels);
+
+ // Get a snapshot for all the histograms
+ JS::Rooted<JS::Value> snapshot(cx.GetJSContext());
+ GetSnapshots(cx.GetJSContext(), mTelemetry,
+ "TELEMETRY_TEST_KEYED_CATEGORICAL", &snapshot, true);
+
+ // Get the histogram from the snapshot
+ JS::Rooted<JS::Value> histogram(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "TELEMETRY_TEST_KEYED_CATEGORICAL", snapshot,
+ &histogram);
+
+ // Check that the sampleKey histogram contains correct number of CommonLabel
+ // samples
+ JS::Rooted<JS::Value> sample(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "sampleKey", histogram, &sample);
+
+ // Get values object from the sample. Each entry in the object maps to a label
+ // in the histogram.
+ JS::Rooted<JS::Value> sampleKeyValues(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "values", sample, &sampleKeyValues);
+
+ // Get the count of CommonLabel
+ JS::Rooted<JS::Value> commonLabelValue(cx.GetJSContext());
+ GetElement(
+ cx.GetJSContext(),
+ static_cast<uint32_t>(
+ Telemetry::LABELS_TELEMETRY_TEST_KEYED_CATEGORICAL::CommonLabel),
+ sampleKeyValues, &commonLabelValue);
+
+ // Check that the value stored in the histogram matches with
+ // |kExpectedCommonLabel|
+ uint32_t uCommonLabelValue = 0;
+ JS::ToUint32(cx.GetJSContext(), commonLabelValue, &uCommonLabelValue);
+ ASSERT_EQ(uCommonLabelValue, kExpectedCommonLabel)
+ << "The sampleKey histogram did not accumulate the correct number of "
+ "CommonLabel samples";
+
+ // Check that the sampleKey histogram contains the correct number of Label2
+ // values Get the count of Label2
+ JS::Rooted<JS::Value> label2Value(cx.GetJSContext());
+ GetElement(cx.GetJSContext(),
+ static_cast<uint32_t>(
+ Telemetry::LABELS_TELEMETRY_TEST_KEYED_CATEGORICAL::Label2),
+ sampleKeyValues, &label2Value);
+
+ // Check that the value stored in the histogram matches with |kExpectedLabel2|
+ uint32_t uLabel2Value = 0;
+ JS::ToUint32(cx.GetJSContext(), label2Value, &uLabel2Value);
+ ASSERT_EQ(uLabel2Value, kExpectedLabel2)
+ << "The sampleKey histogram did not accumulate the correct number of "
+ "Label2 samples";
+}
+
+TEST_F(TelemetryTestFixture, AccumulateTimeDelta) {
+ const uint32_t kExpectedValue = 100;
+ const TimeStamp start = TimeStamp::Now();
+ const TimeDuration delta = TimeDuration::FromMilliseconds(50);
+
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ GetAndClearHistogram(cx.GetJSContext(), mTelemetry, "TELEMETRY_TEST_COUNT"_ns,
+ false);
+
+ // Accumulate in the histogram
+ Telemetry::AccumulateTimeDelta(Telemetry::TELEMETRY_TEST_COUNT, start - delta,
+ start);
+
+ Telemetry::AccumulateTimeDelta(Telemetry::TELEMETRY_TEST_COUNT, start - delta,
+ start);
+
+ Telemetry::AccumulateTimeDelta(Telemetry::TELEMETRY_TEST_COUNT, start, start);
+
+ // end > start timestamp gives zero contribution
+ Telemetry::AccumulateTimeDelta(Telemetry::TELEMETRY_TEST_COUNT, start + delta,
+ start);
+
+ // Get a snapshot for all the histograms
+ JS::Rooted<JS::Value> snapshot(cx.GetJSContext());
+ GetSnapshots(cx.GetJSContext(), mTelemetry, "TELEMETRY_TEST_COUNT", &snapshot,
+ false);
+
+ // Get the histogram from the snapshot
+ JS::Rooted<JS::Value> histogram(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "TELEMETRY_TEST_COUNT", snapshot, &histogram);
+
+ // Get "sum" property from histogram
+ JS::Rooted<JS::Value> sum(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "sum", histogram, &sum);
+
+ // Check that the "sum" stored in the histogram matches with |kExpectedValue|
+ uint32_t uSum = 0;
+ JS::ToUint32(cx.GetJSContext(), sum, &uSum);
+ ASSERT_EQ(uSum, kExpectedValue)
+ << "The histogram is not returning expected value";
+}
+
+TEST_F(TelemetryTestFixture, AccumulateKeyedTimeDelta) {
+ const uint32_t kExpectedValue = 100;
+ const TimeStamp start = TimeStamp::Now();
+ const TimeDuration delta = TimeDuration::FromMilliseconds(50);
+
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ GetAndClearHistogram(cx.GetJSContext(), mTelemetry,
+ "TELEMETRY_TEST_KEYED_COUNT"_ns, true);
+
+ // Accumulate time delta in the provided key within the histogram
+ Telemetry::AccumulateTimeDelta(Telemetry::TELEMETRY_TEST_KEYED_COUNT,
+ "sample"_ns, start - delta, start);
+
+ Telemetry::AccumulateTimeDelta(Telemetry::TELEMETRY_TEST_KEYED_COUNT,
+ "sample"_ns, start - delta, start);
+
+ // end > start timestamp gives zero contribution
+ Telemetry::AccumulateTimeDelta(Telemetry::TELEMETRY_TEST_KEYED_COUNT,
+ "sample"_ns, start + delta, start);
+
+ Telemetry::AccumulateTimeDelta(Telemetry::TELEMETRY_TEST_KEYED_COUNT,
+ "sample"_ns, start, start);
+
+ // Get a snapshot for all the histograms
+ JS::Rooted<JS::Value> snapshot(cx.GetJSContext());
+ GetSnapshots(cx.GetJSContext(), mTelemetry, "TELEMETRY_TEST_KEYED_COUNT",
+ &snapshot, true);
+
+ // Get the histogram from the snapshot
+ JS::Rooted<JS::Value> histogram(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "TELEMETRY_TEST_KEYED_COUNT", snapshot,
+ &histogram);
+
+ // Get "sample" property from histogram
+ JS::Rooted<JS::Value> expectedKeyData(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "sample", histogram, &expectedKeyData);
+
+ // Get "sum" property from keyed data
+ JS::Rooted<JS::Value> sum(cx.GetJSContext());
+ GetProperty(cx.GetJSContext(), "sum", expectedKeyData, &sum);
+
+ // Check that the sum stored in the histogram matches with |kExpectedValue|
+ uint32_t uSum = 0;
+ JS::ToUint32(cx.GetJSContext(), sum, &uSum);
+ ASSERT_EQ(uSum, kExpectedValue)
+ << "The histogram is not returning expected sum";
+}
diff --git a/toolkit/components/telemetry/tests/gtest/TestScalars.cpp b/toolkit/components/telemetry/tests/gtest/TestScalars.cpp
new file mode 100644
index 0000000000..201641e77a
--- /dev/null
+++ b/toolkit/components/telemetry/tests/gtest/TestScalars.cpp
@@ -0,0 +1,622 @@
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+#include "core/TelemetryScalar.h"
+#include "gtest/gtest.h"
+#include "js/Conversions.h"
+#include "js/PropertyAndElement.h" // JS_GetProperty, JS_HasProperty
+#include "mozilla/Telemetry.h"
+#include "mozilla/TelemetryProcessEnums.h"
+#include "mozilla/Unused.h"
+#include "nsJSUtils.h" // nsAutoJSString
+#include "nsThreadUtils.h"
+#include "TelemetryFixture.h"
+#include "TelemetryTestHelpers.h"
+#include "mozilla/glean/GleanMetrics.h"
+#include "mozilla/glean/fog_ffi_generated.h"
+
+using namespace mozilla;
+using namespace TelemetryTestHelpers;
+using namespace mozilla::glean;
+using namespace mozilla::glean::impl;
+using mozilla::Telemetry::ProcessID;
+
+#define EXPECTED_STRING "Nice, expected and creative string."
+
+// Test that we can properly write unsigned scalars using the C++ API.
+TEST_F(TelemetryTestFixture, ScalarUnsigned) {
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ // Make sure we don't get scalars from other tests.
+ Unused << mTelemetry->ClearScalars();
+
+ // Set the test scalar to a known value.
+ const uint32_t kInitialValue = 1172015;
+ const uint32_t kExpectedUint = 1172017;
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_UNSIGNED_INT_KIND,
+ kInitialValue);
+ Telemetry::ScalarAdd(Telemetry::ScalarID::TELEMETRY_TEST_UNSIGNED_INT_KIND,
+ kExpectedUint - kInitialValue);
+
+ // Check the recorded value.
+ JS::Rooted<JS::Value> scalarsSnapshot(cx.GetJSContext());
+ GetScalarsSnapshot(false, cx.GetJSContext(), &scalarsSnapshot);
+ CheckUintScalar("telemetry.test.unsigned_int_kind", cx.GetJSContext(),
+ scalarsSnapshot, kExpectedUint);
+
+ // Try to use SetMaximum.
+ const uint32_t kExpectedUintMaximum = kExpectedUint * 2;
+ Telemetry::ScalarSetMaximum(
+ Telemetry::ScalarID::TELEMETRY_TEST_UNSIGNED_INT_KIND,
+ kExpectedUintMaximum);
+
+// Make sure that calls of the unsupported type don't corrupt the stored value.
+// Don't run this part in debug builds as that intentionally asserts.
+#ifndef DEBUG
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_UNSIGNED_INT_KIND,
+ false);
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_UNSIGNED_INT_KIND,
+ u"test"_ns);
+#endif
+
+ // Check the recorded value.
+ GetScalarsSnapshot(false, cx.GetJSContext(), &scalarsSnapshot);
+ CheckUintScalar("telemetry.test.unsigned_int_kind", cx.GetJSContext(),
+ scalarsSnapshot, kExpectedUintMaximum);
+}
+
+// Test that the AutoScalarTimer records a proper uint32_t value to a
+// scalar once it goes out of scope.
+TEST_F(TelemetryTestFixture, AutoScalarTimer) {
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ // Make sure we don't get scalars from other tests.
+ Unused << mTelemetry->ClearScalars();
+ {
+ Telemetry::AutoScalarTimer<
+ Telemetry::ScalarID::TELEMETRY_TEST_UNSIGNED_INT_KIND>
+ timer;
+ }
+
+ const char* kScalarName = "telemetry.test.unsigned_int_kind";
+
+ // Check that there's a recorded value that is greater than 0. Since
+ // this is a timer, we'll not check the non-deterministic value - just
+ // that it exists.
+ JS::Rooted<JS::Value> scalarsSnapshot(cx.GetJSContext());
+ GetScalarsSnapshot(false, cx.GetJSContext(), &scalarsSnapshot);
+
+ // Validate the value of the test scalar.
+ JS::Rooted<JS::Value> value(cx.GetJSContext());
+ JS::Rooted<JSObject*> scalarObj(cx.GetJSContext(),
+ &scalarsSnapshot.toObject());
+ ASSERT_TRUE(JS_GetProperty(cx.GetJSContext(), scalarObj, kScalarName, &value))
+ << "The test scalar must be reported.";
+
+ JS_GetProperty(cx.GetJSContext(), scalarObj, kScalarName, &value);
+ ASSERT_TRUE(value.isInt32())
+ << "The scalar value must be of the correct type.";
+ ASSERT_TRUE(value.toInt32() >= 0)
+ << "The uint scalar type must contain a value >= 0.";
+}
+
+// Test that we can properly write boolean scalars using the C++ API.
+TEST_F(TelemetryTestFixture, ScalarBoolean) {
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ Unused << mTelemetry->ClearScalars();
+
+ // Set the test scalar to a known value.
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_BOOLEAN_KIND, true);
+
+// Make sure that calls of the unsupported type don't corrupt the stored value.
+// Don't run this part in debug builds as that intentionally asserts.
+#ifndef DEBUG
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_BOOLEAN_KIND,
+ static_cast<uint32_t>(12));
+ Telemetry::ScalarSetMaximum(Telemetry::ScalarID::TELEMETRY_TEST_BOOLEAN_KIND,
+ 20);
+ Telemetry::ScalarAdd(Telemetry::ScalarID::TELEMETRY_TEST_BOOLEAN_KIND, 2);
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_BOOLEAN_KIND,
+ u"test"_ns);
+#endif
+
+ // Check the recorded value.
+ JS::Rooted<JS::Value> scalarsSnapshot(cx.GetJSContext());
+ GetScalarsSnapshot(false, cx.GetJSContext(), &scalarsSnapshot);
+ CheckBoolScalar("telemetry.test.boolean_kind", cx.GetJSContext(),
+ scalarsSnapshot, true);
+}
+
+// Test that we can properly write string scalars using the C++ API.
+TEST_F(TelemetryTestFixture, ScalarString) {
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ Unused << mTelemetry->ClearScalars();
+
+ // Set the test scalar to a known value.
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_STRING_KIND,
+ NS_LITERAL_STRING_FROM_CSTRING(EXPECTED_STRING));
+
+// Make sure that calls of the unsupported type don't corrupt the stored value.
+// Don't run this part in debug builds as that intentionally asserts.
+#ifndef DEBUG
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_STRING_KIND,
+ static_cast<uint32_t>(12));
+ Telemetry::ScalarSetMaximum(Telemetry::ScalarID::TELEMETRY_TEST_STRING_KIND,
+ 20);
+ Telemetry::ScalarAdd(Telemetry::ScalarID::TELEMETRY_TEST_STRING_KIND, 2);
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_STRING_KIND, true);
+#endif
+
+ // Check the recorded value.
+ JS::Rooted<JS::Value> scalarsSnapshot(cx.GetJSContext());
+ GetScalarsSnapshot(false, cx.GetJSContext(), &scalarsSnapshot);
+ CheckStringScalar("telemetry.test.string_kind", cx.GetJSContext(),
+ scalarsSnapshot, EXPECTED_STRING);
+}
+
+// Test that we can properly write keyed unsigned scalars using the C++ API.
+TEST_F(TelemetryTestFixture, KeyedScalarUnsigned) {
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ Unused << mTelemetry->ClearScalars();
+
+ // Set the test scalar to a known value.
+ const char* kScalarName = "telemetry.test.keyed_unsigned_int";
+ const uint32_t kKey1Value = 1172015;
+ const uint32_t kKey2Value = 1172017;
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_UNSIGNED_INT,
+ u"key1"_ns, kKey1Value);
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_UNSIGNED_INT,
+ u"key2"_ns, kKey1Value);
+ Telemetry::ScalarAdd(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_UNSIGNED_INT,
+ u"key2"_ns, 2);
+
+// Make sure that calls of the unsupported type don't corrupt the stored value.
+// Don't run this part in debug builds as that intentionally asserts.
+#ifndef DEBUG
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_UNSIGNED_INT,
+ u"key1"_ns, false);
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_UNSIGNED_INT,
+ u"test"_ns);
+#endif
+
+ // Check the recorded value.
+ JS::Rooted<JS::Value> scalarsSnapshot(cx.GetJSContext());
+ GetScalarsSnapshot(true, cx.GetJSContext(), &scalarsSnapshot);
+
+ // Check the keyed scalar we're interested in.
+ CheckKeyedUintScalar(kScalarName, "key1", cx.GetJSContext(), scalarsSnapshot,
+ kKey1Value);
+ CheckKeyedUintScalar(kScalarName, "key2", cx.GetJSContext(), scalarsSnapshot,
+ kKey2Value);
+ CheckNumberOfProperties(kScalarName, cx.GetJSContext(), scalarsSnapshot, 2);
+
+ // Try to use SetMaximum.
+ const uint32_t kExpectedUintMaximum = kKey1Value * 2;
+ Telemetry::ScalarSetMaximum(
+ Telemetry::ScalarID::TELEMETRY_TEST_KEYED_UNSIGNED_INT, u"key1"_ns,
+ kExpectedUintMaximum);
+
+ GetScalarsSnapshot(true, cx.GetJSContext(), &scalarsSnapshot);
+ // The first key should be different and te second is expected to be the same.
+ CheckKeyedUintScalar(kScalarName, "key1", cx.GetJSContext(), scalarsSnapshot,
+ kExpectedUintMaximum);
+ CheckKeyedUintScalar(kScalarName, "key2", cx.GetJSContext(), scalarsSnapshot,
+ kKey2Value);
+ CheckNumberOfProperties(kScalarName, cx.GetJSContext(), scalarsSnapshot, 2);
+}
+
+TEST_F(TelemetryTestFixture, KeyedScalarBoolean) {
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ Unused << mTelemetry->ClearScalars();
+
+ // Set the test scalar to a known value.
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_BOOLEAN_KIND,
+ u"key1"_ns, false);
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_BOOLEAN_KIND,
+ u"key2"_ns, true);
+
+// Make sure that calls of the unsupported type don't corrupt the stored value.
+// Don't run this part in debug builds as that intentionally asserts.
+#ifndef DEBUG
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_BOOLEAN_KIND,
+ u"key1"_ns, static_cast<uint32_t>(12));
+ Telemetry::ScalarSetMaximum(
+ Telemetry::ScalarID::TELEMETRY_TEST_KEYED_BOOLEAN_KIND, u"key1"_ns, 20);
+ Telemetry::ScalarAdd(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_BOOLEAN_KIND,
+ u"key1"_ns, 2);
+#endif
+
+ // Check the recorded value.
+ JS::Rooted<JS::Value> scalarsSnapshot(cx.GetJSContext());
+ GetScalarsSnapshot(true, cx.GetJSContext(), &scalarsSnapshot);
+
+ // Make sure that the keys contain the expected values.
+ const char* kScalarName = "telemetry.test.keyed_boolean_kind";
+ CheckKeyedBoolScalar(kScalarName, "key1", cx.GetJSContext(), scalarsSnapshot,
+ false);
+ CheckKeyedBoolScalar(kScalarName, "key2", cx.GetJSContext(), scalarsSnapshot,
+ true);
+ CheckNumberOfProperties(kScalarName, cx.GetJSContext(), scalarsSnapshot, 2);
+}
+
+TEST_F(TelemetryTestFixture, NonMainThreadAdd) {
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ Unused << mTelemetry->ClearScalars();
+
+ // Define the function that will be called on the testing thread.
+ nsCOMPtr<nsIRunnable> runnable = NS_NewRunnableFunction(
+ "TelemetryTestFixture_NonMainThreadAdd_Test::TestBody", []() -> void {
+ Telemetry::ScalarAdd(
+ Telemetry::ScalarID::TELEMETRY_TEST_UNSIGNED_INT_KIND, 37);
+ });
+
+ // Spawn the testing thread and run the function.
+ nsCOMPtr<nsIThread> testingThread;
+ nsresult rv =
+ NS_NewNamedThread("Test thread", getter_AddRefs(testingThread), runnable);
+ ASSERT_EQ(rv, NS_OK);
+
+ // Shutdown the thread. This also waits for the runnable to complete.
+ testingThread->Shutdown();
+
+ // Check the recorded value.
+ JS::Rooted<JS::Value> scalarsSnapshot(cx.GetJSContext());
+ GetScalarsSnapshot(false, cx.GetJSContext(), &scalarsSnapshot);
+ CheckUintScalar("telemetry.test.unsigned_int_kind", cx.GetJSContext(),
+ scalarsSnapshot, 37);
+}
+
+TEST_F(TelemetryTestFixture, ScalarUnknownID) {
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ // Make sure we don't get scalars from other tests.
+ Unused << mTelemetry->ClearScalars();
+
+// Don't run this part in debug builds as that intentionally asserts.
+#ifndef DEBUG
+ const uint32_t kTestFakeIds[] = {
+ static_cast<uint32_t>(Telemetry::ScalarID::ScalarCount),
+ static_cast<uint32_t>(Telemetry::ScalarID::ScalarCount) + 378537,
+ std::numeric_limits<uint32_t>::max()};
+
+ for (auto id : kTestFakeIds) {
+ Telemetry::ScalarID scalarId = static_cast<Telemetry::ScalarID>(id);
+ Telemetry::ScalarSet(scalarId, static_cast<uint32_t>(1));
+ Telemetry::ScalarSet(scalarId, true);
+ Telemetry::ScalarSet(scalarId, u"test"_ns);
+ Telemetry::ScalarAdd(scalarId, 1);
+ Telemetry::ScalarSetMaximum(scalarId, 1);
+
+ // Make sure that nothing was recorded in the plain scalars.
+ JS::Rooted<JS::Value> scalarsSnapshot(cx.GetJSContext());
+ GetScalarsSnapshot(false, cx.GetJSContext(), &scalarsSnapshot);
+ ASSERT_TRUE(scalarsSnapshot.isUndefined())
+ << "No scalar must be recorded";
+
+ // Same for the keyed scalars.
+ Telemetry::ScalarSet(scalarId, u"key1"_ns, static_cast<uint32_t>(1));
+ Telemetry::ScalarSet(scalarId, u"key1"_ns, true);
+ Telemetry::ScalarAdd(scalarId, u"key1"_ns, 1);
+ Telemetry::ScalarSetMaximum(scalarId, u"key1"_ns, 1);
+
+ // Make sure that nothing was recorded in the keyed scalars.
+ JS::Rooted<JS::Value> keyedSnapshot(cx.GetJSContext());
+ GetScalarsSnapshot(true, cx.GetJSContext(), &keyedSnapshot);
+ ASSERT_TRUE(keyedSnapshot.isUndefined())
+ << "No keyed scalar must be recorded";
+ }
+#endif
+}
+
+TEST_F(TelemetryTestFixture, ScalarEventSummary) {
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ // Make sure we don't get scalars from other tests.
+ Unused << mTelemetry->ClearScalars();
+
+ const char* kScalarName = "telemetry.event_counts";
+
+ const char* kLongestEvent =
+ "oohwowlookthiscategoryissolong#thismethodislongtooo#"
+ "thisobjectisnoslouch";
+ TelemetryScalar::SummarizeEvent(nsCString(kLongestEvent), ProcessID::Parent,
+ false /* aDynamic */);
+
+ // Check the recorded value.
+ JS::Rooted<JS::Value> scalarsSnapshot(cx.GetJSContext());
+ GetScalarsSnapshot(true, cx.GetJSContext(), &scalarsSnapshot);
+
+ CheckKeyedUintScalar(kScalarName, kLongestEvent, cx.GetJSContext(),
+ scalarsSnapshot, 1);
+
+// Don't run this part in debug builds as that intentionally asserts.
+#ifndef DEBUG
+ const char* kTooLongEvent =
+ "oohwowlookthiscategoryissolong#thismethodislongtooo#"
+ "thisobjectisnoslouch2";
+ TelemetryScalar::SummarizeEvent(nsCString(kTooLongEvent), ProcessID::Parent,
+ false /* aDynamic */);
+
+ GetScalarsSnapshot(true, cx.GetJSContext(), &scalarsSnapshot);
+ CheckNumberOfProperties(kScalarName, cx.GetJSContext(), scalarsSnapshot, 1);
+#endif // #ifndef DEBUG
+
+ // Test we can fill the next 499 keys up to our 500 maximum
+ for (int i = 1; i < 500; i++) {
+ std::ostringstream eventName;
+ eventName << "category#method#object" << i;
+ TelemetryScalar::SummarizeEvent(nsCString(eventName.str().c_str()),
+ ProcessID::Parent, false /* aDynamic */);
+ }
+
+ GetScalarsSnapshot(true, cx.GetJSContext(), &scalarsSnapshot);
+ CheckNumberOfProperties(kScalarName, cx.GetJSContext(), scalarsSnapshot, 500);
+
+// Don't run this part in debug builds as that intentionally asserts.
+#ifndef DEBUG
+ TelemetryScalar::SummarizeEvent(nsCString("whoops#too#many"),
+ ProcessID::Parent, false /* aDynamic */);
+
+ GetScalarsSnapshot(true, cx.GetJSContext(), &scalarsSnapshot);
+ CheckNumberOfProperties(kScalarName, cx.GetJSContext(), scalarsSnapshot, 500);
+#endif // #ifndef DEBUG
+}
+
+TEST_F(TelemetryTestFixture, ScalarEventSummary_Dynamic) {
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ // Make sure we don't get scalars from other tests.
+ Unused << mTelemetry->ClearScalars();
+
+ const char* kScalarName = "telemetry.dynamic_event_counts";
+ const char* kLongestEvent =
+ "oohwowlookthiscategoryissolong#thismethodislongtooo#"
+ "thisobjectisnoslouch";
+ TelemetryScalar::SummarizeEvent(nsCString(kLongestEvent), ProcessID::Parent,
+ true /* aDynamic */);
+ TelemetryScalar::SummarizeEvent(nsCString(kLongestEvent), ProcessID::Content,
+ true /* aDynamic */);
+
+ // Check the recorded value.
+ JS::Rooted<JS::Value> scalarsSnapshot(cx.GetJSContext());
+ GetScalarsSnapshot(true, cx.GetJSContext(), &scalarsSnapshot,
+ ProcessID::Dynamic);
+
+ // Recording in parent or content doesn't matter for dynamic scalars
+ // which all end up in the same place.
+ CheckKeyedUintScalar(kScalarName, kLongestEvent, cx.GetJSContext(),
+ scalarsSnapshot, 2);
+}
+
+TEST_F(TelemetryTestFixture, WrongScalarOperator) {
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ // Make sure we don't get scalars from other tests.
+ Unused << mTelemetry->ClearScalars();
+
+ const uint32_t expectedValue = 1172015;
+
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_UNSIGNED_INT_KIND,
+ expectedValue);
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_STRING_KIND,
+ NS_LITERAL_STRING_FROM_CSTRING(EXPECTED_STRING));
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_BOOLEAN_KIND, true);
+
+ TelemetryScalar::DeserializationStarted();
+
+ Telemetry::ScalarAdd(Telemetry::ScalarID::TELEMETRY_TEST_STRING_KIND, 1447);
+ Telemetry::ScalarAdd(Telemetry::ScalarID::TELEMETRY_TEST_BOOLEAN_KIND, 1447);
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_UNSIGNED_INT_KIND,
+ true);
+ TelemetryScalar::ApplyPendingOperations();
+
+ JS::Rooted<JS::Value> scalarsSnapshot(cx.GetJSContext());
+ GetScalarsSnapshot(false, cx.GetJSContext(), &scalarsSnapshot);
+ CheckStringScalar("telemetry.test.string_kind", cx.GetJSContext(),
+ scalarsSnapshot, EXPECTED_STRING);
+ CheckBoolScalar("telemetry.test.boolean_kind", cx.GetJSContext(),
+ scalarsSnapshot, true);
+ CheckUintScalar("telemetry.test.unsigned_int_kind", cx.GetJSContext(),
+ scalarsSnapshot, expectedValue);
+}
+
+TEST_F(TelemetryTestFixture, WrongKeyedScalarOperator) {
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ // Make sure we don't get scalars from other tests.
+ Unused << mTelemetry->ClearScalars();
+
+ const uint32_t kExpectedUint = 1172017;
+
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_UNSIGNED_INT,
+ u"key1"_ns, kExpectedUint);
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_BOOLEAN_KIND,
+ u"key2"_ns, true);
+
+ TelemetryScalar::DeserializationStarted();
+
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_UNSIGNED_INT,
+ u"key1"_ns, false);
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_BOOLEAN_KIND,
+ u"key2"_ns, static_cast<uint32_t>(13));
+
+ TelemetryScalar::ApplyPendingOperations();
+
+ JS::Rooted<JS::Value> scalarsSnapshot(cx.GetJSContext());
+ GetScalarsSnapshot(true, cx.GetJSContext(), &scalarsSnapshot);
+ CheckKeyedUintScalar("telemetry.test.keyed_unsigned_int", "key1",
+ cx.GetJSContext(), scalarsSnapshot, kExpectedUint);
+ CheckKeyedBoolScalar("telemetry.test.keyed_boolean_kind", "key2",
+ cx.GetJSContext(), scalarsSnapshot, true);
+}
+
+TEST_F(TelemetryTestFixture, TestKeyedScalarAllowedKeys) {
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+ // Make sure we don't get scalars from other tests.
+ Unused << mTelemetry->ClearScalars();
+
+ const uint32_t kExpectedUint = 1172017;
+
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_WITH_KEYS,
+ u"only"_ns, kExpectedUint);
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_WITH_KEYS,
+ u"meant"_ns, kExpectedUint);
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_WITH_KEYS,
+ u"for"_ns, kExpectedUint);
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_WITH_KEYS,
+ u"testing"_ns, kExpectedUint);
+
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_WITH_KEYS,
+ u"invalid"_ns, kExpectedUint);
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_WITH_KEYS,
+ u"not-valid"_ns, kExpectedUint);
+
+ JS::Rooted<JS::Value> scalarsSnapshot(cx.GetJSContext());
+ GetScalarsSnapshot(true, cx.GetJSContext(), &scalarsSnapshot);
+ CheckKeyedUintScalar("telemetry.test.keyed_with_keys", "only",
+ cx.GetJSContext(), scalarsSnapshot, kExpectedUint);
+ CheckKeyedUintScalar("telemetry.test.keyed_with_keys", "meant",
+ cx.GetJSContext(), scalarsSnapshot, kExpectedUint);
+ CheckKeyedUintScalar("telemetry.test.keyed_with_keys", "for",
+ cx.GetJSContext(), scalarsSnapshot, kExpectedUint);
+ CheckKeyedUintScalar("telemetry.test.keyed_with_keys", "testing",
+ cx.GetJSContext(), scalarsSnapshot, kExpectedUint);
+ CheckNumberOfProperties("telemetry.test.keyed_with_keys", cx.GetJSContext(),
+ scalarsSnapshot, 4);
+
+ CheckKeyedUintScalar("telemetry.keyed_scalars_unknown_keys",
+ "telemetry.test.keyed_with_keys", cx.GetJSContext(),
+ scalarsSnapshot, 2);
+}
+
+// Test that we can properly handle too long key.
+TEST_F(TelemetryTestFixture, TooLongKey) {
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ // Make sure we don't get scalars from other tests.
+ Unused << mTelemetry->ClearScalars();
+
+// Don't run this part in debug builds as that intentionally asserts.
+#ifndef DEBUG
+ const char* kScalarName = "telemetry.test.keyed_unsigned_int";
+ const uint32_t kKey1Value = 1172015;
+
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_UNSIGNED_INT,
+ u"123456789012345678901234567890123456789012345678901234"
+ u"5678901234567890morethanseventy"_ns,
+ kKey1Value);
+
+ const uint32_t kDummyUint = 1172017;
+
+ // add dummy value so that object can be created from snapshot
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_WITH_KEYS,
+ u"dummy"_ns, kDummyUint);
+ // Check the recorded value
+ JS::Rooted<JS::Value> scalarsSnapshot(cx.GetJSContext());
+ GetScalarsSnapshot(true, cx.GetJSContext(), &scalarsSnapshot);
+
+ bool foundp = true;
+ JS::Rooted<JSObject*> scalarObj(cx.GetJSContext(),
+ &scalarsSnapshot.toObject());
+ ASSERT_TRUE(
+ JS_HasProperty(cx.GetJSContext(), scalarObj, kScalarName, &foundp));
+ EXPECT_FALSE(foundp);
+#endif // #ifndef DEBUG
+}
+
+// Test that we can properly handle empty key
+TEST_F(TelemetryTestFixture, EmptyKey) {
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ // Make sure we don't get scalars from other tests.
+ Unused << mTelemetry->ClearScalars();
+
+// Don't run this part in debug builds as that intentionally asserts.
+#ifndef DEBUG
+ const char* kScalarName = "telemetry.test.keyed_unsigned_int";
+ const uint32_t kKey1Value = 1172015;
+
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_UNSIGNED_INT,
+ u""_ns, kKey1Value);
+
+ const uint32_t kDummyUint = 1172017;
+
+ // add dummy value so that object can be created from snapshot
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_WITH_KEYS,
+ u"dummy"_ns, kDummyUint);
+
+ // Check the recorded value
+ JS::Rooted<JS::Value> scalarsSnapshot(cx.GetJSContext());
+ GetScalarsSnapshot(true, cx.GetJSContext(), &scalarsSnapshot);
+
+ bool foundp = true;
+ JS::Rooted<JSObject*> scalarObj(cx.GetJSContext(),
+ &scalarsSnapshot.toObject());
+ ASSERT_TRUE(
+ JS_HasProperty(cx.GetJSContext(), scalarObj, kScalarName, &foundp));
+ EXPECT_FALSE(foundp);
+#endif // #ifndef DEBUG
+}
+
+// Test that we can properly handle too many keys
+TEST_F(TelemetryTestFixture, TooManyKeys) {
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+
+ // Make sure we don't get scalars from other tests.
+ Unused << mTelemetry->ClearScalars();
+
+// Don't run this part in debug builds as that intentionally asserts.
+#ifndef DEBUG
+ const char* kScalarName = "telemetry.test.keyed_unsigned_int";
+ const uint32_t kKey1Value = 1172015;
+
+ for (int i = 0; i < 150; i++) {
+ std::u16string key = u"key";
+ char16_t n = i + '0';
+ key.push_back(n);
+ Telemetry::ScalarSet(Telemetry::ScalarID::TELEMETRY_TEST_KEYED_UNSIGNED_INT,
+ nsString(key.c_str()), kKey1Value);
+ }
+
+ // Check the recorded value
+ JS::Rooted<JS::Value> scalarsSnapshot(cx.GetJSContext());
+ GetScalarsSnapshot(true, cx.GetJSContext(), &scalarsSnapshot);
+
+ // Check 100 keys are present.
+ CheckNumberOfProperties(kScalarName, cx.GetJSContext(), scalarsSnapshot, 100);
+#endif // #ifndef DEBUG
+}
+
+TEST_F(TelemetryTestFixture, GleanLabeledGifft) {
+ AutoJSContextWithGlobal cx(mCleanGlobal);
+ // Need to test-reset Glean so it's working
+ nsCString empty;
+ ASSERT_EQ(NS_OK, fog_test_reset(&empty, &empty));
+
+ ASSERT_EQ(mozilla::Nothing(),
+ test_only_ipc::a_labeled_counter.Get("hot_air"_ns)
+ .TestGetValue()
+ .unwrap());
+
+ const char* kScalarName = "telemetry.test.another_mirror_for_labeled_counter";
+ const uint32_t kExpectedUint = 1172017;
+ const int32_t kExpectedInt = (int32_t)1172017;
+
+ test_only_ipc::a_labeled_counter.Get("hot_air"_ns).Add(kExpectedInt);
+ ASSERT_EQ(kExpectedInt, test_only_ipc::a_labeled_counter.Get("hot_air"_ns)
+ .TestGetValue()
+ .unwrap()
+ .ref());
+
+ JS::Rooted<JS::Value> scalarsSnapshot(cx.GetJSContext());
+ GetScalarsSnapshot(true, cx.GetJSContext(), &scalarsSnapshot);
+ CheckKeyedUintScalar(kScalarName, "hot_air", cx.GetJSContext(),
+ scalarsSnapshot, kExpectedUint);
+}
diff --git a/toolkit/components/telemetry/tests/gtest/moz.build b/toolkit/components/telemetry/tests/gtest/moz.build
new file mode 100644
index 0000000000..cdf67dc63d
--- /dev/null
+++ b/toolkit/components/telemetry/tests/gtest/moz.build
@@ -0,0 +1,29 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, you can obtain one at http://mozilla.org/MPL/2.0/.
+
+Library("telemetrytest")
+
+LOCAL_INCLUDES += [
+ "../..",
+]
+
+UNIFIED_SOURCES = [
+ "TelemetryFixture.cpp",
+ "TelemetryTestHelpers.cpp",
+]
+
+if CONFIG["OS_TARGET"] != "Android":
+ UNIFIED_SOURCES += [
+ "TestCombinedStacks.cpp",
+ "TestCounters.cpp",
+ "TestEvents.cpp",
+ "TestHistograms.cpp",
+ "TestScalars.cpp",
+ ]
+
+FINAL_LIBRARY = "xul-gtest"
+
+include("/ipc/chromium/chromium-config.mozbuild")
diff --git a/toolkit/components/telemetry/tests/integration/tests/conftest.py b/toolkit/components/telemetry/tests/integration/tests/conftest.py
new file mode 100644
index 0000000000..e9cbdeff08
--- /dev/null
+++ b/toolkit/components/telemetry/tests/integration/tests/conftest.py
@@ -0,0 +1,321 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+import contextlib
+import os
+import re
+import sys
+import textwrap
+import time
+
+import mozinstall
+import pytest
+from marionette_driver import By, keys
+from marionette_driver.addons import Addons
+from marionette_driver.errors import MarionetteException
+from marionette_driver.marionette import Marionette
+from marionette_driver.wait import Wait
+from six import reraise
+from telemetry_harness.ping_server import PingServer
+
+CANARY_CLIENT_ID = "c0ffeec0-ffee-c0ff-eec0-ffeec0ffeec0"
+SERVER_ROOT = "toolkit/components/telemetry/tests/marionette/harness/www"
+UUID_PATTERN = re.compile(
+ r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
+)
+
+here = os.path.abspath(os.path.dirname(__file__))
+
+"""Get a build object we need to find a Firefox binary"""
+try:
+ from mozbuild.base import MozbuildObject
+
+ build = MozbuildObject.from_environment(cwd=here)
+except ImportError:
+ build = None
+
+
+@pytest.fixture(name="binary")
+def fixture_binary():
+ """Return a Firefox binary"""
+ try:
+ return build.get_binary_path()
+ except Exception:
+ print(str(Exception))
+
+ app = "firefox"
+ bindir = os.path.join(os.environ["PYTHON_TEST_TMP"], app)
+ if os.path.isdir(bindir):
+ try:
+ return mozinstall.get_binary(bindir, app_name=app)
+ except Exception:
+ print(str(Exception))
+
+ if "GECKO_BINARY_PATH" in os.environ:
+ return os.environ["GECKO_BINARY_PATH"]
+
+
+@pytest.fixture(name="marionette")
+def fixture_marionette(binary, ping_server):
+ """Start a marionette session with specific browser prefs"""
+ server_url = "{url}pings".format(url=ping_server.get_url("/"))
+ prefs = {
+ # Clear the region detection url to
+ # * avoid net access in tests
+ # * stabilize browser.search.region to avoid an extra subsession (bug 1579840#c40)
+ "browser.region.network.url": "",
+ # Disable smart sizing because it changes prefs at startup. (bug 1547750)
+ "browser.cache.disk.smart_size.enabled": False,
+ "toolkit.telemetry.server": server_url,
+ "telemetry.fog.test.localhost_port": -1,
+ "toolkit.telemetry.initDelay": 1,
+ "toolkit.telemetry.minSubsessionLength": 0,
+ "datareporting.healthreport.uploadEnabled": True,
+ "datareporting.policy.dataSubmissionEnabled": True,
+ "datareporting.policy.dataSubmissionPolicyBypassNotification": True,
+ "toolkit.telemetry.log.level": "Trace",
+ "toolkit.telemetry.log.dump": True,
+ "toolkit.telemetry.send.overrideOfficialCheck": True,
+ "toolkit.telemetry.testing.disableFuzzingDelay": True,
+ }
+ yield Marionette(host="localhost", port=0, bin=binary, prefs=prefs)
+
+
+@pytest.fixture(name="ping_server")
+def fixture_ping_server():
+ """Run a ping server on localhost on a free port assigned by the OS"""
+ server = PingServer(SERVER_ROOT, "http://localhost:0")
+ server.start()
+ yield server
+ server.stop()
+
+
+class Browser(object):
+ def __init__(self, marionette, ping_server):
+ self.marionette = marionette
+ self.ping_server = ping_server
+ self.addon_ids = []
+
+ def disable_telemetry(self):
+ self.marionette.instance.profile.set_persistent_preferences(
+ {"datareporting.healthreport.uploadEnabled": False}
+ )
+ self.marionette.set_pref("datareporting.healthreport.uploadEnabled", False)
+
+ def enable_search_events(self):
+ """
+ Event Telemetry categories are disabled by default.
+ Search events are in the "navigation" category and are not enabled by
+ default in builds of Firefox, so we enable them here.
+ """
+
+ script = """\
+ Services.telemetry.setEventRecordingEnabled("navigation", true);
+ """
+
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ self.marionette.execute_script(textwrap.dedent(script))
+
+ def enable_telemetry(self):
+ self.marionette.instance.profile.set_persistent_preferences(
+ {"datareporting.healthreport.uploadEnabled": True}
+ )
+ self.marionette.set_pref("datareporting.healthreport.uploadEnabled", True)
+
+ def get_client_id(self):
+ """Return the ID of the current client."""
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ return self.marionette.execute_script(
+ """\
+ const { ClientID } = ChromeUtils.import(
+ "resource://gre/modules/ClientID.jsm"
+ );
+ return ClientID.getCachedClientID();
+ """
+ )
+
+ def get_default_search_engine(self):
+ """Retrieve the identifier of the default search engine.
+
+ We found that it's required to initialize the search service before
+ attempting to retrieve the default search engine. Not calling init
+ would result in a JavaScript error (see bug 1543960 for more
+ information).
+ """
+
+ script = """\
+ let [resolve] = arguments;
+ let searchService = Components.classes[
+ "@mozilla.org/browser/search-service;1"]
+ .getService(Components.interfaces.nsISearchService);
+ return searchService.init().then(function () {
+ resolve(searchService.defaultEngine.identifier);
+ });
+ """
+
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ return self.marionette.execute_async_script(textwrap.dedent(script))
+
+ def install_addon(self):
+ resources_dir = os.path.join(os.path.dirname(__file__), "resources")
+ addon_path = os.path.abspath(os.path.join(resources_dir, "helloworld"))
+
+ try:
+ # Ensure the Environment has init'd so the installed addon
+ # triggers an "environment-change" ping.
+ script = """\
+ let [resolve] = arguments;
+ const { TelemetryEnvironment } = ChromeUtils.import(
+ "resource://gre/modules/TelemetryEnvironment.jsm"
+ );
+ TelemetryEnvironment.onInitialized().then(resolve);
+ """
+
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ self.marionette.execute_async_script(textwrap.dedent(script))
+
+ addons = Addons(self.marionette)
+ addon_id = addons.install(addon_path, temp=True)
+ except MarionetteException as e:
+ pytest.fail("{} - Error installing addon: {} - ".format(e.cause, e))
+ else:
+ self.addon_ids.append(addon_id)
+
+ @contextlib.contextmanager
+ def new_tab(self):
+ """Perform operations in a new tab and then close the new tab."""
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ start_tab = self.marionette.current_window_handle
+ new_tab = self.open_tab(focus=True)
+ self.marionette.switch_to_window(new_tab)
+
+ yield
+
+ self.marionette.close()
+ self.marionette.switch_to_window(start_tab)
+
+ def open_tab(self, focus=False):
+ current_tabs = self.marionette.window_handles
+
+ try:
+ result = self.marionette.open(type="tab", focus=focus)
+ if result["type"] != "tab":
+ raise Exception(
+ "Newly opened browsing context is of type {} and not tab.".format(
+ result["type"]
+ )
+ )
+ except Exception:
+ exc_type, exc_value, exc_traceback = sys.exc_info()
+ reraise(
+ exc_type,
+ exc_type("Failed to trigger opening a new tab: {}".format(exc_value)),
+ exc_traceback,
+ )
+ else:
+ Wait(self.marionette).until(
+ lambda mn: len(mn.window_handles) == len(current_tabs) + 1,
+ message="No new tab has been opened",
+ )
+
+ [new_tab] = list(set(self.marionette.window_handles) - set(current_tabs))
+
+ return new_tab
+
+ def quit(self, in_app=True):
+ self.marionette.quit(in_app=in_app)
+
+ def restart(self):
+ self.marionette.restart(clean=False, in_app=True)
+
+ def search(self, text):
+ """Perform a search via the browser's URL bar."""
+
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ self.marionette.execute_script("gURLBar.select();")
+ urlbar = self.marionette.find_element(By.ID, "urlbar-input")
+ urlbar.send_keys(keys.Keys.DELETE)
+ urlbar.send_keys(text + keys.Keys.ENTER)
+
+ # Wait for 0.1 seconds before proceeding to decrease the chance
+ # of Firefox being shut down before Telemetry is recorded
+ time.sleep(0.1)
+
+ def search_in_new_tab(self, text):
+ """Open a new tab and perform a search via the browser's URL bar,
+ then close the new tab."""
+ with self.new_tab():
+ self.search(text)
+
+ def start_session(self):
+ self.marionette.start_session()
+
+ def wait_for_search_service_init(self):
+ script = """\
+ let [resolve] = arguments;
+ let searchService = Components.classes["@mozilla.org/browser/search-service;1"]
+ .getService(Components.interfaces.nsISearchService);
+ searchService.init().then(resolve);
+ """
+
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ self.marionette.execute_async_script(textwrap.dedent(script))
+
+
+@pytest.fixture(name="browser")
+def fixture_browser(marionette, ping_server):
+ """Return an instance of our Browser object"""
+ browser = Browser(marionette, ping_server)
+ browser.start_session()
+ yield browser
+ browser.quit()
+
+
+class Helpers(object):
+ def __init__(self, ping_server, marionette):
+ self.ping_server = ping_server
+ self.marionette = marionette
+
+ def assert_is_valid_uuid(self, value):
+ """Custom assertion for UUID's"""
+ assert value is not None
+ assert value != ""
+ assert value != CANARY_CLIENT_ID
+ assert re.match(UUID_PATTERN, value) is not None
+
+ def wait_for_ping(self, action_func, ping_filter):
+ [ping] = self.wait_for_pings(action_func, ping_filter, 1)
+ return ping
+
+ def wait_for_pings(self, action_func, ping_filter, count):
+ """Call the given action and wait for pings to come in and return
+ the `count` number of pings, that match the given filter."""
+ # Keep track of the current number of pings
+ current_num_pings = len(self.ping_server.pings)
+
+ # New list to store new pings that satisfy the filter
+ filtered_pings = []
+
+ def wait_func(*args, **kwargs):
+ # Ignore existing pings in self.ping_server.pings
+ new_pings = self.ping_server.pings[current_num_pings:]
+
+ # Filter pings to make sure we wait for the correct ping type
+ filtered_pings[:] = [p for p in new_pings if ping_filter(p)]
+
+ return len(filtered_pings) >= count
+
+ action_func()
+
+ try:
+ Wait(self.marionette, 60).until(wait_func)
+ except Exception as e:
+ pytest.fail("Error waiting for ping: {}".format(e))
+
+ return filtered_pings[:count]
+
+
+@pytest.fixture(name="helpers")
+def fixture_helpers(ping_server, marionette):
+ """Return an instace of our helpers object"""
+ return Helpers(ping_server, marionette)
diff --git a/toolkit/components/telemetry/tests/integration/tests/python.toml b/toolkit/components/telemetry/tests/integration/tests/python.toml
new file mode 100644
index 0000000000..e9ad98226c
--- /dev/null
+++ b/toolkit/components/telemetry/tests/integration/tests/python.toml
@@ -0,0 +1,12 @@
+[DEFAULT]
+subsuite = "telemetry-integration-tests"
+
+["test_deletion_request_ping.py"]
+
+["test_event_ping.py"]
+
+["test_main_tab_scalars.py"]
+
+["test_search_counts_across_sessions.py"]
+
+["test_subsession_management.py"]
diff --git a/toolkit/components/telemetry/tests/integration/tests/resources/helloworld/helloworld.html b/toolkit/components/telemetry/tests/integration/tests/resources/helloworld/helloworld.html
new file mode 100644
index 0000000000..146ad025d9
--- /dev/null
+++ b/toolkit/components/telemetry/tests/integration/tests/resources/helloworld/helloworld.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ body {
+ background-color: lightgrey;
+ }
+ p {
+ font-size: 25px;
+ padding: 25px 50px;
+ }
+ </style>
+ </head>
+ <body>
+ <p>Hello World!</p>
+ </body>
+</html>
diff --git a/toolkit/components/telemetry/tests/integration/tests/resources/helloworld/manifest.json b/toolkit/components/telemetry/tests/integration/tests/resources/helloworld/manifest.json
new file mode 100644
index 0000000000..14ab99fa1c
--- /dev/null
+++ b/toolkit/components/telemetry/tests/integration/tests/resources/helloworld/manifest.json
@@ -0,0 +1,12 @@
+{
+ "description": "Extension to be installed in Telemetry client integration tests.",
+ "manifest_version": 2,
+ "name": "helloworld",
+ "version": "1.0",
+ "homepage_url": "https://hg.mozilla.org/mozilla-central/",
+ "browser_action": {
+ "browser_style": true,
+ "default_title": "Hello World",
+ "default_popup": "helloworld.html"
+ }
+}
diff --git a/toolkit/components/telemetry/tests/integration/tests/test_deletion_request_ping.py b/toolkit/components/telemetry/tests/integration/tests/test_deletion_request_ping.py
new file mode 100644
index 0000000000..2eb74efe38
--- /dev/null
+++ b/toolkit/components/telemetry/tests/integration/tests/test_deletion_request_ping.py
@@ -0,0 +1,64 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+import mozunit
+from telemetry_harness.ping_filters import (
+ ANY_PING,
+ DELETION_REQUEST_PING,
+ MAIN_SHUTDOWN_PING,
+)
+
+
+def test_deletion_request_ping(browser, helpers):
+ """Test the "deletion-request" ping behaviour across sessions"""
+ # Get the client_id after installing an addon
+ client_id = helpers.wait_for_ping(browser.install_addon, ANY_PING)["clientId"]
+
+ # Make sure it's a valid UUID
+ helpers.assert_is_valid_uuid(client_id)
+
+ # Trigger a "deletion-request" ping.
+ ping = helpers.wait_for_ping(browser.disable_telemetry, DELETION_REQUEST_PING)
+
+ assert "clientId" in ping
+ assert "payload" in ping
+ assert "environment" not in ping["payload"]
+
+ # Close Firefox cleanly.
+ browser.quit()
+
+ # Start Firefox.
+ browser.start_session()
+
+ # Trigger an environment change, which isn't allowed to send a ping.
+ browser.install_addon()
+
+ # Ensure we've sent no pings since "optout".
+ assert browser.ping_server.pings[-1] == ping
+
+ # Turn Telemetry back on.
+ browser.enable_telemetry()
+
+ # Close Firefox cleanly, collecting its "main"/"shutdown" ping.
+ main_ping = helpers.wait_for_ping(browser.restart, MAIN_SHUTDOWN_PING)
+
+ # Ensure the "main" ping has changed its client id.
+ assert "clientId" in main_ping
+ new_client_id = main_ping["clientId"]
+ helpers.assert_is_valid_uuid(new_client_id)
+ assert new_client_id != client_id
+
+ # Ensure we note in the ping that the user opted in.
+ parent_scalars = main_ping["payload"]["processes"]["parent"]["scalars"]
+
+ assert "telemetry.data_upload_optin" in parent_scalars
+ assert parent_scalars["telemetry.data_upload_optin"] is True
+
+ # Ensure all pings sent during this test don't have the c0ffee client id.
+ for ping in browser.ping_server.pings:
+ if "clientId" in ping:
+ helpers.assert_is_valid_uuid(ping["clientId"])
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/toolkit/components/telemetry/tests/integration/tests/test_event_ping.py b/toolkit/components/telemetry/tests/integration/tests/test_event_ping.py
new file mode 100644
index 0000000000..9209c562eb
--- /dev/null
+++ b/toolkit/components/telemetry/tests/integration/tests/test_event_ping.py
@@ -0,0 +1,50 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+import mozunit
+from telemetry_harness.ping_filters import EVENT_PING
+
+
+def test_event_ping(browser, helpers):
+ """
+ Barebones test for "event" ping:
+ Search, close Firefox, check "event" ping for search events.
+ """
+ browser.enable_search_events()
+ browser.wait_for_search_service_init()
+ browser.search("mozilla firefox")
+
+ payload = helpers.wait_for_ping(browser.restart, EVENT_PING)["payload"]
+
+ assert "shutdown" == payload["reason"]
+ assert 0 == payload["lostEventsCount"]
+ assert "events" in payload
+ assert "parent" in payload["events"]
+ assert find_event(payload["events"]["parent"])
+
+
+def find_event(events):
+ """Return the first event that has the expected timestamp, category method and object"""
+
+ for event in events:
+ # The event may optionally contain additonal fields
+ [timestamp, category, method, object_id] = event[:4]
+
+ assert timestamp > 0
+
+ if category != "navigation":
+ continue
+
+ if method != "search":
+ continue
+
+ if object_id != "urlbar":
+ continue
+
+ return True
+
+ return False
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/toolkit/components/telemetry/tests/integration/tests/test_main_tab_scalars.py b/toolkit/components/telemetry/tests/integration/tests/test_main_tab_scalars.py
new file mode 100644
index 0000000000..8a7cdd77a4
--- /dev/null
+++ b/toolkit/components/telemetry/tests/integration/tests/test_main_tab_scalars.py
@@ -0,0 +1,33 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+import mozunit
+from telemetry_harness.ping_filters import MAIN_SHUTDOWN_PING
+
+
+def test_main_tab_scalars(browser, helpers):
+ with browser.marionette.using_context(browser.marionette.CONTEXT_CHROME):
+ start_tab = browser.marionette.current_window_handle
+ tab2 = browser.open_tab(focus=True)
+ browser.marionette.switch_to_window(tab2)
+ tab3 = browser.open_tab(focus=True)
+ browser.marionette.switch_to_window(tab3)
+ browser.marionette.close()
+ browser.marionette.switch_to_window(tab2)
+ browser.marionette.close()
+ browser.marionette.switch_to_window(start_tab)
+
+ ping = helpers.wait_for_ping(browser.restart, MAIN_SHUTDOWN_PING)
+
+ assert "main" == ping["type"]
+ assert browser.get_client_id() == ping["clientId"]
+
+ scalars = ping["payload"]["processes"]["parent"]["scalars"]
+
+ assert 3 == scalars["browser.engagement.max_concurrent_tab_count"]
+ assert 2 == scalars["browser.engagement.tab_open_event_count"]
+ assert 1 == scalars["browser.engagement.max_concurrent_window_count"]
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/toolkit/components/telemetry/tests/integration/tests/test_search_counts_across_sessions.py b/toolkit/components/telemetry/tests/integration/tests/test_search_counts_across_sessions.py
new file mode 100644
index 0000000000..34afd305ad
--- /dev/null
+++ b/toolkit/components/telemetry/tests/integration/tests/test_search_counts_across_sessions.py
@@ -0,0 +1,169 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+import mozunit
+from telemetry_harness.ping_filters import (
+ MAIN_ENVIRONMENT_CHANGE_PING,
+ MAIN_SHUTDOWN_PING,
+)
+
+
+def test_search_counts(browser, helpers):
+ """Test for SEARCH_COUNTS across sessions."""
+
+ # Session S1, subsession 1:
+ # - Open browser
+ # - Open new tab
+ # - Perform search (awesome bar or search bar)
+ # - Restart browser in new session
+ search_engine = browser.get_default_search_engine()
+ browser.search_in_new_tab("mozilla firefox")
+ ping1 = helpers.wait_for_ping(browser.restart, MAIN_SHUTDOWN_PING)
+
+ # Session S2, subsession 1:
+ # - Outcome 1
+ # - Received a main ping P1 for previous session
+ # - Ping base contents:
+ # - clientId should be set
+ # - sessionId should be set
+ # - subsessionId should be set
+ # - previousSessionId should not be set
+ # - previousSubsessionId should not be set
+ # - subSessionCounter should be 1
+ # - profileSubSessionCounter should be 1
+ # - reason should be "shutdown"
+ # - Other ping contents:
+ # - SEARCH_COUNTS values should match performed search action
+
+ client_id = ping1["clientId"]
+ helpers.assert_is_valid_uuid(client_id)
+
+ ping1_info = ping1["payload"]["info"]
+ assert "shutdown" == ping1_info["reason"]
+
+ s1_session_id = ping1_info["sessionId"]
+ assert s1_session_id != ""
+
+ s1_s1_subsession_id = ping1_info["subsessionId"]
+ assert s1_s1_subsession_id != ""
+
+ assert ping1_info["previousSessionId"] is None
+ assert ping1_info["previousSubsessionId"] is None
+ assert ping1_info["subsessionCounter"] == 1
+ assert ping1_info["profileSubsessionCounter"] == 1
+
+ scalars1 = ping1["payload"]["processes"]["parent"]["scalars"]
+ assert "browser.engagement.window_open_event_count" not in scalars1
+ assert scalars1["browser.engagement.tab_open_event_count"] == 1
+
+ keyed_histograms1 = ping1["payload"]["keyedHistograms"]
+ search_counts1 = keyed_histograms1["SEARCH_COUNTS"][
+ "{}.urlbar".format(search_engine)
+ ]
+
+ assert search_counts1 == {
+ "range": [1, 2],
+ "bucket_count": 3,
+ "histogram_type": 4,
+ "values": {"1": 0, "0": 1},
+ "sum": 1,
+ }
+
+ # - Install addon
+ # Session S2, subsession 2:
+ # - Outcome 2
+ # - Received a main ping P2 for previous subsession
+ # - Ping base contents:
+ # - clientId should be set to the same value
+ # - sessionId should be set to a new value
+ # - subsessionId should be set to a new value
+ # - previousSessionId should be set to P1s sessionId value
+ # - previousSubsessionId should be set to P1s subsessionId value
+ # - subSessionCounter should be 1
+ # - profileSubSessionCounter should be 2
+ # - reason should be "environment-change"
+ # - Other ping contents:
+ # - SEARCH_COUNTS values should not be in P2
+ # - Verify that there should be no listing for tab scalar as we started a new
+ # session
+
+ ping2 = helpers.wait_for_ping(browser.install_addon, MAIN_ENVIRONMENT_CHANGE_PING)
+
+ assert client_id == ping2["clientId"]
+
+ ping2_info = ping2["payload"]["info"]
+ assert ping2_info["reason"] == "environment-change"
+
+ s2_session_id = ping2_info["sessionId"]
+ assert s2_session_id != s1_session_id
+
+ s2_s1_subsession_id = ping2_info["subsessionId"]
+ assert s2_s1_subsession_id != s1_s1_subsession_id
+
+ assert ping2_info["previousSessionId"] == s1_session_id
+ assert ping2_info["previousSubsessionId"] == s1_s1_subsession_id
+ assert ping2_info["subsessionCounter"] == 1
+ assert ping2_info["profileSubsessionCounter"] == 2
+
+ scalars2 = ping2["payload"]["processes"]["parent"]["scalars"]
+ assert "browser.engagement.window_open_event_count" not in scalars2
+ assert "browser.engagement.tab_open_event_count" not in scalars2
+
+ keyed_histograms2 = ping2["payload"]["keyedHistograms"]
+ assert "SEARCH_COUNTS" not in keyed_histograms2
+
+ # - Perform Search
+ # - Restart Browser
+
+ browser.search("mozilla telemetry")
+ browser.search("python unittest")
+ browser.search("python pytest")
+
+ ping3 = helpers.wait_for_ping(browser.restart, MAIN_SHUTDOWN_PING)
+
+ # Session S3, subsession 1:
+ # - Outcome 3
+ # - Received a main ping P3 for session 2, subsession 1
+ # - Ping base contents:
+ # - clientId should be set to the same value
+ # - sessionId should be set to P2s sessionId value
+ # - subsessionId should be set to a new value
+ # - previousSessionId should be set to P1s sessionId value
+ # - previousSubsessionId should be set to P2s subsessionId value
+ # - subSessionCounter should be 2
+ # - profileSubSessionCounter should be 3
+ # - reason should be "shutdown"
+ # - Other ping contents:
+ # - SEARCH_COUNTS values should be set per above search
+
+ assert client_id == ping3["clientId"]
+
+ ping3_info = ping3["payload"]["info"]
+ assert ping3_info["reason"] == "shutdown"
+ assert ping3_info["sessionId"] == s2_session_id
+
+ s2_s2_subsession_id = ping3_info["subsessionId"]
+ assert s2_s2_subsession_id != s1_s1_subsession_id
+ assert ping3_info["previousSessionId"] == s1_session_id
+ assert ping3_info["previousSubsessionId"] == s2_s1_subsession_id
+ assert ping3_info["subsessionCounter"] == 2
+ assert ping3_info["profileSubsessionCounter"] == 3
+
+ scalars3 = ping3["payload"]["processes"]["parent"]["scalars"]
+ assert "browser.engagement.window_open_event_count" not in scalars3
+
+ keyed_histograms3 = ping3["payload"]["keyedHistograms"]
+ search_counts3 = keyed_histograms3["SEARCH_COUNTS"][
+ "{}.urlbar".format(search_engine)
+ ]
+ assert search_counts3 == {
+ "range": [1, 2],
+ "bucket_count": 3,
+ "histogram_type": 4,
+ "values": {"1": 0, "0": 3},
+ "sum": 3,
+ }
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/toolkit/components/telemetry/tests/integration/tests/test_subsession_management.py b/toolkit/components/telemetry/tests/integration/tests/test_subsession_management.py
new file mode 100644
index 0000000000..b8697ed98b
--- /dev/null
+++ b/toolkit/components/telemetry/tests/integration/tests/test_subsession_management.py
@@ -0,0 +1,147 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+import mozunit
+from telemetry_harness.ping_filters import (
+ MAIN_ENVIRONMENT_CHANGE_PING,
+ MAIN_SHUTDOWN_PING,
+)
+
+
+def test_subsession_management(browser, helpers):
+ """Test for Firefox Telemetry subsession management."""
+
+ # Session S1, subsession 1
+ # Actions:
+ # 1. Open browser
+ # 2. Open a new tab
+ # 3. Restart browser in new session
+
+ with browser.new_tab():
+ # If Firefox Telemetry is working correctly, this will
+ # be sufficient to record a tab open event.
+ pass
+
+ ping1 = helpers.wait_for_ping(browser.restart, MAIN_SHUTDOWN_PING)
+
+ # Session S2, subsession 1
+ # Outcome 1:
+ # Received a main ping P1 for previous session
+ # - Ping base contents:
+ # - clientId should be a valid UUID
+ # - reason should be "shutdown"
+ # - sessionId should be set
+ # - subsessionId should be set
+ # - previousSessionId should not be set
+ # - previousSubsessionId should not be set
+ # - subSessionCounter should be 1
+ # - profileSubSessionCounter should be 1
+ # - Other ping contents:
+ # - tab_open_event_count in scalars
+
+ client_id = ping1["clientId"]
+ helpers.assert_is_valid_uuid(client_id)
+
+ ping1_info = ping1["payload"]["info"]
+ assert ping1_info["reason"] == "shutdown"
+
+ s1_session_id = ping1_info["sessionId"]
+ assert s1_session_id != ""
+
+ s1_s1_subsession_id = ping1_info["subsessionId"]
+ assert s1_s1_subsession_id != ""
+ assert ping1_info["previousSessionId"] is None
+ assert ping1_info["previousSubsessionId"] is None
+ assert ping1_info["subsessionCounter"] == 1
+ assert ping1_info["profileSubsessionCounter"] == 1
+
+ scalars1 = ping1["payload"]["processes"]["parent"]["scalars"]
+ assert "browser.engagement.window_open_event_count" not in scalars1
+ assert scalars1["browser.engagement.tab_open_event_count"] == 1
+
+ # Actions:
+ # 1. Install addon
+
+ ping2 = helpers.wait_for_ping(browser.install_addon, MAIN_ENVIRONMENT_CHANGE_PING)
+
+ [addon_id] = browser.addon_ids # Store the addon ID for verifying ping3 later
+
+ # Session S2, subsession 2
+ # Outcome 2:
+ # Received a main ping P2 for previous subsession
+ # - Ping base contents:
+ # - clientId should be set to the same value
+ # - sessionId should be set to a new value
+ # - subsessionId should be set to a new value
+ # - previousSessionId should be set to P1s sessionId value
+ # - previousSubsessionId should be set to P1s subsessionId value
+ # - subSessionCounter should be 1
+ # - profileSubSessionCounter should be 2
+ # - reason should be "environment-change"
+ # - Other ping contents:
+ # - tab_open_event_count not in scalars
+
+ assert ping2["clientId"] == client_id
+
+ ping2_info = ping2["payload"]["info"]
+ assert ping2_info["reason"] == "environment-change"
+
+ s2_session_id = ping2_info["sessionId"]
+ assert s2_session_id != s1_session_id
+
+ s2_s1_subsession_id = ping2_info["subsessionId"]
+ assert s2_s1_subsession_id != s1_s1_subsession_id
+ assert ping2_info["previousSessionId"] == s1_session_id
+ assert ping2_info["previousSubsessionId"] == s1_s1_subsession_id
+ assert ping2_info["subsessionCounter"] == 1
+ assert ping2_info["profileSubsessionCounter"] == 2
+
+ scalars2 = ping2["payload"]["processes"]["parent"]["scalars"]
+ assert "browser.engagement.window_open_event_count" not in scalars2
+ assert "browser.engagement.tab_open_event_count" not in scalars2
+
+ # Actions
+ # 1. Restart browser in new session
+
+ ping3 = helpers.wait_for_ping(browser.restart, MAIN_SHUTDOWN_PING)
+
+ # Session S3, subsession 1
+ # Outcome 3:
+ # Received a main ping P3 for session 2, subsession 2
+ # - Ping base contents:
+ # - clientId should be set to the same value
+ # - sessionId should be set to P2s sessionId value
+ # - subsessionId should be set to a new value
+ # - previousSessionId should be set to P1s sessionId value
+ # - previousSubsessionId should be set to P2s subsessionId value
+ # - subSessionCounter should be 2
+ # - profileSubSessionCounter should be 3
+ # - reason should be "shutdown"
+ # - Other ping contents:
+ # - addon ID in activeAddons in environment
+
+ assert ping3["clientId"] == client_id
+
+ ping3_info = ping3["payload"]["info"]
+ assert ping3_info["reason"] == "shutdown"
+
+ assert ping3_info["sessionId"] == s2_session_id
+
+ s2_s2_subsession_id = ping3_info["subsessionId"]
+ assert s2_s2_subsession_id != s1_s1_subsession_id
+ assert s2_s2_subsession_id != s2_s1_subsession_id
+ assert ping3_info["previousSessionId"] == s1_session_id
+ assert ping3_info["previousSubsessionId"] == s2_s1_subsession_id
+ assert ping3_info["subsessionCounter"] == 2
+ assert ping3_info["profileSubsessionCounter"] == 3
+
+ scalars3 = ping3["payload"]["processes"]["parent"]["scalars"]
+ assert "browser.engagement.window_open_event_count" not in scalars3
+ assert "browser.engagement.tab_open_event_count" not in scalars3
+
+ active_addons = ping3["environment"]["addons"]["activeAddons"]
+ assert addon_id in active_addons
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/toolkit/components/telemetry/tests/marionette/harness/MANIFEST.in b/toolkit/components/telemetry/tests/marionette/harness/MANIFEST.in
new file mode 100644
index 0000000000..e24a6b1ba6
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/MANIFEST.in
@@ -0,0 +1,3 @@
+exclude MANIFEST.in
+include requirements.txt
+recursive-include telemetry_harness/resources * \ No newline at end of file
diff --git a/toolkit/components/telemetry/tests/marionette/harness/requirements.txt b/toolkit/components/telemetry/tests/marionette/harness/requirements.txt
new file mode 100644
index 0000000000..c15861abd7
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/requirements.txt
@@ -0,0 +1,2 @@
+marionette-harness >= 4.0.0
+requests>=2.11.1 \ No newline at end of file
diff --git a/toolkit/components/telemetry/tests/marionette/harness/setup.py b/toolkit/components/telemetry/tests/marionette/harness/setup.py
new file mode 100644
index 0000000000..38d4d35662
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/setup.py
@@ -0,0 +1,48 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+
+from setuptools import find_packages, setup
+
+PACKAGE_VERSION = "0.1"
+
+THIS_DIR = os.path.dirname(os.path.realpath(__name__))
+
+
+def read(*parts):
+ with open(os.path.join(THIS_DIR, *parts)) as f:
+ return f.read()
+
+
+setup(
+ name="telemetry-harness",
+ version=PACKAGE_VERSION,
+ description=(
+ "Custom Marionette runner classes and entry scripts for "
+ "Telemetry specific Marionette tests."
+ ),
+ classifiers=[
+ "Environment :: Console",
+ "Intended Audience :: Developers",
+ "License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
+ "Natural Language :: English",
+ "Operating System :: OS Independent",
+ "Programming Language :: Python",
+ "Topic :: Software Development :: Libraries :: Python Modules",
+ ],
+ keywords="mozilla",
+ author="Firefox Test Engineering Team",
+ author_email="firefox-test-engineering@mozilla.org",
+ url="https://developer.mozilla.org/en-US/docs/Mozilla/QA/telemetry_harness",
+ license="MPL 2.0",
+ packages=find_packages(),
+ include_package_data=True,
+ zip_safe=False,
+ install_requires=read("requirements.txt").splitlines(),
+ entry_points="""
+ [console_scripts]
+ telemetry-harness = telemetry_harness.runtests:cli
+ """,
+)
diff --git a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/__init__.py b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/__init__.py
new file mode 100644
index 0000000000..6fbe8159b2
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/__init__.py
@@ -0,0 +1,3 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
diff --git a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_ping_filters.py b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_ping_filters.py
new file mode 100644
index 0000000000..d0d006fc0a
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_ping_filters.py
@@ -0,0 +1,31 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+class FOGPingFilter(object):
+ """Ping filter that accepts any FOG pings."""
+
+ def __call__(self, ping):
+ return True
+
+
+class FOGDocTypePingFilter(FOGPingFilter):
+ """Ping filter that accepts FOG pings that match the doc-type."""
+
+ def __init__(self, doc_type):
+ super(FOGDocTypePingFilter, self).__init__()
+ self.doc_type = doc_type
+
+ def __call__(self, ping):
+ if not super(FOGDocTypePingFilter, self).__call__(ping):
+ return False
+
+ # Verify that the given ping was submitted to the URL for the doc_type
+ return ping["request_url"]["doc_type"] == self.doc_type
+
+
+FOG_BACKGROUND_UPDATE_PING = FOGDocTypePingFilter("background-update")
+FOG_BASELINE_PING = FOGDocTypePingFilter("baseline")
+FOG_DELETION_REQUEST_PING = FOGDocTypePingFilter("deletion-request")
+FOG_ONE_PING_ONLY_PING = FOGDocTypePingFilter("one-ping-only")
diff --git a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_ping_server.py b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_ping_server.py
new file mode 100644
index 0000000000..9ad2f60a59
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_ping_server.py
@@ -0,0 +1,86 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import json
+import zlib
+
+import wptserve.logger
+from marionette_harness.runner import httpd
+from mozlog import get_default_logger
+from six.moves.urllib import parse as urlparse
+
+
+class FOGPingServer(object):
+ """HTTP server for receiving Firefox on Glean pings."""
+
+ def __init__(self, server_root, url):
+ self._logger = get_default_logger(component="fog_ping_server")
+
+ # Ensure we see logs from wptserve
+ try:
+ wptserve.logger.set_logger(self._logger)
+ except Exception:
+ # Raises if already been set
+ pass
+
+ self.pings = []
+
+ @httpd.handlers.handler
+ def pings_handler(request, response):
+ """Handler for HTTP requests to the ping server."""
+ request_data = request.body
+
+ if request.headers.get("Content-Encoding") == b"gzip":
+ request_data = zlib.decompress(request_data, zlib.MAX_WBITS | 16)
+
+ request_url = request.route_match.copy()
+
+ self.pings.append(
+ {
+ "request_url": request_url,
+ "payload": json.loads(request_data),
+ "debug_tag": request.headers.get("X-Debug-ID"),
+ }
+ )
+
+ self._logger.info(
+ "pings_handler received '{}' ping".format(request_url["doc_type"])
+ )
+
+ status_code = 200
+ content = "OK"
+ headers = [
+ ("Content-Type", "text/plain"),
+ ("Content-Length", len(content)),
+ ]
+
+ return (status_code, headers, content)
+
+ self._httpd = httpd.FixtureServer(server_root, url=url)
+
+ # See https://mozilla.github.io/glean/book/user/pings/index.html#ping-submission
+ self._httpd.router.register(
+ "POST",
+ "/submit/{application_id}/{doc_type}/{glean_schema_version}/{document_id}",
+ pings_handler,
+ )
+
+ @property
+ def url(self):
+ """Return the URL for the running HTTP FixtureServer."""
+ return self._httpd.get_url("/")
+
+ @property
+ def port(self):
+ """Return the port for the running HTTP FixtureServer."""
+ parse_result = urlparse.urlparse(self.url)
+ return parse_result.port
+
+ def start(self):
+ """Start the HTTP FixtureServer."""
+ return self._httpd.start()
+
+ def stop(self):
+ """Stop the HTTP FixtureServer."""
+ return self._httpd.stop()
diff --git a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_testcase.py b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_testcase.py
new file mode 100644
index 0000000000..c5bc54e9d2
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/fog_testcase.py
@@ -0,0 +1,63 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import mozlog
+
+from telemetry_harness.fog_ping_server import FOGPingServer
+from telemetry_harness.testcase import TelemetryTestCase
+
+
+class FOGTestCase(TelemetryTestCase):
+ """Base testcase class for project FOG."""
+
+ def __init__(self, *args, **kwargs):
+ """Initialize the test case and create a ping server."""
+ super(FOGTestCase, self).__init__(*args, **kwargs)
+ self._logger = mozlog.get_default_logger(component="FOGTestCase")
+
+ def setUp(self, *args, **kwargs):
+ """Set up the test case and create a FOG ping server.
+
+ This test is skipped if the build doesn't support FOG.
+ """
+ super(FOGTestCase, self).setUp(*args, **kwargs)
+
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ fog_android = self.marionette.execute_script(
+ "return AppConstants.MOZ_GLEAN_ANDROID;"
+ )
+
+ if fog_android:
+ # Before we skip this test, we need to quit marionette and the ping
+ # server created in TelemetryTestCase by running tearDown
+ super(FOGTestCase, self).tearDown(*args, **kwargs)
+ self.skipTest("FOG is only initialized when not in an Android build.")
+
+ self.fog_ping_server = FOGPingServer(
+ self.testvars["server_root"], "http://localhost:0"
+ )
+ self.fog_ping_server.start()
+
+ self._logger.info(
+ "Submitting to FOG ping server at {}".format(self.fog_ping_server.url)
+ )
+
+ self.marionette.enforce_gecko_prefs(
+ {
+ "telemetry.fog.test.localhost_port": self.fog_ping_server.port,
+ # Enable FOG logging. 5 means "Verbose". See
+ # https://firefox-source-docs.mozilla.org/xpcom/logging.html
+ # for details.
+ "logging.config.clear_on_startup": False,
+ "logging.config.sync": True,
+ "logging.fog::*": 5,
+ "logging.fog_control::*": 5,
+ "logging.glean::*": 5,
+ "logging.glean_core::*": 5,
+ }
+ )
+
+ def tearDown(self, *args, **kwargs):
+ super(FOGTestCase, self).tearDown(*args, **kwargs)
+ self.fog_ping_server.stop()
diff --git a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/ping_filters.py b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/ping_filters.py
new file mode 100644
index 0000000000..6e003b25d5
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/ping_filters.py
@@ -0,0 +1,75 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+class PingFilter(object):
+ """Ping filter that accepts any pings."""
+
+ def __call__(self, ping):
+ return True
+
+
+class DeletionRequestPingFilter(PingFilter):
+ """Ping filter that accepts deletion-request pings."""
+
+ def __call__(self, ping):
+ if not super(DeletionRequestPingFilter, self).__call__(ping):
+ return False
+
+ return ping["type"] == "deletion-request"
+
+
+class EventPingFilter(PingFilter):
+ """Ping filter that accepts event pings."""
+
+ def __call__(self, ping):
+ if not super(EventPingFilter, self).__call__(ping):
+ return False
+
+ return ping["type"] == "event"
+
+
+class FirstShutdownPingFilter(PingFilter):
+ """Ping filter that accepts first-shutdown pings."""
+
+ def __call__(self, ping):
+ if not super(FirstShutdownPingFilter, self).__call__(ping):
+ return False
+
+ return ping["type"] == "first-shutdown"
+
+
+class MainPingFilter(PingFilter):
+ """Ping filter that accepts main pings."""
+
+ def __call__(self, ping):
+ if not super(MainPingFilter, self).__call__(ping):
+ return False
+
+ return ping["type"] == "main"
+
+
+class MainPingReasonFilter(MainPingFilter):
+ """Ping filter that accepts main pings that match the
+ specified reason.
+ """
+
+ def __init__(self, reason):
+ super(MainPingReasonFilter, self).__init__()
+ self.reason = reason
+
+ def __call__(self, ping):
+ if not super(MainPingReasonFilter, self).__call__(ping):
+ return False
+
+ return ping["payload"]["info"]["reason"] == self.reason
+
+
+ANY_PING = PingFilter()
+DELETION_REQUEST_PING = DeletionRequestPingFilter()
+EVENT_PING = EventPingFilter()
+FIRST_SHUTDOWN_PING = FirstShutdownPingFilter()
+MAIN_PING = MainPingFilter()
+MAIN_SHUTDOWN_PING = MainPingReasonFilter("shutdown")
+MAIN_ENVIRONMENT_CHANGE_PING = MainPingReasonFilter("environment-change")
diff --git a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/ping_server.py b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/ping_server.py
new file mode 100644
index 0000000000..86487672c7
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/ping_server.py
@@ -0,0 +1,77 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import json
+import zlib
+
+import mozlog
+import wptserve.logger
+from marionette_harness.runner import httpd
+
+
+class PingServer(object):
+ """HTTP server for receiving Firefox Client Telemetry pings."""
+
+ def __init__(self, server_root, url):
+ self._logger = mozlog.get_default_logger(component="pingserver")
+
+ # Ensure we see logs from wptserve
+ try:
+ wptserve.logger.set_logger(self._logger)
+ except Exception:
+ # Raises if already been set
+ pass
+ self.pings = []
+
+ @httpd.handlers.handler
+ def pings_handler(request, response):
+ """Handler for HTTP requests to the ping server."""
+ request_data = request.body
+
+ if request.headers.get("Content-Encoding") == b"gzip":
+ request_data = zlib.decompress(request_data, zlib.MAX_WBITS | 16)
+
+ ping_data = json.loads(request_data)
+
+ # We don't have another channel to hand, so stuff this in the ping payload.
+ ping_data["X-PingSender-Version"] = request.headers.get(
+ "X-PingSender-Version", b""
+ )
+
+ # Store JSON data to self.pings to be used by wait_for_pings()
+ self.pings.append(ping_data)
+
+ ping_type = ping_data["type"]
+
+ log_message = "pings_handler received '{}' ping".format(ping_type)
+
+ if ping_type == "main":
+ ping_reason = ping_data["payload"]["info"]["reason"]
+ log_message = "{} with reason '{}'".format(log_message, ping_reason)
+
+ self._logger.info(log_message)
+
+ status_code = 200
+ content = "OK"
+ headers = [
+ ("Content-Type", "text/plain"),
+ ("Content-Length", len(content)),
+ ]
+
+ return (status_code, headers, content)
+
+ self._httpd = httpd.FixtureServer(server_root, url=url)
+ self._httpd.router.register("POST", "/pings*", pings_handler)
+
+ def get_url(self, *args, **kwargs):
+ """Return a URL from the HTTP server."""
+ return self._httpd.get_url(*args, **kwargs)
+
+ def start(self):
+ """Start the HTTP server."""
+ return self._httpd.start()
+
+ def stop(self):
+ """Stop the HTTP server."""
+ return self._httpd.stop()
diff --git a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/resources/dynamic_addon/dynamic-probe-telemetry-extension-signed.xpi b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/resources/dynamic_addon/dynamic-probe-telemetry-extension-signed.xpi
new file mode 100644
index 0000000000..f399815c10
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/resources/dynamic_addon/dynamic-probe-telemetry-extension-signed.xpi
Binary files differ
diff --git a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/resources/helloworld/helloworld.html b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/resources/helloworld/helloworld.html
new file mode 100644
index 0000000000..146ad025d9
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/resources/helloworld/helloworld.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <style>
+ body {
+ background-color: lightgrey;
+ }
+ p {
+ font-size: 25px;
+ padding: 25px 50px;
+ }
+ </style>
+ </head>
+ <body>
+ <p>Hello World!</p>
+ </body>
+</html>
diff --git a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/resources/helloworld/manifest.json b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/resources/helloworld/manifest.json
new file mode 100644
index 0000000000..0e35d8a2e3
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/resources/helloworld/manifest.json
@@ -0,0 +1,12 @@
+{
+ "description": "Extension to be installed in telemetry-tests-client tests.",
+ "manifest_version": 2,
+ "name": "helloworld",
+ "version": "1.0",
+ "homepage_url": "https://hg.mozilla.org/mozilla-central/",
+ "browser_action": {
+ "browser_style": true,
+ "default_title": "Hello World",
+ "default_popup": "helloworld.html"
+ }
+}
diff --git a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/runner.py b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/runner.py
new file mode 100644
index 0000000000..37a91023ce
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/runner.py
@@ -0,0 +1,63 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from marionette_harness import BaseMarionetteTestRunner
+
+from telemetry_harness.testcase import TelemetryTestCase
+
+SERVER_URL = "http://localhost:8000"
+
+
+class TelemetryTestRunner(BaseMarionetteTestRunner):
+ """TestRunner for the telemetry-tests-client suite."""
+
+ def __init__(self, **kwargs):
+ """Set test variables and preferences specific to Firefox client
+ telemetry.
+ """
+
+ # Select the appropriate GeckoInstance
+ kwargs["app"] = "fxdesktop"
+
+ prefs = kwargs.pop("prefs", {})
+
+ prefs["fission.autostart"] = True
+ if kwargs["disable_fission"]:
+ prefs["fission.autostart"] = False
+
+ # Set Firefox Client Telemetry specific preferences
+ prefs.update(
+ {
+ # Clear the region detection url to
+ # * avoid net access in tests
+ # * stabilize browser.search.region to avoid extra subsessions (bug 1579840#c40)
+ "browser.region.network.url": "",
+ # Disable smart sizing because it changes prefs at startup. (bug 1547750)
+ "browser.cache.disk.smart_size.enabled": False,
+ "toolkit.telemetry.server": "{}/pings".format(SERVER_URL),
+ "telemetry.fog.test.localhost_port": -1,
+ "toolkit.telemetry.initDelay": 1,
+ "toolkit.telemetry.minSubsessionLength": 0,
+ "datareporting.healthreport.uploadEnabled": True,
+ "datareporting.policy.dataSubmissionEnabled": True,
+ "datareporting.policy.dataSubmissionPolicyBypassNotification": True,
+ "toolkit.telemetry.log.level": "Trace",
+ "toolkit.telemetry.log.dump": True,
+ "toolkit.telemetry.send.overrideOfficialCheck": True,
+ "toolkit.telemetry.testing.disableFuzzingDelay": True,
+ # Disable Normandy to avoid extra subsessions due to Experiment
+ # activation in tests (bug 1641571)
+ "app.normandy.enabled": False,
+ # Disable Normandy a little harder (bug 1608807).
+ # This should also disable Nimbus.
+ "app.shield.optoutstudies.enabled": False,
+ }
+ )
+
+ super(TelemetryTestRunner, self).__init__(prefs=prefs, **kwargs)
+
+ self.testvars["server_root"] = kwargs["server_root"]
+ self.testvars["server_url"] = SERVER_URL
+
+ self.test_handlers = [TelemetryTestCase]
diff --git a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/runtests.py b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/runtests.py
new file mode 100644
index 0000000000..4ecee669f1
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/runtests.py
@@ -0,0 +1,15 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from marionette_harness.runtests import cli as mn_cli
+
+from telemetry_harness.runner import TelemetryTestRunner
+
+
+def cli(args=None):
+ mn_cli(runner_class=TelemetryTestRunner, args=args)
+
+
+if __name__ == "__main__":
+ cli()
diff --git a/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/testcase.py b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/testcase.py
new file mode 100644
index 0000000000..d30fd67986
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/harness/telemetry_harness/testcase.py
@@ -0,0 +1,233 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import contextlib
+import os
+import re
+import textwrap
+
+from marionette_driver.addons import Addons
+from marionette_driver.errors import MarionetteException
+from marionette_driver.wait import Wait
+from marionette_harness import MarionetteTestCase
+from marionette_harness.runner.mixins.window_manager import WindowManagerMixin
+
+from telemetry_harness.ping_server import PingServer
+
+CANARY_CLIENT_ID = "c0ffeec0-ffee-c0ff-eec0-ffeec0ffeec0"
+UUID_PATTERN = re.compile(
+ r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$"
+)
+
+
+class TelemetryTestCase(WindowManagerMixin, MarionetteTestCase):
+ def __init__(self, *args, **kwargs):
+ """Initialize the test case and create a ping server."""
+ super(TelemetryTestCase, self).__init__(*args, **kwargs)
+
+ def setUp(self, *args, **kwargs):
+ """Set up the test case and start the ping server."""
+
+ self.ping_server = PingServer(
+ self.testvars["server_root"], self.testvars["server_url"]
+ )
+ self.ping_server.start()
+
+ super(TelemetryTestCase, self).setUp(*args, **kwargs)
+
+ # Store IDs of addons installed via self.install_addon()
+ self.addon_ids = []
+
+ with self.marionette.using_context(self.marionette.CONTEXT_CONTENT):
+ self.marionette.navigate("about:about")
+
+ def disable_telemetry(self):
+ """Disable the Firefox Data Collection and Use in the current browser."""
+ self.marionette.instance.profile.set_persistent_preferences(
+ {"datareporting.healthreport.uploadEnabled": False}
+ )
+ self.marionette.set_pref("datareporting.healthreport.uploadEnabled", False)
+
+ def enable_telemetry(self):
+ """Enable the Firefox Data Collection and Use in the current browser."""
+ self.marionette.instance.profile.set_persistent_preferences(
+ {"datareporting.healthreport.uploadEnabled": True}
+ )
+ self.marionette.set_pref("datareporting.healthreport.uploadEnabled", True)
+
+ @contextlib.contextmanager
+ def new_tab(self):
+ """Perform operations in a new tab and then close the new tab."""
+
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ start_tab = self.marionette.current_window_handle
+ new_tab = self.open_tab(focus=True)
+ self.marionette.switch_to_window(new_tab)
+
+ yield
+
+ self.marionette.close()
+ self.marionette.switch_to_window(start_tab)
+
+ def navigate_in_new_tab(self, url):
+ """Open a new tab and navigate to the provided URL."""
+
+ with self.new_tab():
+ with self.marionette.using_context(self.marionette.CONTEXT_CONTENT):
+ self.marionette.navigate(url)
+
+ def assertIsValidUUID(self, value):
+ """Check if the given UUID is valid."""
+
+ self.assertIsNotNone(value)
+ self.assertNotEqual(value, "")
+
+ # Check for client ID that is used when Telemetry upload is disabled
+ self.assertNotEqual(value, CANARY_CLIENT_ID, msg="UUID is CANARY CLIENT ID")
+
+ self.assertIsNotNone(
+ re.match(UUID_PATTERN, value),
+ msg="UUID does not match regular expression",
+ )
+
+ def wait_for_pings(self, action_func, ping_filter, count, ping_server=None):
+ """Call the given action and wait for pings to come in and return
+ the `count` number of pings, that match the given filter.
+ """
+
+ if ping_server is None:
+ ping_server = self.ping_server
+
+ # Keep track of the current number of pings
+ current_num_pings = len(ping_server.pings)
+
+ # New list to store new pings that satisfy the filter
+ filtered_pings = []
+
+ def wait_func(*args, **kwargs):
+ # Ignore existing pings in ping_server.pings
+ new_pings = ping_server.pings[current_num_pings:]
+
+ # Filter pings to make sure we wait for the correct ping type
+ filtered_pings[:] = [p for p in new_pings if ping_filter(p)]
+
+ return len(filtered_pings) >= count
+
+ self.logger.info(
+ "wait_for_pings running action '{action}'.".format(
+ action=action_func.__name__
+ )
+ )
+
+ # Call given action and wait for a ping
+ action_func()
+
+ try:
+ Wait(self.marionette, 60).until(wait_func)
+ except Exception as e:
+ self.fail("Error waiting for ping: {}".format(e))
+
+ return filtered_pings[:count]
+
+ def wait_for_ping(self, action_func, ping_filter, ping_server=None):
+ """Call wait_for_pings() with the given action_func and ping_filter and
+ return the first result.
+ """
+ [ping] = self.wait_for_pings(
+ action_func, ping_filter, 1, ping_server=ping_server
+ )
+ return ping
+
+ def restart_browser(self):
+ """Restarts browser while maintaining the same profile."""
+ return self.marionette.restart(clean=False, in_app=True)
+
+ def start_browser(self):
+ """Start the browser."""
+ return self.marionette.start_session()
+
+ def quit_browser(self):
+ """Quit the browser."""
+ return self.marionette.quit()
+
+ def install_addon(self):
+ """Install a minimal addon."""
+ addon_name = "helloworld"
+ self._install_addon(addon_name)
+
+ def install_dynamic_addon(self):
+ """Install a dynamic probe addon.
+
+ Source Code:
+ https://github.com/mozilla-extensions/dynamic-probe-telemetry-extension
+ """
+ addon_name = "dynamic_addon/dynamic-probe-telemetry-extension-signed.xpi"
+ self._install_addon(addon_name, temp=False)
+
+ def _install_addon(self, addon_name, temp=True):
+ """Logic to install addon and add its ID to self.addons.ids"""
+ resources_dir = os.path.join(os.path.dirname(__file__), "resources")
+ addon_path = os.path.abspath(os.path.join(resources_dir, addon_name))
+
+ try:
+ # Ensure the Environment has init'd so the installed addon
+ # triggers an "environment-change" ping.
+ script = """\
+ let [resolve] = arguments;
+ const { TelemetryEnvironment } = ChromeUtils.import(
+ "resource://gre/modules/TelemetryEnvironment.jsm"
+ );
+ TelemetryEnvironment.onInitialized().then(resolve);
+ """
+
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ self.marionette.execute_async_script(textwrap.dedent(script))
+
+ addons = Addons(self.marionette)
+ addon_id = addons.install(addon_path, temp=temp)
+ except MarionetteException as e:
+ self.fail("{} - Error installing addon: {} - ".format(e.cause, e))
+ else:
+ self.addon_ids.append(addon_id)
+
+ def set_persistent_profile_preferences(self, preferences):
+ """Wrapper for setting persistent preferences on a user profile"""
+ return self.marionette.instance.profile.set_persistent_preferences(preferences)
+
+ def set_preferences(self, preferences):
+ """Wrapper for setting persistent preferences on a user profile"""
+ return self.marionette.set_prefs(preferences)
+
+ @property
+ def client_id(self):
+ """Return the ID of the current client."""
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ return self.marionette.execute_script(
+ """\
+ const { ClientID } = ChromeUtils.import(
+ "resource://gre/modules/ClientID.jsm"
+ );
+ return ClientID.getCachedClientID();
+ """
+ )
+
+ @property
+ def subsession_id(self):
+ """Return the ID of the current subsession."""
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ ping_data = self.marionette.execute_script(
+ """\
+ const { TelemetryController } = ChromeUtils.import(
+ "resource://gre/modules/TelemetryController.jsm"
+ );
+ return TelemetryController.getCurrentPingData(true);
+ """
+ )
+ return ping_data["payload"]["info"]["subsessionId"]
+
+ def tearDown(self, *args, **kwargs):
+ """Stop the ping server and tear down the testcase."""
+ super(TelemetryTestCase, self).tearDown()
+ self.ping_server.stop()
+ self.marionette.quit(in_app=False, clean=True)
diff --git a/toolkit/components/telemetry/tests/marionette/mach_commands.py b/toolkit/components/telemetry/tests/marionette/mach_commands.py
new file mode 100644
index 0000000000..7d796866a2
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/mach_commands.py
@@ -0,0 +1,95 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import argparse
+import logging
+import os
+import sys
+
+from mach.decorators import Command
+from mozbuild.base import BinaryNotFoundException
+from mozbuild.base import MachCommandConditions as conditions
+
+
+def create_parser_tests():
+ from marionette_harness.runtests import MarionetteArguments
+ from mozlog.structured import commandline
+
+ parser = MarionetteArguments()
+ commandline.add_logging_group(parser)
+ return parser
+
+
+def run_telemetry(tests, binary=None, topsrcdir=None, **kwargs):
+ from marionette_harness.runtests import MarionetteHarness
+ from mozlog.structured import commandline
+ from telemetry_harness.runtests import TelemetryTestRunner
+
+ parser = create_parser_tests()
+
+ if not tests:
+ tests = [
+ os.path.join(
+ topsrcdir,
+ "toolkit/components/telemetry/tests/marionette/tests/manifest.toml",
+ )
+ ]
+
+ args = argparse.Namespace(tests=tests)
+
+ args.binary = binary
+ args.logger = kwargs.pop("log", None)
+
+ for k, v in kwargs.items():
+ setattr(args, k, v)
+
+ parser.verify_usage(args)
+
+ os.environ["MOZ_IGNORE_NSS_SHUTDOWN_LEAKS"] = "1"
+
+ # Causes Firefox to crash when using non-local connections.
+ os.environ["MOZ_DISABLE_NONLOCAL_CONNECTIONS"] = "1"
+
+ if not args.logger:
+ args.logger = commandline.setup_logging(
+ "Telemetry Client Tests", args, {"mach": sys.stdout}
+ )
+ failed = MarionetteHarness(TelemetryTestRunner, args=vars(args)).run()
+ if failed > 0:
+ return 1
+ return 0
+
+
+@Command(
+ "telemetry-tests-client",
+ category="testing",
+ description="Run tests specifically for the Telemetry client",
+ conditions=[conditions.is_firefox_or_android],
+ parser=create_parser_tests,
+)
+def telemetry_test(command_context, tests, **kwargs):
+ if "test_objects" in kwargs:
+ tests = []
+ for obj in kwargs["test_objects"]:
+ tests.append(obj["file_relpath"])
+ del kwargs["test_objects"]
+ if not kwargs.get("binary") and conditions.is_firefox(command_context):
+ try:
+ kwargs["binary"] = command_context.get_binary_path("app")
+ except BinaryNotFoundException as e:
+ command_context.log(
+ logging.ERROR,
+ "telemetry-tests-client",
+ {"error": str(e)},
+ "ERROR: {error}",
+ )
+ command_context.log(
+ logging.INFO, "telemetry-tests-client", {"help": e.help()}, "{help}"
+ )
+ return 1
+ if not kwargs.get("server_root"):
+ kwargs[
+ "server_root"
+ ] = "toolkit/components/telemetry/tests/marionette/harness/www"
+ return run_telemetry(tests, topsrcdir=command_context.topsrcdir, **kwargs)
diff --git a/toolkit/components/telemetry/tests/marionette/moz.build b/toolkit/components/telemetry/tests/marionette/moz.build
new file mode 100644
index 0000000000..fdcbc2dede
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/moz.build
@@ -0,0 +1,11 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+TELEMETRY_TESTS_CLIENT_MANIFESTS += ["tests/manifest.toml"]
+
+with Files("**"):
+ BUG_COMPONENT = ("Toolkit", "Telemetry")
+ SCHEDULES.exclusive = ["telemetry-tests-client"]
diff --git a/toolkit/components/telemetry/tests/marionette/tests/client/manifest.toml b/toolkit/components/telemetry/tests/marionette/tests/client/manifest.toml
new file mode 100644
index 0000000000..6c968d636e
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/client/manifest.toml
@@ -0,0 +1,20 @@
+[DEFAULT]
+tags = "client"
+
+["test_deletion_request_ping.py"]
+
+["test_dynamic_probes.py"]
+
+["test_fog_custom_ping.py"]
+
+["test_fog_deletion_request_ping.py"]
+
+["test_fog_user_activity.py"]
+
+["test_main_tab_scalars.py"]
+
+["test_shutdown_pings_succeed.py"]
+
+["test_subsession_management.py"]
+
+["test_unicode_encoding.py"]
diff --git a/toolkit/components/telemetry/tests/marionette/tests/client/test_deletion_request_ping.py b/toolkit/components/telemetry/tests/marionette/tests/client/test_deletion_request_ping.py
new file mode 100644
index 0000000000..92219c7dc7
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/client/test_deletion_request_ping.py
@@ -0,0 +1,83 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from telemetry_harness.ping_filters import (
+ ANY_PING,
+ DELETION_REQUEST_PING,
+ MAIN_SHUTDOWN_PING,
+)
+from telemetry_harness.testcase import TelemetryTestCase
+
+
+class TestDeletionRequestPing(TelemetryTestCase):
+ """Tests for "deletion-request" ping."""
+
+ def test_deletion_request_ping_across_sessions(self):
+ """Test the "deletion-request" ping behaviour across sessions."""
+
+ # Get the client_id.
+ client_id = self.wait_for_ping(self.install_addon, ANY_PING)["clientId"]
+ self.assertIsValidUUID(client_id)
+
+ # Trigger an "deletion-request" ping.
+ ping = self.wait_for_ping(self.disable_telemetry, DELETION_REQUEST_PING)
+
+ self.assertIn("clientId", ping)
+ self.assertIn("payload", ping)
+ self.assertNotIn("environment", ping["payload"])
+
+ # Close Firefox cleanly.
+ self.quit_browser()
+
+ # TODO: Check pending pings aren't persisted
+
+ # Start Firefox.
+ self.start_browser()
+
+ # Trigger an environment change, which isn't allowed to send a ping.
+ self.install_addon()
+
+ # Ensure we've sent no pings since "disabling telemetry".
+ self.assertEqual(self.ping_server.pings[-1], ping)
+
+ # Turn Telemetry back on.
+ self.enable_telemetry()
+
+ # Enabling telemetry resets the client ID,
+ # so we can wait for it to be set.
+ #
+ # **WARNING**
+ #
+ # This MUST NOT be used outside of telemetry tests to wait for that kind of signal.
+ # Reach out to the Telemetry Team if you have a need for that.
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ return self.marionette.execute_async_script(
+ """
+ let [resolve] = arguments;
+ const { ClientID } = ChromeUtils.import(
+ "resource://gre/modules/ClientID.jsm"
+ );
+ ClientID.getClientID().then(resolve);
+ """,
+ script_timeout=1000,
+ )
+
+ # Close Firefox cleanly, collecting its "main"/"shutdown" ping.
+ main_ping = self.wait_for_ping(self.restart_browser, MAIN_SHUTDOWN_PING)
+
+ # Ensure the "main" ping has changed its client id.
+ self.assertIn("clientId", main_ping)
+ self.assertIsValidUUID(main_ping["clientId"])
+ self.assertNotEqual(main_ping["clientId"], client_id)
+
+ # Ensure we note in the ping that the user opted in.
+ parent_scalars = main_ping["payload"]["processes"]["parent"]["scalars"]
+
+ self.assertIn("telemetry.data_upload_optin", parent_scalars)
+ self.assertIs(parent_scalars["telemetry.data_upload_optin"], True)
+
+ # Ensure all pings sent during this test don't have the c0ffee client id.
+ for ping in self.ping_server.pings:
+ if "clientId" in ping:
+ self.assertIsValidUUID(ping["clientId"])
diff --git a/toolkit/components/telemetry/tests/marionette/tests/client/test_dynamic_probes.py b/toolkit/components/telemetry/tests/marionette/tests/client/test_dynamic_probes.py
new file mode 100644
index 0000000000..27cd0eedcb
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/client/test_dynamic_probes.py
@@ -0,0 +1,26 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from telemetry_harness.ping_filters import (
+ MAIN_ENVIRONMENT_CHANGE_PING,
+ MAIN_SHUTDOWN_PING,
+)
+from telemetry_harness.testcase import TelemetryTestCase
+
+
+class TestDynamicProbes(TelemetryTestCase):
+ """Tests for Dynamic Probes."""
+
+ def test_dynamic_probes(self):
+ """Test for dynamic probes."""
+ self.wait_for_ping(self.install_dynamic_addon, MAIN_ENVIRONMENT_CHANGE_PING)
+
+ ping = self.wait_for_ping(self.restart_browser, MAIN_SHUTDOWN_PING)
+
+ [addon_id] = self.addon_ids # check addon id exists
+ active_addons = ping["environment"]["addons"]["activeAddons"]
+ self.assertIn(addon_id, active_addons)
+
+ scalars = ping["payload"]["processes"]["dynamic"]["scalars"]
+ self.assertEqual(scalars["dynamic.probe.counter_scalar"], 1337)
diff --git a/toolkit/components/telemetry/tests/marionette/tests/client/test_fog_custom_ping.py b/toolkit/components/telemetry/tests/marionette/tests/client/test_fog_custom_ping.py
new file mode 100644
index 0000000000..11bf07472c
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/client/test_fog_custom_ping.py
@@ -0,0 +1,24 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from telemetry_harness.fog_ping_filters import FOG_ONE_PING_ONLY_PING
+from telemetry_harness.fog_testcase import FOGTestCase
+
+
+class TestDeletionRequestPing(FOGTestCase):
+ """Tests for the "one-ping-only" FOG custom ping."""
+
+ def test_one_ping_only_ping(self):
+ def send_opo_ping(marionette):
+ ping_sending_script = "GleanPings.onePingOnly.submit();"
+ with marionette.using_context(marionette.CONTEXT_CHROME):
+ marionette.execute_script(ping_sending_script)
+
+ ping1 = self.wait_for_ping(
+ lambda: send_opo_ping(self.marionette),
+ FOG_ONE_PING_ONLY_PING,
+ ping_server=self.fog_ping_server,
+ )
+
+ self.assertNotIn("client_id", ping1["payload"]["client_info"])
diff --git a/toolkit/components/telemetry/tests/marionette/tests/client/test_fog_deletion_request_ping.py b/toolkit/components/telemetry/tests/marionette/tests/client/test_fog_deletion_request_ping.py
new file mode 100644
index 0000000000..5b4fc89e60
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/client/test_fog_deletion_request_ping.py
@@ -0,0 +1,67 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import textwrap
+
+from telemetry_harness.fog_ping_filters import FOG_DELETION_REQUEST_PING
+from telemetry_harness.fog_testcase import FOGTestCase
+
+
+class TestDeletionRequestPing(FOGTestCase):
+ """Tests for FOG deletion-request ping."""
+
+ def test_deletion_request_ping_across_sessions(self):
+ """Test the "deletion-request" ping behaviour across sessions."""
+
+ self.navigate_in_new_tab("about:glean")
+
+ ping1 = self.wait_for_ping(
+ self.disable_telemetry,
+ FOG_DELETION_REQUEST_PING,
+ ping_server=self.fog_ping_server,
+ )
+
+ self.assertIn("ping_info", ping1["payload"])
+ self.assertIn("client_info", ping1["payload"])
+
+ self.assertIn("client_id", ping1["payload"]["client_info"])
+ client_id1 = ping1["payload"]["client_info"]["client_id"]
+ self.assertIsValidUUID(client_id1)
+
+ self.restart_browser()
+
+ # We'd like to assert that a "deletion-request" is the last ping we
+ # ever receive, but it's possible there's another ping on another
+ # thread that gets sent after the sync-sent "deletion-request".
+ # (This is fine, it'll be deleted within 28 days on the server.)
+ # self.assertEqual(self.fog_ping_server.pings[-1], ping1)
+
+ self.enable_telemetry()
+ self.restart_browser()
+
+ debug_tag = "my-test-tag"
+ tagging_script = """\
+ Services.fog.setTagPings("{}");
+ """.format(
+ debug_tag
+ )
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ self.marionette.execute_script(textwrap.dedent(tagging_script))
+ self.navigate_in_new_tab("about:glean")
+
+ ping2 = self.wait_for_ping(
+ self.disable_telemetry,
+ FOG_DELETION_REQUEST_PING,
+ ping_server=self.fog_ping_server,
+ )
+
+ self.assertEqual(ping2["debug_tag"].decode("utf-8"), debug_tag)
+
+ self.assertIn("client_id", ping2["payload"]["client_info"])
+ client_id2 = ping2["payload"]["client_info"]["client_id"]
+ self.assertIsValidUUID(client_id2)
+
+ # Verify that FOG creates a new client ID when a user
+ # opts out of sending technical and interaction data.
+ self.assertNotEqual(client_id2, client_id1)
diff --git a/toolkit/components/telemetry/tests/marionette/tests/client/test_fog_user_activity.py b/toolkit/components/telemetry/tests/marionette/tests/client/test_fog_user_activity.py
new file mode 100644
index 0000000000..35a662df1a
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/client/test_fog_user_activity.py
@@ -0,0 +1,46 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from telemetry_harness.fog_ping_filters import FOG_BASELINE_PING
+from telemetry_harness.fog_testcase import FOGTestCase
+
+
+class TestClientActivity(FOGTestCase):
+ """Tests for client activity and FOG's scheduling of the "baseline" ping."""
+
+ def test_user_activity(self):
+ # First test that restarting the browser sends a "active" ping
+ ping0 = self.wait_for_ping(
+ self.restart_browser, FOG_BASELINE_PING, ping_server=self.fog_ping_server
+ )
+ self.assertEqual("active", ping0["payload"]["ping_info"]["reason"])
+
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ zero_prefs_script = """\
+ Services.prefs.setIntPref("telemetry.fog.test.inactivity_limit", 0);
+ Services.prefs.setIntPref("telemetry.fog.test.activity_limit", 0);
+ """
+ self.marionette.execute_script(zero_prefs_script)
+
+ def user_active(active, marionette):
+ script = "Services.obs.notifyObservers(null, 'user-interaction-{}active')".format(
+ "" if active else "in"
+ )
+ with marionette.using_context(marionette.CONTEXT_CHROME):
+ marionette.execute_script(script)
+
+ ping1 = self.wait_for_ping(
+ lambda: user_active(True, self.marionette),
+ FOG_BASELINE_PING,
+ ping_server=self.fog_ping_server,
+ )
+
+ ping2 = self.wait_for_ping(
+ lambda: user_active(False, self.marionette),
+ FOG_BASELINE_PING,
+ ping_server=self.fog_ping_server,
+ )
+
+ self.assertEqual("active", ping1["payload"]["ping_info"]["reason"])
+ self.assertEqual("inactive", ping2["payload"]["ping_info"]["reason"])
diff --git a/toolkit/components/telemetry/tests/marionette/tests/client/test_main_tab_scalars.py b/toolkit/components/telemetry/tests/marionette/tests/client/test_main_tab_scalars.py
new file mode 100644
index 0000000000..d548d7ccc9
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/client/test_main_tab_scalars.py
@@ -0,0 +1,54 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from telemetry_harness.ping_filters import MAIN_SHUTDOWN_PING
+from telemetry_harness.testcase import TelemetryTestCase
+
+
+class TestMainTabScalars(TelemetryTestCase):
+ """Tests for Telemetry Scalars."""
+
+ def test_main_tab_scalars(self):
+ """Test for Telemetry Scalars."""
+
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ # Bug 1829464: BrowserUsageTelemetry's telemetry collection about
+ # open tabs is async. We manually set the task timeout here to 0 ms
+ # so that it instead happens immediately after a tab opens. This
+ # prevents race conditions between telemetry submission and our
+ # test.
+ self.marionette.execute_script(
+ """
+ const { BrowserUsageTelemetry } = ChromeUtils.import(
+ "resource:///modules/BrowserUsageTelemetry.jsm"
+ );
+
+ BrowserUsageTelemetry._onTabsOpenedTask._timeoutMs = 0;
+ """
+ )
+
+ start_tab = self.marionette.current_window_handle
+
+ tab2 = self.open_tab(focus=True)
+ self.marionette.switch_to_window(tab2)
+
+ tab3 = self.open_tab(focus=True)
+ self.marionette.switch_to_window(tab3)
+
+ self.marionette.close()
+ self.marionette.switch_to_window(tab2)
+
+ self.marionette.close()
+ self.marionette.switch_to_window(start_tab)
+
+ ping = self.wait_for_ping(self.restart_browser, MAIN_SHUTDOWN_PING)
+
+ self.assertEqual(ping["type"], "main")
+ self.assertEqual(ping["clientId"], self.client_id)
+
+ scalars = ping["payload"]["processes"]["parent"]["scalars"]
+
+ self.assertEqual(scalars["browser.engagement.max_concurrent_tab_count"], 3)
+ self.assertEqual(scalars["browser.engagement.tab_open_event_count"], 2)
+ self.assertEqual(scalars["browser.engagement.max_concurrent_window_count"], 1)
diff --git a/toolkit/components/telemetry/tests/marionette/tests/client/test_shutdown_pings_succeed.py b/toolkit/components/telemetry/tests/marionette/tests/client/test_shutdown_pings_succeed.py
new file mode 100644
index 0000000000..c8c1743146
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/client/test_shutdown_pings_succeed.py
@@ -0,0 +1,55 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from marionette_harness import parameterized
+from telemetry_harness.testcase import TelemetryTestCase
+
+
+class TestShutdownPingsSucced(TelemetryTestCase):
+ """Test Firefox shutdown pings."""
+
+ def tearDown(self):
+ super(TestShutdownPingsSucced, self).tearDown()
+
+ # We need a fresh profile next run in order that the "new-profile" and
+ # "first-shutdown" pings are sent.
+ self.marionette.profile = None
+
+ @parameterized("pingsender1", pingsender_version=b"1.0")
+ @parameterized("pingsender2", pingsender_version=b"2.0")
+ def test_shutdown_pings_succeed(self, pingsender_version=b""):
+ """Test that known Firefox shutdown pings are received, with the correct
+ X-PingSender-Version headers."""
+
+ pingsender2_enabled = {b"1.0": False, b"2.0": True}[pingsender_version]
+ self.marionette.set_pref(
+ "toolkit.telemetry.shutdownPingSender.backgroundtask.enabled",
+ pingsender2_enabled,
+ )
+
+ # Map ping type to expected X-PingSender-Version header. Not all pings
+ # will be sent via pingsender, so they might have an empty (binary)
+ # string version.
+ ping_types = {
+ "event": pingsender_version,
+ "first-shutdown": pingsender_version,
+ "main": b"",
+ "new-profile": pingsender_version,
+ }
+
+ # We don't need the browser after this, but the harness expects a
+ # running browser to clean up, so we `restart_browser` rather than
+ # `quit_browser`.
+ pings = self.wait_for_pings(
+ self.restart_browser,
+ lambda p: p["type"] in ping_types.keys(),
+ len(ping_types),
+ )
+
+ self.assertEqual(len(pings), len(ping_types))
+ self.assertEqual(set(ping_types.keys()), set(p["type"] for p in pings))
+
+ self.assertEqual(
+ ping_types, dict((p["type"], p["X-PingSender-Version"]) for p in pings)
+ )
diff --git a/toolkit/components/telemetry/tests/marionette/tests/client/test_subsession_management.py b/toolkit/components/telemetry/tests/marionette/tests/client/test_subsession_management.py
new file mode 100644
index 0000000000..c663f02eae
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/client/test_subsession_management.py
@@ -0,0 +1,147 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from telemetry_harness.ping_filters import (
+ MAIN_ENVIRONMENT_CHANGE_PING,
+ MAIN_SHUTDOWN_PING,
+)
+from telemetry_harness.testcase import TelemetryTestCase
+
+
+class TestSubsessionManagement(TelemetryTestCase):
+ """Tests for Firefox Telemetry subsession management."""
+
+ def test_subsession_management(self):
+ """Test for Firefox Telemetry subsession management."""
+
+ # Session S1, subsession 1
+ # Actions:
+ # 1. Open browser
+ # 2. Open a new tab
+ # 3. Restart browser in new session
+
+ with self.new_tab():
+ # If Firefox Telemetry is working correctly, this will
+ # be sufficient to record a tab open event.
+ pass
+
+ ping1 = self.wait_for_ping(self.restart_browser, MAIN_SHUTDOWN_PING)
+
+ # Session S2, subsession 1
+ # Outcome 1:
+ # Received a main ping P1 for previous session
+ # - Ping base contents:
+ # - clientId should be a valid UUID
+ # - reason should be "shutdown"
+ # - sessionId should be set
+ # - subsessionId should be set
+ # - previousSessionId should not be set
+ # - previousSubsessionId should not be set
+ # - subSessionCounter should be 1
+ # - profileSubSessionCounter should be 1
+ # - Other ping contents:
+ # - tab_open_event_count in scalars
+
+ client_id = ping1["clientId"]
+ self.assertIsValidUUID(client_id)
+
+ ping1_info = ping1["payload"]["info"]
+ self.assertEqual(ping1_info["reason"], "shutdown")
+
+ s1_session_id = ping1_info["sessionId"]
+ self.assertNotEqual(s1_session_id, "")
+
+ s1_s1_subsession_id = ping1_info["subsessionId"]
+ self.assertNotEqual(s1_s1_subsession_id, "")
+ self.assertIsNone(ping1_info["previousSessionId"])
+ self.assertIsNone(ping1_info["previousSubsessionId"])
+ self.assertEqual(ping1_info["subsessionCounter"], 1)
+ self.assertEqual(ping1_info["profileSubsessionCounter"], 1)
+
+ scalars1 = ping1["payload"]["processes"]["parent"]["scalars"]
+ self.assertNotIn("browser.engagement.window_open_event_count", scalars1)
+ self.assertEqual(scalars1["browser.engagement.tab_open_event_count"], 1)
+
+ # Actions:
+ # 1. Install addon
+
+ ping2 = self.wait_for_ping(self.install_addon, MAIN_ENVIRONMENT_CHANGE_PING)
+
+ [addon_id] = self.addon_ids # Store the addon ID for verifying ping3 later
+
+ # Session S2, subsession 2
+ # Outcome 2:
+ # Received a main ping P2 for previous subsession
+ # - Ping base contents:
+ # - clientId should be set to the same value
+ # - sessionId should be set to a new value
+ # - subsessionId should be set to a new value
+ # - previousSessionId should be set to P1s sessionId value
+ # - previousSubsessionId should be set to P1s subsessionId value
+ # - subSessionCounter should be 1
+ # - profileSubSessionCounter should be 2
+ # - reason should be "environment-change"
+ # - Other ping contents:
+ # - tab_open_event_count not in scalars
+
+ self.assertEqual(ping2["clientId"], client_id)
+
+ ping2_info = ping2["payload"]["info"]
+ self.assertEqual(ping2_info["reason"], "environment-change")
+
+ s2_session_id = ping2_info["sessionId"]
+ self.assertNotEqual(s2_session_id, s1_session_id)
+
+ s2_s1_subsession_id = ping2_info["subsessionId"]
+ self.assertNotEqual(s2_s1_subsession_id, s1_s1_subsession_id)
+ self.assertEqual(ping2_info["previousSessionId"], s1_session_id)
+ self.assertEqual(ping2_info["previousSubsessionId"], s1_s1_subsession_id)
+ self.assertEqual(ping2_info["subsessionCounter"], 1)
+ self.assertEqual(ping2_info["profileSubsessionCounter"], 2)
+
+ scalars2 = ping2["payload"]["processes"]["parent"]["scalars"]
+ self.assertNotIn("browser.engagement.window_open_event_count", scalars2)
+ self.assertNotIn("browser.engagement.tab_open_event_count", scalars2)
+
+ # Actions
+ # 1. Restart browser in new session
+
+ ping3 = self.wait_for_ping(self.restart_browser, MAIN_SHUTDOWN_PING)
+
+ # Session S3, subsession 1
+ # Outcome 3:
+ # Received a main ping P3 for session 2, subsession 2
+ # - Ping base contents:
+ # - clientId should be set to the same value
+ # - sessionId should be set to P2s sessionId value
+ # - subsessionId should be set to a new value
+ # - previousSessionId should be set to P1s sessionId value
+ # - previousSubsessionId should be set to P2s subsessionId value
+ # - subSessionCounter should be 2
+ # - profileSubSessionCounter should be 3
+ # - reason should be "shutdown"
+ # - Other ping contents:
+ # - addon ID in activeAddons in environment
+
+ self.assertEqual(ping3["clientId"], client_id)
+
+ ping3_info = ping3["payload"]["info"]
+ self.assertEqual(ping3_info["reason"], "shutdown")
+
+ self.assertEqual(ping3_info["sessionId"], s2_session_id)
+
+ s2_s2_subsession_id = ping3_info["subsessionId"]
+ self.assertNotEqual(s2_s2_subsession_id, s1_s1_subsession_id)
+ self.assertNotEqual(s2_s2_subsession_id, s2_s1_subsession_id)
+ self.assertEqual(ping3_info["previousSessionId"], s1_session_id)
+ self.assertEqual(ping3_info["previousSubsessionId"], s2_s1_subsession_id)
+ self.assertEqual(ping3_info["subsessionCounter"], 2)
+ self.assertEqual(ping3_info["profileSubsessionCounter"], 3)
+
+ scalars3 = ping3["payload"]["processes"]["parent"]["scalars"]
+ self.assertNotIn("browser.engagement.window_open_event_count", scalars3)
+ self.assertNotIn("browser.engagement.tab_open_event_count", scalars3)
+
+ active_addons = ping3["environment"]["addons"]["activeAddons"]
+ self.assertIn(addon_id, active_addons)
diff --git a/toolkit/components/telemetry/tests/marionette/tests/client/test_unicode_encoding.py b/toolkit/components/telemetry/tests/marionette/tests/client/test_unicode_encoding.py
new file mode 100644
index 0000000000..748565c0ff
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/client/test_unicode_encoding.py
@@ -0,0 +1,39 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+from telemetry_harness.ping_filters import MAIN_SHUTDOWN_PING
+from telemetry_harness.testcase import TelemetryTestCase
+
+
+class TestUnicodeEncoding(TelemetryTestCase):
+ """Tests for Firefox Telemetry Unicode encoding."""
+
+ def test_unicode_encoding(self):
+ """Test for Firefox Telemetry Unicode encoding."""
+
+ # We can use any string (not char!) pref to test the round-trip.
+ pref = "app.support.baseURL"
+ orig = "€ —"
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ value = self.marionette.execute_script(
+ r"""
+ Services.prefs.setStringPref("{pref}", "{orig}");
+ return Services.prefs.getStringPref("{pref}");
+ """.format(
+ orig=orig,
+ pref=pref,
+ )
+ )
+
+ self.assertEqual(value, orig)
+
+ # We don't need the browser after this, but the harness expects a
+ # running browser to clean up, so we `restart_browser` rather than
+ # `quit_browser`.
+ ping1 = self.wait_for_ping(self.restart_browser, MAIN_SHUTDOWN_PING)
+
+ self.assertEqual(
+ ping1["environment"]["settings"]["userPrefs"][pref],
+ orig,
+ )
diff --git a/toolkit/components/telemetry/tests/marionette/tests/manifest.toml b/toolkit/components/telemetry/tests/marionette/tests/manifest.toml
new file mode 100644
index 0000000000..18c63bc7cf
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/manifest.toml
@@ -0,0 +1,5 @@
+[DEFAULT]
+
+["include:client/manifest.toml"]
+
+["include:unit/manifest.toml"]
diff --git a/toolkit/components/telemetry/tests/marionette/tests/unit/manifest.toml b/toolkit/components/telemetry/tests/marionette/tests/unit/manifest.toml
new file mode 100644
index 0000000000..bd12114d61
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/unit/manifest.toml
@@ -0,0 +1,4 @@
+[DEFAULT]
+tags = "unit"
+
+["test_ping_server_received_ping.py"]
diff --git a/toolkit/components/telemetry/tests/marionette/tests/unit/test_ping_server_received_ping.py b/toolkit/components/telemetry/tests/marionette/tests/unit/test_ping_server_received_ping.py
new file mode 100644
index 0000000000..ffe606098f
--- /dev/null
+++ b/toolkit/components/telemetry/tests/marionette/tests/unit/test_ping_server_received_ping.py
@@ -0,0 +1,45 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/
+
+import requests
+from telemetry_harness.testcase import TelemetryTestCase
+
+
+class TestPingServer(TelemetryTestCase):
+ def setUp(self, *args, **kwargs):
+ """Set up the test case retrieve the pings URL."""
+ super(TestPingServer, self).setUp(*args, **kwargs)
+ self.pings_url = self.ping_server.get_url("/pings")
+
+ def test_ping_server_received_ping(self):
+ ping_type = "server-test-ping"
+ ping_reason = "unit-test"
+
+ def send_ping_request():
+ """Perform a POST request to the ping server."""
+ data = {"type": ping_type, "reason": ping_reason}
+ headers = {
+ "Content-type": "application/json",
+ "Accept": "text/plain",
+ }
+
+ response = requests.post(self.pings_url, json=data, headers=headers)
+
+ self.assertEqual(
+ response.status_code,
+ 200,
+ msg="Error sending POST request to ping server: {response.text}".format(
+ response=response
+ ),
+ )
+ return response
+
+ def ping_filter_func(ping):
+ return ping["type"] == ping_type
+
+ [ping] = self.wait_for_pings(send_ping_request, ping_filter_func, 1)
+
+ self.assertIsNotNone(ping)
+ self.assertEqual(ping["type"], ping_type)
+ self.assertEqual(ping["reason"], ping_reason)
diff --git a/toolkit/components/telemetry/tests/modules-test.cpp b/toolkit/components/telemetry/tests/modules-test.cpp
new file mode 100644
index 0000000000..c82d3f497d
--- /dev/null
+++ b/toolkit/components/telemetry/tests/modules-test.cpp
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * This source file is used to build different shared libraries:
+ *
+ * - libmodules-test; it is automatically built by our build system (see the
+ * moz.build in the same directory as this file);
+ *
+ * - testUnicodePDB32.dll and testUnicodePDB64.dll; they can be built by
+ * compiling this source file using MSVC and setting the target name to be
+ * "libmodμles", then renaming the resulting file:
+ * cl /Os /Zi modules-test.cpp /LINK /DLL /OUT:libmodμles.dll \
+ * /nodefaultlib /entry:nothing /opt:ref
+ * copy libmodμles.dll testUnicodePDB*ARCH*.dll
+ *
+ * - testNoPDB32.dll and testNoPDB64.dll; they can be built by compiling this
+ * file using MSVC, without enabling generation of a PDB:
+ * cl /Os modules-test.cpp /LINK /DLL /OUT:testNoPDB*ARCH*.dll \
+ * /nodefaultlib /entry:nothing
+ *
+ * Clearly, for testUnicodePDB and testNoPDB both a 32-bit and a 64-bit version
+ * have to be compiled, using the 32-bit and 64-bit MSVC toolchains.
+ *
+ */
+
+void nothing() {}
diff --git a/toolkit/components/telemetry/tests/moz.build b/toolkit/components/telemetry/tests/moz.build
new file mode 100644
index 0000000000..c2b3c06395
--- /dev/null
+++ b/toolkit/components/telemetry/tests/moz.build
@@ -0,0 +1,25 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIST_INSTALL = False
+
+SOURCES += [
+ "modules-test.cpp",
+]
+
+SharedLibrary("modules-test")
+
+NO_PGO = True
+
+TESTING_JS_MODULES += [
+ "utils/TelemetryTestUtils.sys.mjs",
+]
+
+if CONFIG["COMPILE_ENVIRONMENT"]:
+ shared_library = "!%smodules-test%s" % (CONFIG["DLL_PREFIX"], CONFIG["DLL_SUFFIX"])
+ TEST_HARNESS_FILES.xpcshell.toolkit.components.telemetry.tests.unit += [
+ shared_library
+ ]
diff --git a/toolkit/components/telemetry/tests/python/python.toml b/toolkit/components/telemetry/tests/python/python.toml
new file mode 100644
index 0000000000..d29eeb0a20
--- /dev/null
+++ b/toolkit/components/telemetry/tests/python/python.toml
@@ -0,0 +1,14 @@
+[DEFAULT]
+subsuite = "telemetry-python"
+
+["test_gen_event_data_json.py"]
+
+["test_gen_scalar_data_json.py"]
+
+["test_histogramtools_non_strict.py"]
+
+["test_histogramtools_strict.py"]
+
+["test_parse_events.py"]
+
+["test_parse_scalars.py"]
diff --git a/toolkit/components/telemetry/tests/python/test_gen_event_data_json.py b/toolkit/components/telemetry/tests/python/test_gen_event_data_json.py
new file mode 100644
index 0000000000..5caa4cebbc
--- /dev/null
+++ b/toolkit/components/telemetry/tests/python/test_gen_event_data_json.py
@@ -0,0 +1,102 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import json
+import os
+import sys
+import tempfile
+import unittest
+from io import StringIO
+from os import path
+
+import mozunit
+
+TELEMETRY_ROOT_PATH = path.abspath(
+ path.join(path.dirname(__file__), path.pardir, path.pardir)
+)
+sys.path.append(TELEMETRY_ROOT_PATH)
+# The generators live in "build_scripts", account for that.
+# NOTE: if the generators are moved, this logic will need to be updated.
+sys.path.append(path.join(TELEMETRY_ROOT_PATH, "build_scripts"))
+import gen_event_data # noqa: E402
+
+
+class TestEventDataJson(unittest.TestCase):
+ maxDiff = None
+
+ def test_JSON_definitions_generation(self):
+ EVENTS_YAML = b"""
+with.optout:
+ testme1:
+ objects: ["test1"]
+ bug_numbers: [1456415]
+ notification_emails: ["telemetry-client-dev@mozilla.org"]
+ record_in_processes: ["main"]
+ description: opt-out event
+ release_channel_collection: opt-out
+ expiry_version: never
+ products:
+ - firefox
+ extra_keys:
+ message: a message 1
+with.optin:
+ testme2:
+ objects: ["test2"]
+ bug_numbers: [1456415]
+ notification_emails: ["telemetry-client-dev@mozilla.org"]
+ record_in_processes: ["main"]
+ description: opt-in event
+ release_channel_collection: opt-in
+ expiry_version: never
+ products: ['firefox', 'fennec']
+ extra_keys:
+ message: a message 2
+ """
+
+ EXPECTED_JSON = {
+ "with.optout": {
+ "testme1": {
+ "objects": ["test1"],
+ "expired": False,
+ "expires": "never",
+ "methods": ["testme1"],
+ "extra_keys": ["message"],
+ "record_on_release": True,
+ "products": ["firefox"],
+ }
+ },
+ "with.optin": {
+ "testme2": {
+ "objects": ["test2"],
+ "expired": False,
+ "expires": "never",
+ "methods": ["testme2"],
+ "extra_keys": ["message"],
+ "record_on_release": False,
+ "products": ["firefox", "fennec"],
+ }
+ },
+ }
+
+ io = StringIO()
+ try:
+ tmpfile = tempfile.NamedTemporaryFile(suffix=".json", delete=False)
+ # Write the event definition to the temporary file
+ tmpfile.write(EVENTS_YAML)
+ tmpfile.close()
+
+ # Let the parser generate the artifact definitions
+ gen_event_data.generate_JSON_definitions(io, tmpfile.name)
+ finally:
+ if tmpfile:
+ os.unlink(tmpfile.name)
+
+ event_definitions = json.loads(io.getvalue())
+
+ # Check that it generated the correct data
+ self.assertEqual(EXPECTED_JSON, event_definitions)
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/toolkit/components/telemetry/tests/python/test_gen_scalar_data_json.py b/toolkit/components/telemetry/tests/python/test_gen_scalar_data_json.py
new file mode 100644
index 0000000000..ac60e2ae10
--- /dev/null
+++ b/toolkit/components/telemetry/tests/python/test_gen_scalar_data_json.py
@@ -0,0 +1,100 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import json
+import os
+import sys
+import tempfile
+import unittest
+from io import StringIO
+from os import path
+
+import mozunit
+
+TELEMETRY_ROOT_PATH = path.abspath(
+ path.join(path.dirname(__file__), path.pardir, path.pardir)
+)
+sys.path.append(TELEMETRY_ROOT_PATH)
+# The generators live in "build_scripts", account for that.
+sys.path.append(path.join(TELEMETRY_ROOT_PATH, "build_scripts"))
+import gen_scalar_data # noqa: E402
+
+
+class TestScalarDataJson(unittest.TestCase):
+ maxDiff = None
+
+ def test_JSON_definitions_generation(self):
+ SCALARS_YAML = b"""
+newscalar:
+ withoptin:
+ bug_numbers:
+ - 1456415
+ description: opt-in scalar
+ expires: never
+ kind: uint
+ notification_emails: ["telemetry-client-dev@mozilla.org"]
+ record_in_processes: ["main"]
+ release_channel_collection: opt-in
+ products:
+ - firefox
+ keyed: false
+ withoptout:
+ bug_numbers:
+ - 1456415
+ description: opt-out scalar
+ expires: never
+ kind: string
+ notification_emails: ["telemetry-client-dev@mozilla.org"]
+ record_in_processes: ["main"]
+ release_channel_collection: opt-out
+ products: ["firefox", "fennec"]
+ keyed: false
+ """
+
+ EXPECTED_JSON = {
+ "newscalar": {
+ "withoptout": {
+ "kind": "nsITelemetry::SCALAR_TYPE_STRING",
+ "expired": False,
+ "expires": "never",
+ "record_on_release": True,
+ "keyed": False,
+ "keys": [],
+ "stores": ["main"],
+ "products": ["firefox", "fennec"],
+ },
+ "withoptin": {
+ "kind": "nsITelemetry::SCALAR_TYPE_COUNT",
+ "expired": False,
+ "expires": "never",
+ "record_on_release": False,
+ "keyed": False,
+ "keys": [],
+ "stores": ["main"],
+ "products": ["firefox"],
+ },
+ }
+ }
+
+ io = StringIO()
+ try:
+ tmpfile = tempfile.NamedTemporaryFile(suffix=".json", delete=False)
+ # Write the scalar definition to the temporary file
+ tmpfile.write(SCALARS_YAML)
+ tmpfile.close()
+
+ # Let the parser generate the artifact definitions
+ gen_scalar_data.generate_JSON_definitions(io, tmpfile.name)
+ finally:
+ if tmpfile:
+ os.unlink(tmpfile.name)
+
+ scalar_definitions = json.loads(io.getvalue())
+
+ # Check that it generated the correct data
+ self.assertEqual(EXPECTED_JSON, scalar_definitions)
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/toolkit/components/telemetry/tests/python/test_histogramtools_non_strict.py b/toolkit/components/telemetry/tests/python/test_histogramtools_non_strict.py
new file mode 100644
index 0000000000..6c8a3ed62d
--- /dev/null
+++ b/toolkit/components/telemetry/tests/python/test_histogramtools_non_strict.py
@@ -0,0 +1,114 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import json
+import sys
+import unittest
+from os import path
+
+import mozunit
+
+TELEMETRY_ROOT_PATH = path.abspath(
+ path.join(path.dirname(__file__), path.pardir, path.pardir)
+)
+sys.path.append(TELEMETRY_ROOT_PATH)
+# The parsers live in a subdirectory of "build_scripts", account for that.
+# NOTE: if the parsers are moved, this logic will need to be updated.
+sys.path.append(path.join(TELEMETRY_ROOT_PATH, "build_scripts"))
+from mozparsers import parse_histograms # noqa: E402
+
+
+def load_histogram(histograms):
+ """Parse the passed Histogram and return a dictionary mapping histogram
+ names to histogram parameters.
+
+ :param histogram: Histogram as a python dictionary
+ :returns: Parsed Histogram dictionary mapping histogram names to histogram parameters
+ """
+
+ def hook(ps):
+ return parse_histograms.load_histograms_into_dict(ps, strict_type_checks=False)
+
+ return json.loads(json.dumps(histograms), object_pairs_hook=hook)
+
+
+class TestParser(unittest.TestCase):
+ def test_unknown_field(self):
+ SAMPLE_HISTOGRAM = {
+ "A11Y_INSTANTIATED_FLAG": {
+ "record_in_processes": ["main", "content"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "description": "has accessibility support been instantiated",
+ "new_field": "Its a new field",
+ }
+ }
+ histograms = load_histogram(SAMPLE_HISTOGRAM)
+
+ hist = parse_histograms.Histogram(
+ "A11Y_INSTANTIATED_FLAG",
+ histograms["A11Y_INSTANTIATED_FLAG"],
+ strict_type_checks=False,
+ )
+ self.assertEqual(hist.expiration(), "never")
+ self.assertEqual(hist.kind(), "flag")
+ self.assertEqual(hist.record_in_processes(), ["main", "content"])
+
+ def test_non_numeric_expressions(self):
+ SAMPLE_HISTOGRAM = {
+ "TEST_NON_NUMERIC_HISTOGRAM": {
+ "kind": "linear",
+ "description": "sample",
+ "n_buckets": "JS::GCReason::NUM_TELEMETRY_REASONS",
+ "high": "mozilla::StartupTimeline::MAX_EVENT_ID",
+ }
+ }
+
+ histograms = load_histogram(SAMPLE_HISTOGRAM)
+ hist = parse_histograms.Histogram(
+ "TEST_NON_NUMERIC_HISTOGRAM",
+ histograms["TEST_NON_NUMERIC_HISTOGRAM"],
+ strict_type_checks=False,
+ )
+
+ # expected values come off parse_histograms.py
+ self.assertEqual(hist.n_buckets(), 101)
+ self.assertEqual(hist.high(), 12)
+
+ def test_current_histogram(self):
+ HISTOGRAMS_PATH = path.join(TELEMETRY_ROOT_PATH, "Histograms.json")
+ all_histograms = list(
+ parse_histograms.from_files([HISTOGRAMS_PATH], strict_type_checks=False)
+ )
+ test_histogram = [
+ i for i in all_histograms if i.name() == "TELEMETRY_TEST_FLAG"
+ ][0]
+
+ self.assertEqual(test_histogram.expiration(), "never")
+ self.assertEqual(test_histogram.kind(), "flag")
+ self.assertEqual(test_histogram.record_in_processes(), ["main", "content"])
+ self.assertEqual(test_histogram.keyed(), False)
+
+ def test_no_products(self):
+ SAMPLE_HISTOGRAM = {
+ "TEST_EMPTY_PRODUCTS": {
+ "kind": "flag",
+ "description": "sample",
+ }
+ }
+
+ histograms = load_histogram(SAMPLE_HISTOGRAM)
+ hist = parse_histograms.Histogram(
+ "TEST_EMPTY_PRODUCTS",
+ histograms["TEST_EMPTY_PRODUCTS"],
+ strict_type_checks=False,
+ )
+
+ self.assertEqual(hist.kind(), "flag")
+ # bug 1486072: absent `product` key becomes None instead of ["all"]
+ self.assertEqual(hist.products(), None)
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/toolkit/components/telemetry/tests/python/test_histogramtools_strict.py b/toolkit/components/telemetry/tests/python/test_histogramtools_strict.py
new file mode 100644
index 0000000000..2109cd7d35
--- /dev/null
+++ b/toolkit/components/telemetry/tests/python/test_histogramtools_strict.py
@@ -0,0 +1,566 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+import sys
+import unittest
+from os import path
+
+import mozunit
+from test_histogramtools_non_strict import load_histogram
+
+TELEMETRY_ROOT_PATH = path.abspath(
+ path.join(path.dirname(__file__), path.pardir, path.pardir)
+)
+sys.path.append(TELEMETRY_ROOT_PATH)
+# The parsers live in a subdirectory of "build_scripts", account for that.
+# NOTE: if the parsers are moved, this logic will need to be updated.
+sys.path.append(path.join(TELEMETRY_ROOT_PATH, "build_scripts"))
+from mozparsers import parse_histograms
+from mozparsers.shared_telemetry_utils import ParserError
+
+
+class TestParser(unittest.TestCase):
+ def setUp(self):
+ def mockexit(x):
+ raise SystemExit(x)
+
+ self.oldexit = os._exit
+ os._exit = mockexit
+
+ def tearDown(self):
+ os._exit = self.oldexit
+
+ def test_valid_histogram(self):
+ SAMPLE_HISTOGRAM = {
+ "TEST_VALID_HISTOGRAM": {
+ "record_in_processes": ["main", "content", "socket", "utility"],
+ "alert_emails": ["team@mozilla.xyz"],
+ "bug_numbers": [1383793],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "products": ["firefox"],
+ "description": "Test histogram",
+ }
+ }
+ histograms = load_histogram(SAMPLE_HISTOGRAM)
+ parse_histograms.load_allowlist()
+
+ hist = parse_histograms.Histogram(
+ "TEST_VALID_HISTOGRAM",
+ histograms["TEST_VALID_HISTOGRAM"],
+ strict_type_checks=True,
+ )
+
+ ParserError.exit_func()
+ self.assertTrue(hist.expiration(), "never")
+ self.assertTrue(hist.kind(), "boolean")
+ self.assertTrue(hist.record_in_processes, ["main", "content"])
+ self.assertTrue(hist.record_into_store, ["main"])
+
+ def test_missing_bug_numbers(self):
+ SAMPLE_HISTOGRAM = {
+ "TEST_HISTOGRAM_ALLOWLIST_BUG_NUMBERS": {
+ "record_in_processes": ["main", "content"],
+ "alert_emails": ["team@mozilla.xyz"],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "products": ["firefox"],
+ "description": "Test histogram",
+ }
+ }
+ histograms = load_histogram(SAMPLE_HISTOGRAM)
+ parse_histograms.load_allowlist()
+
+ parse_histograms.Histogram(
+ "TEST_HISTOGRAM_ALLOWLIST_BUG_NUMBERS",
+ histograms["TEST_HISTOGRAM_ALLOWLIST_BUG_NUMBERS"],
+ strict_type_checks=True,
+ )
+
+ self.assertRaises(SystemExit, ParserError.exit_func)
+
+ # Set global allowlists for parse_histograms.
+ parse_histograms.allowlists = {
+ "alert_emails": [],
+ "bug_numbers": ["TEST_HISTOGRAM_ALLOWLIST_BUG_NUMBERS"],
+ "n_buckets": [],
+ "expiry_default": [],
+ "kind": [],
+ }
+
+ hist = parse_histograms.Histogram(
+ "TEST_HISTOGRAM_ALLOWLIST_BUG_NUMBERS",
+ histograms["TEST_HISTOGRAM_ALLOWLIST_BUG_NUMBERS"],
+ strict_type_checks=True,
+ )
+
+ ParserError.exit_func()
+ self.assertEqual(hist.expiration(), "never")
+ self.assertEqual(hist.kind(), "boolean")
+ self.assertEqual(hist.record_in_processes(), ["main", "content"])
+ self.assertEqual(hist.keyed(), False)
+
+ parse_histograms.allowlists = None
+
+ def test_missing_alert_emails(self):
+ SAMPLE_HISTOGRAM = {
+ "TEST_HISTOGRAM_ALLOWLIST_ALERT_EMAILS": {
+ "record_in_processes": ["main", "content"],
+ "bug_numbers": [1383793],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "products": ["firefox"],
+ "description": "Test histogram",
+ }
+ }
+ histograms = load_histogram(SAMPLE_HISTOGRAM)
+ parse_histograms.load_allowlist()
+
+ parse_histograms.Histogram(
+ "TEST_HISTOGRAM_ALLOWLIST_ALERT_EMAILS",
+ histograms["TEST_HISTOGRAM_ALLOWLIST_ALERT_EMAILS"],
+ strict_type_checks=True,
+ )
+
+ self.assertRaises(SystemExit, ParserError.exit_func)
+
+ # Set global allowlists for parse_histograms.
+ parse_histograms.allowlists = {
+ "alert_emails": ["TEST_HISTOGRAM_ALLOWLIST_ALERT_EMAILS"],
+ "bug_numbers": [],
+ "n_buckets": [],
+ "expiry_default": [],
+ "kind": [],
+ }
+
+ hist = parse_histograms.Histogram(
+ "TEST_HISTOGRAM_ALLOWLIST_ALERT_EMAILS",
+ histograms["TEST_HISTOGRAM_ALLOWLIST_ALERT_EMAILS"],
+ strict_type_checks=True,
+ )
+
+ ParserError.exit_func()
+ self.assertEqual(hist.expiration(), "never")
+ self.assertEqual(hist.kind(), "boolean")
+ self.assertEqual(hist.record_in_processes(), ["main", "content"])
+ self.assertEqual(hist.keyed(), False)
+
+ parse_histograms.allowlists = None
+
+ def test_high_value(self):
+ SAMPLE_HISTOGRAM = {
+ "TEST_HISTOGRAM_ALLOWLIST_N_BUCKETS": {
+ "record_in_processes": ["main", "content"],
+ "alert_emails": ["team@mozilla.xyz"],
+ "bug_numbers": [1383793],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 1024,
+ "high": 2**64,
+ "n_buckets": 100,
+ "products": ["firefox"],
+ "description": "Test histogram",
+ }
+ }
+ histograms = load_histogram(SAMPLE_HISTOGRAM)
+ parse_histograms.load_allowlist()
+
+ parse_histograms.Histogram(
+ "TEST_HISTOGRAM_ALLOWLIST_N_BUCKETS",
+ histograms["TEST_HISTOGRAM_ALLOWLIST_N_BUCKETS"],
+ strict_type_checks=True,
+ )
+
+ self.assertRaises(SystemExit, ParserError.exit_func)
+
+ def test_high_n_buckets(self):
+ SAMPLE_HISTOGRAM = {
+ "TEST_HISTOGRAM_ALLOWLIST_N_BUCKETS": {
+ "record_in_processes": ["main", "content"],
+ "alert_emails": ["team@mozilla.xyz"],
+ "bug_numbers": [1383793],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 1024,
+ "high": 16777216,
+ "n_buckets": 200,
+ "products": ["firefox"],
+ "description": "Test histogram",
+ }
+ }
+ histograms = load_histogram(SAMPLE_HISTOGRAM)
+ parse_histograms.load_allowlist()
+
+ parse_histograms.Histogram(
+ "TEST_HISTOGRAM_ALLOWLIST_N_BUCKETS",
+ histograms["TEST_HISTOGRAM_ALLOWLIST_N_BUCKETS"],
+ strict_type_checks=True,
+ )
+
+ self.assertRaises(SystemExit, ParserError.exit_func)
+
+ # Set global allowlists for parse_histograms.
+ parse_histograms.allowlists = {
+ "alert_emails": [],
+ "bug_numbers": [],
+ "n_buckets": ["TEST_HISTOGRAM_ALLOWLIST_N_BUCKETS"],
+ "expiry_default": [],
+ "kind": [],
+ }
+
+ hist = parse_histograms.Histogram(
+ "TEST_HISTOGRAM_ALLOWLIST_N_BUCKETS",
+ histograms["TEST_HISTOGRAM_ALLOWLIST_N_BUCKETS"],
+ strict_type_checks=True,
+ )
+
+ ParserError.exit_func()
+ self.assertEqual(hist.expiration(), "never")
+ self.assertEqual(hist.kind(), "exponential")
+ self.assertEqual(hist.record_in_processes(), ["main", "content"])
+ self.assertEqual(hist.keyed(), False)
+ self.assertEqual(hist.low(), 1024)
+ self.assertEqual(hist.high(), 16777216)
+ self.assertEqual(hist.n_buckets(), 200)
+
+ parse_histograms.allowlists = None
+
+ def test_expiry_default(self):
+ SAMPLE_HISTOGRAM = {
+ "TEST_HISTOGRAM_ALLOWLIST_EXPIRY_DEFAULT": {
+ "record_in_processes": ["main", "content"],
+ "expires_in_version": "default",
+ "alert_emails": ["team@mozilla.xyz"],
+ "bug_numbers": [1383793],
+ "kind": "boolean",
+ "products": ["firefox"],
+ "description": "Test histogram",
+ }
+ }
+ histograms = load_histogram(SAMPLE_HISTOGRAM)
+ parse_histograms.load_allowlist()
+
+ parse_histograms.Histogram(
+ "TEST_HISTOGRAM_ALLOWLIST_EXPIRY_DEFAULT",
+ histograms["TEST_HISTOGRAM_ALLOWLIST_EXPIRY_DEFAULT"],
+ strict_type_checks=True,
+ )
+
+ self.assertRaises(SystemExit, ParserError.exit_func)
+
+ # Set global allowlists for parse_histograms.
+ parse_histograms.allowlists = {
+ "alert_emails": [],
+ "bug_numbers": [],
+ "n_buckets": [],
+ "expiry_default": ["TEST_HISTOGRAM_ALLOWLIST_EXPIRY_DEFAULT"],
+ "kind": [],
+ }
+
+ hist = parse_histograms.Histogram(
+ "TEST_HISTOGRAM_ALLOWLIST_EXPIRY_DEFAULT",
+ histograms["TEST_HISTOGRAM_ALLOWLIST_EXPIRY_DEFAULT"],
+ strict_type_checks=True,
+ )
+
+ ParserError.exit_func()
+ self.assertEqual(hist.expiration(), "default")
+ self.assertEqual(hist.kind(), "boolean")
+ self.assertEqual(hist.record_in_processes(), ["main", "content"])
+ self.assertEqual(hist.keyed(), False)
+
+ parse_histograms.allowlists = None
+
+ def test_unsupported_kind_count(self):
+ SAMPLE_HISTOGRAM = {
+ "TEST_HISTOGRAM_ALLOWLIST_KIND": {
+ "record_in_processes": ["main", "content"],
+ "expires_in_version": "never",
+ "kind": "count",
+ "releaseChannelCollection": "opt-out",
+ "alert_emails": ["team@mozilla.xyz"],
+ "bug_numbers": [1383793],
+ "products": ["firefox"],
+ "description": "Test histogram",
+ }
+ }
+ histograms = load_histogram(SAMPLE_HISTOGRAM)
+ parse_histograms.load_allowlist()
+
+ self.assertRaises(
+ SystemExit,
+ parse_histograms.Histogram,
+ "TEST_HISTOGRAM_ALLOWLIST_KIND",
+ histograms["TEST_HISTOGRAM_ALLOWLIST_KIND"],
+ strict_type_checks=True,
+ )
+
+ # Set global allowlists for parse_histograms.
+ parse_histograms.allowlists = {
+ "alert_emails": [],
+ "bug_numbers": [],
+ "n_buckets": [],
+ "expiry_default": [],
+ "kind": ["TEST_HISTOGRAM_ALLOWLIST_KIND"],
+ }
+
+ hist = parse_histograms.Histogram(
+ "TEST_HISTOGRAM_ALLOWLIST_KIND",
+ histograms["TEST_HISTOGRAM_ALLOWLIST_KIND"],
+ strict_type_checks=True,
+ )
+
+ ParserError.exit_func()
+ self.assertEqual(hist.expiration(), "never")
+ self.assertEqual(hist.kind(), "count")
+ self.assertEqual(hist.record_in_processes(), ["main", "content"])
+ self.assertEqual(hist.keyed(), False)
+
+ parse_histograms.allowlists = None
+
+ def test_unsupported_kind_flag(self):
+ SAMPLE_HISTOGRAM = {
+ "TEST_HISTOGRAM_ALLOWLIST_KIND": {
+ "record_in_processes": ["main", "content"],
+ "expires_in_version": "never",
+ "kind": "flag",
+ "alert_emails": ["team@mozilla.xyz"],
+ "bug_numbers": [1383793],
+ "products": ["firefox"],
+ "description": "Test histogram",
+ }
+ }
+ histograms = load_histogram(SAMPLE_HISTOGRAM)
+ parse_histograms.load_allowlist()
+
+ self.assertRaises(
+ SystemExit,
+ parse_histograms.Histogram,
+ "TEST_HISTOGRAM_ALLOWLIST_KIND",
+ histograms["TEST_HISTOGRAM_ALLOWLIST_KIND"],
+ strict_type_checks=True,
+ )
+
+ # Set global allowlists for parse_histograms.
+ parse_histograms.allowlists = {
+ "alert_emails": [],
+ "bug_numbers": [],
+ "n_buckets": [],
+ "expiry_default": [],
+ "kind": ["TEST_HISTOGRAM_ALLOWLIST_KIND"],
+ }
+
+ hist = parse_histograms.Histogram(
+ "TEST_HISTOGRAM_ALLOWLIST_KIND",
+ histograms["TEST_HISTOGRAM_ALLOWLIST_KIND"],
+ strict_type_checks=True,
+ )
+
+ ParserError.exit_func()
+ self.assertEqual(hist.expiration(), "never")
+ self.assertEqual(hist.kind(), "flag")
+ self.assertEqual(hist.record_in_processes(), ["main", "content"])
+ self.assertEqual(hist.keyed(), False)
+
+ parse_histograms.allowlists = None
+
+ def test_multistore(self):
+ SAMPLE_HISTOGRAM = {
+ "TEST_VALID_HISTOGRAM": {
+ "record_in_processes": ["main", "content"],
+ "alert_emails": ["team@mozilla.xyz"],
+ "bug_numbers": [1383793],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Test histogram",
+ "products": ["firefox"],
+ "record_into_store": ["main", "sync"],
+ }
+ }
+ histograms = load_histogram(SAMPLE_HISTOGRAM)
+ parse_histograms.load_allowlist()
+
+ hist = parse_histograms.Histogram(
+ "TEST_VALID_HISTOGRAM",
+ histograms["TEST_VALID_HISTOGRAM"],
+ strict_type_checks=True,
+ )
+
+ ParserError.exit_func()
+ self.assertTrue(hist.expiration(), "never")
+ self.assertTrue(hist.kind(), "boolean")
+ self.assertTrue(hist.record_into_store, ["main", "sync"])
+
+ def test_multistore_empty(self):
+ SAMPLE_HISTOGRAM = {
+ "TEST_HISTOGRAM_EMPTY_MULTISTORE": {
+ "record_in_processes": ["main", "content"],
+ "alert_emails": ["team@mozilla.xyz"],
+ "bug_numbers": [1383793],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Test histogram",
+ "products": ["firefox"],
+ "record_into_store": [],
+ }
+ }
+ histograms = load_histogram(SAMPLE_HISTOGRAM)
+ parse_histograms.load_allowlist()
+
+ parse_histograms.Histogram(
+ "TEST_HISTOGRAM_EMPTY_MULTISTORE",
+ histograms["TEST_HISTOGRAM_EMPTY_MULTISTORE"],
+ strict_type_checks=True,
+ )
+ self.assertRaises(SystemExit, ParserError.exit_func)
+
+ def test_products_absent(self):
+ SAMPLE_HISTOGRAM = {
+ "TEST_NO_PRODUCTS": {
+ "record_in_processes": ["main", "content"],
+ "alert_emails": ["team@mozilla.xyz"],
+ "bug_numbers": [1383793],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Test histogram",
+ }
+ }
+ histograms = load_histogram(SAMPLE_HISTOGRAM)
+ parse_histograms.load_allowlist()
+
+ def test_parse():
+ return parse_histograms.Histogram(
+ "TEST_NO_PRODUCTS",
+ histograms["TEST_NO_PRODUCTS"],
+ strict_type_checks=True,
+ )
+
+ self.assertRaises(SystemExit, test_parse)
+
+ def test_products_empty(self):
+ SAMPLE_HISTOGRAM = {
+ "TEST_EMPTY_PRODUCTS": {
+ "record_in_processes": ["main", "content"],
+ "alert_emails": ["team@mozilla.xyz"],
+ "bug_numbers": [1383793],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Test histogram",
+ "products": [],
+ }
+ }
+ histograms = load_histogram(SAMPLE_HISTOGRAM)
+ parse_histograms.load_allowlist()
+
+ def test_parse():
+ return parse_histograms.Histogram(
+ "TEST_EMPTY_PRODUCTS",
+ histograms["TEST_EMPTY_PRODUCTS"],
+ strict_type_checks=True,
+ )
+
+ self.assertRaises(SystemExit, test_parse)
+
+ def test_products_all(self):
+ SAMPLE_HISTOGRAM = {
+ "TEST_HISTOGRAM_ALL_PRODUCTS": {
+ "record_in_processes": ["main", "content"],
+ "alert_emails": ["team@mozilla.xyz"],
+ "bug_numbers": [1383793],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Test histogram",
+ "products": ["all"],
+ }
+ }
+ histograms = load_histogram(SAMPLE_HISTOGRAM)
+ parse_histograms.load_allowlist()
+
+ parse_histograms.Histogram(
+ "TEST_HISTOGRAM_ALL_PRODUCTS",
+ histograms["TEST_HISTOGRAM_ALL_PRODUCTS"],
+ strict_type_checks=True,
+ )
+ self.assertRaises(SystemExit, ParserError.exit_func)
+
+ def test_gv_streaming_unsupported_kind(self):
+ SAMPLE_HISTOGRAM = {
+ "TEST_HISTOGRAM_GV_STREAMING": {
+ "record_in_processes": ["main", "content"],
+ "alert_emails": ["team@mozilla.xyz"],
+ "bug_numbers": [1383793],
+ "expires_in_version": "never",
+ "kind": "boolean",
+ "description": "Test histogram",
+ "products": ["geckoview_streaming"],
+ }
+ }
+ histograms = load_histogram(SAMPLE_HISTOGRAM)
+ parse_histograms.load_allowlist()
+ parse_histograms.Histogram(
+ "TEST_HISTOGRAM_GV_STREAMING",
+ histograms["TEST_HISTOGRAM_GV_STREAMING"],
+ strict_type_checks=True,
+ )
+ self.assertRaises(SystemExit, ParserError.exit_func)
+
+ def test_gv_streaming_keyed(self):
+ SAMPLE_HISTOGRAM = {
+ "TEST_HISTOGRAM_GV_STREAMING": {
+ "record_in_processes": ["main", "content"],
+ "alert_emails": ["team@mozilla.xyz"],
+ "bug_numbers": [1383793],
+ "expires_in_version": "never",
+ "kind": "exponential",
+ "low": 1024,
+ "high": 2**64,
+ "n_buckets": 100,
+ "keyed": "true",
+ "description": "Test histogram",
+ "products": ["geckoview_streaming"],
+ }
+ }
+ histograms = load_histogram(SAMPLE_HISTOGRAM)
+ parse_histograms.load_allowlist()
+ parse_histograms.Histogram(
+ "TEST_HISTOGRAM_GV_STREAMING",
+ histograms["TEST_HISTOGRAM_GV_STREAMING"],
+ strict_type_checks=True,
+ )
+
+ self.assertRaises(SystemExit, ParserError.exit_func)
+
+ def test_enumerated_histogram_with_100_buckets(self):
+ SAMPLE_HISTOGRAM = {
+ "TEST_100_BUCKETS_HISTOGRAM": {
+ "record_in_processes": ["main", "content", "socket", "utility"],
+ "alert_emails": ["team@mozilla.xyz"],
+ "bug_numbers": [1383793],
+ "expires_in_version": "never",
+ "kind": "enumerated",
+ "n_values": 100,
+ "products": ["firefox"],
+ "description": "Test histogram",
+ }
+ }
+ histograms = load_histogram(SAMPLE_HISTOGRAM)
+ parse_histograms.load_allowlist()
+
+ hist = parse_histograms.Histogram(
+ "TEST_100_BUCKETS_HISTOGRAM",
+ histograms["TEST_100_BUCKETS_HISTOGRAM"],
+ strict_type_checks=True,
+ )
+
+ ParserError.exit_func()
+ self.assertTrue(hist.expiration(), "never")
+ self.assertTrue(hist.kind(), "enumerated")
+ self.assertTrue(hist.n_buckets(), 101)
+ self.assertTrue(hist.record_in_processes, ["main", "content"])
+ self.assertTrue(hist.record_into_store, ["main"])
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/toolkit/components/telemetry/tests/python/test_parse_events.py b/toolkit/components/telemetry/tests/python/test_parse_events.py
new file mode 100644
index 0000000000..0b7b91efcc
--- /dev/null
+++ b/toolkit/components/telemetry/tests/python/test_parse_events.py
@@ -0,0 +1,166 @@
+# This Source Code Form is subject to the terms of Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+import sys
+import unittest
+from os import path
+
+import mozunit
+import yaml
+
+TELEMETRY_ROOT_PATH = path.abspath(
+ path.join(path.dirname(__file__), path.pardir, path.pardir)
+)
+sys.path.append(TELEMETRY_ROOT_PATH)
+# The parsers live in a subdirectory of "build_scripts", account for that.
+# NOTE: if the parsers are moved, this logic will need to be updated.
+sys.path.append(path.join(TELEMETRY_ROOT_PATH, "build_scripts"))
+from mozparsers import parse_events
+from mozparsers.shared_telemetry_utils import ParserError
+
+
+def load_event(event):
+ """Parse the passed event and return a dictionary
+
+ :param event: Event as YAML string
+ :returns: Parsed Event dictionary
+ """
+ return yaml.safe_load(event)
+
+
+class TestParser(unittest.TestCase):
+ def setUp(self):
+ def mockexit(x):
+ raise SystemExit(x)
+
+ self.oldexit = os._exit
+ os._exit = mockexit
+
+ def tearDown(self):
+ os._exit = self.oldexit
+
+ def test_valid_event_defaults(self):
+ SAMPLE_EVENT = """
+objects: ["object1", "object2"]
+bug_numbers: [12345]
+notification_emails: ["test01@mozilla.com", "test02@mozilla.com"]
+record_in_processes: ["main"]
+description: This is a test entry for Telemetry.
+products: ["firefox"]
+expiry_version: never
+"""
+ name = "test_event"
+ event = load_event(SAMPLE_EVENT)
+ evt = parse_events.EventData("CATEGORY", name, event, strict_type_checks=True)
+ ParserError.exit_func()
+
+ self.assertEqual(evt.methods, [name])
+ self.assertEqual(evt.record_in_processes, ["main"])
+ self.assertEqual(evt.objects, ["object1", "object2"])
+ self.assertEqual(evt.products, ["firefox"])
+ self.assertEqual(evt.operating_systems, ["all"])
+ self.assertEqual(evt.extra_keys, [])
+
+ def test_wrong_collection(self):
+ SAMPLE_EVENT = """
+objects: ["object1", "object2"]
+bug_numbers: [12345]
+notification_emails: ["test01@mozilla.com", "test02@mozilla.com"]
+record_in_processes: ["main"]
+description: This is a test entry for Telemetry.
+expiry_version: never
+products: ["firefox"]
+release_channel_collection: none
+"""
+ event = load_event(SAMPLE_EVENT)
+ parse_events.EventData("CATEGORY", "test_event", event, strict_type_checks=True)
+
+ self.assertRaises(SystemExit, ParserError.exit_func)
+
+ def test_valid_event_custom(self):
+ SAMPLE_EVENT = """
+methods: ["method1", "method2"]
+objects: ["object1", "object2"]
+bug_numbers: [12345]
+notification_emails: ["test01@mozilla.com", "test02@mozilla.com"]
+record_in_processes: ["content"]
+description: This is a test entry for Telemetry.
+expiry_version: never
+extra_keys:
+ key1: test1
+ key2: test2
+products:
+ - fennec
+operating_systems:
+ - windows
+"""
+ name = "test_event"
+ event = load_event(SAMPLE_EVENT)
+ evt = parse_events.EventData("CATEGORY", name, event, strict_type_checks=True)
+ ParserError.exit_func()
+
+ self.assertEqual(evt.methods, ["method1", "method2"])
+ self.assertEqual(evt.objects, ["object1", "object2"])
+ self.assertEqual(evt.record_in_processes, ["content"])
+ self.assertEqual(evt.products, ["fennec"])
+ self.assertEqual(evt.operating_systems, ["windows"])
+ self.assertEqual(sorted(evt.extra_keys), ["key1", "key2"])
+
+ def test_absent_products(self):
+ SAMPLE_EVENT = """
+methods: ["method1", "method2"]
+objects: ["object1", "object2"]
+bug_numbers: [12345]
+notification_emails: ["test01@mozilla.com", "test02@mozilla.com"]
+record_in_processes: ["content"]
+description: This is a test entry for Telemetry.
+expiry_version: never
+"""
+ event = load_event(SAMPLE_EVENT)
+ self.assertRaises(
+ SystemExit,
+ lambda: parse_events.EventData(
+ "CATEGORY", "test_event", event, strict_type_checks=True
+ ),
+ )
+
+ def test_empty_products(self):
+ SAMPLE_EVENT = """
+methods: ["method1", "method2"]
+objects: ["object1", "object2"]
+bug_numbers: [12345]
+notification_emails: ["test01@mozilla.com", "test02@mozilla.com"]
+record_in_processes: ["content"]
+description: This is a test entry for Telemetry.
+products: []
+expiry_version: never
+"""
+ event = load_event(SAMPLE_EVENT)
+ self.assertRaises(
+ SystemExit,
+ lambda: parse_events.EventData(
+ "CATEGORY", "test_event", event, strict_type_checks=True
+ ),
+ )
+
+ def test_geckoview_streaming_product(self):
+ SAMPLE_EVENT = """
+methods: ["method1", "method2"]
+objects: ["object1", "object2"]
+bug_numbers: [12345]
+notification_emails: ["test01@mozilla.com", "test02@mozilla.com"]
+record_in_processes: ["content"]
+description: This is a test entry for Telemetry.
+products: ["geckoview_streaming"]
+expiry_version: never
+"""
+ event = load_event(SAMPLE_EVENT)
+ parse_events.EventData("CATEGORY", "test_event", event, strict_type_checks=True)
+
+ self.assertRaises(SystemExit, ParserError.exit_func)
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/toolkit/components/telemetry/tests/python/test_parse_scalars.py b/toolkit/components/telemetry/tests/python/test_parse_scalars.py
new file mode 100644
index 0000000000..c699cdb4d8
--- /dev/null
+++ b/toolkit/components/telemetry/tests/python/test_parse_scalars.py
@@ -0,0 +1,267 @@
+# This Source Code Form is subject to the terms of Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import os
+import sys
+import unittest
+from os import path
+
+import mozunit
+import yaml
+
+TELEMETRY_ROOT_PATH = path.abspath(
+ path.join(path.dirname(__file__), path.pardir, path.pardir)
+)
+sys.path.append(TELEMETRY_ROOT_PATH)
+# The parsers live in a subdirectory of "build_scripts", account for that.
+# NOTE: if the parsers are moved, this logic will need to be updated.
+sys.path.append(path.join(TELEMETRY_ROOT_PATH, "build_scripts"))
+from mozparsers import parse_scalars
+from mozparsers.shared_telemetry_utils import ParserError
+
+
+def load_scalar(scalar):
+ """Parse the passed Scalar and return a dictionary
+
+ :param scalar: Scalar as YAML string
+ :returns: Parsed Scalar dictionary
+ """
+ return yaml.safe_load(scalar)
+
+
+class TestParser(unittest.TestCase):
+ def setUp(self):
+ def mockexit(x):
+ raise SystemExit(x)
+
+ self.oldexit = os._exit
+ os._exit = mockexit
+
+ def tearDown(self):
+ os._exit = self.oldexit
+
+ def test_valid_email_address(self):
+ SAMPLE_SCALAR_VALID_ADDRESSES = """
+description: A nice one-line description.
+expires: never
+record_in_processes:
+ - 'main'
+kind: uint
+notification_emails:
+ - test01@mozilla.com
+ - test02@mozilla.com
+products: ["firefox"]
+bug_numbers:
+ - 12345
+"""
+ scalar = load_scalar(SAMPLE_SCALAR_VALID_ADDRESSES)
+ sclr = parse_scalars.ScalarType(
+ "CATEGORY", "PROVE", scalar, strict_type_checks=True
+ )
+ ParserError.exit_func()
+
+ self.assertEqual(
+ sclr.notification_emails, ["test01@mozilla.com", "test02@mozilla.com"]
+ )
+
+ def test_invalid_email_address(self):
+ SAMPLE_SCALAR_INVALID_ADDRESSES = """
+description: A nice one-line description.
+expires: never
+record_in_processes:
+ - 'main'
+kind: uint
+notification_emails:
+ - test01@mozilla.com, test02@mozilla.com
+products: ["firefox"]
+bug_numbers:
+ - 12345
+"""
+ scalar = load_scalar(SAMPLE_SCALAR_INVALID_ADDRESSES)
+ parse_scalars.ScalarType("CATEGORY", "PROVE", scalar, strict_type_checks=True)
+
+ self.assertRaises(SystemExit, ParserError.exit_func)
+
+ def test_multistore_default(self):
+ SAMPLE_SCALAR = """
+description: A nice one-line description.
+expires: never
+record_in_processes:
+ - 'main'
+kind: uint
+notification_emails:
+ - test01@mozilla.com
+products: ["firefox"]
+bug_numbers:
+ - 12345
+"""
+ scalar = load_scalar(SAMPLE_SCALAR)
+ sclr = parse_scalars.ScalarType(
+ "CATEGORY", "PROVE", scalar, strict_type_checks=True
+ )
+ ParserError.exit_func()
+
+ self.assertEqual(sclr.record_into_store, ["main"])
+
+ def test_multistore_extended(self):
+ SAMPLE_SCALAR = """
+description: A nice one-line description.
+expires: never
+record_in_processes:
+ - 'main'
+kind: uint
+notification_emails:
+ - test01@mozilla.com
+bug_numbers:
+ - 12345
+products: ["firefox"]
+record_into_store:
+ - main
+ - sync
+"""
+ scalar = load_scalar(SAMPLE_SCALAR)
+ sclr = parse_scalars.ScalarType(
+ "CATEGORY", "PROVE", scalar, strict_type_checks=True
+ )
+ ParserError.exit_func()
+
+ self.assertEqual(sclr.record_into_store, ["main", "sync"])
+
+ def test_multistore_empty(self):
+ SAMPLE_SCALAR = """
+description: A nice one-line description.
+expires: never
+record_in_processes:
+ - 'main'
+kind: uint
+notification_emails:
+ - test01@mozilla.com
+bug_numbers:
+ - 12345
+products: ["firefox"]
+record_into_store: []
+"""
+ scalar = load_scalar(SAMPLE_SCALAR)
+ parse_scalars.ScalarType("CATEGORY", "PROVE", scalar, strict_type_checks=True)
+ self.assertRaises(SystemExit, ParserError.exit_func)
+
+ def test_operating_systems_default(self):
+ SAMPLE_SCALAR = """
+description: A nice one-line description.
+expires: never
+record_in_processes:
+ - 'main'
+kind: uint
+notification_emails:
+ - test01@mozilla.com
+products: ["firefox"]
+bug_numbers:
+ - 12345
+"""
+ scalar = load_scalar(SAMPLE_SCALAR)
+ sclr = parse_scalars.ScalarType(
+ "CATEGORY", "PROVE", scalar, strict_type_checks=True
+ )
+ ParserError.exit_func()
+
+ self.assertEqual(sclr.operating_systems, ["all"])
+
+ def test_operating_systems_custom(self):
+ SAMPLE_SCALAR = """
+description: A nice one-line description.
+expires: never
+record_in_processes:
+ - 'main'
+kind: uint
+notification_emails:
+ - test01@mozilla.com
+bug_numbers:
+ - 12345
+products: ["firefox"]
+operating_systems:
+ - windows
+"""
+ scalar = load_scalar(SAMPLE_SCALAR)
+ sclr = parse_scalars.ScalarType(
+ "CATEGORY", "PROVE", scalar, strict_type_checks=True
+ )
+ ParserError.exit_func()
+
+ self.assertEqual(sclr.operating_systems, ["windows"])
+
+ def test_operating_systems_empty(self):
+ SAMPLE_SCALAR = """
+description: A nice one-line description.
+expires: never
+record_in_processes:
+ - 'main'
+kind: uint
+notification_emails:
+ - test01@mozilla.com
+bug_numbers:
+ - 12345
+products: ["firefox"]
+operating_systems: []
+"""
+ scalar = load_scalar(SAMPLE_SCALAR)
+ parse_scalars.ScalarType("CATEGORY", "PROVE", scalar, strict_type_checks=True)
+ self.assertRaises(SystemExit, ParserError.exit_func)
+
+ def test_products_absent(self):
+ SAMPLE_SCALAR = """
+description: A nice one-line description.
+expires: never
+record_in_processes:
+ - 'main'
+kind: uint
+notification_emails:
+ - test01@mozilla.com
+bug_numbers:
+ - 12345
+"""
+
+ scalar = load_scalar(SAMPLE_SCALAR)
+ parse_scalars.ScalarType("CATEGORY", "PROVE", scalar, strict_type_checks=True)
+ self.assertRaises(SystemExit, ParserError.exit_func)
+
+ def test_products_empty(self):
+ SAMPLE_SCALAR = """
+description: A nice one-line description.
+expires: never
+record_in_processes:
+ - 'main'
+kind: uint
+notification_emails:
+ - test01@mozilla.com
+products: []
+bug_numbers:
+ - 12345
+"""
+
+ scalar = load_scalar(SAMPLE_SCALAR)
+ parse_scalars.ScalarType("CATEGORY", "PROVE", scalar, strict_type_checks=True)
+ self.assertRaises(SystemExit, ParserError.exit_func)
+
+ def test_gv_streaming_keyed(self):
+ SAMPLE_SCALAR = """
+description: A nice one-line description.
+expires: never
+record_in_processes:
+ - 'main'
+kind: uint
+notification_emails:
+ - test01@mozilla.com
+products: ['geckoview_streaming']
+keyed: true
+bug_numbers:
+ - 12345
+"""
+
+ scalar = load_scalar(SAMPLE_SCALAR)
+ parse_scalars.ScalarType("CATEGORY", "PROVE", scalar, strict_type_checks=True)
+ self.assertRaises(SystemExit, ParserError.exit_func)
+
+
+if __name__ == "__main__":
+ mozunit.main()
diff --git a/toolkit/components/telemetry/tests/unit/TelemetryArchiveTesting.sys.mjs b/toolkit/components/telemetry/tests/unit/TelemetryArchiveTesting.sys.mjs
new file mode 100644
index 0000000000..0ec25213a4
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/TelemetryArchiveTesting.sys.mjs
@@ -0,0 +1,76 @@
+import { TelemetryArchive } from "resource://gre/modules/TelemetryArchive.sys.mjs";
+
+function checkForProperties(ping, expected) {
+ for (let [props, val] of expected) {
+ let test = ping;
+ for (let prop of props) {
+ test = test[prop];
+ if (test === undefined) {
+ return false;
+ }
+ }
+ if (test !== val) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/**
+ * A helper object that allows test code to check whether a telemetry ping
+ * was properly saved. To use, first initialize to collect the starting pings
+ * and then check for new ping data.
+ */
+function Checker() {}
+Checker.prototype = {
+ promiseInit() {
+ this._pingMap = new Map();
+ return TelemetryArchive.promiseArchivedPingList().then(plist => {
+ for (let ping of plist) {
+ this._pingMap.set(ping.id, ping);
+ }
+ });
+ },
+
+ /**
+ * Find and return a new ping with certain properties.
+ *
+ * @param expected: an array of [['prop'...], 'value'] to check
+ * For example:
+ * [
+ * [['environment', 'build', 'applicationId'], '20150101010101'],
+ * [['version'], 1],
+ * [['metadata', 'OOMAllocationSize'], 123456789],
+ * ]
+ * @returns a matching ping if found, or null
+ */
+ async promiseFindPing(type, expected) {
+ let candidates = [];
+ let plist = await TelemetryArchive.promiseArchivedPingList();
+ for (let ping of plist) {
+ if (this._pingMap.has(ping.id)) {
+ continue;
+ }
+ if (ping.type == type) {
+ candidates.push(ping);
+ }
+ }
+
+ for (let candidate of candidates) {
+ let ping = await TelemetryArchive.promiseArchivedPingById(candidate.id);
+ if (checkForProperties(ping, expected)) {
+ return ping;
+ }
+ }
+ return null;
+ },
+};
+
+export const TelemetryArchiveTesting = {
+ setup() {
+ Services.prefs.setCharPref("toolkit.telemetry.log.level", "Trace");
+ Services.prefs.setBoolPref("toolkit.telemetry.archive.enabled", true);
+ },
+
+ Checker,
+};
diff --git a/toolkit/components/telemetry/tests/unit/TelemetryEnvironmentTesting.sys.mjs b/toolkit/components/telemetry/tests/unit/TelemetryEnvironmentTesting.sys.mjs
new file mode 100644
index 0000000000..0df6dfc2c1
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/TelemetryEnvironmentTesting.sys.mjs
@@ -0,0 +1,875 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
+
+const lazy = {};
+
+ChromeUtils.defineESModuleGetters(lazy, {
+ AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
+ Assert: "resource://testing-common/Assert.sys.mjs",
+ // AttributionCode is only needed for Firefox
+ AttributionCode: "resource:///modules/AttributionCode.sys.mjs",
+
+ MockRegistrar: "resource://testing-common/MockRegistrar.sys.mjs",
+});
+
+const gIsWindows = AppConstants.platform == "win";
+const gIsMac = AppConstants.platform == "macosx";
+const gIsAndroid = AppConstants.platform == "android";
+const gIsLinux = AppConstants.platform == "linux";
+
+const MILLISECONDS_PER_MINUTE = 60 * 1000;
+const MILLISECONDS_PER_HOUR = 60 * MILLISECONDS_PER_MINUTE;
+const MILLISECONDS_PER_DAY = 24 * MILLISECONDS_PER_HOUR;
+
+const PLATFORM_VERSION = "1.9.2";
+const APP_VERSION = "1";
+const APP_ID = "xpcshell@tests.mozilla.org";
+const APP_NAME = "XPCShell";
+
+const DISTRIBUTION_ID = "distributor-id";
+const DISTRIBUTION_VERSION = "4.5.6b";
+const DISTRIBUTOR_NAME = "Some Distributor";
+const DISTRIBUTOR_CHANNEL = "A Channel";
+const PARTNER_NAME = "test";
+const PARTNER_ID = "NicePartner-ID-3785";
+
+// The profile reset date, in milliseconds (Today)
+const PROFILE_RESET_DATE_MS = Date.now();
+// The profile creation date, in milliseconds (Yesterday).
+const PROFILE_FIRST_USE_MS = PROFILE_RESET_DATE_MS - MILLISECONDS_PER_DAY;
+const PROFILE_CREATION_DATE_MS = PROFILE_FIRST_USE_MS - MILLISECONDS_PER_DAY;
+
+const GFX_VENDOR_ID = "0xabcd";
+const GFX_DEVICE_ID = "0x1234";
+
+const EXPECTED_HDD_FIELDS = ["profile", "binary", "system"];
+
+// Valid attribution code to write so that settings.attribution can be tested.
+const ATTRIBUTION_CODE = "source%3Dgoogle.com%26dlsource%3Dunittest";
+
+function truncateToDays(aMsec) {
+ return Math.floor(aMsec / MILLISECONDS_PER_DAY);
+}
+
+var SysInfo = {
+ overrides: {},
+
+ getProperty(name) {
+ // Assert.ok(false, "Mock SysInfo: " + name + ", " + JSON.stringify(this.overrides));
+ if (name in this.overrides) {
+ return this.overrides[name];
+ }
+
+ return this._genuine.QueryInterface(Ci.nsIPropertyBag).getProperty(name);
+ },
+
+ getPropertyAsACString(name) {
+ return this.get(name);
+ },
+
+ getPropertyAsUint32(name) {
+ return this.get(name);
+ },
+
+ get(name) {
+ return this._genuine.QueryInterface(Ci.nsIPropertyBag2).get(name);
+ },
+
+ get diskInfo() {
+ return this._genuine.QueryInterface(Ci.nsISystemInfo).diskInfo;
+ },
+
+ get osInfo() {
+ return this._genuine.QueryInterface(Ci.nsISystemInfo).osInfo;
+ },
+
+ get processInfo() {
+ return this._genuine.QueryInterface(Ci.nsISystemInfo).processInfo;
+ },
+
+ QueryInterface: ChromeUtils.generateQI(["nsIPropertyBag2", "nsISystemInfo"]),
+};
+
+/**
+ * TelemetryEnvironmentTesting - tools for testing the telemetry environment
+ * reporting.
+ */
+export var TelemetryEnvironmentTesting = {
+ EXPECTED_HDD_FIELDS,
+
+ init(appInfo) {
+ this.appInfo = appInfo;
+ },
+
+ setSysInfoOverrides(overrides) {
+ SysInfo.overrides = overrides;
+ },
+
+ spoofGfxAdapter() {
+ try {
+ let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(
+ Ci.nsIGfxInfoDebug
+ );
+ gfxInfo.spoofVendorID(GFX_VENDOR_ID);
+ gfxInfo.spoofDeviceID(GFX_DEVICE_ID);
+ } catch (x) {
+ // If we can't test gfxInfo, that's fine, we'll note it later.
+ }
+ },
+
+ spoofProfileReset() {
+ return IOUtils.writeJSON(
+ PathUtils.join(PathUtils.profileDir, "times.json"),
+ {
+ created: PROFILE_CREATION_DATE_MS,
+ reset: PROFILE_RESET_DATE_MS,
+ firstUse: PROFILE_FIRST_USE_MS,
+ }
+ );
+ },
+
+ spoofPartnerInfo() {
+ let prefsToSpoof = {};
+ prefsToSpoof["distribution.id"] = DISTRIBUTION_ID;
+ prefsToSpoof["distribution.version"] = DISTRIBUTION_VERSION;
+ prefsToSpoof["app.distributor"] = DISTRIBUTOR_NAME;
+ prefsToSpoof["app.distributor.channel"] = DISTRIBUTOR_CHANNEL;
+ prefsToSpoof["app.partner.test"] = PARTNER_NAME;
+ prefsToSpoof["mozilla.partner.id"] = PARTNER_ID;
+
+ // Spoof the preferences.
+ for (let pref in prefsToSpoof) {
+ Services.prefs
+ .getDefaultBranch(null)
+ .setStringPref(pref, prefsToSpoof[pref]);
+ }
+ },
+
+ async spoofAttributionData() {
+ if (gIsWindows) {
+ 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) {
+ lazy.AttributionCode.attributionFile.remove(false);
+ lazy.AttributionCode._clearCache();
+ } else if (gIsMac) {
+ const { MacAttribution } = ChromeUtils.importESModule(
+ "resource:///modules/MacAttribution.sys.mjs"
+ );
+ await MacAttribution.delAttributionString();
+ }
+ },
+
+ registerFakeSysInfo() {
+ lazy.MockRegistrar.register("@mozilla.org/system-info;1", SysInfo);
+ },
+
+ /**
+ * Check that a value is a string and not empty.
+ *
+ * @param aValue The variable to check.
+ * @return True if |aValue| has type "string" and is not empty, False otherwise.
+ */
+ checkString(aValue) {
+ return typeof aValue == "string" && aValue != "";
+ },
+
+ /**
+ * If value is non-null, check if it's a valid string.
+ *
+ * @param aValue The variable to check.
+ * @return True if it's null or a valid string, false if it's non-null and an invalid
+ * string.
+ */
+ checkNullOrString(aValue) {
+ if (aValue) {
+ return this.checkString(aValue);
+ } else if (aValue === null) {
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * If value is non-null, check if it's a boolean.
+ *
+ * @param aValue The variable to check.
+ * @return True if it's null or a valid boolean, false if it's non-null and an invalid
+ * boolean.
+ */
+ checkNullOrBool(aValue) {
+ return aValue === null || typeof aValue == "boolean";
+ },
+
+ checkBuildSection(data) {
+ const expectedInfo = {
+ applicationId: APP_ID,
+ applicationName: APP_NAME,
+ buildId: this.appInfo.appBuildID,
+ version: APP_VERSION,
+ vendor: "Mozilla",
+ platformVersion: PLATFORM_VERSION,
+ xpcomAbi: "noarch-spidermonkey",
+ };
+
+ lazy.Assert.ok(
+ "build" in data,
+ "There must be a build section in Environment."
+ );
+
+ for (let f in expectedInfo) {
+ lazy.Assert.ok(
+ this.checkString(data.build[f]),
+ f + " must be a valid string."
+ );
+ lazy.Assert.equal(
+ data.build[f],
+ expectedInfo[f],
+ f + " must have the correct value."
+ );
+ }
+
+ // Make sure architecture is in the environment.
+ lazy.Assert.ok(this.checkString(data.build.architecture));
+
+ lazy.Assert.equal(
+ data.build.updaterAvailable,
+ AppConstants.MOZ_UPDATER,
+ "build.updaterAvailable must equal AppConstants.MOZ_UPDATER"
+ );
+ },
+
+ checkSettingsSection(data) {
+ const EXPECTED_FIELDS_TYPES = {
+ blocklistEnabled: "boolean",
+ e10sEnabled: "boolean",
+ e10sMultiProcesses: "number",
+ fissionEnabled: "boolean",
+ intl: "object",
+ locale: "string",
+ telemetryEnabled: "boolean",
+ update: "object",
+ userPrefs: "object",
+ };
+
+ lazy.Assert.ok(
+ "settings" in data,
+ "There must be a settings section in Environment."
+ );
+
+ for (let f in EXPECTED_FIELDS_TYPES) {
+ lazy.Assert.equal(
+ typeof data.settings[f],
+ EXPECTED_FIELDS_TYPES[f],
+ f + " must have the correct type."
+ );
+ }
+
+ // This property is not always present, but when it is, it must be a number.
+ if ("launcherProcessState" in data.settings) {
+ lazy.Assert.equal(typeof data.settings.launcherProcessState, "number");
+ }
+
+ // Check "addonCompatibilityCheckEnabled" separately.
+ lazy.Assert.equal(
+ data.settings.addonCompatibilityCheckEnabled,
+ lazy.AddonManager.checkCompatibility
+ );
+
+ // Check "isDefaultBrowser" separately, as it is not available on Android an can either be
+ // null or boolean on other platforms.
+ if (gIsAndroid) {
+ lazy.Assert.ok(
+ !("isDefaultBrowser" in data.settings),
+ "Must not be available on Android."
+ );
+ } else if ("isDefaultBrowser" in data.settings) {
+ // isDefaultBrowser might not be available in the payload, since it's
+ // gathered after the session was restored.
+ lazy.Assert.ok(this.checkNullOrBool(data.settings.isDefaultBrowser));
+ }
+
+ // Check "channel" separately, as it can either be null or string.
+ let update = data.settings.update;
+ lazy.Assert.ok(this.checkNullOrString(update.channel));
+ lazy.Assert.equal(typeof update.enabled, "boolean");
+ lazy.Assert.equal(typeof update.autoDownload, "boolean");
+ lazy.Assert.equal(typeof update.background, "boolean");
+
+ // Check sandbox settings exist and make sense
+ if (data.settings.sandbox.effectiveContentProcessLevel !== null) {
+ lazy.Assert.equal(
+ typeof data.settings.sandbox.effectiveContentProcessLevel,
+ "number",
+ "sandbox.effectiveContentProcessLevel must have the correct type"
+ );
+ }
+
+ if (data.settings.sandbox.contentWin32kLockdownState !== null) {
+ lazy.Assert.equal(
+ typeof data.settings.sandbox.contentWin32kLockdownState,
+ "number",
+ "sandbox.contentWin32kLockdownState must have the correct type"
+ );
+
+ let win32kLockdownState =
+ data.settings.sandbox.contentWin32kLockdownState;
+ lazy.Assert.ok(win32kLockdownState >= 1 && win32kLockdownState <= 17);
+ }
+
+ // Check "defaultSearchEngine" separately, as it can either be undefined or string.
+ if ("defaultSearchEngine" in data.settings) {
+ this.checkString(data.settings.defaultSearchEngine);
+ lazy.Assert.equal(typeof data.settings.defaultSearchEngineData, "object");
+ }
+
+ if ("defaultPrivateSearchEngineData" in data.settings) {
+ lazy.Assert.equal(
+ typeof data.settings.defaultPrivateSearchEngineData,
+ "object"
+ );
+ }
+
+ if ((gIsWindows || gIsMac) && AppConstants.MOZ_BUILD_APP == "browser") {
+ lazy.Assert.equal(typeof data.settings.attribution, "object");
+ lazy.Assert.equal(data.settings.attribution.source, "google.com");
+ lazy.Assert.equal(data.settings.attribution.dlsource, "unittest");
+ }
+
+ this.checkIntlSettings(data.settings);
+ },
+
+ checkIntlSettings({ intl }) {
+ let fields = [
+ "requestedLocales",
+ "availableLocales",
+ "appLocales",
+ "acceptLanguages",
+ ];
+
+ for (let field of fields) {
+ lazy.Assert.ok(Array.isArray(intl[field]), `${field} is an array`);
+ }
+
+ // These fields may be null if they aren't ready yet. This is mostly to deal
+ // with test failures on Android, but they aren't guaranteed to exist.
+ let optionalFields = ["systemLocales", "regionalPrefsLocales"];
+
+ for (let field of optionalFields) {
+ let isArray = Array.isArray(intl[field]);
+ let isNull = intl[field] === null;
+ lazy.Assert.ok(isArray || isNull, `${field} is an array or null`);
+ }
+ },
+
+ checkProfileSection(data) {
+ lazy.Assert.ok(
+ "profile" in data,
+ "There must be a profile section in Environment."
+ );
+ lazy.Assert.equal(
+ data.profile.creationDate,
+ truncateToDays(PROFILE_CREATION_DATE_MS)
+ );
+ lazy.Assert.equal(
+ data.profile.resetDate,
+ truncateToDays(PROFILE_RESET_DATE_MS)
+ );
+ lazy.Assert.equal(
+ data.profile.firstUseDate,
+ truncateToDays(PROFILE_FIRST_USE_MS)
+ );
+ },
+
+ checkPartnerSection(data, isInitial) {
+ const EXPECTED_FIELDS = {
+ distributionId: DISTRIBUTION_ID,
+ distributionVersion: DISTRIBUTION_VERSION,
+ partnerId: PARTNER_ID,
+ distributor: DISTRIBUTOR_NAME,
+ distributorChannel: DISTRIBUTOR_CHANNEL,
+ };
+
+ lazy.Assert.ok(
+ "partner" in data,
+ "There must be a partner section in Environment."
+ );
+
+ for (let f in EXPECTED_FIELDS) {
+ let expected = isInitial ? null : EXPECTED_FIELDS[f];
+ lazy.Assert.strictEqual(
+ data.partner[f],
+ expected,
+ f + " must have the correct value."
+ );
+ }
+
+ // Check that "partnerNames" exists and contains the correct element.
+ lazy.Assert.ok(Array.isArray(data.partner.partnerNames));
+ if (isInitial) {
+ lazy.Assert.equal(data.partner.partnerNames.length, 0);
+ } else {
+ lazy.Assert.ok(data.partner.partnerNames.includes(PARTNER_NAME));
+ }
+ },
+
+ checkGfxAdapter(data) {
+ const EXPECTED_ADAPTER_FIELDS_TYPES = {
+ description: "string",
+ vendorID: "string",
+ deviceID: "string",
+ subsysID: "string",
+ RAM: "number",
+ driver: "string",
+ driverVendor: "string",
+ driverVersion: "string",
+ driverDate: "string",
+ GPUActive: "boolean",
+ };
+
+ for (let f in EXPECTED_ADAPTER_FIELDS_TYPES) {
+ lazy.Assert.ok(f in data, f + " must be available.");
+
+ if (data[f]) {
+ // Since we have a non-null value, check if it has the correct type.
+ lazy.Assert.equal(
+ typeof data[f],
+ EXPECTED_ADAPTER_FIELDS_TYPES[f],
+ f + " must have the correct type."
+ );
+ }
+ }
+ },
+
+ checkSystemSection(data, assertProcessData) {
+ const EXPECTED_FIELDS = [
+ "memoryMB",
+ "cpu",
+ "os",
+ "hdd",
+ "gfx",
+ "appleModelId",
+ ];
+
+ lazy.Assert.ok(
+ "system" in data,
+ "There must be a system section in Environment."
+ );
+
+ // Make sure we have all the top level sections and fields.
+ for (let f of EXPECTED_FIELDS) {
+ lazy.Assert.ok(f in data.system, f + " must be available.");
+ }
+
+ lazy.Assert.ok(
+ Number.isFinite(data.system.memoryMB),
+ "MemoryMB must be a number."
+ );
+
+ if (assertProcessData) {
+ if (gIsWindows || gIsMac || gIsLinux) {
+ let EXTRA_CPU_FIELDS = [
+ "cores",
+ "model",
+ "family",
+ "stepping",
+ "l2cacheKB",
+ "l3cacheKB",
+ "speedMHz",
+ "vendor",
+ "name",
+ ];
+
+ for (let f of EXTRA_CPU_FIELDS) {
+ // Note this is testing TelemetryEnvironment.js only, not that the
+ // values are valid - null is the fallback.
+ lazy.Assert.ok(
+ f in data.system.cpu,
+ f + " must be available under cpu."
+ );
+ }
+
+ if (gIsWindows) {
+ lazy.Assert.equal(
+ typeof data.system.isWow64,
+ "boolean",
+ "isWow64 must be available on Windows and have the correct type."
+ );
+ lazy.Assert.equal(
+ typeof data.system.isWowARM64,
+ "boolean",
+ "isWowARM64 must be available on Windows and have the correct type."
+ );
+ lazy.Assert.equal(
+ typeof data.system.hasWinPackageId,
+ "boolean",
+ "hasWinPackageId must be available on Windows and have the correct type."
+ );
+ // This is only sent for Mozilla produced MSIX packages
+ lazy.Assert.ok(
+ !("winPackageFamilyName" in data.system) ||
+ data.system.winPackageFamilyName === null ||
+ typeof data.system.winPackageFamilyName === "string",
+ "winPackageFamilyName must be a string if non null"
+ );
+ lazy.Assert.ok(
+ "virtualMaxMB" in data.system,
+ "virtualMaxMB must be available."
+ );
+ lazy.Assert.ok(
+ Number.isFinite(data.system.virtualMaxMB),
+ "virtualMaxMB must be a number."
+ );
+
+ for (let f of [
+ "count",
+ "model",
+ "family",
+ "stepping",
+ "l2cacheKB",
+ "l3cacheKB",
+ "speedMHz",
+ ]) {
+ lazy.Assert.ok(
+ Number.isFinite(data.system.cpu[f]),
+ f + " must be a number if non null."
+ );
+ }
+ }
+
+ // These should be numbers if they are not null
+ for (let f of [
+ "count",
+ "model",
+ "family",
+ "stepping",
+ "l2cacheKB",
+ "l3cacheKB",
+ "speedMHz",
+ ]) {
+ lazy.Assert.ok(
+ !(f in data.system.cpu) ||
+ data.system.cpu[f] === null ||
+ Number.isFinite(data.system.cpu[f]),
+ f + " must be a number if non null."
+ );
+ }
+
+ // We insist these are available
+ for (let f of ["cores"]) {
+ lazy.Assert.ok(
+ !(f in data.system.cpu) || Number.isFinite(data.system.cpu[f]),
+ f + " must be a number if non null."
+ );
+ }
+ }
+ }
+
+ let cpuData = data.system.cpu;
+
+ lazy.Assert.ok(
+ Array.isArray(cpuData.extensions),
+ "CPU extensions must be available."
+ );
+
+ let osData = data.system.os;
+ lazy.Assert.ok(this.checkNullOrString(osData.name));
+ lazy.Assert.ok(this.checkNullOrString(osData.version));
+ lazy.Assert.ok(this.checkNullOrString(osData.locale));
+
+ // Service pack is only available on Windows.
+ if (gIsWindows) {
+ lazy.Assert.ok(
+ Number.isFinite(osData.servicePackMajor),
+ "ServicePackMajor must be a number."
+ );
+ lazy.Assert.ok(
+ Number.isFinite(osData.servicePackMinor),
+ "ServicePackMinor must be a number."
+ );
+ if ("windowsBuildNumber" in osData) {
+ // This might not be available on all Windows platforms.
+ lazy.Assert.ok(
+ Number.isFinite(osData.windowsBuildNumber),
+ "windowsBuildNumber must be a number."
+ );
+ }
+ if ("windowsUBR" in osData) {
+ // This might not be available on all Windows platforms.
+ lazy.Assert.ok(
+ osData.windowsUBR === null || Number.isFinite(osData.windowsUBR),
+ "windowsUBR must be null or a number."
+ );
+ }
+ } else if (gIsAndroid) {
+ lazy.Assert.ok(this.checkNullOrString(osData.kernelVersion));
+ } else if (gIsLinux) {
+ lazy.Assert.ok(this.checkNullOrString(osData.distro));
+ lazy.Assert.ok(this.checkNullOrString(osData.distroVersion));
+ }
+
+ for (let disk of EXPECTED_HDD_FIELDS) {
+ lazy.Assert.ok(this.checkNullOrString(data.system.hdd[disk].model));
+ lazy.Assert.ok(this.checkNullOrString(data.system.hdd[disk].revision));
+ lazy.Assert.ok(this.checkNullOrString(data.system.hdd[disk].type));
+ }
+
+ let gfxData = data.system.gfx;
+ lazy.Assert.ok("D2DEnabled" in gfxData);
+ lazy.Assert.ok("DWriteEnabled" in gfxData);
+ lazy.Assert.ok("Headless" in gfxData);
+ lazy.Assert.ok("TargetFrameRate" in gfxData);
+ lazy.Assert.equal(typeof gfxData.TargetFrameRate, "number");
+ lazy.Assert.ok("EmbeddedInFirefoxReality" in gfxData);
+ // DWriteVersion is disabled due to main thread jank and will be enabled
+ // again as part of bug 1154500.
+ // Assert.ok("DWriteVersion" in gfxData);
+ if (gIsWindows) {
+ lazy.Assert.equal(typeof gfxData.D2DEnabled, "boolean");
+ lazy.Assert.equal(typeof gfxData.DWriteEnabled, "boolean");
+ lazy.Assert.equal(typeof gfxData.EmbeddedInFirefoxReality, "boolean");
+ // As above, will be enabled again as part of bug 1154500.
+ // Assert.ok(this.checkString(gfxData.DWriteVersion));
+ }
+
+ lazy.Assert.ok("adapters" in gfxData);
+ lazy.Assert.ok(
+ !!gfxData.adapters.length,
+ "There must be at least one GFX adapter."
+ );
+ for (let adapter of gfxData.adapters) {
+ this.checkGfxAdapter(adapter);
+ }
+ lazy.Assert.equal(typeof gfxData.adapters[0].GPUActive, "boolean");
+ lazy.Assert.ok(
+ gfxData.adapters[0].GPUActive,
+ "The first GFX adapter must be active."
+ );
+
+ lazy.Assert.ok(Array.isArray(gfxData.monitors));
+ if (gIsWindows || gIsMac || gIsLinux) {
+ lazy.Assert.ok(
+ gfxData.monitors.length >= 1,
+ "There is at least one monitor."
+ );
+ lazy.Assert.equal(typeof gfxData.monitors[0].screenWidth, "number");
+ lazy.Assert.equal(typeof gfxData.monitors[0].screenHeight, "number");
+ if (gIsWindows) {
+ lazy.Assert.equal(typeof gfxData.monitors[0].refreshRate, "number");
+ lazy.Assert.equal(typeof gfxData.monitors[0].pseudoDisplay, "boolean");
+ }
+ if (gIsMac) {
+ lazy.Assert.equal(typeof gfxData.monitors[0].scale, "number");
+ }
+ }
+
+ lazy.Assert.equal(typeof gfxData.features, "object");
+ lazy.Assert.equal(typeof gfxData.features.compositor, "string");
+
+ lazy.Assert.equal(typeof gfxData.features.gpuProcess, "object");
+ lazy.Assert.equal(typeof gfxData.features.gpuProcess.status, "string");
+
+ try {
+ // If we've not got nsIGfxInfoDebug, then this will throw and stop us doing
+ // this test.
+ let gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(
+ Ci.nsIGfxInfoDebug
+ );
+
+ if (gIsWindows || gIsMac) {
+ lazy.Assert.equal(GFX_VENDOR_ID, gfxData.adapters[0].vendorID);
+ lazy.Assert.equal(GFX_DEVICE_ID, gfxData.adapters[0].deviceID);
+ }
+
+ let features = gfxInfo.getFeatures();
+ lazy.Assert.equal(features.compositor, gfxData.features.compositor);
+ lazy.Assert.equal(
+ features.gpuProcess.status,
+ gfxData.features.gpuProcess.status
+ );
+ lazy.Assert.equal(features.opengl, gfxData.features.opengl);
+ lazy.Assert.equal(features.webgl, gfxData.features.webgl);
+ } catch (e) {}
+
+ if (gIsMac) {
+ lazy.Assert.ok(this.checkString(data.system.appleModelId));
+ } else {
+ lazy.Assert.ok(this.checkNullOrString(data.system.appleModelId));
+ }
+
+ // This feature is only available on Windows
+ if (AppConstants.platform == "win") {
+ lazy.Assert.ok(
+ "sec" in data.system,
+ "sec must be available under data.system"
+ );
+
+ let SEC_FIELDS = ["antivirus", "antispyware", "firewall"];
+ for (let f of SEC_FIELDS) {
+ lazy.Assert.ok(
+ f in data.system.sec,
+ f + " must be available under data.system.sec"
+ );
+
+ let value = data.system.sec[f];
+ // value is null on Windows Server
+ lazy.Assert.ok(
+ value === null || Array.isArray(value),
+ f + " must be either null or an array"
+ );
+ if (Array.isArray(value)) {
+ for (let product of value) {
+ lazy.Assert.equal(
+ typeof product,
+ "string",
+ "Each element of " + f + " must be a string"
+ );
+ }
+ }
+ }
+ }
+ },
+
+ checkActiveAddon(data, partialRecord) {
+ let signedState = "number";
+ // system add-ons have an undefined signState
+ if (data.isSystem) {
+ signedState = "undefined";
+ }
+
+ const EXPECTED_ADDON_FIELDS_TYPES = {
+ version: "string",
+ scope: "number",
+ type: "string",
+ updateDay: "number",
+ isSystem: "boolean",
+ isWebExtension: "boolean",
+ multiprocessCompatible: "boolean",
+ };
+
+ const FULL_ADDON_FIELD_TYPES = {
+ blocklisted: "boolean",
+ name: "string",
+ userDisabled: "boolean",
+ appDisabled: "boolean",
+ foreignInstall: "boolean",
+ hasBinaryComponents: "boolean",
+ installDay: "number",
+ signedState,
+ };
+
+ let fields = EXPECTED_ADDON_FIELDS_TYPES;
+ if (!partialRecord) {
+ fields = Object.assign({}, fields, FULL_ADDON_FIELD_TYPES);
+ }
+
+ for (let [name, type] of Object.entries(fields)) {
+ lazy.Assert.ok(name in data, name + " must be available.");
+ lazy.Assert.equal(
+ typeof data[name],
+ type,
+ name + " must have the correct type."
+ );
+ }
+
+ if (!partialRecord) {
+ // We check "description" separately, as it can be null.
+ lazy.Assert.ok(this.checkNullOrString(data.description));
+ }
+ },
+
+ checkTheme(data) {
+ const EXPECTED_THEME_FIELDS_TYPES = {
+ id: "string",
+ blocklisted: "boolean",
+ name: "string",
+ userDisabled: "boolean",
+ appDisabled: "boolean",
+ version: "string",
+ scope: "number",
+ foreignInstall: "boolean",
+ installDay: "number",
+ updateDay: "number",
+ };
+
+ for (let f in EXPECTED_THEME_FIELDS_TYPES) {
+ lazy.Assert.ok(f in data, f + " must be available.");
+ lazy.Assert.equal(
+ typeof data[f],
+ EXPECTED_THEME_FIELDS_TYPES[f],
+ f + " must have the correct type."
+ );
+ }
+
+ // We check "description" separately, as it can be null.
+ lazy.Assert.ok(this.checkNullOrString(data.description));
+ },
+
+ checkActiveGMPlugin(data) {
+ // GMP plugin version defaults to null until GMPDownloader runs to update it.
+ if (data.version) {
+ lazy.Assert.equal(typeof data.version, "string");
+ }
+ lazy.Assert.equal(typeof data.userDisabled, "boolean");
+ lazy.Assert.equal(typeof data.applyBackgroundUpdates, "number");
+ },
+
+ checkAddonsSection(data, expectBrokenAddons, partialAddonsRecords) {
+ const EXPECTED_FIELDS = ["activeAddons", "theme", "activeGMPlugins"];
+
+ lazy.Assert.ok(
+ "addons" in data,
+ "There must be an addons section in Environment."
+ );
+ for (let f of EXPECTED_FIELDS) {
+ lazy.Assert.ok(f in data.addons, f + " must be available.");
+ }
+
+ // Check the active addons, if available.
+ if (!expectBrokenAddons) {
+ let activeAddons = data.addons.activeAddons;
+ for (let addon in activeAddons) {
+ this.checkActiveAddon(activeAddons[addon], partialAddonsRecords);
+ }
+ }
+
+ // Check "theme" structure.
+ if (Object.keys(data.addons.theme).length !== 0) {
+ this.checkTheme(data.addons.theme);
+ }
+
+ // Check active GMPlugins
+ let activeGMPlugins = data.addons.activeGMPlugins;
+ for (let gmPlugin in activeGMPlugins) {
+ this.checkActiveGMPlugin(activeGMPlugins[gmPlugin]);
+ }
+ },
+
+ checkEnvironmentData(data, options = {}) {
+ const {
+ isInitial = false,
+ expectBrokenAddons = false,
+ assertProcessData = false,
+ } = options;
+
+ this.checkBuildSection(data);
+ this.checkSettingsSection(data);
+ this.checkProfileSection(data);
+ this.checkPartnerSection(data, isInitial);
+ this.checkSystemSection(data, assertProcessData);
+ this.checkAddonsSection(data, expectBrokenAddons);
+ },
+};
diff --git a/toolkit/components/telemetry/tests/unit/data/search-extensions/engines.json b/toolkit/components/telemetry/tests/unit/data/search-extensions/engines.json
new file mode 100644
index 0000000000..d745558b73
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/data/search-extensions/engines.json
@@ -0,0 +1,14 @@
+{
+ "data": [
+ {
+ "webExtension": {
+ "id": "telemetrySearchIdentifier@search.mozilla.org"
+ },
+ "appliesTo": [
+ {
+ "included": { "everywhere": true }
+ }
+ ]
+ }
+ ]
+}
diff --git a/toolkit/components/telemetry/tests/unit/data/search-extensions/telemetrySearchIdentifier/manifest.json b/toolkit/components/telemetry/tests/unit/data/search-extensions/telemetrySearchIdentifier/manifest.json
new file mode 100644
index 0000000000..b0b949b635
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/data/search-extensions/telemetrySearchIdentifier/manifest.json
@@ -0,0 +1,29 @@
+{
+ "name": "telemetrySearchIdentifier",
+ "manifest_version": 2,
+ "version": "1.0",
+ "description": "telemetrySearchIdentifier",
+ "browser_specific_settings": {
+ "gecko": {
+ "id": "telemetrySearchIdentifier@search.mozilla.org"
+ }
+ },
+ "hidden": true,
+ "chrome_settings_overrides": {
+ "search_provider": {
+ "name": "telemetrySearchIdentifier",
+ "search_url": "https://ar.wikipedia.org/wiki/%D8%AE%D8%A7%D8%B5:%D8%A8%D8%AD%D8%AB",
+ "params": [
+ {
+ "name": "search",
+ "value": "{searchTerms}"
+ },
+ {
+ "name": "sourceId",
+ "value": "Mozilla-search"
+ }
+ ],
+ "suggest_url": "https://ar.wikipedia.org/w/api.php?action=opensearch&search={searchTerms}"
+ }
+ }
+}
diff --git a/toolkit/components/telemetry/tests/unit/engine.xml b/toolkit/components/telemetry/tests/unit/engine.xml
new file mode 100644
index 0000000000..2304fcdd7b
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/engine.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine-telemetry</ShortName>
+<Url type="text/html" method="GET" template="http://www.example.com/search">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/toolkit/components/telemetry/tests/unit/file_UninstallPing.worker.js b/toolkit/components/telemetry/tests/unit/file_UninstallPing.worker.js
new file mode 100644
index 0000000000..6bb9e6582f
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/file_UninstallPing.worker.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/. */
+
+/* import-globals-from /toolkit/components/workerloader/require.js */
+importScripts("resource://gre/modules/workers/require.js");
+
+const PromiseWorker = require("resource://gre/modules/workers/PromiseWorker.js");
+
+const Agent = {
+ _file: null,
+ open(path) {
+ this._file = IOUtils.openFileForSyncReading(path);
+ },
+ close() {
+ this._file.close();
+ },
+};
+
+// This boilerplate connects the PromiseWorker to the Agent so
+// that messages from the main thread map to methods on the
+// Agent.
+const worker = new PromiseWorker.AbstractWorker();
+worker.dispatch = function (method, args = []) {
+ return Agent[method](...args);
+};
+worker.postMessage = function (result, ...transfers) {
+ self.postMessage(result, ...transfers);
+};
+worker.close = function () {
+ self.close();
+};
+self.addEventListener("message", msg => worker.handleMessage(msg));
+self.addEventListener("unhandledrejection", function (error) {
+ throw error.reason;
+});
diff --git a/toolkit/components/telemetry/tests/unit/head.js b/toolkit/components/telemetry/tests/unit/head.js
new file mode 100644
index 0000000000..7a9d8e41a2
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/head.js
@@ -0,0 +1,582 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ AddonTestUtils: "resource://testing-common/AddonTestUtils.sys.mjs",
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
+ HttpServer: "resource://testing-common/httpd.sys.mjs",
+ Log: "resource://gre/modules/Log.sys.mjs",
+ NetUtil: "resource://gre/modules/NetUtil.sys.mjs",
+ TelemetryController: "resource://gre/modules/TelemetryController.sys.mjs",
+ TelemetryScheduler: "resource://gre/modules/TelemetryScheduler.sys.mjs",
+ TelemetrySend: "resource://gre/modules/TelemetrySend.sys.mjs",
+ TelemetryStorage: "resource://gre/modules/TelemetryStorage.sys.mjs",
+ TelemetryUtils: "resource://gre/modules/TelemetryUtils.sys.mjs",
+});
+
+const gIsWindows = AppConstants.platform == "win";
+const gIsMac = AppConstants.platform == "macosx";
+const gIsAndroid = AppConstants.platform == "android";
+const gIsLinux = AppConstants.platform == "linux";
+
+// Desktop Firefox, ie. not mobile Firefox or Thunderbird.
+const gIsFirefox = AppConstants.MOZ_APP_NAME == "firefox";
+
+const Telemetry = Services.telemetry;
+
+const MILLISECONDS_PER_MINUTE = 60 * 1000;
+const MILLISECONDS_PER_HOUR = 60 * MILLISECONDS_PER_MINUTE;
+const MILLISECONDS_PER_DAY = 24 * MILLISECONDS_PER_HOUR;
+
+const UUID_REGEX =
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
+
+var gGlobalScope = this;
+
+const PingServer = {
+ _httpServer: null,
+ _started: false,
+ _defers: [Promise.withResolvers()],
+ _currentDeferred: 0,
+ _logger: null,
+
+ get port() {
+ return this._httpServer.identity.primaryPort;
+ },
+
+ get host() {
+ return this._httpServer.identity.primaryHost;
+ },
+
+ get started() {
+ return this._started;
+ },
+
+ get _log() {
+ if (!this._logger) {
+ this._logger = Log.repository.getLoggerWithMessagePrefix(
+ "Toolkit.Telemetry",
+ "PingServer::"
+ );
+ }
+
+ return this._logger;
+ },
+
+ registerPingHandler(handler) {
+ const wrapped = wrapWithExceptionHandler(handler);
+ this._httpServer.registerPrefixHandler("/submit/telemetry/", wrapped);
+ },
+
+ resetPingHandler() {
+ this.registerPingHandler((request, response) => {
+ let r = request;
+ this._log.trace(
+ `defaultPingHandler() - ${r.method} ${r.scheme}://${r.host}:${r.port}${r.path}`
+ );
+ let deferred = this._defers[this._defers.length - 1];
+ this._defers.push(Promise.withResolvers());
+ deferred.resolve(request);
+ });
+ },
+
+ start() {
+ this._httpServer = new HttpServer();
+ this._httpServer.start(-1);
+ this._started = true;
+ this.clearRequests();
+ this.resetPingHandler();
+ },
+
+ stop() {
+ return new Promise(resolve => {
+ this._httpServer.stop(resolve);
+ this._started = false;
+ });
+ },
+
+ clearRequests() {
+ this._defers = [Promise.withResolvers()];
+ this._currentDeferred = 0;
+ },
+
+ promiseNextRequest() {
+ const deferred = this._defers[this._currentDeferred++];
+ // Send the ping to the consumer on the next tick, so that the completion gets
+ // signaled to Telemetry.
+ return new Promise(r =>
+ Services.tm.dispatchToMainThread(() => r(deferred.promise))
+ );
+ },
+
+ promiseNextPing() {
+ return this.promiseNextRequest().then(request =>
+ decodeRequestPayload(request)
+ );
+ },
+
+ async promiseNextRequests(count) {
+ let results = [];
+ for (let i = 0; i < count; ++i) {
+ results.push(await this.promiseNextRequest());
+ }
+
+ return results;
+ },
+
+ promiseNextPings(count) {
+ return this.promiseNextRequests(count).then(requests => {
+ return Array.from(requests, decodeRequestPayload);
+ });
+ },
+};
+
+/**
+ * Decode the payload of an HTTP request into a ping.
+ * @param {Object} request The data representing an HTTP request (nsIHttpRequest).
+ * @return {Object} The decoded ping payload.
+ */
+function decodeRequestPayload(request) {
+ let s = request.bodyInputStream;
+ let payload = null;
+
+ if (
+ request.hasHeader("content-encoding") &&
+ request.getHeader("content-encoding") == "gzip"
+ ) {
+ let observer = {
+ buffer: "",
+ onStreamComplete(loader, context, status, length, result) {
+ // String.fromCharCode can only deal with 500,000 characters
+ // at a time, so chunk the result into parts of that size.
+ const chunkSize = 500000;
+ for (let offset = 0; offset < result.length; offset += chunkSize) {
+ this.buffer += String.fromCharCode.apply(
+ String,
+ result.slice(offset, offset + chunkSize)
+ );
+ }
+ },
+ };
+
+ let scs = Cc["@mozilla.org/streamConverters;1"].getService(
+ Ci.nsIStreamConverterService
+ );
+ let listener = Cc["@mozilla.org/network/stream-loader;1"].createInstance(
+ Ci.nsIStreamLoader
+ );
+ listener.init(observer);
+ let converter = scs.asyncConvertData(
+ "gzip",
+ "uncompressed",
+ listener,
+ null
+ );
+ converter.onStartRequest(null, null);
+ converter.onDataAvailable(null, s, 0, s.available());
+ converter.onStopRequest(null, null, null);
+ let unicodeConverter = Cc[
+ "@mozilla.org/intl/scriptableunicodeconverter"
+ ].createInstance(Ci.nsIScriptableUnicodeConverter);
+ unicodeConverter.charset = "UTF-8";
+ let utf8string = unicodeConverter.ConvertToUnicode(observer.buffer);
+ utf8string += unicodeConverter.Finish();
+ payload = JSON.parse(utf8string);
+ } else {
+ let bytes = NetUtil.readInputStream(s, s.available());
+ payload = JSON.parse(new TextDecoder().decode(bytes));
+ }
+
+ if (payload && "clientId" in payload) {
+ // Check for canary value
+ Assert.notEqual(
+ TelemetryUtils.knownClientID,
+ payload.clientId,
+ `Known clientId shouldn't appear in a "${payload.type}" ping on the server.`
+ );
+ }
+
+ return payload;
+}
+
+function checkPingFormat(aPing, aType, aHasClientId, aHasEnvironment) {
+ const PING_FORMAT_VERSION = 4;
+ const MANDATORY_PING_FIELDS = [
+ "type",
+ "id",
+ "creationDate",
+ "version",
+ "application",
+ "payload",
+ ];
+
+ const APPLICATION_TEST_DATA = {
+ buildId: gAppInfo.appBuildID,
+ name: APP_NAME,
+ version: APP_VERSION,
+ displayVersion: AppConstants.MOZ_APP_VERSION_DISPLAY,
+ vendor: "Mozilla",
+ platformVersion: PLATFORM_VERSION,
+ xpcomAbi: "noarch-spidermonkey",
+ };
+
+ // Check that the ping contains all the mandatory fields.
+ for (let f of MANDATORY_PING_FIELDS) {
+ Assert.ok(f in aPing, f + " must be available.");
+ }
+
+ Assert.equal(aPing.type, aType, "The ping must have the correct type.");
+ Assert.equal(
+ aPing.version,
+ PING_FORMAT_VERSION,
+ "The ping must have the correct version."
+ );
+
+ // Test the application section.
+ for (let f in APPLICATION_TEST_DATA) {
+ Assert.equal(
+ aPing.application[f],
+ APPLICATION_TEST_DATA[f],
+ f + " must have the correct value."
+ );
+ }
+
+ // We can't check the values for channel and architecture. Just make
+ // sure they are in.
+ Assert.ok(
+ "architecture" in aPing.application,
+ "The application section must have an architecture field."
+ );
+ Assert.ok(
+ "channel" in aPing.application,
+ "The application section must have a channel field."
+ );
+
+ // Check the clientId and environment fields, as needed.
+ Assert.equal("clientId" in aPing, aHasClientId);
+ Assert.equal("environment" in aPing, aHasEnvironment);
+}
+
+function wrapWithExceptionHandler(f) {
+ function wrapper(...args) {
+ try {
+ f(...args);
+ } catch (ex) {
+ if (typeof ex != "object") {
+ throw ex;
+ }
+ dump("Caught exception: " + ex.message + "\n");
+ dump(ex.stack);
+ do_test_finished();
+ }
+ }
+ return wrapper;
+}
+
+async function loadAddonManager(...args) {
+ AddonTestUtils.init(gGlobalScope);
+ AddonTestUtils.overrideCertDB();
+ createAppInfo(...args);
+
+ // As we're not running in application, we need to setup the features directory
+ // used by system add-ons.
+ const distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "app0"]);
+ AddonTestUtils.registerDirectory("XREAppFeat", distroDir);
+ await AddonTestUtils.overrideBuiltIns({
+ system: ["tel-system-xpi@tests.mozilla.org"],
+ });
+ return AddonTestUtils.promiseStartupManager();
+}
+
+function finishAddonManagerStartup() {
+ Services.obs.notifyObservers(null, "test-load-xpi-database");
+}
+
+var gAppInfo = null;
+
+function createAppInfo(
+ ID = APP_ID,
+ name = APP_NAME,
+ version = APP_VERSION,
+ platformVersion = PLATFORM_VERSION
+) {
+ AddonTestUtils.createAppInfo(ID, name, version, platformVersion);
+ gAppInfo = AddonTestUtils.appInfo;
+}
+
+// Fake the timeout functions for the TelemetryScheduler.
+function fakeSchedulerTimer(set, clear) {
+ const { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryScheduler.sys.mjs"
+ );
+ Policy.setSchedulerTickTimeout = set;
+ Policy.clearSchedulerTickTimeout = clear;
+}
+
+/* global TelemetrySession:false, TelemetryEnvironment:false, TelemetryController:false,
+ TelemetryStorage:false, TelemetrySend:false, TelemetryReportingPolicy:false
+ */
+
+/**
+ * Fake the current date.
+ * This passes all received arguments to a new Date constructor and
+ * uses the resulting date to fake the time in Telemetry modules.
+ *
+ * @return Date The new faked date.
+ */
+function fakeNow(...args) {
+ const date = new Date(...args);
+ const modules = [
+ ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySession.sys.mjs"
+ ),
+ ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryEnvironment.sys.mjs"
+ ),
+ ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryControllerParent.sys.mjs"
+ ),
+ ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryStorage.sys.mjs"
+ ),
+ ChromeUtils.importESModule("resource://gre/modules/TelemetrySend.sys.mjs"),
+ ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryReportingPolicy.sys.mjs"
+ ),
+ ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryScheduler.sys.mjs"
+ ),
+ ];
+
+ for (let m of modules) {
+ m.Policy.now = () => date;
+ }
+
+ return new Date(date);
+}
+
+function fakeMonotonicNow(ms) {
+ const { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySession.sys.mjs"
+ );
+ Policy.monotonicNow = () => ms;
+ return ms;
+}
+
+// Fake the timeout functions for TelemetryController sending.
+function fakePingSendTimer(set, clear) {
+ const { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySend.sys.mjs"
+ );
+ let obj = Cu.cloneInto({ set, clear }, TelemetrySend, {
+ cloneFunctions: true,
+ });
+ Policy.setSchedulerTickTimeout = obj.set;
+ Policy.clearSchedulerTickTimeout = obj.clear;
+}
+
+function fakeMidnightPingFuzzingDelay(delayMs) {
+ const { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySend.sys.mjs"
+ );
+ Policy.midnightPingFuzzingDelay = () => delayMs;
+}
+
+function fakeGeneratePingId(func) {
+ const { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryControllerParent.sys.mjs"
+ );
+ Policy.generatePingId = func;
+}
+
+function fakeCachedClientId(uuid) {
+ const { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryControllerParent.sys.mjs"
+ );
+ Policy.getCachedClientID = () => uuid;
+}
+
+// Fake the gzip compression for the next ping to be sent out
+// and immediately reset to the original function.
+function fakeGzipCompressStringForNextPing(length) {
+ const { Policy, gzipCompressString } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySend.sys.mjs"
+ );
+ let largePayload = generateString(length);
+ Policy.gzipCompressString = data => {
+ Policy.gzipCompressString = gzipCompressString;
+ return largePayload;
+ };
+}
+
+function fakeIntlReady() {
+ const { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryEnvironment.sys.mjs"
+ );
+ Policy._intlLoaded = true;
+ // Dispatch the observer event in case the promise has been registered already.
+ Services.obs.notifyObservers(null, "browser-delayed-startup-finished");
+}
+
+// Override the uninstall ping file names
+function fakeUninstallPingPath(aPathFcn) {
+ const { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryStorage.sys.mjs"
+ );
+ Policy.getUninstallPingPath =
+ aPathFcn ||
+ (id => ({
+ directory: new FileUtils.File(PathUtils.profileDir),
+ file: `uninstall_ping_0123456789ABCDEF_${id}.json`,
+ }));
+}
+
+// Return a date that is |offset| ms in the future from |date|.
+function futureDate(date, offset) {
+ return new Date(date.getTime() + offset);
+}
+
+function truncateToDays(aMsec) {
+ return Math.floor(aMsec / MILLISECONDS_PER_DAY);
+}
+
+// Returns a promise that resolves to true when the passed promise rejects,
+// false otherwise.
+function promiseRejects(promise) {
+ return promise.then(
+ () => false,
+ () => true
+ );
+}
+
+// Generates a random string of at least a specific length.
+function generateRandomString(length) {
+ let string = "";
+
+ while (string.length < length) {
+ string += Math.random().toString(36);
+ }
+
+ return string.substring(0, length);
+}
+
+function generateString(length) {
+ return new Array(length + 1).join("a");
+}
+
+// Short-hand for retrieving the histogram with that id.
+function getHistogram(histogramId) {
+ return Telemetry.getHistogramById(histogramId);
+}
+
+// Short-hand for retrieving the snapshot of the Histogram with that id.
+function getSnapshot(histogramId) {
+ return Telemetry.getHistogramById(histogramId).snapshot();
+}
+
+// Helper for setting an empty list of Environment preferences to watch.
+function setEmptyPrefWatchlist() {
+ const { TelemetryEnvironment } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryEnvironment.sys.mjs"
+ );
+ return TelemetryEnvironment.onInitialized().then(() =>
+ TelemetryEnvironment.testWatchPreferences(new Map())
+ );
+}
+
+if (runningInParent) {
+ // Set logging preferences for all the tests.
+ Services.prefs.setCharPref("toolkit.telemetry.log.level", "Trace");
+ // Telemetry archiving should be on.
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.ArchiveEnabled, true);
+ // Telemetry xpcshell tests cannot show the infobar.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.BypassNotification,
+ true
+ );
+ // FHR uploads should be enabled.
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+ // Many tests expect the shutdown and the new-profile to not be sent on shutdown
+ // and will fail if receive an unexpected ping. Let's globally disable these features:
+ // the relevant tests will enable these prefs when needed.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.ShutdownPingSender,
+ false
+ );
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.ShutdownPingSenderFirstSession,
+ false
+ );
+ Services.prefs.setBoolPref("toolkit.telemetry.newProfilePing.enabled", false);
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.FirstShutdownPingEnabled,
+ false
+ );
+ // Turn off Health Ping submission.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.HealthPingEnabled,
+ false
+ );
+
+ // Speed up child process accumulations
+ Services.prefs.setIntPref(TelemetryUtils.Preferences.IPCBatchTimeout, 10);
+
+ // Non-unified Telemetry (e.g. Fennec on Android) needs the preference to be set
+ // in order to enable Telemetry.
+ if (Services.prefs.getBoolPref(TelemetryUtils.Preferences.Unified, false)) {
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.OverridePreRelease,
+ true
+ );
+ } else {
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.TelemetryEnabled,
+ true
+ );
+ }
+
+ fakePingSendTimer(
+ (callback, timeout) => {
+ Services.tm.dispatchToMainThread(() => callback());
+ },
+ () => {}
+ );
+
+ // This gets imported via fakeNow();
+ registerCleanupFunction(() => TelemetrySend.shutdown());
+}
+
+TelemetryController.testInitLogging();
+
+// Avoid timers interrupting test behavior.
+fakeSchedulerTimer(
+ () => {},
+ () => {}
+);
+// Make pind sending predictable.
+fakeMidnightPingFuzzingDelay(0);
+
+// Avoid using the directory service, which is not registered in some tests.
+fakeUninstallPingPath();
+
+const PLATFORM_VERSION = "1.9.2";
+const APP_VERSION = "1";
+const APP_ID = "xpcshell@tests.mozilla.org";
+const APP_NAME = "XPCShell";
+
+const DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC =
+ "distribution-customization-complete";
+
+const PLUGIN2_NAME = "Quicktime";
+const PLUGIN2_DESC = "A mock Quicktime plugin";
+const PLUGIN2_VERSION = "2.3";
+//
+// system add-ons are enabled at startup, so record date when the test starts
+const SYSTEM_ADDON_INSTALL_DATE = Date.now();
diff --git a/toolkit/components/telemetry/tests/unit/testNoPDB32.dll b/toolkit/components/telemetry/tests/unit/testNoPDB32.dll
new file mode 100644
index 0000000000..e7f9febc4b
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/testNoPDB32.dll
Binary files differ
diff --git a/toolkit/components/telemetry/tests/unit/testNoPDB64.dll b/toolkit/components/telemetry/tests/unit/testNoPDB64.dll
new file mode 100644
index 0000000000..19f95c98ed
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/testNoPDB64.dll
Binary files differ
diff --git a/toolkit/components/telemetry/tests/unit/testNoPDBAArch64.dll b/toolkit/components/telemetry/tests/unit/testNoPDBAArch64.dll
new file mode 100755
index 0000000000..ecfff07036
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/testNoPDBAArch64.dll
Binary files differ
diff --git a/toolkit/components/telemetry/tests/unit/testUnicodePDB32.dll b/toolkit/components/telemetry/tests/unit/testUnicodePDB32.dll
new file mode 100644
index 0000000000..d3eec65ea5
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/testUnicodePDB32.dll
Binary files differ
diff --git a/toolkit/components/telemetry/tests/unit/testUnicodePDB64.dll b/toolkit/components/telemetry/tests/unit/testUnicodePDB64.dll
new file mode 100644
index 0000000000..c11f8453de
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/testUnicodePDB64.dll
Binary files differ
diff --git a/toolkit/components/telemetry/tests/unit/testUnicodePDBAArch64.dll b/toolkit/components/telemetry/tests/unit/testUnicodePDBAArch64.dll
new file mode 100755
index 0000000000..a892a84315
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/testUnicodePDBAArch64.dll
Binary files differ
diff --git a/toolkit/components/telemetry/tests/unit/test_ChildEvents.js b/toolkit/components/telemetry/tests/unit/test_ChildEvents.js
new file mode 100644
index 0000000000..392febd5dc
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_ChildEvents.js
@@ -0,0 +1,222 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+);
+
+const MESSAGE_CHILD_TEST_DONE = "ChildTest:Done";
+
+const RECORDED_CONTENT_EVENTS = [
+ ["telemetry.test", "content_only", "object1"],
+ ["telemetry.test", "main_and_content", "object1"],
+ ["telemetry.test", "content_only", "object1", "some value"],
+ ["telemetry.test", "content_only", "object1", null, { foo: "x", bar: "y" }],
+ [
+ "telemetry.test",
+ "content_only",
+ "object1",
+ "some value",
+ { foo: "x", bar: "y" },
+ ],
+];
+
+const UNRECORDED_CONTENT_EVENTS = [["telemetry.test", "main_only", "object1"]];
+
+const RECORDED_PARENT_EVENTS = [
+ ["telemetry.test", "main_and_content", "object1"],
+ ["telemetry.test", "main_only", "object1"],
+];
+
+const UNRECORDED_PARENT_EVENTS = [
+ ["telemetry.test", "content_only", "object1"],
+];
+
+const RECORDED_DYNAMIC_EVENTS = [
+ ["telemetry.test.dynamic", "test1", "object1"],
+ ["telemetry.test.dynamic", "test2", "object1"],
+];
+
+function run_child_test() {
+ // Record some events in the "content" process.
+ RECORDED_CONTENT_EVENTS.forEach(e => Telemetry.recordEvent(...e));
+ // These events should not be recorded for the content process.
+ UNRECORDED_CONTENT_EVENTS.forEach(e => Telemetry.recordEvent(...e));
+ // Record some dynamic events from the content process.
+ RECORDED_DYNAMIC_EVENTS.forEach(e => Telemetry.recordEvent(...e));
+}
+
+/**
+ * This function waits until content events are reported into the
+ * events snapshot.
+ */
+async function waitForContentEvents() {
+ await ContentTaskUtils.waitForCondition(() => {
+ const snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ return (
+ Object.keys(snapshot).includes("content") &&
+ Object.keys(snapshot).includes("dynamic")
+ );
+ });
+}
+
+add_task(async function () {
+ if (!runningInParent) {
+ TelemetryController.testSetupContent();
+ run_child_test();
+ do_send_remote_message(MESSAGE_CHILD_TEST_DONE);
+ return;
+ }
+
+ // Setup.
+ do_get_profile(true);
+ await loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
+ finishAddonManagerStartup();
+ fakeIntlReady();
+ await TelemetryController.testSetup();
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+ // Enable recording for the test event category.
+ Telemetry.setEventRecordingEnabled("telemetry.test", true);
+
+ // Register dynamic test events.
+ Telemetry.registerEvents("telemetry.test.dynamic", {
+ // Event with only required fields.
+ test1: {
+ methods: ["test1"],
+ objects: ["object1"],
+ },
+ // Event with extra_keys.
+ test2: {
+ methods: ["test2", "test2b"],
+ objects: ["object1"],
+ extra_keys: ["key1", "key2"],
+ },
+ });
+
+ // Run test in child, don't wait for it to finish: just wait for the
+ // MESSAGE_CHILD_TEST_DONE.
+ const timestampBeforeChildEvents = Telemetry.msSinceProcessStart();
+ run_test_in_child("test_ChildEvents.js");
+ await do_await_remote_message(MESSAGE_CHILD_TEST_DONE);
+
+ // Once events are set by the content process, they don't immediately get
+ // sent to the parent process. Wait for the Telemetry IPC Timer to trigger
+ // and batch send the data back to the parent process.
+ await waitForContentEvents();
+ const timestampAfterChildEvents = Telemetry.msSinceProcessStart();
+
+ // Also record some events in the parent.
+ RECORDED_PARENT_EVENTS.forEach(e => Telemetry.recordEvent(...e));
+ UNRECORDED_PARENT_EVENTS.forEach(e => Telemetry.recordEvent(...e));
+
+ let snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+
+ Assert.ok("parent" in snapshot, "Should have main process section");
+ Assert.ok(
+ !!snapshot.parent.length,
+ "Main process section should have events."
+ );
+ Assert.ok("content" in snapshot, "Should have child process section");
+ Assert.ok(
+ !!snapshot.content.length,
+ "Child process section should have events."
+ );
+ Assert.ok("dynamic" in snapshot, "Should have dynamic process section");
+ Assert.ok(
+ !!snapshot.dynamic.length,
+ "Dynamic process section should have events."
+ );
+
+ // Check that the expected events are present from the content process.
+ let contentEvents = snapshot.content.map(e => e.slice(1));
+ Assert.equal(
+ contentEvents.length,
+ RECORDED_CONTENT_EVENTS.length,
+ "Should match expected event count."
+ );
+ for (let i = 0; i < RECORDED_CONTENT_EVENTS.length; ++i) {
+ Assert.deepEqual(
+ contentEvents[i],
+ RECORDED_CONTENT_EVENTS[i],
+ "Should have recorded expected event."
+ );
+ }
+
+ // Check that the expected events are present from the parent process.
+ let parentEvents = snapshot.parent.map(e => e.slice(1));
+ Assert.equal(
+ parentEvents.length,
+ RECORDED_PARENT_EVENTS.length,
+ "Should match expected event count."
+ );
+ for (let i = 0; i < RECORDED_PARENT_EVENTS.length; ++i) {
+ Assert.deepEqual(
+ parentEvents[i],
+ RECORDED_PARENT_EVENTS[i],
+ "Should have recorded expected event."
+ );
+ }
+
+ // Check that the expected dynamic events are present.
+ let dynamicEvents = snapshot.dynamic.map(e => e.slice(1));
+ Assert.equal(
+ dynamicEvents.length,
+ RECORDED_DYNAMIC_EVENTS.length,
+ "Should match expected event count."
+ );
+ for (let i = 0; i < RECORDED_DYNAMIC_EVENTS.length; ++i) {
+ Assert.deepEqual(
+ dynamicEvents[i],
+ RECORDED_DYNAMIC_EVENTS[i],
+ "Should have recorded expected event."
+ );
+ }
+
+ // Check that the event timestamps are in the expected ranges.
+ let contentTimestamps = snapshot.content.map(e => e[0]);
+ let parentTimestamps = snapshot.parent.map(e => e[0]);
+
+ Assert.ok(
+ contentTimestamps.every(
+ ts =>
+ ts > Math.floor(timestampBeforeChildEvents) &&
+ ts < timestampAfterChildEvents
+ ),
+ "All content event timestamps should be in the expected time range."
+ );
+ Assert.ok(
+ parentTimestamps.every(ts => ts >= Math.floor(timestampAfterChildEvents)),
+ "All parent event timestamps should be in the expected time range."
+ );
+
+ // Make sure all events are cleared from storage properly.
+ snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ );
+ Assert.greaterOrEqual(
+ Object.keys(snapshot).length,
+ 2,
+ "Should have events from at least two processes."
+ );
+ snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ true
+ );
+ Assert.equal(
+ Object.keys(snapshot).length,
+ 0,
+ "Should have cleared all events from storage."
+ );
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_ChildHistograms.js b/toolkit/components/telemetry/tests/unit/test_ChildHistograms.js
new file mode 100644
index 0000000000..5da3fc6647
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_ChildHistograms.js
@@ -0,0 +1,333 @@
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { TelemetrySession } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySession.sys.mjs"
+);
+const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+);
+
+const MESSAGE_CHILD_TEST_DONE = "ChildTest:Done";
+
+function run_child_test() {
+ // Setup histograms with some fixed values.
+ let flagHist = Telemetry.getHistogramById("TELEMETRY_TEST_FLAG");
+ flagHist.add(1);
+ let countHist = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT");
+ Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_COUNT", false);
+ countHist.add();
+ Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_COUNT", true);
+ countHist.add();
+ countHist.add();
+ let categHist = Telemetry.getHistogramById("TELEMETRY_TEST_CATEGORICAL");
+ categHist.add("Label2");
+ categHist.add("Label3");
+
+ let flagKeyed = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_FLAG");
+ flagKeyed.add("a", 1);
+ flagKeyed.add("b", 1);
+ let countKeyed = Telemetry.getKeyedHistogramById(
+ "TELEMETRY_TEST_KEYED_COUNT"
+ );
+ Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_KEYED_COUNT", false);
+ countKeyed.add("a");
+ countKeyed.add("b");
+ Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_KEYED_COUNT", true);
+ countKeyed.add("a");
+ countKeyed.add("b");
+ countKeyed.add("b");
+
+ // Test record_in_processes
+ let contentLinear = Telemetry.getHistogramById(
+ "TELEMETRY_TEST_CONTENT_PROCESS"
+ );
+ contentLinear.add(10);
+ let contentKeyed = Telemetry.getKeyedHistogramById(
+ "TELEMETRY_TEST_KEYED_CONTENT_PROCESS"
+ );
+ contentKeyed.add("content", 1);
+ let contentFlag = Telemetry.getHistogramById(
+ "TELEMETRY_TEST_FLAG_CONTENT_PROCESS"
+ );
+ contentFlag.add(true);
+ let mainFlag = Telemetry.getHistogramById("TELEMETRY_TEST_FLAG_MAIN_PROCESS");
+ mainFlag.add(true);
+ let allLinear = Telemetry.getHistogramById("TELEMETRY_TEST_ALL_PROCESSES");
+ allLinear.add(10);
+ let allChildLinear = Telemetry.getHistogramById(
+ "TELEMETRY_TEST_ALL_CHILD_PROCESSES"
+ );
+ allChildLinear.add(10);
+
+ // Test snapshot APIs.
+ // Should be forbidden in content processes.
+ Assert.throws(
+ () => Telemetry.getHistogramById("TELEMETRY_TEST_COUNT").snapshot(),
+ /Histograms can only be snapshotted in the parent process/,
+ "Snapshotting should be forbidden in the content process"
+ );
+
+ Assert.throws(
+ () =>
+ Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_COUNT").snapshot(),
+ /Keyed histograms can only be snapshotted in the parent process/,
+ "Snapshotting should be forbidden in the content process"
+ );
+
+ Assert.throws(
+ () => Telemetry.getHistogramById("TELEMETRY_TEST_COUNT").clear(),
+ /Histograms can only be cleared in the parent process/,
+ "Clearing should be forbidden in the content process"
+ );
+
+ Assert.throws(
+ () => Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_COUNT").clear(),
+ /Keyed histograms can only be cleared in the parent process/,
+ "Clearing should be forbidden in the content process"
+ );
+
+ Assert.throws(
+ () => Telemetry.getSnapshotForHistograms(),
+ /NS_ERROR_FAILURE/,
+ "Snapshotting should be forbidden in the content process"
+ );
+
+ Assert.throws(
+ () => Telemetry.getSnapshotForKeyedHistograms(),
+ /NS_ERROR_FAILURE/,
+ "Snapshotting should be forbidden in the content process"
+ );
+}
+
+function check_histogram_values(payload) {
+ const hs = payload.histograms;
+ Assert.ok("TELEMETRY_TEST_COUNT" in hs, "Should have count test histogram.");
+ Assert.ok("TELEMETRY_TEST_FLAG" in hs, "Should have flag test histogram.");
+ Assert.ok(
+ "TELEMETRY_TEST_CATEGORICAL" in hs,
+ "Should have categorical test histogram."
+ );
+ Assert.equal(
+ hs.TELEMETRY_TEST_COUNT.sum,
+ 2,
+ "Count test histogram should have the right value."
+ );
+ Assert.equal(
+ hs.TELEMETRY_TEST_FLAG.sum,
+ 1,
+ "Flag test histogram should have the right value."
+ );
+ Assert.equal(
+ hs.TELEMETRY_TEST_CATEGORICAL.sum,
+ 3,
+ "Categorical test histogram should have the right sum."
+ );
+
+ const kh = payload.keyedHistograms;
+ Assert.ok(
+ "TELEMETRY_TEST_KEYED_COUNT" in kh,
+ "Should have keyed count test histogram."
+ );
+ Assert.ok(
+ "TELEMETRY_TEST_KEYED_FLAG" in kh,
+ "Should have keyed flag test histogram."
+ );
+ Assert.equal(
+ kh.TELEMETRY_TEST_KEYED_COUNT.a.sum,
+ 1,
+ "Keyed count test histogram should have the right value."
+ );
+ Assert.equal(
+ kh.TELEMETRY_TEST_KEYED_COUNT.b.sum,
+ 2,
+ "Keyed count test histogram should have the right value."
+ );
+ Assert.equal(
+ kh.TELEMETRY_TEST_KEYED_FLAG.a.sum,
+ 1,
+ "Keyed flag test histogram should have the right value."
+ );
+ Assert.equal(
+ kh.TELEMETRY_TEST_KEYED_FLAG.b.sum,
+ 1,
+ "Keyed flag test histogram should have the right value."
+ );
+}
+
+add_task(async function () {
+ if (!runningInParent) {
+ TelemetryController.testSetupContent();
+ run_child_test();
+ dump("... done with child test\n");
+ do_send_remote_message(MESSAGE_CHILD_TEST_DONE);
+ return;
+ }
+
+ // Setup.
+ do_get_profile(true);
+ await loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
+ finishAddonManagerStartup();
+ fakeIntlReady();
+ await TelemetryController.testSetup();
+ if (runningInParent) {
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+ }
+
+ // Run test in child, don't wait for it to finish.
+ run_test_in_child("test_ChildHistograms.js");
+ await do_await_remote_message(MESSAGE_CHILD_TEST_DONE);
+
+ await ContentTaskUtils.waitForCondition(() => {
+ let payload = TelemetrySession.getPayload("test-ping");
+ return (
+ payload &&
+ "processes" in payload &&
+ "content" in payload.processes &&
+ "histograms" in payload.processes.content &&
+ "TELEMETRY_TEST_COUNT" in payload.processes.content.histograms
+ );
+ });
+
+ // Test record_in_processes in main process, too
+ let contentLinear = Telemetry.getHistogramById(
+ "TELEMETRY_TEST_CONTENT_PROCESS"
+ );
+ contentLinear.add(20);
+ let contentKeyed = Telemetry.getKeyedHistogramById(
+ "TELEMETRY_TEST_KEYED_CONTENT_PROCESS"
+ );
+ contentKeyed.add("parent", 1);
+ let contentFlag = Telemetry.getHistogramById(
+ "TELEMETRY_TEST_FLAG_CONTENT_PROCESS"
+ );
+ contentFlag.add(true);
+ let mainFlag = Telemetry.getHistogramById("TELEMETRY_TEST_FLAG_MAIN_PROCESS");
+ mainFlag.add(true);
+ let allLinear = Telemetry.getHistogramById("TELEMETRY_TEST_ALL_PROCESSES");
+ allLinear.add(20);
+ let allChildLinear = Telemetry.getHistogramById(
+ "TELEMETRY_TEST_ALL_CHILD_PROCESSES"
+ );
+ allChildLinear.add(20);
+ let countKeyed = Telemetry.getKeyedHistogramById(
+ "TELEMETRY_TEST_KEYED_COUNT"
+ );
+ countKeyed.add("a");
+
+ const payload = TelemetrySession.getPayload("test-ping");
+ Assert.ok("processes" in payload, "Should have processes section");
+ Assert.ok(
+ "content" in payload.processes,
+ "Should have child process section"
+ );
+ Assert.ok(
+ "histograms" in payload.processes.content,
+ "Child process section should have histograms."
+ );
+ Assert.ok(
+ "keyedHistograms" in payload.processes.content,
+ "Child process section should have keyed histograms."
+ );
+ check_histogram_values(payload.processes.content);
+
+ // Check record_in_processes
+ // Content Process
+ let hs = payload.processes.content.histograms;
+ let khs = payload.processes.content.keyedHistograms;
+ Assert.ok(
+ "TELEMETRY_TEST_CONTENT_PROCESS" in hs,
+ "Should have content process histogram"
+ );
+ Assert.equal(
+ hs.TELEMETRY_TEST_CONTENT_PROCESS.sum,
+ 10,
+ "Should have correct value"
+ );
+ Assert.ok(
+ "TELEMETRY_TEST_KEYED_CONTENT_PROCESS" in khs,
+ "Should have keyed content process histogram"
+ );
+ Assert.equal(
+ khs.TELEMETRY_TEST_KEYED_CONTENT_PROCESS.content.sum,
+ 1,
+ "Should have correct value"
+ );
+ Assert.ok(
+ "TELEMETRY_TEST_FLAG_CONTENT_PROCESS" in hs,
+ "Should have content process histogram"
+ );
+ Assert.equal(
+ hs.TELEMETRY_TEST_FLAG_CONTENT_PROCESS.sum,
+ 1,
+ "Should have correct value"
+ );
+ Assert.ok(
+ "TELEMETRY_TEST_ALL_PROCESSES" in hs,
+ "Should have content process histogram"
+ );
+ Assert.equal(
+ hs.TELEMETRY_TEST_ALL_PROCESSES.sum,
+ 10,
+ "Should have correct value"
+ );
+ Assert.ok(
+ "TELEMETRY_TEST_ALL_CHILD_PROCESSES" in hs,
+ "Should have content process histogram"
+ );
+ Assert.equal(
+ hs.TELEMETRY_TEST_ALL_CHILD_PROCESSES.sum,
+ 10,
+ "Should have correct value"
+ );
+ Assert.ok(
+ !("TELEMETRY_TEST_FLAG_MAIN_PROCESS" in hs),
+ "Should not have main process histogram in child process payload"
+ );
+
+ // Main Process
+ let mainHs = payload.histograms;
+ let mainKhs = payload.keyedHistograms;
+ Assert.ok(
+ !("TELEMETRY_TEST_CONTENT_PROCESS" in mainHs),
+ "Should not have content process histogram in main process payload"
+ );
+ Assert.ok(
+ !("TELEMETRY_TEST_KEYED_CONTENT_PROCESS" in mainKhs),
+ "Should not have keyed content process histogram in main process payload"
+ );
+ Assert.ok(
+ !("TELEMETRY_TEST_FLAG_CONTENT_PROCESS" in mainHs),
+ "Should not have content process histogram in main process payload"
+ );
+ Assert.ok(
+ "TELEMETRY_TEST_ALL_PROCESSES" in mainHs,
+ "Should have all-process histogram in main process payload"
+ );
+ Assert.equal(
+ mainHs.TELEMETRY_TEST_ALL_PROCESSES.sum,
+ 20,
+ "Should have correct value"
+ );
+ Assert.ok(
+ !("TELEMETRY_TEST_ALL_CHILD_PROCESSES" in mainHs),
+ "Should not have all-child process histogram in main process payload"
+ );
+ Assert.ok(
+ "TELEMETRY_TEST_FLAG_MAIN_PROCESS" in mainHs,
+ "Should have main process histogram in main process payload"
+ );
+ Assert.equal(
+ mainHs.TELEMETRY_TEST_FLAG_MAIN_PROCESS.sum,
+ 1,
+ "Should have correct value"
+ );
+ Assert.equal(
+ mainKhs.TELEMETRY_TEST_KEYED_COUNT.a.sum,
+ 1,
+ "Should have correct value in parent"
+ );
+
+ do_test_finished();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_ChildScalars.js b/toolkit/components/telemetry/tests/unit/test_ChildScalars.js
new file mode 100644
index 0000000000..775288fed3
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_ChildScalars.js
@@ -0,0 +1,241 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { TelemetrySession } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySession.sys.mjs"
+);
+const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+);
+
+const MESSAGE_CHILD_TEST_DONE = "ChildTest:Done";
+
+const UINT_SCALAR = "telemetry.test.unsigned_int_kind";
+const KEYED_UINT_SCALAR = "telemetry.test.keyed_unsigned_int";
+const KEYED_BOOL_SCALAR = "telemetry.test.keyed_boolean_kind";
+const CONTENT_ONLY_UINT_SCALAR = "telemetry.test.content_only_uint";
+const ALL_PROCESSES_UINT_SCALAR = "telemetry.test.all_processes_uint";
+const ALL_CHILD_PROCESSES_STRING_SCALAR =
+ "telemetry.test.all_child_processes_string";
+
+function run_child_test() {
+ // Attempt to set some scalar values from the "content" process.
+ // The next scalars are not allowed to be recorded in the content process.
+ Telemetry.scalarSet(UINT_SCALAR, 1);
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, "should-not-be-recorded", 1);
+
+ // The next scalars shou be recorded in only the content process.
+ Telemetry.scalarSet(CONTENT_ONLY_UINT_SCALAR, 37);
+ Telemetry.scalarSet(ALL_CHILD_PROCESSES_STRING_SCALAR, "all-child-processes");
+
+ // The next scalar will be recorded in the parent and content processes.
+ Telemetry.keyedScalarSet(KEYED_BOOL_SCALAR, "content-key", true);
+ Telemetry.keyedScalarSet(KEYED_BOOL_SCALAR, "content-key2", false);
+ Telemetry.scalarSet(ALL_PROCESSES_UINT_SCALAR, 37);
+}
+
+function setParentScalars() {
+ // The following scalars are not allowed to be recorded in the parent process.
+ Telemetry.scalarSet(CONTENT_ONLY_UINT_SCALAR, 15);
+ Telemetry.scalarSet(ALL_CHILD_PROCESSES_STRING_SCALAR, "all-child-processes");
+
+ // The next ones will be recorded only in the parent.
+ Telemetry.scalarSet(UINT_SCALAR, 15);
+
+ // This last batch will be available both in the parent and child processes.
+ Telemetry.keyedScalarSet(KEYED_BOOL_SCALAR, "parent-key", false);
+ Telemetry.scalarSet(ALL_PROCESSES_UINT_SCALAR, 37);
+}
+
+function checkParentScalars(processData) {
+ const scalars = processData.scalars;
+ const keyedScalars = processData.keyedScalars;
+
+ // Check the plain scalars, make sure we're only recording what we expect.
+ Assert.ok(
+ !(CONTENT_ONLY_UINT_SCALAR in scalars),
+ "Scalars must not be recorded in other processes unless allowed."
+ );
+ Assert.ok(
+ !(ALL_CHILD_PROCESSES_STRING_SCALAR in scalars),
+ "Scalars must not be recorded in other processes unless allowed."
+ );
+ Assert.ok(
+ UINT_SCALAR in scalars,
+ `${UINT_SCALAR} must be recorded in the parent process.`
+ );
+ Assert.equal(
+ scalars[UINT_SCALAR],
+ 15,
+ `${UINT_SCALAR} must have the correct value (parent process).`
+ );
+ Assert.ok(
+ ALL_PROCESSES_UINT_SCALAR in scalars,
+ `${ALL_PROCESSES_UINT_SCALAR} must be recorded in the parent process.`
+ );
+ Assert.equal(
+ scalars[ALL_PROCESSES_UINT_SCALAR],
+ 37,
+ `${ALL_PROCESSES_UINT_SCALAR} must have the correct value (parent process).`
+ );
+
+ // Now check the keyed scalars.
+ Assert.ok(
+ KEYED_BOOL_SCALAR in keyedScalars,
+ `${KEYED_BOOL_SCALAR} must be recorded in the parent process.`
+ );
+ Assert.ok(
+ "parent-key" in keyedScalars[KEYED_BOOL_SCALAR],
+ `${KEYED_BOOL_SCALAR} must be recorded in the parent process.`
+ );
+ Assert.equal(
+ Object.keys(keyedScalars[KEYED_BOOL_SCALAR]).length,
+ 1,
+ `${KEYED_BOOL_SCALAR} must only contain the expected key in parent process.`
+ );
+ Assert.equal(
+ keyedScalars[KEYED_BOOL_SCALAR]["parent-key"],
+ false,
+ `${KEYED_BOOL_SCALAR} must have the correct value (parent process).`
+ );
+}
+
+function checkContentScalars(processData) {
+ const scalars = processData.scalars;
+ const keyedScalars = processData.keyedScalars;
+
+ // Check the plain scalars for the content process.
+ Assert.ok(
+ !(UINT_SCALAR in scalars),
+ "Scalars must not be recorded in other processes unless allowed."
+ );
+ Assert.ok(
+ !(KEYED_UINT_SCALAR in keyedScalars),
+ "Keyed scalars must not be recorded in other processes unless allowed."
+ );
+ Assert.ok(
+ CONTENT_ONLY_UINT_SCALAR in scalars,
+ `${CONTENT_ONLY_UINT_SCALAR} must be recorded in the content process.`
+ );
+ Assert.equal(
+ scalars[CONTENT_ONLY_UINT_SCALAR],
+ 37,
+ `${CONTENT_ONLY_UINT_SCALAR} must have the correct value (content process).`
+ );
+ Assert.ok(
+ ALL_CHILD_PROCESSES_STRING_SCALAR in scalars,
+ `${ALL_CHILD_PROCESSES_STRING_SCALAR} must be recorded in the content process.`
+ );
+ Assert.equal(
+ scalars[ALL_CHILD_PROCESSES_STRING_SCALAR],
+ "all-child-processes",
+ `${ALL_CHILD_PROCESSES_STRING_SCALAR} must have the correct value (content process).`
+ );
+ Assert.ok(
+ ALL_PROCESSES_UINT_SCALAR in scalars,
+ `${ALL_PROCESSES_UINT_SCALAR} must be recorded in the content process.`
+ );
+ Assert.equal(
+ scalars[ALL_PROCESSES_UINT_SCALAR],
+ 37,
+ `${ALL_PROCESSES_UINT_SCALAR} must have the correct value (content process).`
+ );
+
+ // Check the keyed scalars.
+ Assert.ok(
+ KEYED_BOOL_SCALAR in keyedScalars,
+ `${KEYED_BOOL_SCALAR} must be recorded in the content process.`
+ );
+ Assert.ok(
+ "content-key" in keyedScalars[KEYED_BOOL_SCALAR],
+ `${KEYED_BOOL_SCALAR} must be recorded in the content process.`
+ );
+ Assert.ok(
+ "content-key2" in keyedScalars[KEYED_BOOL_SCALAR],
+ `${KEYED_BOOL_SCALAR} must be recorded in the content process.`
+ );
+ Assert.equal(
+ keyedScalars[KEYED_BOOL_SCALAR]["content-key"],
+ true,
+ `${KEYED_BOOL_SCALAR} must have the correct value (content process).`
+ );
+ Assert.equal(
+ keyedScalars[KEYED_BOOL_SCALAR]["content-key2"],
+ false,
+ `${KEYED_BOOL_SCALAR} must have the correct value (content process).`
+ );
+ Assert.equal(
+ Object.keys(keyedScalars[KEYED_BOOL_SCALAR]).length,
+ 2,
+ `${KEYED_BOOL_SCALAR} must contain the expected keys in content process.`
+ );
+}
+
+/**
+ * This function waits until content scalars are reported into the
+ * scalar snapshot.
+ */
+async function waitForContentScalars() {
+ await ContentTaskUtils.waitForCondition(() => {
+ const scalars = Telemetry.getSnapshotForScalars("main", false);
+ return Object.keys(scalars).includes("content");
+ });
+}
+
+add_task(async function () {
+ if (!runningInParent) {
+ TelemetryController.testSetupContent();
+ run_child_test();
+ do_send_remote_message(MESSAGE_CHILD_TEST_DONE);
+ return;
+ }
+
+ // Setup.
+ do_get_profile(true);
+ await loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
+ finishAddonManagerStartup();
+ fakeIntlReady();
+ await TelemetryController.testSetup();
+ if (runningInParent) {
+ setParentScalars();
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+ }
+
+ // Run test in child, don't wait for it to finish: just wait for the
+ // MESSAGE_CHILD_TEST_DONE.
+ run_test_in_child("test_ChildScalars.js");
+ await do_await_remote_message(MESSAGE_CHILD_TEST_DONE);
+
+ // Once scalars are set by the content process, they don't immediately get
+ // sent to the parent process. Wait for the Telemetry IPC Timer to trigger
+ // and batch send the data back to the parent process.
+ await waitForContentScalars();
+
+ // Get an "environment-changed" ping rather than a "test-ping", as
+ // scalar measurements are only supported in subsession pings.
+ const payload = TelemetrySession.getPayload("environment-change");
+
+ // Validate the scalar data.
+ Assert.ok("processes" in payload, "Should have processes section");
+ Assert.ok(
+ "content" in payload.processes,
+ "Should have child process section"
+ );
+ Assert.ok(
+ "scalars" in payload.processes.content,
+ "Child process section should have scalars."
+ );
+ Assert.ok(
+ "keyedScalars" in payload.processes.content,
+ "Child process section should have keyed scalars."
+ );
+ checkParentScalars(payload.processes.parent);
+ checkContentScalars(payload.processes.content);
+
+ do_test_finished();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_CoveragePing.js b/toolkit/components/telemetry/tests/unit/test_CoveragePing.js
new file mode 100644
index 0000000000..fac9c9d9f6
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_CoveragePing.js
@@ -0,0 +1,118 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { NetUtil } = ChromeUtils.importESModule(
+ "resource://gre/modules/NetUtil.sys.mjs"
+);
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+const COVERAGE_VERSION = "2";
+
+const COVERAGE_ENABLED_PREF = "toolkit.coverage.enabled";
+const OPT_OUT_PREF = "toolkit.coverage.opt-out";
+const ALREADY_RUN_PREF = `toolkit.coverage.already-run.v${COVERAGE_VERSION}`;
+const COVERAGE_UUID_PREF = `toolkit.coverage.uuid.v${COVERAGE_VERSION}`;
+const TELEMETRY_ENABLED_PREF = "datareporting.healthreport.uploadEnabled";
+const REPORTING_ENDPOINT_BASE_PREF = "toolkit.coverage.endpoint.base";
+const REPORTING_ENDPOINT = "submit/coverage/coverage";
+
+Services.prefs.setIntPref("toolkit.coverage.log-level", 20);
+
+add_task(async function setup() {
+ let uuid = "test123";
+ Services.prefs.setCharPref(COVERAGE_UUID_PREF, uuid);
+
+ const server = new HttpServer();
+ server.start(-1);
+ const serverPort = server.identity.primaryPort;
+
+ Services.prefs.setCharPref(
+ REPORTING_ENDPOINT_BASE_PREF,
+ `http://localhost:${serverPort}`
+ );
+
+ server.registerPathHandler(
+ `/${REPORTING_ENDPOINT}/${COVERAGE_VERSION}/${uuid}`,
+ (request, response) => {
+ equal(request.method, "PUT");
+ let telemetryEnabled = Services.prefs.getBoolPref(
+ TELEMETRY_ENABLED_PREF,
+ false
+ );
+
+ let requestBody = NetUtil.readInputStreamToString(
+ request.bodyInputStream,
+ request.bodyInputStream.available()
+ );
+
+ let resultObj = JSON.parse(requestBody);
+
+ deepEqual(Object.keys(resultObj), [
+ "appUpdateChannel",
+ "osName",
+ "osVersion",
+ "telemetryEnabled",
+ ]);
+
+ if (telemetryEnabled) {
+ ok(resultObj.telemetryEnabled);
+ } else {
+ ok(!resultObj.telemetryEnabled);
+ }
+
+ const response_body = "OK";
+ response.bodyOutputStream.write(response_body, response_body.length);
+ server.stop();
+ }
+ );
+
+ // Trigger a proper telemetry init.
+ do_get_profile(true);
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+
+ await TelemetryController.testSetup();
+});
+
+add_task(async function test_prefs() {
+ // Telemetry reporting setting does not control this ping, but it
+ // reported by this ping.
+ Services.prefs.setBoolPref(TELEMETRY_ENABLED_PREF, false);
+
+ // should not run if enabled pref is false
+ Services.prefs.setBoolPref(COVERAGE_ENABLED_PREF, false);
+ Services.prefs.setBoolPref(ALREADY_RUN_PREF, false);
+ Services.prefs.setBoolPref(OPT_OUT_PREF, false);
+
+ await TelemetryController.testReset();
+
+ let alreadyRun = Services.prefs.getBoolPref(ALREADY_RUN_PREF, false);
+ ok(!alreadyRun, "should not have run with enabled pref false");
+
+ // should not run if opt-out pref is true
+ Services.prefs.setBoolPref(COVERAGE_ENABLED_PREF, true);
+ Services.prefs.setBoolPref(ALREADY_RUN_PREF, false);
+ Services.prefs.setBoolPref(OPT_OUT_PREF, true);
+
+ await TelemetryController.testReset();
+
+ // should run if opt-out pref is false and coverage is enabled
+ Services.prefs.setBoolPref(COVERAGE_ENABLED_PREF, true);
+ Services.prefs.setBoolPref(ALREADY_RUN_PREF, false);
+ Services.prefs.setBoolPref(OPT_OUT_PREF, false);
+
+ await TelemetryController.testReset();
+
+ // the telemetry setting should be set correctly
+ Services.prefs.setBoolPref(TELEMETRY_ENABLED_PREF, true);
+
+ await TelemetryController.testReset();
+
+ alreadyRun = Services.prefs.getBoolPref(ALREADY_RUN_PREF, false);
+
+ ok(alreadyRun, "should run if no opt-out and enabled");
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_EventPing.js b/toolkit/components/telemetry/tests/unit/test_EventPing.js
new file mode 100644
index 0000000000..450e88a846
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_EventPing.js
@@ -0,0 +1,280 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ TelemetryEventPing: "resource://gre/modules/EventPing.sys.mjs",
+});
+
+function checkPingStructure(type, payload, options) {
+ Assert.equal(
+ type,
+ TelemetryEventPing.EVENT_PING_TYPE,
+ "Should be an event ping."
+ );
+ // Check the payload for required fields.
+ Assert.ok("reason" in payload, "Payload must have reason.");
+ Assert.ok(
+ "processStartTimestamp" in payload,
+ "Payload must have processStartTimestamp."
+ );
+ Assert.ok("sessionId" in payload, "Payload must have sessionId.");
+ Assert.ok("subsessionId" in payload, "Payload must have subsessionId.");
+ Assert.ok("lostEventsCount" in payload, "Payload must have lostEventsCount.");
+ Assert.ok("events" in payload, "Payload must have events.");
+}
+
+function fakePolicy(set, clear, send) {
+ let { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/EventPing.sys.mjs"
+ );
+ Policy.setTimeout = set;
+ Policy.clearTimeout = clear;
+ Policy.sendPing = send;
+}
+
+function pass() {
+ /* intentionally empty */
+}
+function fail() {
+ Assert.ok(false, "Not allowed");
+}
+
+function recordEvents(howMany) {
+ for (let i = 0; i < howMany; i++) {
+ Telemetry.recordEvent("telemetry.test", "test1", "object1");
+ }
+}
+
+add_task(async function setup() {
+ // Trigger a proper telemetry init.
+ do_get_profile(true);
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+
+ await TelemetryController.testSetup();
+ TelemetryEventPing.testReset();
+ Telemetry.setEventRecordingEnabled("telemetry.test", true);
+});
+
+// Tests often take the form of faking policy within faked policy.
+// This is to allow us to record events in addition to any that were
+// recorded to trigger the submit in the first place.
+// This works because we start the timer at the top of _submitPing, giving us
+// this opportunity.
+// This results in things looking this way:
+/*
+fakePolicy((callback, delay) => {
+ // Code that runs at the top of _submitPing
+ fakePolicy(pass, pass, (type, payload, options) => {
+ // Code that runs at the bottom of _submitPing
+ });
+}, pass, fail);
+// Code that triggers _submitPing to run
+*/
+
+add_task(async function test_eventLimitReached() {
+ Telemetry.clearEvents();
+ TelemetryEventPing.testReset();
+
+ let pingCount = 0;
+
+ fakePolicy(pass, pass, fail);
+ recordEvents(999);
+ fakePolicy(
+ (callback, delay) => {
+ Telemetry.recordEvent("telemetry.test", "test2", "object1");
+ fakePolicy(pass, pass, (type, payload, options) => {
+ checkPingStructure(type, payload, options);
+ Assert.ok(options.addClientId, "Adds the client id.");
+ Assert.ok(options.addEnvironment, "Adds the environment.");
+ Assert.ok(!options.usePingSender, "Doesn't require pingsender.");
+ Assert.equal(
+ payload.reason,
+ TelemetryEventPing.Reason.MAX,
+ "Sending because we hit max"
+ );
+ Assert.equal(
+ payload.events.parent.length,
+ 1000,
+ "Has one thousand events"
+ );
+ Assert.equal(payload.lostEventsCount, 0, "Lost no events");
+ Assert.ok(
+ !payload.events.parent.some(ev => ev[1] === "test2"),
+ "Should not have included the final event (yet)."
+ );
+ pingCount++;
+ });
+ },
+ pass,
+ fail
+ );
+ // Now trigger the submit.
+ Telemetry.recordEvent("telemetry.test", "test1", "object1");
+ Assert.equal(pingCount, 1, "Should have sent a ping");
+
+ // With a recent MAX ping sent, record another max amount of events (and then two extras).
+ fakePolicy(fail, fail, fail);
+ recordEvents(998);
+ fakePolicy(
+ (callback, delay) => {
+ Telemetry.recordEvent("telemetry.test", "test2", "object2");
+ Telemetry.recordEvent("telemetry.test", "test2", "object2");
+ fakePolicy(pass, pass, (type, payload, options) => {
+ checkPingStructure(type, payload, options);
+ Assert.ok(options.addClientId, "Adds the client id.");
+ Assert.ok(options.addEnvironment, "Adds the environment.");
+ Assert.ok(!options.usePingSender, "Doesn't require pingsender.");
+ Assert.equal(
+ payload.reason,
+ TelemetryEventPing.Reason.MAX,
+ "Sending because we hit max"
+ );
+ Assert.equal(
+ payload.events.parent.length,
+ 1000,
+ "Has one thousand events"
+ );
+ Assert.equal(payload.lostEventsCount, 2, "Lost two events");
+ Assert.equal(
+ payload.events.parent[0][2],
+ "test2",
+ "The first event of the second bunch should be the leftover event of the first bunch."
+ );
+ Assert.ok(
+ !payload.events.parent.some(ev => ev[3] === "object2"),
+ "Should not have included any of the lost two events."
+ );
+ pingCount++;
+ });
+ callback(); // Trigger the send immediately.
+ },
+ pass,
+ fail
+ );
+ recordEvents(1);
+ Assert.equal(pingCount, 2, "Should have sent a second ping");
+
+ // Ensure we send a subsequent MAX ping exactly on 1000 events, and without
+ // the two events we lost.
+ fakePolicy(fail, fail, fail);
+ recordEvents(999);
+ fakePolicy((callback, delay) => {
+ fakePolicy(pass, pass, (type, payload, options) => {
+ checkPingStructure(type, payload, options);
+ Assert.ok(options.addClientId, "Adds the client id.");
+ Assert.ok(options.addEnvironment, "Adds the environment.");
+ Assert.ok(!options.usePingSender, "Doesn't require pingsender.");
+ Assert.equal(
+ payload.reason,
+ TelemetryEventPing.Reason.MAX,
+ "Sending because we hit max"
+ );
+ Assert.equal(
+ payload.events.parent.length,
+ 1000,
+ "Has one thousand events"
+ );
+ Assert.equal(payload.lostEventsCount, 0, "Lost no events");
+ Assert.ok(
+ !payload.events.parent.some(ev => ev[3] === "object2"),
+ "Should not have included any of the lost two events from the previous bunch."
+ );
+ pingCount++;
+ });
+ callback(); // Trigger the send immediately
+ });
+ recordEvents(1);
+ Assert.equal(pingCount, 3, "Should have sent a third ping");
+});
+
+add_task(async function test_timers() {
+ Telemetry.clearEvents();
+ TelemetryEventPing.testReset();
+
+ // Immediately after submitting a MAX ping, we should set the timer for the
+ // next interval.
+ recordEvents(999);
+ fakePolicy(
+ (callback, delay) => {
+ Assert.equal(
+ delay,
+ TelemetryEventPing.minFrequency,
+ "Timer should be started with the min frequency"
+ );
+ },
+ pass,
+ pass
+ );
+ recordEvents(1);
+
+ fakePolicy(
+ (callback, delay) => {
+ Assert.ok(
+ delay <= TelemetryEventPing.maxFrequency,
+ "Timer should be at most the max frequency for a subsequent MAX ping."
+ );
+ },
+ pass,
+ pass
+ );
+ recordEvents(1000);
+});
+
+add_task(async function test_periodic() {
+ Telemetry.clearEvents();
+ TelemetryEventPing.testReset();
+
+ fakePolicy(
+ (callback, delay) => {
+ Assert.equal(
+ delay,
+ TelemetryEventPing.minFrequency,
+ "Timer should default to the min frequency"
+ );
+ fakePolicy(pass, pass, (type, payload, options) => {
+ checkPingStructure(type, payload, options);
+ Assert.ok(options.addClientId, "Adds the client id.");
+ Assert.ok(options.addEnvironment, "Adds the environment.");
+ Assert.ok(!options.usePingSender, "Doesn't require pingsender.");
+ Assert.equal(
+ payload.reason,
+ TelemetryEventPing.Reason.PERIODIC,
+ "Sending because we hit a timer"
+ );
+ Assert.equal(payload.events.parent.length, 1, "Has one event");
+ Assert.equal(payload.lostEventsCount, 0, "Lost no events");
+ });
+ callback();
+ },
+ pass,
+ fail
+ );
+
+ recordEvents(1);
+ TelemetryEventPing._startTimer();
+});
+
+// Ensure this is the final test in the suite, as it shuts things down.
+add_task(async function test_shutdown() {
+ Telemetry.clearEvents();
+ TelemetryEventPing.testReset();
+
+ recordEvents(999);
+ fakePolicy(pass, pass, (type, payload, options) => {
+ Assert.ok(options.addClientId, "Adds the client id.");
+ Assert.ok(options.addEnvironment, "Adds the environment.");
+ Assert.ok(options.usePingSender, "Asks for pingsender.");
+ Assert.equal(
+ payload.reason,
+ TelemetryEventPing.Reason.SHUTDOWN,
+ "Sending because we are shutting down"
+ );
+ Assert.equal(payload.events.parent.length, 999, "Has 999 events");
+ Assert.equal(payload.lostEventsCount, 0, "No lost events");
+ });
+ TelemetryEventPing.shutdown();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_HealthPing.js b/toolkit/components/telemetry/tests/unit/test_HealthPing.js
new file mode 100644
index 0000000000..3ea288157e
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_HealthPing.js
@@ -0,0 +1,282 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This tests the public Telemetry API for submitting Health pings.
+
+"use strict";
+
+ChromeUtils.defineESModuleGetters(this, {
+ TelemetryHealthPing: "resource://gre/modules/HealthPing.sys.mjs",
+});
+
+function checkHealthPingStructure(ping, expectedFailuresDict) {
+ let payload = ping.payload;
+ Assert.equal(
+ ping.type,
+ TelemetryHealthPing.HEALTH_PING_TYPE,
+ "Should have recorded a health ping."
+ );
+
+ for (let [key, value] of Object.entries(expectedFailuresDict)) {
+ Assert.deepEqual(
+ payload[key],
+ value,
+ "Should have recorded correct entry with key: " + key
+ );
+ }
+}
+
+function fakeHealthSchedulerTimer(set, clear) {
+ let { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/HealthPing.sys.mjs"
+ );
+ Policy.setSchedulerTickTimeout = set;
+ Policy.clearSchedulerTickTimeout = clear;
+}
+
+add_setup(async function setup() {
+ // Trigger a proper telemetry init.
+ do_get_profile(true);
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.HealthPingEnabled,
+ true
+ );
+
+ await TelemetryController.testSetup();
+ PingServer.start();
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+ Services.prefs.setStringPref(
+ TelemetryUtils.Preferences.Server,
+ "http://localhost:" + PingServer.port
+ );
+});
+
+registerCleanupFunction(async function cleanup() {
+ await PingServer.stop();
+});
+
+add_task(async function test_sendImmediately() {
+ PingServer.clearRequests();
+ TelemetryHealthPing.testReset();
+
+ await TelemetryHealthPing.recordSendFailure("testProblem");
+ let ping = await PingServer.promiseNextPing();
+ checkHealthPingStructure(ping, {
+ [TelemetryHealthPing.FailureType.SEND_FAILURE]: {
+ testProblem: 1,
+ },
+ os: TelemetryHealthPing.OsInfo,
+ reason: TelemetryHealthPing.Reason.IMMEDIATE,
+ });
+});
+
+add_task(async function test_sendOnDelay() {
+ PingServer.clearRequests();
+ TelemetryHealthPing.testReset();
+
+ // This first failure should immediately trigger a ping. After this, subsequent failures should be throttled.
+ await TelemetryHealthPing.recordSendFailure("testFailure");
+ let testPing = await PingServer.promiseNextPing();
+ Assert.equal(
+ testPing.type,
+ TelemetryHealthPing.HEALTH_PING_TYPE,
+ "Should have recorded a health ping."
+ );
+
+ // Retrieve delayed call back.
+ let pingSubmissionCallBack = null;
+ fakeHealthSchedulerTimer(
+ callBack => (pingSubmissionCallBack = callBack),
+ () => {}
+ );
+
+ // Record two failures, health ping must not be send now.
+ await TelemetryHealthPing.recordSendFailure("testFailure");
+ await TelemetryHealthPing.recordSendFailure("testFailure");
+
+ // Wait for sending delayed health ping.
+ await pingSubmissionCallBack();
+
+ let ping = await PingServer.promiseNextPing();
+ checkHealthPingStructure(ping, {
+ [TelemetryHealthPing.FailureType.SEND_FAILURE]: {
+ testFailure: 2,
+ },
+ os: TelemetryHealthPing.OsInfo,
+ reason: TelemetryHealthPing.Reason.DELAYED,
+ });
+});
+
+add_task(async function test_sendOverSizedPing() {
+ TelemetryHealthPing.testReset();
+ PingServer.clearRequests();
+ let OVER_SIZED_PING_TYPE = "over-sized-ping";
+ let overSizedData = generateRandomString(2 * 1024 * 1024);
+
+ await TelemetryController.submitExternalPing(OVER_SIZED_PING_TYPE, {
+ data: overSizedData,
+ });
+ let ping = await PingServer.promiseNextPing();
+
+ checkHealthPingStructure(ping, {
+ [TelemetryHealthPing.FailureType.DISCARDED_FOR_SIZE]: {
+ [OVER_SIZED_PING_TYPE]: 1,
+ },
+ os: TelemetryHealthPing.OsInfo,
+ reason: TelemetryHealthPing.Reason.IMMEDIATE,
+ });
+});
+
+add_task(async function test_sendOnlyTopTenDiscardedPings() {
+ TelemetryHealthPing.testReset();
+ await TelemetrySend.reset();
+ PingServer.clearRequests();
+ let PING_TYPE = "sort-discarded";
+
+ // This first failure should immediately trigger a ping. After this, subsequent failures should be throttled.
+ await TelemetryHealthPing.recordSendFailure("testFailure");
+ let testPing = await PingServer.promiseNextPing();
+ Assert.equal(
+ testPing.type,
+ TelemetryHealthPing.HEALTH_PING_TYPE,
+ "Should have recorded a health ping."
+ );
+
+ // Retrieve delayed call back.
+ let pingSubmissionCallBack = null;
+ fakeHealthSchedulerTimer(
+ callBack => (pingSubmissionCallBack = callBack),
+ () => {}
+ );
+
+ // Add failures
+ for (let i = 1; i < 12; i++) {
+ for (let j = 1; j < i; j++) {
+ TelemetryHealthPing.recordDiscardedPing(PING_TYPE + i);
+ }
+ }
+
+ await TelemetrySend.reset();
+ await pingSubmissionCallBack();
+ let ping = await PingServer.promiseNextPing();
+
+ checkHealthPingStructure(ping, {
+ os: TelemetryHealthPing.OsInfo,
+ reason: TelemetryHealthPing.Reason.DELAYED,
+ [TelemetryHealthPing.FailureType.DISCARDED_FOR_SIZE]: {
+ [PING_TYPE + 11]: 10,
+ [PING_TYPE + 10]: 9,
+ [PING_TYPE + 9]: 8,
+ [PING_TYPE + 8]: 7,
+ [PING_TYPE + 7]: 6,
+ [PING_TYPE + 6]: 5,
+ [PING_TYPE + 5]: 4,
+ [PING_TYPE + 4]: 3,
+ [PING_TYPE + 3]: 2,
+ [PING_TYPE + 2]: 1,
+ },
+ });
+});
+
+add_task(async function test_discardedForSizePending() {
+ TelemetryHealthPing.testReset();
+ PingServer.clearRequests();
+
+ const PING_TYPE = "discarded-for-size-pending";
+
+ const OVERSIZED_PING_ID = "9b21ec8f-f762-4d28-a2c1-44e1c4694f24";
+ // Create a pending oversized ping.
+ let overSizedPayload = generateRandomString(2 * 1024 * 1024);
+ const OVERSIZED_PING = {
+ id: OVERSIZED_PING_ID,
+ type: PING_TYPE,
+ creationDate: new Date().toISOString(),
+ // Generate a 2MB string to use as the ping payload.
+ payload: overSizedPayload,
+ };
+
+ // Test loadPendingPing.
+ await TelemetryStorage.savePendingPing(OVERSIZED_PING);
+ // Try to manually load the oversized ping.
+ await Assert.rejects(
+ TelemetryStorage.loadPendingPing(OVERSIZED_PING_ID),
+ /loadPendingPing - exceeded the maximum ping size/,
+ "The oversized ping should have been pruned."
+ );
+
+ let ping = await PingServer.promiseNextPing();
+ checkHealthPingStructure(ping, {
+ [TelemetryHealthPing.FailureType.DISCARDED_FOR_SIZE]: {
+ "<unknown>": 1,
+ },
+ os: TelemetryHealthPing.OsInfo,
+ reason: TelemetryHealthPing.Reason.IMMEDIATE,
+ });
+
+ // Test _scanPendingPings.
+ TelemetryHealthPing.testReset();
+ await TelemetryStorage.savePendingPing(OVERSIZED_PING);
+ await TelemetryStorage.loadPendingPingList();
+
+ ping = await PingServer.promiseNextPing();
+ checkHealthPingStructure(ping, {
+ [TelemetryHealthPing.FailureType.DISCARDED_FOR_SIZE]: {
+ "<unknown>": 1,
+ },
+ os: TelemetryHealthPing.OsInfo,
+ reason: TelemetryHealthPing.Reason.IMMEDIATE,
+ });
+});
+
+add_task(async function test_usePingSenderOnShutdown() {
+ if (
+ gIsAndroid ||
+ (AppConstants.platform == "linux" && !Services.appinfo.is64Bit)
+ ) {
+ // We don't support the pingsender on Android, yet, see bug 1335917.
+ // We also don't support the pingsender testing on Treeherder for
+ // Linux 32 bit (due to missing libraries). So skip it there too.
+ // See bug 1310703 comment 78.
+ return;
+ }
+
+ TelemetryHealthPing.testReset();
+ await TelemetrySend.reset();
+ PingServer.clearRequests();
+
+ // This first failure should immediately trigger a ping.
+ // After this, subsequent failures should be throttled.
+ await TelemetryHealthPing.recordSendFailure("testFailure");
+ await PingServer.promiseNextPing();
+
+ TelemetryHealthPing.recordSendFailure("testFailure");
+ let nextRequest = PingServer.promiseNextRequest();
+
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ let request = await nextRequest;
+ let ping = decodeRequestPayload(request);
+
+ checkHealthPingStructure(ping, {
+ [TelemetryHealthPing.FailureType.SEND_FAILURE]: {
+ testFailure: 1,
+ },
+ os: TelemetryHealthPing.OsInfo,
+ reason: TelemetryHealthPing.Reason.SHUT_DOWN,
+ });
+
+ // Check that the health ping is sent at shutdown using the pingsender.
+ Assert.equal(
+ request.getHeader("User-Agent"),
+ "pingsender/1.0",
+ "Should have received the correct user agent string."
+ );
+ Assert.equal(
+ request.getHeader("X-PingSender-Version"),
+ "1.0",
+ "Should have received the correct PingSender version string."
+ );
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_MigratePendingPings.js b/toolkit/components/telemetry/tests/unit/test_MigratePendingPings.js
new file mode 100644
index 0000000000..bb618bb8da
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_MigratePendingPings.js
@@ -0,0 +1,153 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+"use strict";
+
+const { TelemetryStorage } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryStorage.sys.mjs"
+);
+const { TelemetryUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryUtils.sys.mjs"
+);
+const { makeFakeAppDir } = ChromeUtils.importESModule(
+ "resource://testing-common/AppData.sys.mjs"
+);
+
+// The name of the pending pings directory outside of the user profile,
+// in the user app data directory.
+const PENDING_PING_DIR_NAME = "Pending Pings";
+
+// Create a directory inside the profile and register it as UAppData, so
+// we can stick fake crash pings inside there. We put it inside the profile
+// just because we know that will get cleaned up after the test runs.
+async function createFakeAppDir() {
+ // Create "<profile>/UAppData/Pending Pings".
+ const pendingPingsPath = PathUtils.join(
+ PathUtils.profileDir,
+ "UAppData",
+ PENDING_PING_DIR_NAME
+ );
+ await IOUtils.makeDirectory(pendingPingsPath, {
+ ignoreExisting: true,
+ createAncestors: true,
+ });
+
+ await makeFakeAppDir();
+}
+
+add_task(async function setup() {
+ // Init the profile.
+ do_get_profile();
+ await createFakeAppDir();
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+});
+
+add_task(async function test_migrateUnsentPings() {
+ const PINGS = [
+ {
+ type: "crash",
+ id: TelemetryUtils.generateUUID(),
+ payload: { foo: "bar" },
+ dateCreated: new Date(2010, 1, 1, 10, 0, 0),
+ },
+ {
+ type: "other",
+ id: TelemetryUtils.generateUUID(),
+ payload: { moo: "meh" },
+ dateCreated: new Date(2010, 2, 1, 10, 2, 0),
+ },
+ ];
+ const APP_DATA_DIR = Services.dirsvc.get("UAppData", Ci.nsIFile).path;
+ const APPDATA_PINGS_DIR = PathUtils.join(APP_DATA_DIR, PENDING_PING_DIR_NAME);
+
+ // Create some pending pings outside of the user profile.
+ for (let ping of PINGS) {
+ const pingPath = PathUtils.join(APPDATA_PINGS_DIR, ping.id + ".json");
+ await TelemetryStorage.savePingToFile(ping, pingPath, true);
+ }
+
+ // Make sure the pending ping list is empty.
+ await TelemetryStorage.testClearPendingPings();
+
+ // Start the migration from TelemetryStorage.
+ let pendingPings = await TelemetryStorage.loadPendingPingList();
+ Assert.equal(
+ pendingPings.length,
+ 2,
+ "TelemetryStorage must have migrated 2 pings."
+ );
+
+ for (let ping of PINGS) {
+ // Verify that the pings were migrated and are among the pending pings.
+ Assert.ok(
+ pendingPings.find(p => p.id == ping.id),
+ "The ping must have been migrated."
+ );
+
+ // Try to load the migrated ping from the user profile.
+ let migratedPing = await TelemetryStorage.loadPendingPing(ping.id);
+ Assert.equal(
+ ping.id,
+ migratedPing.id,
+ "Should have loaded the correct ping id."
+ );
+ Assert.equal(
+ ping.type,
+ migratedPing.type,
+ "Should have loaded the correct ping type."
+ );
+ Assert.deepEqual(
+ ping.payload,
+ migratedPing.payload,
+ "Should have loaded the correct payload."
+ );
+
+ // Verify that the pings are no longer outside of the user profile.
+ const pingPath = PathUtils.join(APPDATA_PINGS_DIR, ping.id + ".json");
+ Assert.ok(
+ !(await IOUtils.exists(pingPath)),
+ "The ping should not be in the Pending Pings directory anymore."
+ );
+ }
+});
+
+add_task(async function test_migrateIncompatiblePing() {
+ const APP_DATA_DIR = Services.dirsvc.get("UAppData", Ci.nsIFile).path;
+ const APPDATA_PINGS_DIR = PathUtils.join(APP_DATA_DIR, PENDING_PING_DIR_NAME);
+
+ // Create a ping incompatible with migration outside of the user profile.
+ const pingPath = PathUtils.join(APPDATA_PINGS_DIR, "incompatible.json");
+ await TelemetryStorage.savePingToFile({ incom: "patible" }, pingPath, true);
+
+ // Ensure the pending ping list is empty.
+ await TelemetryStorage.testClearPendingPings();
+ TelemetryStorage.reset();
+
+ // Start the migration from TelemetryStorage.
+ let pendingPings = await TelemetryStorage.loadPendingPingList();
+ Assert.equal(
+ pendingPings.length,
+ 0,
+ "TelemetryStorage must have migrated no pings." +
+ JSON.stringify(pendingPings)
+ );
+
+ Assert.ok(
+ !(await IOUtils.exists(pingPath)),
+ "The incompatible ping must have been deleted by the migration"
+ );
+});
+
+add_task(async function teardown() {
+ // Delete the UAppData directory and make sure nothing breaks.
+ const APP_DATA_DIR = Services.dirsvc.get("UAppData", Ci.nsIFile).path;
+ await IOUtils.remove(APP_DATA_DIR, { recursive: true });
+ Assert.ok(
+ !(await IOUtils.exists(APP_DATA_DIR)),
+ "The UAppData directory must not exist anymore."
+ );
+ TelemetryStorage.reset();
+ await TelemetryStorage.loadPendingPingList();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_ModulesPing.js b/toolkit/components/telemetry/tests/unit/test_ModulesPing.js
new file mode 100644
index 0000000000..f533d753e9
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_ModulesPing.js
@@ -0,0 +1,297 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { ctypes } = ChromeUtils.importESModule(
+ "resource://gre/modules/ctypes.sys.mjs"
+);
+
+const MAX_NAME_LENGTH = 64;
+
+// The following libraries (except libxul) are all built from the
+// toolkit/components/telemetry/tests/modules-test.cpp file, which contains
+// instructions on how to build them.
+const libModules = ctypes.libraryName("modules-test");
+const libUnicode = ctypes.libraryName("modμles-test");
+const libLongName =
+ "lorem_ipsum_dolor_sit_amet_consectetur_adipiscing_elit_Fusce_sit_amet_tellus_non_magna_euismod_vestibulum_Vivamus_turpis_duis.dll";
+
+function chooseDLL(x86, x64, aarch64) {
+ let xpcomabi = Services.appinfo.XPCOMABI;
+ let cpu = xpcomabi.split("-")[0];
+ switch (cpu) {
+ case "aarch64":
+ return aarch64;
+ case "x86_64":
+ return x64;
+ case "x86":
+ return x86;
+ // This case only happens on Android, which gets skipped below. The previous
+ // code was returning the x86 version when testing for arm.
+ case "arm":
+ return x86;
+ default:
+ Assert.ok(false, "unexpected CPU type: " + cpu);
+ return x86;
+ }
+}
+
+const libUnicodePDB = chooseDLL(
+ "testUnicodePDB32.dll",
+ "testUnicodePDB64.dll",
+ "testUnicodePDBAArch64.dll"
+);
+const libNoPDB = chooseDLL(
+ "testNoPDB32.dll",
+ "testNoPDB64.dll",
+ "testNoPDBAArch64.dll"
+);
+const libxul = PathUtils.filename(PathUtils.xulLibraryPath);
+
+const libModulesFile = do_get_file(libModules).path;
+const libUnicodeFile = PathUtils.join(
+ PathUtils.parent(libModulesFile),
+ libUnicode
+);
+const libLongNameFile = PathUtils.join(
+ PathUtils.parent(libModulesFile),
+ libLongName
+);
+const libUnicodePDBFile = do_get_file(libUnicodePDB).path;
+const libNoPDBFile = do_get_file(libNoPDB).path;
+
+let libModulesHandle,
+ libUnicodeHandle,
+ libLongNameHandle,
+ libUnicodePDBHandle,
+ libNoPDBHandle;
+
+let expectedLibs;
+if (AppConstants.platform === "win") {
+ const version = AppConstants.MOZ_APP_VERSION.substring(
+ 0,
+ AppConstants.MOZ_APP_VERSION.indexOf(".") + 2
+ );
+
+ expectedLibs = [
+ {
+ name: libxul,
+ debugName: libxul.replace(".dll", ".pdb"),
+ version,
+ },
+ {
+ name: libModules,
+ debugName: libModules.replace(".dll", ".pdb"),
+ version,
+ },
+ {
+ name: libUnicode,
+ debugName: libModules.replace(".dll", ".pdb"),
+ version,
+ },
+ {
+ name: libLongName.substring(0, MAX_NAME_LENGTH - 1) + "…",
+ debugName: libModules.replace(".dll", ".pdb"),
+ version,
+ },
+ {
+ name: libUnicodePDB,
+ debugName: "libmodμles.pdb",
+ version: null,
+ },
+ {
+ name: libNoPDB,
+ debugName: null,
+ version: null,
+ },
+ {
+ // We choose this DLL because it's guaranteed to exist in our process and
+ // be signed on all Windows versions that we support.
+ name: "ntdll.dll",
+ // debugName changes depending on OS version and is irrelevant to this test
+ // version changes depending on OS version and is irrelevant to this test
+ certSubject: "Microsoft Windows",
+ },
+ ];
+} else {
+ expectedLibs = [
+ {
+ name: libxul,
+ debugName: libxul,
+ version: null,
+ },
+ {
+ name: libModules,
+ debugName: libModules,
+ version: null,
+ },
+ {
+ name: libUnicode,
+ debugName: libUnicode,
+ version: null,
+ },
+ {
+ name: libLongName.substring(0, MAX_NAME_LENGTH - 1) + "…",
+ debugName: libLongName.substring(0, MAX_NAME_LENGTH - 1) + "…",
+ version: null,
+ },
+ ];
+}
+
+add_task(async function setup() {
+ do_get_profile();
+
+ await IOUtils.copy(libModulesFile, libUnicodeFile);
+ await IOUtils.copy(libModulesFile, libLongNameFile);
+
+ libModulesHandle = ctypes.open(libModulesFile);
+ libUnicodeHandle = ctypes.open(libUnicodeFile);
+ libLongNameHandle = ctypes.open(libLongNameFile);
+ if (AppConstants.platform === "win") {
+ libUnicodePDBHandle = ctypes.open(libUnicodePDBFile);
+ libNoPDBHandle = ctypes.open(libNoPDBFile);
+ }
+
+ // Pretend the untrustedmodules ping has already been sent now to get it out
+ // of the way and avoid confusing the test with our PingServer receiving two
+ // pings during our test.
+ Services.prefs.setIntPref(
+ "app.update.lastUpdateTime.telemetry_untrustedmodules_ping",
+ Math.round(Date.now() / 1000)
+ );
+
+ // Force the timer to fire (using a small interval).
+ Cc["@mozilla.org/updates/timer-manager;1"]
+ .getService(Ci.nsIObserver)
+ .observe(null, "utm-test-init", "");
+ Services.prefs.setIntPref("toolkit.telemetry.modulesPing.interval", 0);
+ Services.prefs.setStringPref("app.update.url", "http://localhost");
+
+ // Start the local ping server and setup Telemetry to use it during the tests.
+ PingServer.start();
+ Services.prefs.setStringPref(
+ TelemetryUtils.Preferences.Server,
+ "http://localhost:" + PingServer.port
+ );
+});
+
+registerCleanupFunction(function () {
+ if (libModulesHandle) {
+ libModulesHandle.close();
+ }
+ if (libUnicodeHandle) {
+ libUnicodeHandle.close();
+ }
+ if (libLongNameHandle) {
+ libLongNameHandle.close();
+ }
+ if (libUnicodePDBHandle) {
+ libUnicodePDBHandle.close();
+ }
+ if (libNoPDBHandle) {
+ libNoPDBHandle.close();
+ }
+
+ return IOUtils.remove(libUnicodeFile)
+ .then(() => IOUtils.remove(libLongNameFile))
+ .then(() => PingServer.stop());
+});
+
+add_task(
+ {
+ skip_if: () => !AppConstants.MOZ_GECKO_PROFILER,
+ },
+ async function test_send_ping() {
+ await TelemetryController.testSetup();
+
+ let found = await PingServer.promiseNextPing();
+ Assert.ok(!!found, "Telemetry ping submitted.");
+ Assert.strictEqual(found.type, "modules", "Ping type is 'modules'");
+ Assert.ok(found.environment, "'modules' ping has an environment.");
+ Assert.ok(!!found.clientId, "'modules' ping has a client ID.");
+ Assert.ok(
+ !!found.payload.modules,
+ "Telemetry ping payload contains the 'modules' array."
+ );
+
+ let nameComparator;
+ if (AppConstants.platform === "win") {
+ // Do case-insensitive checking of file/module names on Windows
+ nameComparator = function (a, b) {
+ if (typeof a === "string" && typeof b === "string") {
+ return a.toLowerCase() === b.toLowerCase();
+ }
+
+ return a === b;
+ };
+ } else {
+ nameComparator = function (a, b) {
+ return a === b;
+ };
+ }
+
+ for (let lib of expectedLibs) {
+ let test_lib = found.payload.modules.find(module =>
+ nameComparator(module.name, lib.name)
+ );
+
+ Assert.ok(!!test_lib, "There is a '" + lib.name + "' module.");
+
+ if ("version" in lib) {
+ if (lib.version !== null) {
+ Assert.ok(
+ test_lib.version.startsWith(lib.version),
+ "The version of the " +
+ lib.name +
+ " module (" +
+ test_lib.version +
+ ") is correct (it starts with '" +
+ lib.version +
+ "')."
+ );
+ } else {
+ Assert.strictEqual(
+ test_lib.version,
+ null,
+ "The version of the " + lib.name + " module is null."
+ );
+ }
+ }
+
+ if ("debugName" in lib) {
+ Assert.ok(
+ nameComparator(test_lib.debugName, lib.debugName),
+ "The " + lib.name + " module has the correct debug name."
+ );
+ }
+
+ if (lib.debugName === null) {
+ Assert.strictEqual(
+ test_lib.debugID,
+ null,
+ "The " + lib.name + " module doesn't have a debug ID."
+ );
+ } else {
+ Assert.greater(
+ test_lib.debugID.length,
+ 0,
+ "The " + lib.name + " module has a debug ID."
+ );
+ }
+
+ if ("certSubject" in lib) {
+ Assert.strictEqual(
+ test_lib.certSubject,
+ lib.certSubject,
+ "The " + lib.name + " module has the expected cert subject."
+ );
+ }
+ }
+
+ let test_lib = found.payload.modules.find(
+ module => module.name === libLongName
+ );
+ Assert.ok(!test_lib, "There isn't a '" + libLongName + "' module.");
+ }
+);
diff --git a/toolkit/components/telemetry/tests/unit/test_PingAPI.js b/toolkit/components/telemetry/tests/unit/test_PingAPI.js
new file mode 100644
index 0000000000..a6ef791f6a
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_PingAPI.js
@@ -0,0 +1,709 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+// This tests the public Telemetry API for submitting pings.
+
+"use strict";
+
+const { ClientID } = ChromeUtils.importESModule(
+ "resource://gre/modules/ClientID.sys.mjs"
+);
+const { TelemetryArchive } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryArchive.sys.mjs"
+);
+
+ChromeUtils.defineLazyGetter(this, "gPingsArchivePath", function () {
+ return PathUtils.join(PathUtils.profileDir, "datareporting", "archived");
+});
+
+/**
+ * Fakes the archive storage quota.
+ * @param {Integer} aArchiveQuota The new quota, in bytes.
+ */
+function fakeStorageQuota(aArchiveQuota) {
+ let { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryStorage.sys.mjs"
+ );
+ Policy.getArchiveQuota = () => aArchiveQuota;
+}
+
+/**
+ * Lists all the valid archived pings and their metadata, sorted by creation date.
+ *
+ * @return {Object[]} A list of objects with the extracted data in the form:
+ * { timestamp: <number>,
+ * id: <string>,
+ * type: <string>,
+ * size: <integer> }
+ */
+var getArchivedPingsInfo = async function () {
+ let archivedPings = [];
+
+ // Iterate through the subdirs of |gPingsArchivePath|.
+ for (const dir of await IOUtils.getChildren(gPingsArchivePath)) {
+ const { type } = await IOUtils.stat(dir);
+ if (type != "directory") {
+ continue;
+ }
+
+ // Then get a list of the files for the current subdir.
+ for (const filePath of await IOUtils.getChildren(dir)) {
+ const fileInfo = await IOUtils.stat(filePath);
+ if (fileInfo.type == "directory") {
+ continue;
+ }
+ let pingInfo = TelemetryStorage._testGetArchivedPingDataFromFileName(
+ PathUtils.filename(filePath)
+ );
+ if (!pingInfo) {
+ // This is not a valid archived ping, skip it.
+ continue;
+ }
+ // Find the size of the ping and then add the info to the array.
+ pingInfo.size = fileInfo.size;
+ archivedPings.push(pingInfo);
+ }
+ }
+
+ // Sort the list by creation date and then return it.
+ archivedPings.sort((a, b) => b.timestamp - a.timestamp);
+ return archivedPings;
+};
+
+add_task(async function test_setup() {
+ do_get_profile(true);
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+});
+
+add_task(async function test_archivedPings() {
+ // TelemetryController should not be fully initialized at this point.
+ // Submitting pings should still work fine.
+
+ const PINGS = [
+ {
+ type: "test-ping-api-1",
+ payload: { foo: "bar" },
+ dateCreated: new Date(2010, 1, 1, 10, 0, 0),
+ },
+ {
+ type: "test-ping-api-2",
+ payload: { moo: "meh" },
+ dateCreated: new Date(2010, 2, 1, 10, 0, 0),
+ },
+ ];
+
+ // Submit pings and check the ping list.
+ let expectedPingList = [];
+
+ for (let data of PINGS) {
+ fakeNow(data.dateCreated);
+ data.id = await TelemetryController.submitExternalPing(
+ data.type,
+ data.payload
+ );
+ let list = await TelemetryArchive.promiseArchivedPingList();
+
+ expectedPingList.push({
+ id: data.id,
+ type: data.type,
+ timestampCreated: data.dateCreated.getTime(),
+ });
+ Assert.deepEqual(
+ list,
+ expectedPingList,
+ "Archived ping list should contain submitted pings"
+ );
+ }
+
+ // Check loading the archived pings.
+ let checkLoadingPings = async function () {
+ for (let data of PINGS) {
+ let ping = await TelemetryArchive.promiseArchivedPingById(data.id);
+ Assert.equal(ping.id, data.id, "Archived ping should have matching id");
+ Assert.equal(
+ ping.type,
+ data.type,
+ "Archived ping should have matching type"
+ );
+ Assert.equal(
+ ping.creationDate,
+ data.dateCreated.toISOString(),
+ "Archived ping should have matching creation date"
+ );
+ }
+ };
+
+ await checkLoadingPings();
+
+ // Check that we find the archived pings again by scanning after a restart.
+ await TelemetryController.testReset();
+
+ let pingList = await TelemetryArchive.promiseArchivedPingList();
+ Assert.deepEqual(
+ expectedPingList,
+ pingList,
+ "Should have submitted pings in archive list after restart"
+ );
+ await checkLoadingPings();
+
+ // Write invalid pings into the archive with both valid and invalid names.
+ let writeToArchivedDir = async function (
+ dirname,
+ filename,
+ content,
+ compressed
+ ) {
+ const dirPath = PathUtils.join(gPingsArchivePath, dirname);
+ await IOUtils.makeDirectory(dirPath, { ignoreExisting: true });
+ const filePath = PathUtils.join(dirPath, filename);
+ const options = { tmpPath: filePath + ".tmp", mode: "overwrite" };
+ if (compressed) {
+ options.compress = true;
+ }
+ await IOUtils.writeUTF8(filePath, content, options);
+ };
+
+ const FAKE_ID1 = "10000000-0123-0123-0123-0123456789a1";
+ const FAKE_ID2 = "20000000-0123-0123-0123-0123456789a2";
+ const FAKE_ID3 = "20000000-0123-0123-0123-0123456789a3";
+ const FAKE_TYPE = "foo";
+
+ // These should get rejected.
+ await writeToArchivedDir("xx", "foo.json", "{}");
+ await writeToArchivedDir("2010-02", "xx.xx.xx.json", "{}");
+ // This one should get picked up...
+ await writeToArchivedDir(
+ "2010-02",
+ "1." + FAKE_ID1 + "." + FAKE_TYPE + ".json",
+ "{}"
+ );
+ // ... but get overwritten by this one.
+ await writeToArchivedDir(
+ "2010-02",
+ "2." + FAKE_ID1 + "." + FAKE_TYPE + ".json",
+ ""
+ );
+ // This should get picked up fine.
+ await writeToArchivedDir(
+ "2010-02",
+ "3." + FAKE_ID2 + "." + FAKE_TYPE + ".json",
+ ""
+ );
+ // This compressed ping should get picked up fine as well.
+ await writeToArchivedDir(
+ "2010-02",
+ "4." + FAKE_ID3 + "." + FAKE_TYPE + ".jsonlz4",
+ ""
+ );
+
+ expectedPingList.push({
+ id: FAKE_ID1,
+ type: "foo",
+ timestampCreated: 2,
+ });
+ expectedPingList.push({
+ id: FAKE_ID2,
+ type: "foo",
+ timestampCreated: 3,
+ });
+ expectedPingList.push({
+ id: FAKE_ID3,
+ type: "foo",
+ timestampCreated: 4,
+ });
+ expectedPingList.sort((a, b) => a.timestampCreated - b.timestampCreated);
+
+ // Reset the TelemetryArchive so we scan the archived dir again.
+ await TelemetryController.testReset();
+
+ // Check that we are still picking up the valid archived pings on disk,
+ // plus the valid ones above.
+ pingList = await TelemetryArchive.promiseArchivedPingList();
+ Assert.deepEqual(
+ expectedPingList,
+ pingList,
+ "Should have picked up valid archived pings"
+ );
+ await checkLoadingPings();
+
+ // Now check that we fail to load the two invalid pings from above.
+ Assert.ok(
+ await promiseRejects(TelemetryArchive.promiseArchivedPingById(FAKE_ID1)),
+ "Should have rejected invalid ping"
+ );
+ Assert.ok(
+ await promiseRejects(TelemetryArchive.promiseArchivedPingById(FAKE_ID2)),
+ "Should have rejected invalid ping"
+ );
+});
+
+add_task(async function test_archiveCleanup() {
+ const PING_TYPE = "foo";
+
+ // Empty the archive.
+ await IOUtils.remove(gPingsArchivePath, { recursive: true });
+
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SCAN_PING_COUNT").clear();
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_DIRECTORIES_COUNT").clear();
+ // Also reset these histograms to make sure normal sized pings don't get counted.
+ Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_ARCHIVED").clear();
+ Telemetry.getHistogramById(
+ "TELEMETRY_DISCARDED_ARCHIVED_PINGS_SIZE_MB"
+ ).clear();
+
+ // Build the cache. Nothing should be evicted as there's no ping directory.
+ await TelemetryController.testReset();
+ await TelemetryStorage.testCleanupTaskPromise();
+ await TelemetryArchive.promiseArchivedPingList();
+
+ let h = Telemetry.getHistogramById(
+ "TELEMETRY_ARCHIVE_SCAN_PING_COUNT"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ 0,
+ "Telemetry must report 0 pings scanned if no archive dir exists."
+ );
+ // One directory out of four was removed as well.
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_ARCHIVE_EVICTED_OLD_DIRS"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ 0,
+ "Telemetry must report 0 evicted dirs if no archive dir exists."
+ );
+
+ let expectedPrunedInfo = [];
+ let expectedNotPrunedInfo = [];
+
+ let checkArchive = async function () {
+ // Check that the pruned pings are not on disk anymore.
+ for (let prunedInfo of expectedPrunedInfo) {
+ await Assert.rejects(
+ TelemetryArchive.promiseArchivedPingById(prunedInfo.id),
+ /TelemetryStorage.loadArchivedPing - no ping with id/,
+ "Ping " + prunedInfo.id + " should have been pruned."
+ );
+ const pingPath = TelemetryStorage._testGetArchivedPingPath(
+ prunedInfo.id,
+ prunedInfo.creationDate,
+ PING_TYPE
+ );
+ Assert.ok(
+ !(await IOUtils.exists(pingPath)),
+ "The ping should not be on the disk anymore."
+ );
+ }
+
+ // Check that the expected pings are there.
+ for (let expectedInfo of expectedNotPrunedInfo) {
+ Assert.ok(
+ await TelemetryArchive.promiseArchivedPingById(expectedInfo.id),
+ "Ping" + expectedInfo.id + " should be in the archive."
+ );
+ }
+ };
+
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SESSION_PING_COUNT").clear();
+
+ // Create a ping which should be pruned because it is past the retention period.
+ let date = fakeNow(2010, 1, 1, 1, 0, 0);
+ let firstDate = date;
+ let pingId = await TelemetryController.submitExternalPing(PING_TYPE, {}, {});
+ expectedPrunedInfo.push({ id: pingId, creationDate: date });
+
+ // Create a ping which should be kept because it is within the retention period.
+ const oldestDirectoryDate = fakeNow(2010, 2, 1, 1, 0, 0);
+ pingId = await TelemetryController.submitExternalPing(PING_TYPE, {}, {});
+ expectedNotPrunedInfo.push({ id: pingId, creationDate: oldestDirectoryDate });
+
+ // Create 20 other pings which are within the retention period, but would be affected
+ // by the disk quota.
+ for (let month of [3, 4]) {
+ for (let minute = 0; minute < 10; minute++) {
+ date = fakeNow(2010, month, 1, 1, minute, 0);
+ pingId = await TelemetryController.submitExternalPing(PING_TYPE, {}, {});
+ expectedNotPrunedInfo.push({ id: pingId, creationDate: date });
+ }
+ }
+
+ // We expect all the pings we archived to be in this histogram.
+ h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SESSION_PING_COUNT");
+ Assert.equal(
+ h.snapshot().sum,
+ 22,
+ "All the pings must be live-accumulated in the histogram."
+ );
+ // Reset the histogram that will be populated by the archive scan.
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OLD_DIRS").clear();
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_OLDEST_DIRECTORY_AGE").clear();
+
+ // Move the current date 60 days ahead of the first ping.
+ fakeNow(futureDate(firstDate, 60 * MILLISECONDS_PER_DAY));
+ // Reset TelemetryArchive and TelemetryController to start the startup cleanup.
+ await TelemetryController.testReset();
+ // Wait for the cleanup to finish.
+ await TelemetryStorage.testCleanupTaskPromise();
+ // Then scan the archived dir.
+ await TelemetryArchive.promiseArchivedPingList();
+
+ // Check that the archive is in the correct state.
+ await checkArchive();
+
+ // Make sure the ping count is correct after the scan (one ping was removed).
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_ARCHIVE_SCAN_PING_COUNT"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ 21,
+ "The histogram must count all the pings in the archive."
+ );
+ // One directory out of four was removed as well.
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_ARCHIVE_EVICTED_OLD_DIRS"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ 1,
+ "Telemetry must correctly report removed archive directories."
+ );
+ // Check that the remaining directories are correctly counted.
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_ARCHIVE_DIRECTORIES_COUNT"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ 3,
+ "Telemetry must correctly report the remaining archive directories."
+ );
+ // Check that the remaining directories are correctly counted.
+ const oldestAgeInMonths = 1;
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_ARCHIVE_OLDEST_DIRECTORY_AGE"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ oldestAgeInMonths,
+ "Telemetry must correctly report age of the oldest directory in the archive."
+ );
+
+ // We need to test the archive size before we hit the quota, otherwise a special
+ // value is recorded.
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SIZE_MB").clear();
+ Telemetry.getHistogramById("TELEMETRY_ARCHIVE_EVICTED_OVER_QUOTA").clear();
+ Telemetry.getHistogramById(
+ "TELEMETRY_ARCHIVE_EVICTING_OVER_QUOTA_MS"
+ ).clear();
+
+ // Move the current date 60 days ahead of the second ping.
+ fakeNow(futureDate(oldestDirectoryDate, 60 * MILLISECONDS_PER_DAY));
+ // Reset TelemetryController and TelemetryArchive.
+ await TelemetryController.testReset();
+ // Wait for the cleanup to finish.
+ await TelemetryStorage.testCleanupTaskPromise();
+ // Then scan the archived dir again.
+ await TelemetryArchive.promiseArchivedPingList();
+
+ // Move the oldest ping to the unexpected pings list.
+ expectedPrunedInfo.push(expectedNotPrunedInfo.shift());
+ // Check that the archive is in the correct state.
+ await checkArchive();
+
+ // Find how much disk space the archive takes.
+ const archivedPingsInfo = await getArchivedPingsInfo();
+ let archiveSizeInBytes = archivedPingsInfo.reduce(
+ (lastResult, element) => lastResult + element.size,
+ 0
+ );
+
+ // Check that the correct values for quota probes are reported when no quota is hit.
+ h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SIZE_MB").snapshot();
+ Assert.equal(
+ h.sum,
+ Math.round(archiveSizeInBytes / 1024 / 1024),
+ "Telemetry must report the correct archive size."
+ );
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_ARCHIVE_EVICTED_OVER_QUOTA"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ 0,
+ "Telemetry must report 0 evictions if quota is not hit."
+ );
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_ARCHIVE_EVICTING_OVER_QUOTA_MS"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ 0,
+ "Telemetry must report a null elapsed time if quota is not hit."
+ );
+
+ // Set the quota to 80% of the space.
+ const testQuotaInBytes = archiveSizeInBytes * 0.8;
+ fakeStorageQuota(testQuotaInBytes);
+
+ // The storage prunes archived pings until we reach 90% of the requested storage quota.
+ // Based on that, find how many pings should be kept.
+ const safeQuotaSize = testQuotaInBytes * 0.9;
+ let sizeInBytes = 0;
+ let pingsWithinQuota = [];
+ let pingsOutsideQuota = [];
+
+ for (let pingInfo of archivedPingsInfo) {
+ sizeInBytes += pingInfo.size;
+ if (sizeInBytes >= safeQuotaSize) {
+ pingsOutsideQuota.push({
+ id: pingInfo.id,
+ creationDate: new Date(pingInfo.timestamp),
+ });
+ continue;
+ }
+ pingsWithinQuota.push({
+ id: pingInfo.id,
+ creationDate: new Date(pingInfo.timestamp),
+ });
+ }
+
+ expectedNotPrunedInfo = pingsWithinQuota;
+ expectedPrunedInfo = expectedPrunedInfo.concat(pingsOutsideQuota);
+
+ // Reset TelemetryArchive and TelemetryController to start the startup cleanup.
+ await TelemetryController.testReset();
+ await TelemetryStorage.testCleanupTaskPromise();
+ await TelemetryArchive.promiseArchivedPingList();
+ // Check that the archive is in the correct state.
+ await checkArchive();
+
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_ARCHIVE_EVICTED_OVER_QUOTA"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ pingsOutsideQuota.length,
+ "Telemetry must correctly report the over quota pings evicted from the archive."
+ );
+ h = Telemetry.getHistogramById("TELEMETRY_ARCHIVE_SIZE_MB").snapshot();
+ Assert.equal(
+ h.sum,
+ 300,
+ "Archive quota was hit, a special size must be reported."
+ );
+
+ // Trigger a cleanup again and make sure we're not removing anything.
+ await TelemetryController.testReset();
+ await TelemetryStorage.testCleanupTaskPromise();
+ await TelemetryArchive.promiseArchivedPingList();
+ await checkArchive();
+
+ const OVERSIZED_PING_ID = "9b21ec8f-f762-4d28-a2c1-44e1c4694f24";
+ // Create and archive an oversized, uncompressed, ping.
+ const OVERSIZED_PING = {
+ id: OVERSIZED_PING_ID,
+ type: PING_TYPE,
+ creationDate: new Date().toISOString(),
+ // Generate a ~2MB string to use as the payload.
+ payload: generateRandomString(2 * 1024 * 1024),
+ };
+ await TelemetryArchive.promiseArchivePing(OVERSIZED_PING);
+
+ // Get the size of the archived ping.
+ const oversizedPingPath =
+ TelemetryStorage._testGetArchivedPingPath(
+ OVERSIZED_PING.id,
+ new Date(OVERSIZED_PING.creationDate),
+ PING_TYPE
+ ) + "lz4";
+ const archivedPingSizeMB = Math.floor(
+ (await IOUtils.stat(oversizedPingPath)).size / 1024 / 1024
+ );
+
+ // We expect the oversized ping to be pruned when scanning the archive.
+ expectedPrunedInfo.push({
+ id: OVERSIZED_PING_ID,
+ creationDate: new Date(OVERSIZED_PING.creationDate),
+ });
+
+ // Scan the archive.
+ await TelemetryController.testReset();
+ await TelemetryStorage.testCleanupTaskPromise();
+ await TelemetryArchive.promiseArchivedPingList();
+ // The following also checks that non oversized pings are not removed.
+ await checkArchive();
+
+ // Make sure we're correctly updating the related histograms.
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_PING_SIZE_EXCEEDED_ARCHIVED"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ 1,
+ "Telemetry must report 1 oversized ping in the archive."
+ );
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_DISCARDED_ARCHIVED_PINGS_SIZE_MB"
+ ).snapshot();
+ Assert.equal(
+ h.values[archivedPingSizeMB],
+ 1,
+ "Telemetry must report the correct size for the oversized ping."
+ );
+});
+
+add_task(async function test_clientId() {
+ // Check that a ping submitted after the delayed telemetry initialization completed
+ // should get a valid client id.
+ await TelemetryController.testReset();
+ const clientId = await ClientID.getClientID();
+
+ let id = await TelemetryController.submitExternalPing(
+ "test-type",
+ {},
+ { addClientId: true }
+ );
+ let ping = await TelemetryArchive.promiseArchivedPingById(id);
+
+ Assert.ok(!!ping, "Should have loaded the ping.");
+ Assert.ok("clientId" in ping, "Ping should have a client id.");
+ Assert.ok(UUID_REGEX.test(ping.clientId), "Client id is in UUID format.");
+ Assert.equal(
+ ping.clientId,
+ clientId,
+ "Ping client id should match the global client id."
+ );
+
+ // We should have cached the client id now. Lets confirm that by
+ // checking the client id on a ping submitted before the async
+ // controller setup is finished.
+ let promiseSetup = TelemetryController.testReset();
+ id = await TelemetryController.submitExternalPing(
+ "test-type",
+ {},
+ { addClientId: true }
+ );
+ ping = await TelemetryArchive.promiseArchivedPingById(id);
+ Assert.equal(ping.clientId, clientId);
+
+ // Finish setup.
+ await promiseSetup;
+});
+
+add_task(async function test_InvalidPingType() {
+ const TYPES = [
+ "a",
+ "-",
+ "¿€€€?",
+ "-foo-",
+ "-moo",
+ "zoo-",
+ ".bar",
+ "asfd.asdf",
+ ];
+
+ for (let type of TYPES) {
+ let histogram = Telemetry.getKeyedHistogramById(
+ "TELEMETRY_INVALID_PING_TYPE_SUBMITTED"
+ );
+ Assert.ok(
+ !(type in histogram.snapshot()),
+ "Should not have counted this invalid ping yet: " + type
+ );
+ Assert.ok(
+ promiseRejects(TelemetryController.submitExternalPing(type, {})),
+ "Ping type should have been rejected."
+ );
+ Assert.equal(
+ histogram.snapshot()[type].sum,
+ 1,
+ "Should have counted this as an invalid ping type."
+ );
+ }
+});
+
+add_task(async function test_InvalidPayloadType() {
+ const PAYLOAD_TYPES = [19, "string", [1, 2, 3, 4], null, undefined];
+
+ let histogram = Telemetry.getHistogramById(
+ "TELEMETRY_INVALID_PAYLOAD_SUBMITTED"
+ );
+ for (let i = 0; i < PAYLOAD_TYPES.length; i++) {
+ histogram.clear();
+ Assert.equal(
+ histogram.snapshot().sum,
+ 0,
+ "Should not have counted this invalid payload yet: " +
+ JSON.stringify(PAYLOAD_TYPES[i])
+ );
+ Assert.ok(
+ await promiseRejects(
+ TelemetryController.submitExternalPing("payload-test", PAYLOAD_TYPES[i])
+ ),
+ "Payload type should have been rejected."
+ );
+ Assert.equal(
+ histogram.snapshot().sum,
+ 1,
+ "Should have counted this as an invalid payload type."
+ );
+ }
+});
+
+add_task(async function test_currentPingData() {
+ await TelemetryController.testSetup();
+
+ // Setup test data.
+ let h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTOUT");
+ h.clear();
+ h.add(1);
+ let k = Telemetry.getKeyedHistogramById(
+ "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT"
+ );
+ k.clear();
+ k.add("a", 1);
+
+ // Get current ping data objects and check that their data is sane.
+ for (let subsession of [true, false]) {
+ let ping = TelemetryController.getCurrentPingData(subsession);
+
+ Assert.ok(!!ping, "Should have gotten a ping.");
+ Assert.equal(ping.type, "main", "Ping should have correct type.");
+ const expectedReason = subsession
+ ? "gather-subsession-payload"
+ : "gather-payload";
+ Assert.equal(
+ ping.payload.info.reason,
+ expectedReason,
+ "Ping should have the correct reason."
+ );
+
+ let id = "TELEMETRY_TEST_RELEASE_OPTOUT";
+ Assert.ok(
+ id in ping.payload.histograms,
+ "Payload should have test count histogram."
+ );
+ Assert.equal(
+ ping.payload.histograms[id].sum,
+ 1,
+ "Test count value should match."
+ );
+ id = "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT";
+ Assert.ok(
+ id in ping.payload.keyedHistograms,
+ "Payload should have keyed test histogram."
+ );
+ Assert.equal(
+ ping.payload.keyedHistograms[id].a.sum,
+ 1,
+ "Keyed test value should match."
+ );
+ }
+});
+
+add_task(async function test_shutdown() {
+ await TelemetryController.testShutdown();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_PingSender.js b/toolkit/components/telemetry/tests/unit/test_PingSender.js
new file mode 100644
index 0000000000..2f640eed4a
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_PingSender.js
@@ -0,0 +1,281 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+
+// This tests submitting a ping using the stand-alone pingsender program.
+
+"use strict";
+
+const { TelemetrySend } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySend.sys.mjs"
+);
+const { TelemetryStorage } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryStorage.sys.mjs"
+);
+const { TelemetryUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryUtils.sys.mjs"
+);
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+function generateTestPingData() {
+ return {
+ type: "test-pingsender-type",
+ id: TelemetryUtils.generateUUID(),
+ creationDate: new Date().toISOString(),
+ version: 4,
+ payload: {
+ dummy: "stuff",
+ },
+ };
+}
+
+function testSendingPings(pingPaths) {
+ const url = "http://localhost:" + PingServer.port + "/submit/telemetry/";
+ const pings = pingPaths.map(path => {
+ return {
+ url,
+ path,
+ };
+ });
+ TelemetrySend.testRunPingSender(pings, (_, topic, __) => {
+ switch (topic) {
+ case "process-finished": // finished indicates an exit code of 0
+ Assert.ok(true, "Pingsender should be able to post to localhost");
+ break;
+ case "process-failed": // failed indicates an exit code != 0
+ Assert.ok(false, "Pingsender should be able to post to localhost");
+ break;
+ }
+ });
+}
+
+/**
+ * Wait for a ping file to be deleted from the pending pings directory.
+ */
+function waitForPingDeletion(pingId) {
+ const path = PathUtils.join(TelemetryStorage.pingDirectoryPath, pingId);
+
+ let checkFn = (resolve, reject) =>
+ setTimeout(() => {
+ IOUtils.exists(path).then(exists => {
+ if (!exists) {
+ Assert.ok(true, `${pingId} was deleted`);
+ resolve();
+ } else {
+ checkFn(resolve, reject);
+ }
+ }, reject);
+ }, 250);
+
+ return new Promise((resolve, reject) => checkFn(resolve, reject));
+}
+
+add_task(async function setup() {
+ // Init the profile.
+ do_get_profile(true);
+
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+
+ // Start the ping server and let Telemetry know about it.
+ PingServer.start();
+});
+
+async function test_pingSender(version = "1.0") {
+ // Generate a new ping and save it among the pending pings.
+ const data = generateTestPingData();
+ await TelemetryStorage.savePing(data, true);
+
+ // Get the local path of the saved ping.
+ const pingPath = PathUtils.join(TelemetryStorage.pingDirectoryPath, data.id);
+
+ // Spawn an HTTP server that returns an error. We will be running the
+ // PingSender twice, trying to send the ping to this server. After the
+ // second time, we will resolve |deferred404Hit|.
+ let failingServer = new HttpServer();
+ let deferred404Hit = Promise.withResolvers();
+ let hitCount = 0;
+ failingServer.registerPathHandler("/lookup_fail", (metadata, response) => {
+ response.setStatusLine("1.1", 404, "Not Found");
+ hitCount++;
+
+ if (hitCount >= 2) {
+ // Resolve the promise on the next tick.
+ Services.tm.dispatchToMainThread(() => deferred404Hit.resolve());
+ }
+ });
+ failingServer.start(-1);
+
+ // Try to send the ping twice using the pingsender (we expect 404 both times).
+ const errorUrl =
+ "http://localhost:" + failingServer.identity.primaryPort + "/lookup_fail";
+ TelemetrySend.testRunPingSender([{ url: errorUrl, path: pingPath }]);
+ TelemetrySend.testRunPingSender([{ url: errorUrl, path: pingPath }]);
+
+ // Wait until we hit the 404 server twice. After that, make sure that the ping
+ // still exists locally.
+ await deferred404Hit.promise;
+ Assert.ok(
+ await IOUtils.exists(pingPath),
+ "The pending ping must not be deleted if we fail to send using the PingSender"
+ );
+
+ // Try to send it using the pingsender.
+ testSendingPings([pingPath]);
+
+ let req = await PingServer.promiseNextRequest();
+ let ping = decodeRequestPayload(req);
+
+ Assert.equal(
+ req.getHeader("User-Agent"),
+ `pingsender/${version}`,
+ "Should have received the correct user agent string."
+ );
+ Assert.equal(
+ req.getHeader("X-PingSender-Version"),
+ version,
+ "Should have received the correct PingSender version string."
+ );
+ Assert.equal(
+ req.getHeader("Content-Encoding"),
+ "gzip",
+ "Should have a gzip encoded ping."
+ );
+ Assert.ok(req.getHeader("Date"), "Should have received a Date header.");
+ Assert.equal(ping.id, data.id, "Should have received the correct ping id.");
+ Assert.equal(
+ ping.type,
+ data.type,
+ "Should have received the correct ping type."
+ );
+ Assert.deepEqual(
+ ping.payload,
+ data.payload,
+ "Should have received the correct payload."
+ );
+
+ // Check that the PingSender removed the pending ping.
+ await waitForPingDeletion(data.id);
+
+ // Shut down the failing server.
+ await new Promise(r => failingServer.stop(r));
+}
+
+add_task(async function test_pingsender1() {
+ let orig = Services.prefs.getBoolPref(
+ "toolkit.telemetry.shutdownPingSender.backgroundtask.enabled",
+ false
+ );
+ try {
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.shutdownPingSender.backgroundtask.enabled",
+ false
+ );
+ await test_pingSender("1.0");
+ } finally {
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.shutdownPingSender.backgroundtask.enabled",
+ orig
+ );
+ }
+});
+
+add_task(async function test_pingsender2() {
+ let orig = Services.prefs.getBoolPref(
+ "toolkit.telemetry.shutdownPingSender.backgroundtask.enabled",
+ false
+ );
+ try {
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.shutdownPingSender.backgroundtask.enabled",
+ true
+ );
+ await test_pingSender("2.0");
+ } finally {
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.shutdownPingSender.backgroundtask.enabled",
+ orig
+ );
+ }
+});
+
+add_task(async function test_bannedDomains() {
+ // Generate a new ping and save it among the pending pings.
+ const data = generateTestPingData();
+ await TelemetryStorage.savePing(data, true);
+
+ // Get the local path of the saved ping.
+ const pingPath = PathUtils.join(TelemetryStorage.pingDirectoryPath, data.id);
+
+ // Confirm we can't send a ping to another destination url
+ let bannedUris = [
+ "https://example.com",
+ "http://localhost.com",
+ "http://localHOST.com",
+ "http://localhost@example.com",
+ "http://localhost:bob@example.com",
+ "http://localhost:localhost@localhost.example.com",
+ ];
+ for (let url of bannedUris) {
+ let result = await new Promise(resolve =>
+ TelemetrySend.testRunPingSender(
+ [{ url, path: pingPath }],
+ (_, topic, __) => {
+ switch (topic) {
+ case "process-finished": // finished indicates an exit code of 0
+ case "process-failed": // failed indicates an exit code != 0
+ resolve(topic);
+ }
+ }
+ )
+ );
+ Assert.equal(
+ result,
+ "process-failed",
+ `Pingsender should not be able to post to ${url}`
+ );
+ }
+});
+
+add_task(async function test_pingSender_multiple_pings() {
+ // Generate two new pings and save them among the pending pings.
+ const data = [generateTestPingData(), generateTestPingData()];
+
+ for (const d of data) {
+ await TelemetryStorage.savePing(d, true);
+ }
+
+ // Get the local path of the saved pings.
+ const pingPaths = data.map(d =>
+ PathUtils.join(TelemetryStorage.pingDirectoryPath, d.id)
+ );
+
+ // Try to send them using the pingsender.
+ testSendingPings(pingPaths);
+
+ // Check the pings. We don't have an ordering guarantee, so we move the
+ // elements to a new array when we find them.
+ let data2 = [];
+ while (data.length) {
+ let req = await PingServer.promiseNextRequest();
+ let ping = decodeRequestPayload(req);
+ let idx = data.findIndex(d => d.id == ping.id);
+ Assert.ok(
+ idx >= 0,
+ `Should have received the correct ping id: ${data[idx].id}`
+ );
+ data2.push(data[idx]);
+ data.splice(idx, 1);
+ }
+
+ // Check that the PingSender removed the pending pings.
+ for (const d of data2) {
+ await waitForPingDeletion(d.id);
+ }
+});
+
+add_task(async function cleanup() {
+ await PingServer.stop();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_RDDScalars.js b/toolkit/components/telemetry/tests/unit/test_RDDScalars.js
new file mode 100644
index 0000000000..e7078012d2
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_RDDScalars.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+);
+
+const RDD_ONLY_UINT_SCALAR = "telemetry.test.rdd_only_uint";
+
+const rddProcessTest = () => {
+ return Cc["@mozilla.org/rdd-process-test;1"].createInstance(
+ Ci.nsIRddProcessTest
+ );
+};
+
+/**
+ * This function waits until rdd scalars are reported into the
+ * scalar snapshot.
+ */
+async function waitForRddScalars() {
+ await ContentTaskUtils.waitForCondition(() => {
+ const scalars = Telemetry.getSnapshotForScalars("main", false);
+ return Object.keys(scalars).includes("rdd");
+ }, "Waiting for rdd scalars to have been set");
+}
+
+add_setup(async function setup_telemetry_rdd() {
+ do_get_profile(true);
+ await TelemetryController.testSetup();
+});
+
+add_task(async function test_scalars_in_rdd_process() {
+ Telemetry.clearScalars();
+ const pid = await rddProcessTest().testTelemetryProbes();
+ info(`Started some RDD: ${pid}`);
+
+ // Once scalars are set by the rdd process, they don't immediately get
+ // sent to the parent process. Wait for the Telemetry IPC Timer to trigger
+ // and batch send the data back to the parent process.
+ await waitForRddScalars();
+
+ Assert.equal(
+ Telemetry.getSnapshotForScalars("main", false).rdd[RDD_ONLY_UINT_SCALAR],
+ 42,
+ `${RDD_ONLY_UINT_SCALAR} must have the correct value (rdd process).`
+ );
+
+ await rddProcessTest().stopProcess();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_SocketScalars.js b/toolkit/components/telemetry/tests/unit/test_SocketScalars.js
new file mode 100644
index 0000000000..a685a611b2
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_SocketScalars.js
@@ -0,0 +1,49 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+);
+
+const SOCKET_ONLY_UINT_SCALAR = "telemetry.test.socket_only_uint";
+
+/**
+ * This function waits until socket scalars are reported into the
+ * scalar snapshot.
+ */
+async function waitForSocketScalars() {
+ await ContentTaskUtils.waitForCondition(() => {
+ const scalars = Telemetry.getSnapshotForScalars("main", false);
+ return Object.keys(scalars).includes("socket");
+ }, "Waiting for socket scalars to have been set");
+}
+
+add_task(async function test_scalars_in_socket_process() {
+ Assert.ok(
+ Services.prefs.getBoolPref("network.process.enabled"),
+ "Socket process should be enabled"
+ );
+
+ do_get_profile(true);
+ await TelemetryController.testSetup();
+
+ Services.io.socketProcessTelemetryPing();
+
+ // Once scalars are set by the socket process, they don't immediately get
+ // sent to the parent process. Wait for the Telemetry IPC Timer to trigger
+ // and batch send the data back to the parent process.
+ // Note: this requires the socket process to be enabled (see bug 1716307).
+ await waitForSocketScalars();
+
+ Assert.equal(
+ Telemetry.getSnapshotForScalars("main", false).socket[
+ SOCKET_ONLY_UINT_SCALAR
+ ],
+ 42,
+ `${SOCKET_ONLY_UINT_SCALAR} must have the correct value (socket process).`
+ );
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_SubsessionChaining.js b/toolkit/components/telemetry/tests/unit/test_SubsessionChaining.js
new file mode 100644
index 0000000000..cebd134994
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_SubsessionChaining.js
@@ -0,0 +1,286 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+const { TelemetryArchive } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryArchive.sys.mjs"
+);
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { TelemetryEnvironment } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryEnvironment.sys.mjs"
+);
+
+const MS_IN_ONE_HOUR = 60 * 60 * 1000;
+const MS_IN_ONE_DAY = 24 * MS_IN_ONE_HOUR;
+
+const PREF_BRANCH = "toolkit.telemetry.";
+
+const REASON_ABORTED_SESSION = "aborted-session";
+const REASON_DAILY = "daily";
+const REASON_ENVIRONMENT_CHANGE = "environment-change";
+const REASON_SHUTDOWN = "shutdown";
+
+var promiseValidateArchivedPings = async function (aExpectedReasons) {
+ // The list of ping reasons which mark the session end (and must reset the subsession
+ // count).
+ const SESSION_END_PING_REASONS = new Set([
+ REASON_ABORTED_SESSION,
+ REASON_SHUTDOWN,
+ ]);
+
+ let list = await TelemetryArchive.promiseArchivedPingList();
+
+ // We're just interested in the "main" pings.
+ list = list.filter(p => p.type == "main");
+
+ Assert.equal(
+ aExpectedReasons.length,
+ list.length,
+ "All the expected pings must be received."
+ );
+
+ let previousPing = await TelemetryArchive.promiseArchivedPingById(list[0].id);
+ Assert.equal(
+ aExpectedReasons.shift(),
+ previousPing.payload.info.reason,
+ "Telemetry should only get pings with expected reasons."
+ );
+ Assert.equal(
+ previousPing.payload.info.previousSessionId,
+ null,
+ "The first session must report a null previous session id."
+ );
+ Assert.equal(
+ previousPing.payload.info.previousSubsessionId,
+ null,
+ "The first subsession must report a null previous subsession id."
+ );
+ Assert.equal(
+ previousPing.payload.info.profileSubsessionCounter,
+ 1,
+ "profileSubsessionCounter must be 1 the first time."
+ );
+ Assert.equal(
+ previousPing.payload.info.subsessionCounter,
+ 1,
+ "subsessionCounter must be 1 the first time."
+ );
+
+ let expectedSubsessionCounter = 1;
+ let expectedPreviousSessionId = previousPing.payload.info.sessionId;
+
+ for (let i = 1; i < list.length; i++) {
+ let currentPing = await TelemetryArchive.promiseArchivedPingById(
+ list[i].id
+ );
+ let currentInfo = currentPing.payload.info;
+ let previousInfo = previousPing.payload.info;
+ info(
+ "Archive entry " +
+ i +
+ " - id: " +
+ currentPing.id +
+ ", reason: " +
+ currentInfo.reason
+ );
+
+ Assert.equal(
+ aExpectedReasons.shift(),
+ currentInfo.reason,
+ "Telemetry should only get pings with expected reasons."
+ );
+ Assert.equal(
+ currentInfo.previousSessionId,
+ expectedPreviousSessionId,
+ "Telemetry must correctly chain session identifiers."
+ );
+ Assert.equal(
+ currentInfo.previousSubsessionId,
+ previousInfo.subsessionId,
+ "Telemetry must correctly chain subsession identifiers."
+ );
+ Assert.equal(
+ currentInfo.profileSubsessionCounter,
+ previousInfo.profileSubsessionCounter + 1,
+ "Telemetry must correctly track the profile subsessions count."
+ );
+ Assert.equal(
+ currentInfo.subsessionCounter,
+ expectedSubsessionCounter,
+ "The subsession counter should be monotonically increasing."
+ );
+
+ // Store the current ping as previous.
+ previousPing = currentPing;
+ // Reset the expected subsession counter, if required. Otherwise increment the expected
+ // subsession counter.
+ // If this is the final subsession of a session we need to update expected values accordingly.
+ if (SESSION_END_PING_REASONS.has(currentInfo.reason)) {
+ expectedSubsessionCounter = 1;
+ expectedPreviousSessionId = currentInfo.sessionId;
+ } else {
+ expectedSubsessionCounter++;
+ }
+ }
+};
+
+add_task(async function test_setup() {
+ do_test_pending();
+
+ // Addon manager needs a profile directory
+ do_get_profile();
+ await loadAddonManager(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "1.9.2"
+ );
+ finishAddonManagerStartup();
+ fakeIntlReady();
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+});
+
+add_task(async function test_subsessionsChaining() {
+ if (gIsAndroid) {
+ // We don't support subsessions yet on Android, so skip the next checks.
+ return;
+ }
+
+ const PREF_TEST = PREF_BRANCH + "test.pref1";
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_VALUE }],
+ ]);
+ Services.prefs.clearUserPref(PREF_TEST);
+
+ // Fake the clock data to manually trigger an aborted-session ping and a daily ping.
+ // This is also helpful to make sure we get the archived pings in an expected order.
+ let now = fakeNow(2009, 9, 18, 0, 0, 0);
+ let monotonicNow = fakeMonotonicNow(1000);
+
+ let moveClockForward = minutes => {
+ let ms = minutes * MILLISECONDS_PER_MINUTE;
+ now = fakeNow(futureDate(now, ms));
+ monotonicNow = fakeMonotonicNow(monotonicNow + ms);
+ };
+
+ // Keep track of the ping reasons we're expecting in this test.
+ let expectedReasons = [];
+
+ // Start and shut down Telemetry. We expect a shutdown ping with profileSubsessionCounter: 1,
+ // subsessionCounter: 1, subsessionId: A, and previousSubsessionId: null to be archived.
+ await TelemetryController.testSetup();
+ await TelemetryController.testShutdown();
+ expectedReasons.push(REASON_SHUTDOWN);
+
+ // Start Telemetry but don't wait for it to initialise before shutting down. We expect a
+ // shutdown ping with profileSubsessionCounter: 2, subsessionCounter: 1, subsessionId: B
+ // and previousSubsessionId: A to be archived.
+ moveClockForward(30);
+ TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ expectedReasons.push(REASON_SHUTDOWN);
+
+ // Start Telemetry and simulate an aborted-session ping. We expect an aborted-session ping
+ // with profileSubsessionCounter: 3, subsessionCounter: 1, subsessionId: C and
+ // previousSubsessionId: B to be archived.
+ let schedulerTickCallback = null;
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+ moveClockForward(6);
+ // Trigger the an aborted session ping save. When testing,we are not saving the aborted-session
+ // ping as soon as Telemetry starts, otherwise we would end up with unexpected pings being
+ // sent when calling |TelemetryController.testReset()|, thus breaking some tests.
+ Assert.ok(!!schedulerTickCallback);
+ await schedulerTickCallback();
+ expectedReasons.push(REASON_ABORTED_SESSION);
+
+ // Start Telemetry and trigger an environment change through a pref modification. We expect
+ // an environment-change ping with profileSubsessionCounter: 4, subsessionCounter: 1,
+ // subsessionId: D and previousSubsessionId: C to be archived.
+ moveClockForward(30);
+ await TelemetryController.testReset();
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+ moveClockForward(30);
+ Services.prefs.setIntPref(PREF_TEST, 1);
+ expectedReasons.push(REASON_ENVIRONMENT_CHANGE);
+
+ // Shut down Telemetry. We expect a shutdown ping with profileSubsessionCounter: 5,
+ // subsessionCounter: 2, subsessionId: E and previousSubsessionId: D to be archived.
+ moveClockForward(30);
+ await TelemetryController.testShutdown();
+ expectedReasons.push(REASON_SHUTDOWN);
+
+ // Start Telemetry and trigger a daily ping. We expect a daily ping with
+ // profileSubsessionCounter: 6, subsessionCounter: 1, subsessionId: F and
+ // previousSubsessionId: E to be archived.
+ moveClockForward(30);
+ await TelemetryController.testReset();
+
+ // Delay the callback around midnight.
+ now = fakeNow(futureDate(now, MS_IN_ONE_DAY));
+ // Trigger the daily ping.
+ await schedulerTickCallback();
+ expectedReasons.push(REASON_DAILY);
+
+ // Trigger an environment change ping. We expect an environment-changed ping with
+ // profileSubsessionCounter: 7, subsessionCounter: 2, subsessionId: G and
+ // previousSubsessionId: F to be archived.
+ moveClockForward(30);
+ Services.prefs.setIntPref(PREF_TEST, 0);
+ expectedReasons.push(REASON_ENVIRONMENT_CHANGE);
+
+ // Shut down Telemetry and trigger a shutdown ping.
+ moveClockForward(30);
+ await TelemetryController.testShutdown();
+ expectedReasons.push(REASON_SHUTDOWN);
+
+ // Start Telemetry and trigger an environment change.
+ await TelemetryController.testReset();
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+ moveClockForward(30);
+ Services.prefs.setIntPref(PREF_TEST, 1);
+ expectedReasons.push(REASON_ENVIRONMENT_CHANGE);
+
+ // Don't shut down, instead trigger an aborted-session ping.
+ moveClockForward(6);
+ // Trigger the an aborted session ping save.
+ await schedulerTickCallback();
+ expectedReasons.push(REASON_ABORTED_SESSION);
+
+ // Start Telemetry and trigger a daily ping.
+ moveClockForward(30);
+ await TelemetryController.testReset();
+ // Delay the callback around midnight.
+ now = futureDate(now, MS_IN_ONE_DAY);
+ fakeNow(now);
+ // Trigger the daily ping.
+ await schedulerTickCallback();
+ expectedReasons.push(REASON_DAILY);
+
+ // Trigger an environment change.
+ moveClockForward(30);
+ Services.prefs.setIntPref(PREF_TEST, 0);
+ expectedReasons.push(REASON_ENVIRONMENT_CHANGE);
+
+ // And an aborted-session ping again.
+ moveClockForward(6);
+ // Trigger the an aborted session ping save.
+ await schedulerTickCallback();
+ expectedReasons.push(REASON_ABORTED_SESSION);
+
+ // Make sure the aborted-session ping gets archived.
+ await TelemetryController.testReset();
+
+ await promiseValidateArchivedPings(expectedReasons);
+});
+
+add_task(async function () {
+ await TelemetryController.testShutdown();
+ do_test_finished();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_SyncPingIntegration.js b/toolkit/components/telemetry/tests/unit/test_SyncPingIntegration.js
new file mode 100644
index 0000000000..16a18644e9
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_SyncPingIntegration.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+// Enable the collection (during test) for all products so even products
+// that don't collect the data will be able to run the test without failure.
+Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+);
+
+add_setup(async function test_setup() {
+ // Addon manager needs a profile directory
+ do_get_profile();
+});
+
+add_task(async function test_register_twice_fails() {
+ TelemetryController.registerSyncPingShutdown(() => {});
+ Assert.throws(
+ () => TelemetryController.registerSyncPingShutdown(() => {}),
+ /The sync ping shutdown handler is already registered./
+ );
+ await TelemetryController.testReset();
+});
+
+add_task(async function test_reset_clears_handler() {
+ await TelemetryController.testSetup();
+ TelemetryController.registerSyncPingShutdown(() => {});
+ await TelemetryController.testReset();
+ // If this works the reset must have cleared it.
+ TelemetryController.registerSyncPingShutdown(() => {});
+ await TelemetryController.testReset();
+});
+
+add_task(async function test_shutdown_handler_submits() {
+ let handlerCalled = false;
+ await TelemetryController.testSetup();
+ TelemetryController.registerSyncPingShutdown(() => {
+ handlerCalled = true;
+ // and submit a ping.
+ let ping = {
+ why: "shutdown",
+ };
+ TelemetryController.submitExternalPing("sync", ping);
+ });
+
+ await TelemetryController.testShutdown();
+ Assert.ok(handlerCalled);
+ await TelemetryController.testReset();
+});
+
+add_task(async function test_shutdown_handler_no_submit() {
+ let handlerCalled = false;
+ await TelemetryController.testSetup();
+ TelemetryController.registerSyncPingShutdown(() => {
+ handlerCalled = true;
+ // but don't submit a ping.
+ });
+
+ await TelemetryController.testShutdown();
+ Assert.ok(handlerCalled);
+});
+
+// NB: The last test in this file *must not* restart TelemetryController, or it
+// will cause intermittent timeouts for this test.
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryAndroidEnvironment.js b/toolkit/components/telemetry/tests/unit/test_TelemetryAndroidEnvironment.js
new file mode 100644
index 0000000000..b5035ff56c
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryAndroidEnvironment.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+/* Android-only TelemetryEnvironment xpcshell test that ensures that the device data is stored in the Environment.
+ */
+
+const { TelemetryEnvironment } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryEnvironment.sys.mjs"
+);
+
+/**
+ * Check that a value is a string and not empty.
+ *
+ * @param aValue The variable to check.
+ * @return True if |aValue| has type "string" and is not empty, False otherwise.
+ */
+function checkString(aValue) {
+ return typeof aValue == "string" && aValue != "";
+}
+
+/**
+ * If value is non-null, check if it's a valid string.
+ *
+ * @param aValue The variable to check.
+ * @return True if it's null or a valid string, false if it's non-null and an invalid
+ * string.
+ */
+function checkNullOrString(aValue) {
+ if (aValue) {
+ return checkString(aValue);
+ } else if (aValue === null) {
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * If value is non-null, check if it's a boolean.
+ *
+ * @param aValue The variable to check.
+ * @return True if it's null or a valid boolean, false if it's non-null and an invalid
+ * boolean.
+ */
+function checkNullOrBool(aValue) {
+ return aValue === null || typeof aValue == "boolean";
+}
+
+function checkSystemSection(data) {
+ Assert.ok("system" in data, "There must be a system section in Environment.");
+ // Device data is only available on Android.
+ if (gIsAndroid) {
+ let deviceData = data.system.device;
+ Assert.ok(checkNullOrString(deviceData.model));
+ Assert.ok(checkNullOrString(deviceData.manufacturer));
+ Assert.ok(checkNullOrString(deviceData.hardware));
+ Assert.ok(checkNullOrBool(deviceData.isTablet));
+ }
+}
+
+add_task(async function test_systemEnvironment() {
+ let environmentData = TelemetryEnvironment.currentEnvironment;
+ checkSystemSection(environmentData);
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryChildEvents_buildFaster.js b/toolkit/components/telemetry/tests/unit/test_TelemetryChildEvents_buildFaster.js
new file mode 100644
index 0000000000..00a45c8b12
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryChildEvents_buildFaster.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+);
+
+const MESSAGE_CHILD_TEST_DONE = "ChildTest:Done";
+
+const TEST_STATIC_EVENT_NAME = "telemetry.test";
+const TEST_EVENT_NAME = "telemetry.test.child";
+
+function run_child_test() {
+ Telemetry.recordEvent(TEST_EVENT_NAME, "child", "builtin");
+ Telemetry.recordEvent(TEST_STATIC_EVENT_NAME, "main_and_content", "object1");
+ Telemetry.recordEvent(TEST_EVENT_NAME, "child", "anotherone");
+}
+
+/**
+ * This function waits until content events are reported into the
+ * events snapshot.
+ */
+async function waitForContentEvents() {
+ await ContentTaskUtils.waitForCondition(() => {
+ const snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ return Object.keys(snapshot).includes("content");
+ });
+}
+
+add_task(async function test_setup() {
+ if (!runningInParent) {
+ TelemetryController.testSetupContent();
+ run_child_test();
+ do_send_remote_message(MESSAGE_CHILD_TEST_DONE);
+ return;
+ }
+
+ // Setup.
+ do_get_profile(true);
+ await loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
+ finishAddonManagerStartup();
+ fakeIntlReady();
+ await TelemetryController.testSetup();
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+ // Enable recording for the test event category.
+
+ // Register some dynamic builtin test events.
+ Telemetry.registerBuiltinEvents(TEST_EVENT_NAME, {
+ dynamic: {
+ methods: ["dynamic", "child"],
+ objects: ["builtin", "anotherone"],
+ },
+ dynamic_expired: {
+ methods: ["check"],
+ objects: ["expiry"],
+ expired: true,
+ },
+ });
+ Telemetry.setEventRecordingEnabled(TEST_STATIC_EVENT_NAME, true);
+ Telemetry.setEventRecordingEnabled(TEST_EVENT_NAME, true);
+
+ Telemetry.recordEvent(TEST_EVENT_NAME, "dynamic", "builtin");
+ Telemetry.recordEvent(TEST_STATIC_EVENT_NAME, "main_and_content", "object1");
+ Telemetry.recordEvent(TEST_EVENT_NAME, "dynamic", "anotherone");
+ Telemetry.recordEvent(TEST_EVENT_NAME, "check", "expiry");
+
+ // Run test in child, don't wait for it to finish: just wait for the
+ // MESSAGE_CHILD_TEST_DONE.
+ run_test_in_child("test_TelemetryChildEvents_buildFaster.js");
+ await do_await_remote_message(MESSAGE_CHILD_TEST_DONE);
+
+ // Once events are set by the content process, they don't immediately get
+ // sent to the parent process. Wait for the Telemetry IPC Timer to trigger
+ // and batch send the data back to the parent process.
+ await waitForContentEvents();
+
+ let snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ Assert.ok("parent" in snapshot, "Should have parent events in the snapshot.");
+ Assert.ok(
+ "content" in snapshot,
+ "Should have content events in the snapshot."
+ );
+
+ // All events should now be recorded in the right order
+ let expectedParent = [
+ [TEST_EVENT_NAME, "dynamic", "builtin"],
+ [TEST_STATIC_EVENT_NAME, "main_and_content", "object1"],
+ [TEST_EVENT_NAME, "dynamic", "anotherone"],
+ ];
+ let expectedContent = [
+ [TEST_EVENT_NAME, "child", "builtin"],
+ [TEST_STATIC_EVENT_NAME, "main_and_content", "object1"],
+ [TEST_EVENT_NAME, "child", "anotherone"],
+ ];
+
+ Assert.equal(
+ snapshot.parent.length,
+ expectedParent.length,
+ "Should have recorded the right amount of events in parent."
+ );
+ for (let i = 0; i < expectedParent.length; ++i) {
+ Assert.deepEqual(
+ snapshot.parent[i].slice(1),
+ expectedParent[i],
+ "Should have recorded the expected event data in parent."
+ );
+ }
+
+ Assert.equal(
+ snapshot.content.length,
+ expectedContent.length,
+ "Should have recorded the right amount of events in content."
+ );
+ for (let i = 0; i < expectedContent.length; ++i) {
+ Assert.deepEqual(
+ snapshot.content[i].slice(1),
+ expectedContent[i],
+ "Should have recorded the expected event data in content."
+ );
+ }
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryClientID_reset.js b/toolkit/components/telemetry/tests/unit/test_TelemetryClientID_reset.js
new file mode 100644
index 0000000000..889de4498a
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryClientID_reset.js
@@ -0,0 +1,191 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+const { ClientID } = ChromeUtils.importESModule(
+ "resource://gre/modules/ClientID.sys.mjs"
+);
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { TelemetryStorage } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryStorage.sys.mjs"
+);
+const { TelemetryUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryUtils.sys.mjs"
+);
+
+const DELETION_REQUEST_PING_TYPE = "deletion-request";
+const TEST_PING_TYPE = "test-ping-type";
+
+function sendPing(addEnvironment = false) {
+ let options = {
+ addClientId: true,
+ addEnvironment,
+ };
+ return TelemetryController.submitExternalPing(TEST_PING_TYPE, {}, options);
+}
+
+add_task(async function test_setup() {
+ // Addon manager needs a profile directory
+ do_get_profile();
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+
+ await new Promise(resolve =>
+ Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(resolve))
+ );
+
+ PingServer.start();
+ Services.prefs.setStringPref(
+ TelemetryUtils.Preferences.Server,
+ "http://localhost:" + PingServer.port
+ );
+ await TelemetryController.testSetup();
+});
+
+/**
+ * Testing the following scenario:
+ *
+ * 1. Telemetry upload gets disabled
+ * 2. Canary client ID is set
+ * 3. Instance is shut down
+ * 4. Telemetry upload flag is toggled
+ * 5. Instance is started again
+ * 6. Detect that upload is enabled and reset client ID
+ *
+ * This scenario e.g. happens when switching between channels
+ * with and without the deletion-request ping reset included.
+ */
+add_task(async function test_clientid_reset_after_reenabling() {
+ await sendPing();
+ let ping = await PingServer.promiseNextPing();
+ Assert.equal(ping.type, TEST_PING_TYPE, "The ping must be a test ping");
+ Assert.ok("clientId" in ping);
+
+ let firstClientId = ping.clientId;
+ Assert.notEqual(
+ TelemetryUtils.knownClientID,
+ firstClientId,
+ "Client ID should be valid and random"
+ );
+
+ // Disable FHR upload: this should trigger a deletion-request ping.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ false
+ );
+
+ ping = await PingServer.promiseNextPing();
+ Assert.equal(
+ ping.type,
+ DELETION_REQUEST_PING_TYPE,
+ "The ping must be a deletion-request ping"
+ );
+ Assert.equal(ping.clientId, firstClientId);
+ let clientId = await ClientID.getClientID();
+ Assert.equal(TelemetryUtils.knownClientID, clientId);
+
+ // Now shutdown the instance
+ await TelemetryController.testShutdown();
+ await TelemetryStorage.testClearPendingPings();
+
+ // Flip the pref again
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+
+ // Start the instance
+ await TelemetryController.testReset();
+
+ let newClientId = await ClientID.getClientID();
+ Assert.notEqual(
+ TelemetryUtils.knownClientID,
+ newClientId,
+ "Client ID should be valid and random"
+ );
+ Assert.notEqual(
+ firstClientId,
+ newClientId,
+ "Client ID should be newly generated"
+ );
+});
+
+/**
+ * Testing the following scenario:
+ * (Reverse of the first test)
+ *
+ * 1. Telemetry upload gets disabled, canary client ID is set
+ * 2. Telemetry upload is enabled
+ * 3. New client ID is generated.
+ * 3. Instance is shut down
+ * 4. Telemetry upload flag is toggled
+ * 5. Instance is started again
+ * 6. Detect that upload is disabled and sets canary client ID
+ *
+ * This scenario e.g. happens when switching between channels
+ * with and without the deletion-request ping reset included.
+ */
+add_task(async function test_clientid_canary_after_disabling() {
+ await sendPing();
+ let ping = await PingServer.promiseNextPing();
+ Assert.equal(ping.type, TEST_PING_TYPE, "The ping must be a test ping");
+ Assert.ok("clientId" in ping);
+
+ let firstClientId = ping.clientId;
+ Assert.notEqual(
+ TelemetryUtils.knownClientID,
+ firstClientId,
+ "Client ID should be valid and random"
+ );
+
+ // Disable FHR upload: this should trigger a deletion-request ping.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ false
+ );
+
+ ping = await PingServer.promiseNextPing();
+ Assert.equal(
+ ping.type,
+ DELETION_REQUEST_PING_TYPE,
+ "The ping must be a deletion-request ping"
+ );
+ Assert.equal(ping.clientId, firstClientId);
+ let clientId = await ClientID.getClientID();
+ Assert.equal(TelemetryUtils.knownClientID, clientId);
+
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+ await sendPing();
+ ping = await PingServer.promiseNextPing();
+ Assert.equal(ping.type, TEST_PING_TYPE, "The ping must be a test ping");
+ Assert.notEqual(
+ firstClientId,
+ ping.clientId,
+ "Client ID should be newly generated"
+ );
+
+ // Now shutdown the instance
+ await TelemetryController.testShutdown();
+ await TelemetryStorage.testClearPendingPings();
+
+ // Flip the pref again
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ false
+ );
+
+ // Start the instance
+ await TelemetryController.testReset();
+
+ let newClientId = await ClientID.getClientID();
+ Assert.equal(
+ TelemetryUtils.knownClientID,
+ newClientId,
+ "Client ID should be a canary when upload disabled"
+ );
+});
+
+add_task(async function stopServer() {
+ await PingServer.stop();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryController.js b/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
new file mode 100644
index 0000000000..5af7adf6b0
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryController.js
@@ -0,0 +1,1225 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+/* This testcase triggers two telemetry pings.
+ *
+ * Telemetry code keeps histograms of past telemetry pings. The first
+ * ping populates these histograms. One of those histograms is then
+ * checked in the second request.
+ */
+
+const { ClientID } = ChromeUtils.importESModule(
+ "resource://gre/modules/ClientID.sys.mjs"
+);
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { TelemetryStorage } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryStorage.sys.mjs"
+);
+const { TelemetrySend } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySend.sys.mjs"
+);
+const { TelemetryArchive } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryArchive.sys.mjs"
+);
+const { TelemetryUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryUtils.sys.mjs"
+);
+const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+);
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+const { TelemetryArchiveTesting } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryArchiveTesting.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ JsonSchemaValidator:
+ "resource://gre/modules/components-utils/JsonSchemaValidator.sys.mjs",
+ jwcrypto: "resource://services-crypto/jwcrypto.sys.mjs",
+});
+
+const PING_FORMAT_VERSION = 4;
+const DELETION_REQUEST_PING_TYPE = "deletion-request";
+const TEST_PING_TYPE = "test-ping-type";
+
+var gClientID = null;
+
+ChromeUtils.defineLazyGetter(this, "DATAREPORTING_PATH", async function () {
+ return PathUtils.join(PathUtils.profileDir, "datareporting");
+});
+
+function sendPing(aSendClientId, aSendEnvironment) {
+ if (PingServer.started) {
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+ } else {
+ TelemetrySend.setServer("http://doesnotexist");
+ }
+
+ let options = {
+ addClientId: aSendClientId,
+ addEnvironment: aSendEnvironment,
+ };
+ return TelemetryController.submitExternalPing(TEST_PING_TYPE, {}, options);
+}
+
+function checkPingFormat(aPing, aType, aHasClientId, aHasEnvironment) {
+ const MANDATORY_PING_FIELDS = [
+ "type",
+ "id",
+ "creationDate",
+ "version",
+ "application",
+ "payload",
+ ];
+
+ const APPLICATION_TEST_DATA = {
+ buildId: gAppInfo.appBuildID,
+ name: APP_NAME,
+ version: APP_VERSION,
+ displayVersion: AppConstants.MOZ_APP_VERSION_DISPLAY,
+ vendor: "Mozilla",
+ platformVersion: PLATFORM_VERSION,
+ xpcomAbi: "noarch-spidermonkey",
+ };
+
+ // Check that the ping contains all the mandatory fields.
+ for (let f of MANDATORY_PING_FIELDS) {
+ Assert.ok(f in aPing, f + " must be available.");
+ }
+
+ Assert.equal(aPing.type, aType, "The ping must have the correct type.");
+ Assert.equal(
+ aPing.version,
+ PING_FORMAT_VERSION,
+ "The ping must have the correct version."
+ );
+
+ // Test the application section.
+ for (let f in APPLICATION_TEST_DATA) {
+ Assert.equal(
+ aPing.application[f],
+ APPLICATION_TEST_DATA[f],
+ f + " must have the correct value."
+ );
+ }
+
+ // We can't check the values for channel and architecture. Just make
+ // sure they are in.
+ Assert.ok(
+ "architecture" in aPing.application,
+ "The application section must have an architecture field."
+ );
+ Assert.ok(
+ "channel" in aPing.application,
+ "The application section must have a channel field."
+ );
+
+ // Check the clientId and environment fields, as needed.
+ Assert.equal("clientId" in aPing, aHasClientId);
+ Assert.equal("environment" in aPing, aHasEnvironment);
+}
+
+add_task(async function test_setup() {
+ // Addon manager needs a profile directory
+ do_get_profile();
+ await loadAddonManager(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "1.9.2"
+ );
+ finishAddonManagerStartup();
+ fakeIntlReady();
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+
+ await new Promise(resolve =>
+ Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(resolve))
+ );
+});
+
+add_task(async function asyncSetup() {
+ await TelemetryController.testSetup();
+});
+
+// Ensure that not overwriting an existing file fails silently
+add_task(async function test_overwritePing() {
+ let ping = { id: "foo" };
+ await TelemetryStorage.savePing(ping, true);
+ await TelemetryStorage.savePing(ping, false);
+ await TelemetryStorage.cleanupPingFile(ping);
+});
+
+// Checks that a sent ping is correctly received by a dummy http server.
+add_task(async function test_simplePing() {
+ PingServer.start();
+ // Update the Telemetry Server preference with the address of the local server.
+ // Otherwise we might end up sending stuff to a non-existing server after
+ // |TelemetryController.testReset| is called.
+ Services.prefs.setStringPref(
+ TelemetryUtils.Preferences.Server,
+ "http://localhost:" + PingServer.port
+ );
+
+ await sendPing(false, false);
+ let request = await PingServer.promiseNextRequest();
+
+ let ping = decodeRequestPayload(request);
+ checkPingFormat(ping, TEST_PING_TYPE, false, false);
+});
+
+add_task(async function test_disableDataUpload() {
+ const OPTIN_PROBE = "telemetry.data_upload_optin";
+ const isUnified = Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.Unified,
+ false
+ );
+ if (!isUnified) {
+ // Skipping the test if unified telemetry is off, as no deletion-request ping will be generated.
+ return;
+ }
+
+ // Check that the optin probe is not set.
+ // (If there are no recorded scalars, "parent" will be undefined).
+ let snapshot = Telemetry.getSnapshotForScalars("main", false).parent || {};
+ Assert.ok(
+ !(OPTIN_PROBE in snapshot),
+ "Data optin scalar should not be set at start"
+ );
+
+ // Send a first ping to get the current used client id
+ await sendPing(true, false);
+ let ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, true, false);
+ let firstClientId = ping.clientId;
+
+ Assert.ok(firstClientId, "Test ping needs a client ID");
+ Assert.notEqual(
+ TelemetryUtils.knownClientID,
+ firstClientId,
+ "Client ID should be valid and random"
+ );
+
+ // The next step should trigger an event, watch for it.
+ let disableObserved = TestUtils.topicObserved(
+ TelemetryUtils.TELEMETRY_UPLOAD_DISABLED_TOPIC
+ );
+
+ // Disable FHR upload: this should trigger a deletion-request ping.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ false
+ );
+
+ // Wait for the disable event
+ await disableObserved;
+
+ ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, DELETION_REQUEST_PING_TYPE, true, false);
+ // Wait on ping activity to settle.
+ await TelemetrySend.testWaitOnOutgoingPings();
+
+ snapshot = Telemetry.getSnapshotForScalars("main", false).parent || {};
+ Assert.ok(
+ !(OPTIN_PROBE in snapshot),
+ "Data optin scalar should not be set after opt out"
+ );
+
+ // Restore FHR Upload.
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+
+ // We need to wait until the scalar is set
+ await ContentTaskUtils.waitForCondition(() => {
+ const scalarSnapshot = Telemetry.getSnapshotForScalars("main", false);
+ return (
+ Object.keys(scalarSnapshot).includes("parent") &&
+ OPTIN_PROBE in scalarSnapshot.parent
+ );
+ });
+
+ snapshot = Telemetry.getSnapshotForScalars("main", false).parent || {};
+ Assert.ok(
+ snapshot[OPTIN_PROBE],
+ "Enabling data upload should set optin probe"
+ );
+
+ // The clientId should've been reset when we restored FHR Upload.
+ let secondClientId = TelemetryController.getCurrentPingData().clientId;
+ Assert.notEqual(
+ firstClientId,
+ secondClientId,
+ "The client id must have changed"
+ );
+ // Simulate a failure in sending the deletion-request ping by disabling the HTTP server.
+ await PingServer.stop();
+
+ // Try to send a ping. It will be saved as pending and get deleted when disabling upload.
+ TelemetryController.submitExternalPing(TEST_PING_TYPE, {});
+
+ // Disable FHR upload to send a deletion-request ping again.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ false
+ );
+ // Wait for the deletion-request ping to be submitted.
+ await TelemetryController.testPromiseDeletionRequestPingSubmitted();
+
+ // Wait on sending activity to settle, as |TelemetryController.testReset()| doesn't do that.
+ await TelemetrySend.testWaitOnOutgoingPings();
+ // Wait for the pending pings to be deleted. Resetting TelemetryController doesn't
+ // trigger the shutdown, so we need to call it ourselves.
+ await TelemetryStorage.shutdown();
+ // Simulate a restart, and spin the send task.
+ await TelemetryController.testReset();
+
+ // Disabling Telemetry upload must clear out all the pending pings.
+ let pendingPings = await TelemetryStorage.loadPendingPingList();
+ Assert.equal(
+ pendingPings.length,
+ 1,
+ "All the pending pings should have been deleted, except the deletion-request ping"
+ );
+
+ // Enable the ping server again.
+ PingServer.start();
+ // We set the new server using the pref, otherwise it would get reset with
+ // |TelemetryController.testReset|.
+ Services.prefs.setStringPref(
+ TelemetryUtils.Preferences.Server,
+ "http://localhost:" + PingServer.port
+ );
+
+ // Stop the sending task and then start it again.
+ await TelemetrySend.shutdown();
+ // Reset the controller to spin the ping sending task.
+ await TelemetryController.testReset();
+
+ // Re-enable Telemetry
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+
+ // Send a test ping
+ await sendPing(true, false);
+
+ // We should have received the test ping first.
+ ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, true, false);
+
+ // The data in the test ping should be different than before
+ Assert.notEqual(
+ TelemetryUtils.knownClientID,
+ ping.clientId,
+ "Client ID should be reset to a random value"
+ );
+ Assert.notEqual(
+ firstClientId,
+ ping.clientId,
+ "Client ID should be different from the previous value"
+ );
+
+ // The "deletion-request" ping should come next, as it was pending.
+ ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, DELETION_REQUEST_PING_TYPE, true, false);
+ Assert.equal(
+ secondClientId,
+ ping.clientId,
+ "Deletion must be requested for correct client id"
+ );
+
+ // Wait on ping activity to settle before moving on to the next test. If we were
+ // to shut down telemetry, even though the PingServer caught the expected pings,
+ // TelemetrySend could still be processing them (clearing pings would happen in
+ // a couple of ticks). Shutting down would cancel the request and save them as
+ // pending pings.
+ await TelemetrySend.testWaitOnOutgoingPings();
+});
+
+add_task(async function test_pingHasClientId() {
+ // Make sure we have no cached client ID for this test: we'll try to send
+ // a ping with it while Telemetry is being initialized.
+ Services.prefs.clearUserPref(TelemetryUtils.Preferences.CachedClientId);
+ await TelemetryController.testShutdown();
+ await ClientID._reset();
+ await TelemetryStorage.testClearPendingPings();
+ // And also clear the counter histogram since we're here.
+ let h = Telemetry.getHistogramById(
+ "TELEMETRY_PING_SUBMISSION_WAITING_CLIENTID"
+ );
+ h.clear();
+
+ // Init telemetry and try to send a ping with a client ID.
+ let promisePingSetup = TelemetryController.testReset();
+ await sendPing(true, false);
+ Assert.equal(
+ h.snapshot().sum,
+ 1,
+ "We must have a ping waiting for the clientId early during startup."
+ );
+ // Wait until we are fully initialized. Pings will be assembled but won't get
+ // sent before then.
+ await promisePingSetup;
+
+ let ping = await PingServer.promiseNextPing();
+ // Fetch the client ID after initializing and fetching the the ping, so we
+ // don't unintentionally trigger its loading. We'll still need the client ID
+ // to see if the ping looks sane.
+ gClientID = await ClientID.getClientID();
+
+ checkPingFormat(ping, TEST_PING_TYPE, true, false);
+ Assert.equal(
+ ping.clientId,
+ gClientID,
+ "The correct clientId must be reported."
+ );
+
+ // Shutdown Telemetry so we can safely restart it.
+ await TelemetryController.testShutdown();
+ await TelemetryStorage.testClearPendingPings();
+
+ // We should have cached the client ID now. Lets confirm that by checking it before
+ // the async ping setup is finished.
+ h.clear();
+ promisePingSetup = TelemetryController.testReset();
+ await sendPing(true, false);
+ await promisePingSetup;
+
+ // Check that we received the cached client id.
+ Assert.equal(h.snapshot().sum, 0, "We must have used the cached clientId.");
+ ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, true, false);
+ Assert.equal(
+ ping.clientId,
+ gClientID,
+ "Telemetry should report the correct cached clientId."
+ );
+
+ // Check that sending a ping without relying on the cache, after the
+ // initialization, still works.
+ Services.prefs.clearUserPref(TelemetryUtils.Preferences.CachedClientId);
+ await TelemetryController.testShutdown();
+ await TelemetryStorage.testClearPendingPings();
+ await TelemetryController.testReset();
+ await sendPing(true, false);
+ ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, true, false);
+ Assert.equal(
+ ping.clientId,
+ gClientID,
+ "The correct clientId must be reported."
+ );
+ Assert.equal(
+ h.snapshot().sum,
+ 0,
+ "No ping should have been waiting for a clientId."
+ );
+});
+
+add_task(async function test_pingHasEnvironment() {
+ // Send a ping with the environment data.
+ await sendPing(false, true);
+ let ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, false, true);
+
+ // Test a field in the environment build section.
+ Assert.equal(ping.application.buildId, ping.environment.build.buildId);
+});
+
+add_task(async function test_pingHasEnvironmentAndClientId() {
+ // Send a ping with the environment data and client id.
+ await sendPing(true, true);
+ let ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, true, true);
+
+ // Test a field in the environment build section.
+ Assert.equal(ping.application.buildId, ping.environment.build.buildId);
+ // Test that we have the correct clientId.
+ Assert.equal(
+ ping.clientId,
+ gClientID,
+ "The correct clientId must be reported."
+ );
+});
+
+add_task(async function test_archivePings() {
+ let now = new Date(2009, 10, 18, 12, 0, 0);
+ fakeNow(now);
+
+ // Disable ping upload so that pings don't get sent.
+ // With unified telemetry the FHR upload pref controls this,
+ // with non-unified telemetry the Telemetry enabled pref.
+ const isUnified = Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.Unified,
+ false
+ );
+ const uploadPref = isUnified
+ ? TelemetryUtils.Preferences.FhrUploadEnabled
+ : TelemetryUtils.Preferences.TelemetryEnabled;
+ Services.prefs.setBoolPref(uploadPref, false);
+
+ // If we're using unified telemetry, disabling ping upload will generate a "deletion-request" ping. Catch it.
+ if (isUnified) {
+ let ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, DELETION_REQUEST_PING_TYPE, true, false);
+ }
+
+ // Register a new Ping Handler that asserts if a ping is received, then send a ping.
+ PingServer.registerPingHandler(() =>
+ Assert.ok(false, "Telemetry must not send pings if not allowed to.")
+ );
+ let pingId = await sendPing(true, true);
+
+ // Check that the ping was archived, even with upload disabled.
+ let ping = await TelemetryArchive.promiseArchivedPingById(pingId);
+ Assert.equal(
+ ping.id,
+ pingId,
+ "TelemetryController should still archive pings."
+ );
+
+ // Check that pings don't get archived if not allowed to.
+ now = new Date(2010, 10, 18, 12, 0, 0);
+ fakeNow(now);
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.ArchiveEnabled, false);
+ pingId = await sendPing(true, true);
+ let promise = TelemetryArchive.promiseArchivedPingById(pingId);
+ Assert.ok(
+ await promiseRejects(promise),
+ "TelemetryController should not archive pings if the archive pref is disabled."
+ );
+
+ // Enable archiving and the upload so that pings get sent and archived again.
+ Services.prefs.setBoolPref(uploadPref, true);
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.ArchiveEnabled, true);
+
+ now = new Date(2014, 6, 18, 22, 0, 0);
+ fakeNow(now);
+ // Restore the non asserting ping handler.
+ PingServer.resetPingHandler();
+ pingId = await sendPing(true, true);
+
+ // Check that we archive pings when successfully sending them.
+ await PingServer.promiseNextPing();
+ ping = await TelemetryArchive.promiseArchivedPingById(pingId);
+ Assert.equal(
+ ping.id,
+ pingId,
+ "TelemetryController should still archive pings if ping upload is enabled."
+ );
+});
+
+// Test that we fuzz the submission time around midnight properly
+// to avoid overloading the telemetry servers.
+add_task(async function test_midnightPingSendFuzzing() {
+ const fuzzingDelay = 60 * 60 * 1000;
+ fakeMidnightPingFuzzingDelay(fuzzingDelay);
+ let now = new Date(2030, 5, 1, 11, 0, 0);
+ fakeNow(now);
+
+ let waitForTimer = () =>
+ new Promise(resolve => {
+ fakePingSendTimer(
+ (callback, timeout) => {
+ resolve([callback, timeout]);
+ },
+ () => {}
+ );
+ });
+
+ PingServer.clearRequests();
+ await TelemetryController.testReset();
+
+ // A ping after midnight within the fuzzing delay should not get sent.
+ now = new Date(2030, 5, 2, 0, 40, 0);
+ fakeNow(now);
+ PingServer.registerPingHandler((req, res) => {
+ Assert.ok(false, "No ping should be received yet.");
+ });
+ let timerPromise = waitForTimer();
+ await sendPing(true, true);
+ let [timerCallback, timerTimeout] = await timerPromise;
+ Assert.ok(!!timerCallback);
+ Assert.deepEqual(
+ futureDate(now, timerTimeout),
+ new Date(2030, 5, 2, 1, 0, 0)
+ );
+
+ // A ping just before the end of the fuzzing delay should not get sent.
+ now = new Date(2030, 5, 2, 0, 59, 59);
+ fakeNow(now);
+ timerPromise = waitForTimer();
+ await sendPing(true, true);
+ [timerCallback, timerTimeout] = await timerPromise;
+ Assert.deepEqual(timerTimeout, 1 * 1000);
+
+ // Restore the previous ping handler.
+ PingServer.resetPingHandler();
+
+ // Setting the clock to after the fuzzing delay, we should trigger the two ping sends
+ // with the timer callback.
+ now = futureDate(now, timerTimeout);
+ fakeNow(now);
+ await timerCallback();
+ const pings = await PingServer.promiseNextPings(2);
+ for (let ping of pings) {
+ checkPingFormat(ping, TEST_PING_TYPE, true, true);
+ }
+ await TelemetrySend.testWaitOnOutgoingPings();
+
+ // Moving the clock further we should still send pings immediately.
+ now = futureDate(now, 5 * 60 * 1000);
+ await sendPing(true, true);
+ let ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, true, true);
+ await TelemetrySend.testWaitOnOutgoingPings();
+
+ // Check that pings shortly before midnight are immediately sent.
+ now = fakeNow(2030, 5, 3, 23, 59, 0);
+ await sendPing(true, true);
+ ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, TEST_PING_TYPE, true, true);
+ await TelemetrySend.testWaitOnOutgoingPings();
+
+ // Clean-up.
+ fakeMidnightPingFuzzingDelay(0);
+ fakePingSendTimer(
+ () => {},
+ () => {}
+ );
+});
+
+add_task(async function test_changePingAfterSubmission() {
+ // Submit a ping with a custom payload.
+ let payload = { canary: "test" };
+ let pingPromise = TelemetryController.submitExternalPing(
+ TEST_PING_TYPE,
+ payload
+ );
+
+ // Change the payload with a predefined value.
+ payload.canary = "changed";
+
+ // Wait for the ping to be archived.
+ const pingId = await pingPromise;
+
+ // Make sure our changes didn't affect the submitted payload.
+ let archivedCopy = await TelemetryArchive.promiseArchivedPingById(pingId);
+ Assert.equal(
+ archivedCopy.payload.canary,
+ "test",
+ "The payload must not be changed after being submitted."
+ );
+});
+
+add_task(async function test_telemetryCleanFHRDatabase() {
+ const FHR_DBNAME_PREF = "datareporting.healthreport.dbName";
+ const CUSTOM_DB_NAME = "unlikely.to.be.used.sqlite";
+ const DEFAULT_DB_NAME = "healthreport.sqlite";
+
+ // Check that we're able to remove a FHR DB with a custom name.
+ const profileDir = PathUtils.profileDir;
+ const CUSTOM_DB_PATHS = [
+ PathUtils.join(profileDir, CUSTOM_DB_NAME),
+ PathUtils.join(profileDir, CUSTOM_DB_NAME + "-wal"),
+ PathUtils.join(profileDir, CUSTOM_DB_NAME + "-shm"),
+ ];
+ Services.prefs.setStringPref(FHR_DBNAME_PREF, CUSTOM_DB_NAME);
+
+ // Write fake DB files to the profile directory.
+ for (let dbFilePath of CUSTOM_DB_PATHS) {
+ await IOUtils.writeUTF8(dbFilePath, "some data");
+ }
+
+ // Trigger the cleanup and check that the files were removed.
+ await TelemetryStorage.removeFHRDatabase();
+ for (let dbFilePath of CUSTOM_DB_PATHS) {
+ try {
+ await IOUtils.read(dbFilePath);
+ } catch (e) {
+ Assert.ok(DOMException.isInstance(e));
+ Assert.equal(
+ e.name,
+ "NotFoundError",
+ "The DB must not be on the disk anymore: " + dbFilePath
+ );
+ }
+ }
+
+ // We should not break anything if there's no DB file.
+ await TelemetryStorage.removeFHRDatabase();
+
+ // Check that we're able to remove a FHR DB with the default name.
+ Services.prefs.clearUserPref(FHR_DBNAME_PREF);
+
+ const DEFAULT_DB_PATHS = [
+ PathUtils.join(profileDir, DEFAULT_DB_NAME),
+ PathUtils.join(profileDir, DEFAULT_DB_NAME + "-wal"),
+ PathUtils.join(profileDir, DEFAULT_DB_NAME + "-shm"),
+ ];
+
+ // Write fake DB files to the profile directory.
+ for (let dbFilePath of DEFAULT_DB_PATHS) {
+ await IOUtils.writeUTF8(dbFilePath, "some data");
+ }
+
+ // Trigger the cleanup and check that the files were removed.
+ await TelemetryStorage.removeFHRDatabase();
+ for (let dbFilePath of DEFAULT_DB_PATHS) {
+ try {
+ await IOUtils.read(dbFilePath);
+ } catch (e) {
+ Assert.ok(DOMException.isInstance(e));
+ Assert.equal(
+ e.name,
+ "NotFoundError",
+ "The DB must not be on the disk anymore: " + dbFilePath
+ );
+ }
+ }
+});
+
+add_task(async function test_sendNewProfile() {
+ if (
+ gIsAndroid ||
+ (AppConstants.platform == "linux" && !Services.appinfo.is64Bit)
+ ) {
+ // We don't support the pingsender on Android, yet, see bug 1335917.
+ // We also don't suppor the pingsender testing on Treeherder for
+ // Linux 32 bit (due to missing libraries). So skip it there too.
+ // See bug 1310703 comment 78.
+ return;
+ }
+
+ const NEWPROFILE_PING_TYPE = "new-profile";
+ const PREF_NEWPROFILE_ENABLED = "toolkit.telemetry.newProfilePing.enabled";
+ const PREF_NEWPROFILE_DELAY = "toolkit.telemetry.newProfilePing.delay";
+
+ // Make sure Telemetry is shut down before beginning and that we have
+ // no pending pings.
+ let resetTest = async function () {
+ await TelemetryController.testShutdown();
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+ };
+ await resetTest();
+
+ // Make sure to reset all the new-profile ping prefs.
+ const stateFilePath = PathUtils.join(
+ await DATAREPORTING_PATH,
+ "session-state.json"
+ );
+ await IOUtils.remove(stateFilePath);
+ Services.prefs.setIntPref(PREF_NEWPROFILE_DELAY, 1);
+ Services.prefs.setBoolPref(PREF_NEWPROFILE_ENABLED, true);
+
+ // Check that a new-profile ping is sent on the first session.
+ let nextReq = PingServer.promiseNextRequest();
+ await TelemetryController.testReset();
+ let req = await nextReq;
+ let ping = decodeRequestPayload(req);
+ checkPingFormat(ping, NEWPROFILE_PING_TYPE, true, true);
+ Assert.equal(
+ ping.payload.reason,
+ "startup",
+ "The new-profile ping generated after startup must have the correct reason"
+ );
+ Assert.ok(
+ "parent" in ping.payload.processes,
+ "The new-profile ping generated after startup must have processes.parent data"
+ );
+
+ // Check that is not sent with the pingsender during startup.
+ Assert.throws(
+ () => req.getHeader("X-PingSender-Version"),
+ /NS_ERROR_NOT_AVAILABLE/,
+ "Should not have used the pingsender."
+ );
+
+ // Make sure that the new-profile ping is sent at shutdown if it wasn't sent before.
+ await resetTest();
+ await IOUtils.remove(stateFilePath);
+ Services.prefs.clearUserPref(PREF_NEWPROFILE_DELAY);
+
+ nextReq = PingServer.promiseNextRequest();
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ req = await nextReq;
+ ping = decodeRequestPayload(req);
+ checkPingFormat(ping, NEWPROFILE_PING_TYPE, true, true);
+ Assert.equal(
+ ping.payload.reason,
+ "shutdown",
+ "The new-profile ping generated at shutdown must have the correct reason"
+ );
+ Assert.ok(
+ "parent" in ping.payload.processes,
+ "The new-profile ping generated at shutdown must have processes.parent data"
+ );
+
+ // Check that the new-profile ping is sent at shutdown using the pingsender.
+ Assert.equal(
+ req.getHeader("User-Agent"),
+ "pingsender/1.0",
+ "Should have received the correct user agent string."
+ );
+ Assert.equal(
+ req.getHeader("X-PingSender-Version"),
+ "1.0",
+ "Should have received the correct PingSender version string."
+ );
+
+ // Check that no new-profile ping is sent on second sessions, not at startup
+ // nor at shutdown.
+ await resetTest();
+ PingServer.registerPingHandler(() =>
+ Assert.ok(false, "The new-profile ping must be sent only on new profiles.")
+ );
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+
+ // Check that we don't send the new-profile ping if the profile already contains
+ // a state file (but no "newProfilePingSent" property).
+ await resetTest();
+ await IOUtils.remove(stateFilePath);
+ const sessionState = {
+ sessionId: null,
+ subsessionId: null,
+ profileSubsessionCounter: 3785,
+ };
+ await IOUtils.writeJSON(stateFilePath, sessionState);
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+
+ // Reset the pref and restart Telemetry.
+ Services.prefs.clearUserPref(PREF_NEWPROFILE_ENABLED);
+ PingServer.resetPingHandler();
+});
+
+add_task(async function test_encryptedPing() {
+ if (gIsAndroid) {
+ // The underlying jwcrypto module being used here is not currently available on Android.
+ return;
+ }
+
+ const ECDH_PARAMS = {
+ name: "ECDH",
+ namedCurve: "P-256",
+ };
+
+ const privateKey = {
+ crv: "P-256",
+ d: "rcs093UlGDG6piwHenmSDoAxbzMIXT43JkQbkt3xEmI",
+ ext: true,
+ key_ops: ["deriveKey"],
+ kty: "EC",
+ x: "h12feyTYBZ__wO_AnM1a5-KTDlko3-YyQ_en19jyrs0",
+ y: "6GSfzo14ehDyH5E-xCOedJDAYlN0AGPMCtIgFbheLko",
+ };
+
+ const publicKey = {
+ crv: "P-256",
+ ext: true,
+ kty: "EC",
+ x: "h12feyTYBZ__wO_AnM1a5-KTDlko3-YyQ_en19jyrs0",
+ y: "6GSfzo14ehDyH5E-xCOedJDAYlN0AGPMCtIgFbheLko",
+ };
+
+ const pioneerId = "12345";
+ const schemaName = "abc";
+ const schemaNamespace = "def";
+ const schemaVersion = 2;
+
+ Services.prefs.setStringPref("toolkit.telemetry.pioneerId", pioneerId);
+
+ // Stop the sending task and then start it again.
+ await TelemetrySend.shutdown();
+ // Reset the controller to spin the ping sending task.
+ await TelemetryController.testReset();
+
+ // Submit a ping with a custom payload, which will be encrypted.
+ let payload = { canary: "test" };
+ let pingPromise = TelemetryController.submitExternalPing(
+ "pioneer-study",
+ payload,
+ {
+ studyName: "pioneer-dev-1@allizom.org",
+ addPioneerId: true,
+ useEncryption: true,
+ encryptionKeyId: "pioneer-dev-20200423",
+ publicKey,
+ schemaName,
+ schemaNamespace,
+ schemaVersion,
+ }
+ );
+
+ // Wait for the ping to be archived.
+ const pingId = await pingPromise;
+
+ let archivedCopy = await TelemetryArchive.promiseArchivedPingById(pingId);
+
+ Assert.notEqual(
+ archivedCopy.payload.encryptedData,
+ payload,
+ "The encrypted payload must not match the plaintext."
+ );
+
+ Assert.equal(
+ archivedCopy.payload.pioneerId,
+ pioneerId,
+ "Pioneer ID in ping must match the pref."
+ );
+
+ // Validate ping against schema.
+ const schema = {
+ $schema: "http://json-schema.org/draft-04/schema#",
+ properties: {
+ application: {
+ additionalProperties: false,
+ properties: {
+ architecture: {
+ type: "string",
+ },
+ buildId: {
+ pattern: "^[0-9]{10}",
+ type: "string",
+ },
+ channel: {
+ type: "string",
+ },
+ displayVersion: {
+ pattern: "^[0-9]{2,3}\\.",
+ type: "string",
+ },
+ name: {
+ type: "string",
+ },
+ platformVersion: {
+ pattern: "^[0-9]{2,3}\\.",
+ type: "string",
+ },
+ vendor: {
+ type: "string",
+ },
+ version: {
+ pattern: "^[0-9]{2,3}\\.",
+ type: "string",
+ },
+ xpcomAbi: {
+ type: "string",
+ },
+ },
+ required: [
+ "architecture",
+ "buildId",
+ "channel",
+ "name",
+ "platformVersion",
+ "version",
+ "vendor",
+ "xpcomAbi",
+ ],
+ type: "object",
+ },
+ creationDate: {
+ pattern:
+ "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3}Z$",
+ type: "string",
+ },
+ id: {
+ pattern:
+ "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$",
+ type: "string",
+ },
+ payload: {
+ description: "",
+ properties: {
+ encryptedData: {
+ description: "JOSE/JWE encrypted payload.",
+ type: "string",
+ },
+ encryptionKeyId: {
+ description: "JOSE/JWK key id, e.g. pioneer-20170520.",
+ type: "string",
+ },
+ pioneerId: {
+ description: "Custom pioneer id, must not be Telemetry clientId",
+ pattern:
+ "^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$",
+ type: "string",
+ },
+ schemaName: {
+ description:
+ "Name of a schema used for validation of the encryptedData",
+ maxLength: 100,
+ minLength: 1,
+ pattern: "^\\S+$",
+ type: "string",
+ },
+ schemaNamespace: {
+ description:
+ "The namespace of the schema used for validation and routing to a dataset.",
+ maxLength: 100,
+ minLength: 1,
+ pattern: "^\\S+$",
+ type: "string",
+ },
+ schemaVersion: {
+ description: "Integer version number of the schema",
+ minimum: 1,
+ type: "integer",
+ },
+ studyName: {
+ description: "Name of a particular study. Usually the addon_id.",
+ maxLength: 100,
+ minLength: 1,
+ pattern: "^\\S+$",
+ type: "string",
+ },
+ },
+ required: [
+ "encryptedData",
+ "encryptionKeyId",
+ "pioneerId",
+ "studyName",
+ "schemaName",
+ "schemaNamespace",
+ "schemaVersion",
+ ],
+ title: "pioneer-study",
+ type: "object",
+ },
+ type: {
+ description: "doc_type, restated",
+ enum: ["pioneer-study"],
+ type: "string",
+ },
+ version: {
+ maximum: 4,
+ minimum: 4,
+ type: "integer",
+ },
+ },
+ required: [
+ "application",
+ "creationDate",
+ "id",
+ "payload",
+ "type",
+ "version",
+ ],
+ title: "pioneer-study",
+ type: "object",
+ };
+
+ const result = JsonSchemaValidator.validate(archivedCopy, schema);
+
+ Assert.ok(
+ result.valid,
+ `Archived ping should validate against schema: ${result.error}`
+ );
+
+ // check that payload can be decrypted.
+ const privateJWK = await crypto.subtle.importKey(
+ "jwk",
+ privateKey,
+ ECDH_PARAMS,
+ false,
+ ["deriveKey"]
+ );
+
+ const decryptedJWE = await jwcrypto.decryptJWE(
+ archivedCopy.payload.encryptedData,
+ privateJWK
+ );
+
+ Assert.deepEqual(
+ JSON.parse(new TextDecoder("utf-8").decode(decryptedJWE)),
+ payload,
+ "decrypted payload should match"
+ );
+});
+
+add_task(async function test_encryptedPing_overrideId() {
+ if (gIsAndroid) {
+ // The underlying jwcrypto module being used here is not currently available on Android.
+ return;
+ }
+
+ const publicKey = {
+ crv: "P-256",
+ ext: true,
+ kty: "EC",
+ x: "h12feyTYBZ__wO_AnM1a5-KTDlko3-YyQ_en19jyrs0",
+ y: "6GSfzo14ehDyH5E-xCOedJDAYlN0AGPMCtIgFbheLko",
+ };
+
+ const prefPioneerId = "12345";
+ const overriddenPioneerId = "c0ffeeaa-bbbb-abab-baba-eeff0ceeff0c";
+ const schemaName = "abc";
+ const schemaNamespace = "def";
+ const schemaVersion = 2;
+
+ Services.prefs.setStringPref("toolkit.telemetry.pioneerId", prefPioneerId);
+
+ let archiveTester = new TelemetryArchiveTesting.Checker();
+ await archiveTester.promiseInit();
+
+ // Submit a ping with a custom payload, which will be encrypted.
+ let payload = { canary: "test" };
+ let pingPromise = TelemetryController.submitExternalPing(
+ "test-pioneer-study-override",
+ payload,
+ {
+ studyName: "pioneer-dev-1@allizom.org",
+ addPioneerId: true,
+ overridePioneerId: overriddenPioneerId,
+ useEncryption: true,
+ encryptionKeyId: "pioneer-dev-20200423",
+ publicKey,
+ schemaName,
+ schemaNamespace,
+ schemaVersion,
+ }
+ );
+
+ // Wait for the ping to be submitted, to have the ping id to scan the
+ // archive for.
+ const pingId = await pingPromise;
+
+ // And then wait for the ping to be available in the archive.
+ await TestUtils.waitForCondition(
+ () => archiveTester.promiseFindPing("test-pioneer-study-override", []),
+ "Failed to find the pioneer ping"
+ );
+
+ let archivedCopy = await TelemetryArchive.promiseArchivedPingById(pingId);
+
+ Assert.notEqual(
+ archivedCopy.payload.encryptedData,
+ payload,
+ "The encrypted payload must not match the plaintext."
+ );
+
+ Assert.equal(
+ archivedCopy.payload.pioneerId,
+ overriddenPioneerId,
+ "Pioneer ID in ping must match the provided override."
+ );
+});
+
+// Testing shutdown and checking that pings sent afterwards are rejected.
+add_task(async function test_pingRejection() {
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ await sendPing(false, false).then(
+ () => Assert.ok(false, "Pings submitted after shutdown must be rejected."),
+ () => Assert.ok(true, "Ping submitted after shutdown correctly rejected.")
+ );
+});
+
+add_task(async function test_newCanRecordsMatchTheOld() {
+ Assert.equal(
+ Telemetry.canRecordBase,
+ Telemetry.canRecordReleaseData,
+ "Release Data is the new way to say Base Collection"
+ );
+ Assert.equal(
+ Telemetry.canRecordExtended,
+ Telemetry.canRecordPrereleaseData,
+ "Prerelease Data is the new way to say Extended Collection"
+ );
+});
+
+add_task(function test_histogram_filtering() {
+ const COUNT_ID = "TELEMETRY_TEST_COUNT";
+ const KEYED_ID = "TELEMETRY_TEST_KEYED_COUNT";
+ const count = Telemetry.getHistogramById(COUNT_ID);
+ const keyed = Telemetry.getKeyedHistogramById(KEYED_ID);
+
+ count.add(1);
+ keyed.add("a", 1);
+
+ let snapshot = Telemetry.getSnapshotForHistograms(
+ "main",
+ false,
+ /* filter */ false
+ ).parent;
+ let keyedSnapshot = Telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ false,
+ /* filter */ false
+ ).parent;
+ Assert.ok(COUNT_ID in snapshot, "test histogram should be snapshotted");
+ Assert.ok(
+ KEYED_ID in keyedSnapshot,
+ "test keyed histogram should be snapshotted"
+ );
+
+ snapshot = Telemetry.getSnapshotForHistograms(
+ "main",
+ false,
+ /* filter */ true
+ ).parent;
+ keyedSnapshot = Telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ false,
+ /* filter */ true
+ ).parent;
+ Assert.ok(
+ !(COUNT_ID in snapshot),
+ "test histogram should not be snapshotted"
+ );
+ Assert.ok(
+ !(KEYED_ID in keyedSnapshot),
+ "test keyed histogram should not be snapshotted"
+ );
+});
+
+add_task(function test_scalar_filtering() {
+ const COUNT_ID = "telemetry.test.unsigned_int_kind";
+ const KEYED_ID = "telemetry.test.keyed_unsigned_int";
+
+ Telemetry.scalarSet(COUNT_ID, 2);
+ Telemetry.keyedScalarSet(KEYED_ID, "a", 2);
+
+ let snapshot = Telemetry.getSnapshotForScalars(
+ "main",
+ false,
+ /* filter */ false
+ ).parent;
+ let keyedSnapshot = Telemetry.getSnapshotForKeyedScalars(
+ "main",
+ false,
+ /* filter */ false
+ ).parent;
+ Assert.ok(COUNT_ID in snapshot, "test scalars should be snapshotted");
+ Assert.ok(
+ KEYED_ID in keyedSnapshot,
+ "test keyed scalars should be snapshotted"
+ );
+
+ snapshot = Telemetry.getSnapshotForScalars(
+ "main",
+ false,
+ /* filter */ true
+ ).parent;
+ keyedSnapshot = Telemetry.getSnapshotForKeyedScalars(
+ "main",
+ false,
+ /* filter */ true
+ ).parent;
+ Assert.ok(!(COUNT_ID in snapshot), "test scalars should not be snapshotted");
+ Assert.ok(
+ !(KEYED_ID in keyedSnapshot),
+ "test keyed scalars should not be snapshotted"
+ );
+});
+
+add_task(async function stopServer() {
+ await PingServer.stop();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryControllerBuildID.js b/toolkit/components/telemetry/tests/unit/test_TelemetryControllerBuildID.js
new file mode 100644
index 0000000000..126684fe82
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryControllerBuildID.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+/* Test inclusion of previous build ID in telemetry pings when build ID changes.
+ * bug 841028
+ *
+ * Cases to cover:
+ * 1) Run with no "previousBuildID" stored in prefs:
+ * -> no previousBuildID in telemetry system info, new value set in prefs.
+ * 2) previousBuildID in prefs, equal to current build ID:
+ * -> no previousBuildID in telemetry, prefs not updated.
+ * 3) previousBuildID in prefs, not equal to current build ID:
+ * -> previousBuildID in telemetry, new value set in prefs.
+ */
+
+"use strict";
+
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { TelemetrySession } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySession.sys.mjs"
+);
+
+// Set up our dummy AppInfo object so we can control the appBuildID.
+const { getAppInfo, updateAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+updateAppInfo();
+
+// Check that when run with no previous build ID stored, we update the pref but do not
+// put anything into the metadata.
+add_task(async function test_firstRun() {
+ await TelemetryController.testReset();
+ let metadata = TelemetrySession.getMetadata();
+ Assert.equal(false, "previousBuildID" in metadata);
+ let appBuildID = getAppInfo().appBuildID;
+ let buildIDPref = Services.prefs.getCharPref(
+ TelemetryUtils.Preferences.PreviousBuildID
+ );
+ Assert.equal(appBuildID, buildIDPref);
+});
+
+// Check that a subsequent run with the same build ID does not put prev build ID in
+// metadata. Assumes testFirstRun() has already been called to set the previousBuildID pref.
+add_task(async function test_secondRun() {
+ await TelemetryController.testReset();
+ let metadata = TelemetrySession.getMetadata();
+ Assert.equal(false, "previousBuildID" in metadata);
+});
+
+// Set up telemetry with a different app build ID and check that the old build ID
+// is returned in the metadata and the pref is updated to the new build ID.
+// Assumes testFirstRun() has been called to set the previousBuildID pref.
+const NEW_BUILD_ID = "20130314";
+add_task(async function test_newBuild() {
+ let info = getAppInfo();
+ let oldBuildID = info.appBuildID;
+ info.appBuildID = NEW_BUILD_ID;
+ await TelemetryController.testReset();
+ let metadata = TelemetrySession.getMetadata();
+ Assert.equal(metadata.previousBuildId, oldBuildID);
+ let buildIDPref = Services.prefs.getCharPref(
+ TelemetryUtils.Preferences.PreviousBuildID
+ );
+ Assert.equal(NEW_BUILD_ID, buildIDPref);
+});
+
+function run_test() {
+ // Make sure we have a profile directory.
+ do_get_profile();
+
+ run_next_test();
+}
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryControllerShutdown.js b/toolkit/components/telemetry/tests/unit/test_TelemetryControllerShutdown.js
new file mode 100644
index 0000000000..95ef3789d5
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryControllerShutdown.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that TelemetryController sends close to shutdown don't lead
+// to AsyncShutdown timeouts.
+
+"use strict";
+
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { TelemetrySend } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySend.sys.mjs"
+);
+const { AsyncShutdown } = ChromeUtils.importESModule(
+ "resource://gre/modules/AsyncShutdown.sys.mjs"
+);
+
+function contentHandler(metadata, response) {
+ dump("contentHandler called for path: " + metadata._path + "\n");
+ // We intentionally don't finish writing the response here to let the
+ // client time out.
+ response.processAsync();
+ response.setHeader("Content-Type", "text/plain");
+}
+
+add_task(async function test_setup() {
+ // Addon manager needs a profile directory
+ do_get_profile();
+ await loadAddonManager(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "1.9.2"
+ );
+ finishAddonManagerStartup();
+ fakeIntlReady();
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+});
+
+/**
+ * Ensures that TelemetryController does not hang processing shutdown
+ * phases. Assumes that Telemetry shutdown routines do not take longer than
+ * CRASH_TIMEOUT_MS to complete.
+ */
+add_task(async function test_sendTelemetryShutsDownWithinReasonableTimeout() {
+ const CRASH_TIMEOUT_MS = 10 * 1000;
+ // Enable testing mode for AsyncShutdown, otherwise some testing-only functionality
+ // is not available.
+ Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true);
+ // Reducing the max delay for waitiing on phases to complete from 1 minute
+ // (standard) to 20 seconds to avoid blocking the tests in case of misbehavior.
+ Services.prefs.setIntPref(
+ "toolkit.asyncshutdown.crash_timeout",
+ CRASH_TIMEOUT_MS
+ );
+
+ let httpServer = new HttpServer();
+ httpServer.registerPrefixHandler("/", contentHandler);
+ httpServer.start(-1);
+
+ await TelemetryController.testSetup();
+ TelemetrySend.setServer(
+ "http://localhost:" + httpServer.identity.primaryPort
+ );
+ let submissionPromise = TelemetryController.submitExternalPing(
+ "test-ping-type",
+ {}
+ );
+
+ // Trigger the AsyncShutdown phase TelemetryController hangs off.
+ AsyncShutdown.profileBeforeChange._trigger();
+ AsyncShutdown.sendTelemetry._trigger();
+ // Now wait for the ping submission.
+ await submissionPromise;
+
+ // If we get here, we didn't time out in the shutdown routines.
+ Assert.ok(true, "Didn't time out on shutdown.");
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryController_idle.js b/toolkit/components/telemetry/tests/unit/test_TelemetryController_idle.js
new file mode 100644
index 0000000000..a3191f50c0
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryController_idle.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Check that TelemetrySession notifies correctly on idle-daily.
+
+const { TelemetrySession } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySession.sys.mjs"
+);
+
+var gHttpServer = null;
+
+add_task(async function test_setup() {
+ do_get_profile();
+
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+
+ // Start the webserver to check if the pending ping correctly arrives.
+ gHttpServer = new HttpServer();
+ gHttpServer.start(-1);
+});
+
+add_task(async function testSendPendingOnIdleDaily() {
+ // Create a valid pending ping.
+ const PENDING_PING = {
+ id: "2133234d-4ea1-44f4-909e-ce8c6c41e0fc",
+ type: "test-ping",
+ version: 4,
+ application: {},
+ payload: {},
+ };
+ await TelemetryStorage.savePing(PENDING_PING, true);
+
+ // Telemetry will not send this ping at startup, because it's not overdue.
+ await TelemetryController.testSetup();
+ TelemetrySend.setServer(
+ "http://localhost:" + gHttpServer.identity.primaryPort
+ );
+
+ let pendingPromise = new Promise(resolve =>
+ gHttpServer.registerPrefixHandler("/submit/telemetry/", request =>
+ resolve(request)
+ )
+ );
+
+ let gatherPromise = Promise.withResolvers();
+ Services.obs.addObserver(gatherPromise.resolve, "gather-telemetry");
+
+ // Check that we are correctly receiving the gather-telemetry notification.
+ TelemetrySession.observe(null, "idle-daily", null);
+ await gatherPromise.promise;
+ Assert.ok(true, "Received gather-telemetry notification.");
+
+ Services.obs.removeObserver(gatherPromise.resolve, "gather-telemetry");
+
+ // Check that the pending ping is correctly received.
+ let { TelemetrySendImpl } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySend.sys.mjs"
+ );
+ TelemetrySendImpl.observe(null, "idle-daily", null);
+ let request = await pendingPromise;
+ let ping = decodeRequestPayload(request);
+
+ // Validate the ping data.
+ Assert.equal(ping.id, PENDING_PING.id);
+ Assert.equal(ping.type, PENDING_PING.type);
+
+ await new Promise(resolve => gHttpServer.stop(resolve));
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
new file mode 100644
index 0000000000..aa9fbf11f0
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment.js
@@ -0,0 +1,1472 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { AddonManager, AddonManagerPrivate } = ChromeUtils.importESModule(
+ "resource://gre/modules/AddonManager.sys.mjs"
+);
+const { TelemetryEnvironment } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryEnvironment.sys.mjs"
+);
+const { SearchTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SearchTestUtils.sys.mjs"
+);
+const { TelemetryEnvironmentTesting } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryEnvironmentTesting.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ ExtensionTestUtils:
+ "resource://testing-common/ExtensionXPCShellUtils.sys.mjs",
+});
+
+async function installXPIFromURL(url) {
+ let install = await AddonManager.getInstallForURL(url);
+ return install.install();
+}
+
+// The webserver hosting the addons.
+var gHttpServer = null;
+// The URL of the webserver root.
+var gHttpRoot = null;
+// The URL of the data directory, on the webserver.
+var gDataRoot = null;
+
+function MockAddonWrapper(aAddon) {
+ this.addon = aAddon;
+}
+MockAddonWrapper.prototype = {
+ get id() {
+ return this.addon.id;
+ },
+
+ get type() {
+ return this.addon.type;
+ },
+
+ get appDisabled() {
+ return false;
+ },
+
+ get isCompatible() {
+ return true;
+ },
+
+ get isPlatformCompatible() {
+ return true;
+ },
+
+ get scope() {
+ return AddonManager.SCOPE_PROFILE;
+ },
+
+ get foreignInstall() {
+ return false;
+ },
+
+ get providesUpdatesSecurely() {
+ return true;
+ },
+
+ get blocklistState() {
+ return 0; // Not blocked.
+ },
+
+ get pendingOperations() {
+ return AddonManager.PENDING_NONE;
+ },
+
+ get permissions() {
+ return AddonManager.PERM_CAN_UNINSTALL | AddonManager.PERM_CAN_DISABLE;
+ },
+
+ get isActive() {
+ return true;
+ },
+
+ get name() {
+ return this.addon.name;
+ },
+
+ get version() {
+ return this.addon.version;
+ },
+
+ get creator() {
+ return new AddonManagerPrivate.AddonAuthor(this.addon.author);
+ },
+
+ get userDisabled() {
+ return this.appDisabled;
+ },
+};
+
+function createMockAddonProvider(aName) {
+ let mockProvider = {
+ _addons: [],
+
+ get name() {
+ return aName;
+ },
+
+ addAddon(aAddon) {
+ this._addons.push(aAddon);
+ AddonManagerPrivate.callAddonListeners(
+ "onInstalled",
+ new MockAddonWrapper(aAddon)
+ );
+ },
+
+ async getAddonsByTypes(aTypes) {
+ return this._addons
+ .filter(a => !aTypes || aTypes.includes(a.type))
+ .map(a => new MockAddonWrapper(a));
+ },
+
+ shutdown() {
+ return Promise.resolve();
+ },
+ };
+
+ return mockProvider;
+}
+
+add_task(async function setup() {
+ TelemetryEnvironmentTesting.registerFakeSysInfo();
+ TelemetryEnvironmentTesting.spoofGfxAdapter();
+ do_get_profile();
+
+ // We need to ensure FOG is initialized, otherwise we will panic trying to get test values.
+ Services.fog.initializeFOG();
+
+ // The system add-on must be installed before AddonManager is started.
+ const distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "app0"]);
+ distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ do_get_file("system.xpi").copyTo(
+ distroDir,
+ "tel-system-xpi@tests.mozilla.org.xpi"
+ );
+ let system_addon = FileUtils.File(distroDir.path);
+ system_addon.append("tel-system-xpi@tests.mozilla.org.xpi");
+ system_addon.lastModifiedTime = SYSTEM_ADDON_INSTALL_DATE;
+ await loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
+
+ TelemetryEnvironmentTesting.init(gAppInfo);
+
+ // The test runs in a fresh profile so starting the AddonManager causes
+ // the addons database to be created (as does setting new theme).
+ // For test_addonsStartup below, we want to test a "warm" startup where
+ // there is already a database on disk. Simulate that here by just
+ // restarting the AddonManager.
+ await AddonTestUtils.promiseShutdownManager();
+ await AddonTestUtils.overrideBuiltIns({ system: [] });
+ AddonTestUtils.addonStartup.remove(true);
+ await AddonTestUtils.promiseStartupManager();
+
+ // Setup a webserver to serve Addons, etc.
+ gHttpServer = new HttpServer();
+ gHttpServer.start(-1);
+ let port = gHttpServer.identity.primaryPort;
+ gHttpRoot = "http://localhost:" + port + "/";
+ gDataRoot = gHttpRoot + "data/";
+ gHttpServer.registerDirectory("/data/", do_get_cwd());
+ registerCleanupFunction(() => gHttpServer.stop(() => {}));
+
+ // Create the attribution data file, so that settings.attribution will exist.
+ // The attribution functionality only exists in Firefox.
+ if (AppConstants.MOZ_BUILD_APP == "browser") {
+ TelemetryEnvironmentTesting.spoofAttributionData();
+ registerCleanupFunction(async function () {
+ await TelemetryEnvironmentTesting.cleanupAttributionData;
+ });
+ }
+
+ await TelemetryEnvironmentTesting.spoofProfileReset();
+ await TelemetryEnvironment.delayedInit();
+ await SearchTestUtils.useTestEngines("data", "search-extensions");
+});
+
+add_task(async function test_checkEnvironment() {
+ // During startup we have partial addon records.
+ // First make sure we haven't yet read the addons DB, then test that
+ // we have some partial addons data.
+ Assert.equal(
+ AddonManagerPrivate.isDBLoaded(),
+ false,
+ "addons database is not loaded"
+ );
+
+ let data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkAddonsSection(data, false, true);
+
+ // Check that settings.intl is lazily loaded.
+ Assert.equal(
+ typeof data.settings.intl,
+ "object",
+ "intl is initially an object"
+ );
+ Assert.equal(
+ Object.keys(data.settings.intl).length,
+ 0,
+ "intl is initially empty"
+ );
+
+ // Now continue with startup.
+ let initPromise = TelemetryEnvironment.onInitialized();
+ finishAddonManagerStartup();
+
+ // Fake the delayed startup event for intl data to load.
+ fakeIntlReady();
+
+ let environmentData = await initPromise;
+ TelemetryEnvironmentTesting.checkEnvironmentData(environmentData, {
+ isInitial: true,
+ });
+
+ TelemetryEnvironmentTesting.spoofPartnerInfo();
+ Services.obs.notifyObservers(null, DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC);
+
+ environmentData = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(environmentData, {
+ assertProcessData: true,
+ });
+});
+
+add_task(async function test_prefWatchPolicies() {
+ const PREF_TEST_1 = "toolkit.telemetry.test.pref_new";
+ const PREF_TEST_2 = "toolkit.telemetry.test.pref1";
+ const PREF_TEST_3 = "toolkit.telemetry.test.pref2";
+ const PREF_TEST_4 = "toolkit.telemetry.test.pref_old";
+ const PREF_TEST_5 = "toolkit.telemetry.test.requiresRestart";
+
+ const expectedValue = "some-test-value";
+ const unexpectedValue = "unexpected-test-value";
+
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST_1, { what: TelemetryEnvironment.RECORD_PREF_VALUE }],
+ [PREF_TEST_2, { what: TelemetryEnvironment.RECORD_PREF_STATE }],
+ [PREF_TEST_3, { what: TelemetryEnvironment.RECORD_PREF_STATE }],
+ [PREF_TEST_4, { what: TelemetryEnvironment.RECORD_PREF_VALUE }],
+ [
+ PREF_TEST_5,
+ { what: TelemetryEnvironment.RECORD_PREF_VALUE, requiresRestart: true },
+ ],
+ ]);
+
+ Services.prefs.setStringPref(PREF_TEST_4, expectedValue);
+ Services.prefs.setStringPref(PREF_TEST_5, expectedValue);
+
+ // Set the Environment preferences to watch.
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+ let deferred = Promise.withResolvers();
+
+ // Check that the pref values are missing or present as expected
+ Assert.strictEqual(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_1],
+ undefined
+ );
+ Assert.strictEqual(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_4],
+ expectedValue
+ );
+ Assert.strictEqual(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_5],
+ expectedValue
+ );
+
+ TelemetryEnvironment.registerChangeListener(
+ "testWatchPrefs",
+ (reason, data) => deferred.resolve(data)
+ );
+ let oldEnvironmentData = TelemetryEnvironment.currentEnvironment;
+
+ // Trigger a change in the watched preferences.
+ Services.prefs.setStringPref(PREF_TEST_1, expectedValue);
+ Services.prefs.setBoolPref(PREF_TEST_2, false);
+ Services.prefs.setStringPref(PREF_TEST_5, unexpectedValue);
+ let eventEnvironmentData = await deferred.promise;
+
+ // Unregister the listener.
+ TelemetryEnvironment.unregisterChangeListener("testWatchPrefs");
+
+ // Check environment contains the correct data.
+ Assert.deepEqual(oldEnvironmentData, eventEnvironmentData);
+ let userPrefs = TelemetryEnvironment.currentEnvironment.settings.userPrefs;
+
+ Assert.equal(
+ userPrefs[PREF_TEST_1],
+ expectedValue,
+ "Environment contains the correct preference value."
+ );
+ Assert.equal(
+ userPrefs[PREF_TEST_2],
+ "<user-set>",
+ "Report that the pref was user set but the value is not shown."
+ );
+ Assert.ok(
+ !(PREF_TEST_3 in userPrefs),
+ "Do not report if preference not user set."
+ );
+ Assert.equal(
+ userPrefs[PREF_TEST_5],
+ expectedValue,
+ "The pref value in the environment data should still be the same"
+ );
+});
+
+add_task(async function test_prefWatch_prefReset() {
+ const PREF_TEST = "toolkit.telemetry.test.pref1";
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_STATE }],
+ ]);
+
+ // Set the preference to a non-default value.
+ Services.prefs.setBoolPref(PREF_TEST, false);
+
+ // Set the Environment preferences to watch.
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+ let deferred = Promise.withResolvers();
+ TelemetryEnvironment.registerChangeListener(
+ "testWatchPrefs_reset",
+ deferred.resolve
+ );
+
+ Assert.strictEqual(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST],
+ "<user-set>"
+ );
+
+ // Trigger a change in the watched preferences.
+ Services.prefs.clearUserPref(PREF_TEST);
+ await deferred.promise;
+
+ Assert.strictEqual(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST],
+ undefined
+ );
+
+ // Unregister the listener.
+ TelemetryEnvironment.unregisterChangeListener("testWatchPrefs_reset");
+});
+
+add_task(async function test_prefDefault() {
+ const PREF_TEST = "toolkit.telemetry.test.defaultpref1";
+ const expectedValue = "some-test-value";
+
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, { what: TelemetryEnvironment.RECORD_DEFAULTPREF_VALUE }],
+ ]);
+
+ // Set the preference to a default value.
+ Services.prefs.getDefaultBranch(null).setCharPref(PREF_TEST, expectedValue);
+
+ // Set the Environment preferences to watch.
+ // We're not watching, but this function does the setup we need.
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+
+ Assert.strictEqual(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST],
+ expectedValue
+ );
+});
+
+add_task(async function test_prefDefaultState() {
+ const PREF_TEST = "toolkit.telemetry.test.defaultpref2";
+ const expectedValue = "some-test-value";
+
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, { what: TelemetryEnvironment.RECORD_DEFAULTPREF_STATE }],
+ ]);
+
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+
+ Assert.equal(
+ PREF_TEST in TelemetryEnvironment.currentEnvironment.settings.userPrefs,
+ false
+ );
+
+ // Set the preference to a default value.
+ Services.prefs.getDefaultBranch(null).setCharPref(PREF_TEST, expectedValue);
+
+ Assert.strictEqual(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST],
+ "<set>"
+ );
+});
+
+add_task(async function test_prefInvalid() {
+ const PREF_TEST_1 = "toolkit.telemetry.test.invalid1";
+ const PREF_TEST_2 = "toolkit.telemetry.test.invalid2";
+
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST_1, { what: TelemetryEnvironment.RECORD_DEFAULTPREF_VALUE }],
+ [PREF_TEST_2, { what: TelemetryEnvironment.RECORD_DEFAULTPREF_STATE }],
+ ]);
+
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+
+ Assert.strictEqual(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_1],
+ undefined
+ );
+ Assert.strictEqual(
+ TelemetryEnvironment.currentEnvironment.settings.userPrefs[PREF_TEST_2],
+ undefined
+ );
+});
+
+add_task(async function test_addonsWatch_InterestingChange() {
+ const ADDON_INSTALL_URL = gDataRoot + "restartless.xpi";
+ const ADDON_ID = "tel-restartless-webext@tests.mozilla.org";
+ // We only expect a single notification for each install, uninstall, enable, disable.
+ const EXPECTED_NOTIFICATIONS = 4;
+
+ let receivedNotifications = 0;
+
+ let registerCheckpointPromise = aExpected => {
+ return new Promise(resolve =>
+ TelemetryEnvironment.registerChangeListener(
+ "testWatchAddons_Changes" + aExpected,
+ (reason, data) => {
+ Assert.equal(reason, "addons-changed");
+ receivedNotifications++;
+ resolve();
+ }
+ )
+ );
+ };
+
+ let assertCheckpoint = aExpected => {
+ Assert.equal(receivedNotifications, aExpected);
+ TelemetryEnvironment.unregisterChangeListener(
+ "testWatchAddons_Changes" + aExpected
+ );
+ };
+
+ // Test for receiving one notification after each change.
+ let checkpointPromise = registerCheckpointPromise(1);
+ await installXPIFromURL(ADDON_INSTALL_URL);
+ await checkpointPromise;
+ assertCheckpoint(1);
+ Assert.ok(
+ ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons
+ );
+
+ checkpointPromise = registerCheckpointPromise(2);
+ let addon = await AddonManager.getAddonByID(ADDON_ID);
+ await addon.disable();
+ await checkpointPromise;
+ assertCheckpoint(2);
+ Assert.ok(
+ !(ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons)
+ );
+
+ checkpointPromise = registerCheckpointPromise(3);
+ let startupPromise = AddonTestUtils.promiseWebExtensionStartup(ADDON_ID);
+ await addon.enable();
+ await checkpointPromise;
+ assertCheckpoint(3);
+ Assert.ok(
+ ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons
+ );
+ await startupPromise;
+
+ checkpointPromise = registerCheckpointPromise(4);
+ (await AddonManager.getAddonByID(ADDON_ID)).uninstall();
+ await checkpointPromise;
+ assertCheckpoint(4);
+ Assert.ok(
+ !(ADDON_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons)
+ );
+
+ Assert.equal(
+ receivedNotifications,
+ EXPECTED_NOTIFICATIONS,
+ "We must only receive the notifications we expect."
+ );
+});
+
+add_task(async function test_addonsWatch_NotInterestingChange() {
+ // Plugins from GMPProvider are listed separately in addons.activeGMPlugins.
+ // We simulate the "plugin" type in this test and verify that it is excluded.
+ const PLUGIN_ID = "tel-fake-gmp-plugin@tests.mozilla.org";
+ // "theme" type is already covered by addons.theme, so we aren't interested.
+ const THEME_ID = "tel-theme@tests.mozilla.org";
+ // "dictionary" type should be in addon.activeAddons.
+ const DICT_ID = "tel-dict@tests.mozilla.org";
+
+ let receivedNotification = false;
+ let deferred = Promise.withResolvers();
+ TelemetryEnvironment.registerChangeListener("testNotInteresting", () => {
+ Assert.ok(
+ !receivedNotification,
+ "Should not receive multiple notifications"
+ );
+ receivedNotification = true;
+ deferred.resolve();
+ });
+
+ // "plugin" type, to verify that non-XPIProvider types such as the "plugin"
+ // type from GMPProvider are not included in activeAddons.
+ let fakePluginProvider = createMockAddonProvider("Fake GMPProvider");
+ AddonManagerPrivate.registerProvider(fakePluginProvider);
+ fakePluginProvider.addAddon({
+ id: PLUGIN_ID,
+ name: "Fake plugin",
+ version: "1",
+ type: "plugin",
+ });
+
+ // "theme" type.
+ let themeXpi = AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ theme: {},
+ browser_specific_settings: { gecko: { id: THEME_ID } },
+ },
+ });
+ let themeAddon = (await AddonTestUtils.promiseInstallFile(themeXpi)).addon;
+
+ let dictXpi = AddonTestUtils.createTempWebExtensionFile({
+ manifest: {
+ dictionaries: {},
+ browser_specific_settings: { gecko: { id: DICT_ID } },
+ },
+ });
+ let dictAddon = (await AddonTestUtils.promiseInstallFile(dictXpi)).addon;
+
+ await deferred.promise;
+ Assert.ok(
+ !(PLUGIN_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons),
+ "GMP plugins should not appear in active addons."
+ );
+ Assert.ok(
+ !(THEME_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons),
+ "Themes should not appear in active addons."
+ );
+ Assert.ok(
+ DICT_ID in TelemetryEnvironment.currentEnvironment.addons.activeAddons,
+ "Dictionaries should appear in active addons."
+ );
+
+ TelemetryEnvironment.unregisterChangeListener("testNotInteresting");
+
+ AddonManagerPrivate.unregisterProvider(fakePluginProvider);
+ await themeAddon.uninstall();
+ await dictAddon.uninstall();
+});
+
+add_task(async function test_addons() {
+ const ADDON_INSTALL_URL = gDataRoot + "restartless.xpi";
+ const ADDON_ID = "tel-restartless-webext@tests.mozilla.org";
+ const ADDON_INSTALL_DATE = truncateToDays(Date.now());
+ const EXPECTED_ADDON_DATA = {
+ blocklisted: false,
+ description: "A restartless addon which gets enabled without a reboot.",
+ name: "XPI Telemetry Restartless Test",
+ userDisabled: false,
+ appDisabled: false,
+ version: "1.0",
+ scope: 1,
+ type: "extension",
+ foreignInstall: false,
+ hasBinaryComponents: false,
+ installDay: ADDON_INSTALL_DATE,
+ updateDay: ADDON_INSTALL_DATE,
+ signedState: AddonManager.SIGNEDSTATE_PRIVILEGED,
+ isSystem: false,
+ isWebExtension: true,
+ multiprocessCompatible: true,
+ quarantineIgnoredByUser: false,
+ // quarantineIgnoredByApp expected to be true because
+ // the test addon is signed as privileged (see signedState).
+ quarantineIgnoredByApp: true,
+ };
+ const SYSTEM_ADDON_ID = "tel-system-xpi@tests.mozilla.org";
+ const EXPECTED_SYSTEM_ADDON_DATA = {
+ blocklisted: false,
+ description: "A system addon which is shipped with Firefox.",
+ name: "XPI Telemetry System Add-on Test",
+ userDisabled: false,
+ appDisabled: false,
+ version: "1.0",
+ scope: 1,
+ type: "extension",
+ foreignInstall: false,
+ hasBinaryComponents: false,
+ installDay: truncateToDays(SYSTEM_ADDON_INSTALL_DATE),
+ updateDay: truncateToDays(SYSTEM_ADDON_INSTALL_DATE),
+ signedState: undefined,
+ isSystem: true,
+ isWebExtension: true,
+ multiprocessCompatible: true,
+ quarantineIgnoredByUser: false,
+ // quarantineIgnoredByApp expected to be true because
+ // the test addon is a system addon (see isSystem).
+ quarantineIgnoredByApp: true,
+ };
+
+ const WEBEXTENSION_ADDON_ID = "tel-webextension-xpi@tests.mozilla.org";
+ const WEBEXTENSION_ADDON_INSTALL_DATE = truncateToDays(Date.now());
+ const EXPECTED_WEBEXTENSION_ADDON_DATA = {
+ blocklisted: false,
+ description: "A webextension addon.",
+ name: "XPI Telemetry WebExtension Add-on Test",
+ userDisabled: false,
+ appDisabled: false,
+ version: "1.0",
+ scope: 1,
+ type: "extension",
+ foreignInstall: false,
+ hasBinaryComponents: false,
+ installDay: WEBEXTENSION_ADDON_INSTALL_DATE,
+ updateDay: WEBEXTENSION_ADDON_INSTALL_DATE,
+ signedState: AddonManager.SIGNEDSTATE_PRIVILEGED,
+ isSystem: false,
+ isWebExtension: true,
+ multiprocessCompatible: true,
+ quarantineIgnoredByUser: false,
+ // quarantineIgnoredByApp expected to be true because
+ // the test addon is signed as privileged (see signedState).
+ quarantineIgnoredByApp: true,
+ };
+
+ let deferred = Promise.withResolvers();
+ TelemetryEnvironment.registerChangeListener(
+ "test_WebExtension",
+ (reason, data) => {
+ Assert.equal(reason, "addons-changed");
+ deferred.resolve();
+ }
+ );
+
+ // Install an add-on so we have some data.
+ let addon = await installXPIFromURL(ADDON_INSTALL_URL);
+
+ // Install a webextension as well.
+ // Note: all addons are webextensions, so doing this again is redundant...
+ ExtensionTestUtils.init(this);
+
+ let webextension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ name: "XPI Telemetry WebExtension Add-on Test",
+ description: "A webextension addon.",
+ version: "1.0",
+ browser_specific_settings: {
+ gecko: {
+ id: WEBEXTENSION_ADDON_ID,
+ },
+ },
+ },
+ });
+
+ await webextension.startup();
+ await deferred.promise;
+ TelemetryEnvironment.unregisterChangeListener("test_WebExtension");
+
+ let data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+
+ // Check addon data.
+ Assert.ok(
+ ADDON_ID in data.addons.activeAddons,
+ "We must have one active addon."
+ );
+ let targetAddon = data.addons.activeAddons[ADDON_ID];
+ for (let f in EXPECTED_ADDON_DATA) {
+ Assert.equal(
+ targetAddon[f],
+ EXPECTED_ADDON_DATA[f],
+ f + " must have the correct value."
+ );
+ }
+
+ // Check system add-on data.
+ Assert.ok(
+ SYSTEM_ADDON_ID in data.addons.activeAddons,
+ "We must have one active system addon."
+ );
+ let targetSystemAddon = data.addons.activeAddons[SYSTEM_ADDON_ID];
+ for (let f in EXPECTED_SYSTEM_ADDON_DATA) {
+ Assert.equal(
+ targetSystemAddon[f],
+ EXPECTED_SYSTEM_ADDON_DATA[f],
+ f + " must have the correct value."
+ );
+ }
+
+ // Check webextension add-on data.
+ Assert.ok(
+ WEBEXTENSION_ADDON_ID in data.addons.activeAddons,
+ "We must have one active webextension addon."
+ );
+ let targetWebExtensionAddon = data.addons.activeAddons[WEBEXTENSION_ADDON_ID];
+ for (let f in EXPECTED_WEBEXTENSION_ADDON_DATA) {
+ Assert.equal(
+ targetWebExtensionAddon[f],
+ EXPECTED_WEBEXTENSION_ADDON_DATA[f],
+ f + " must have the correct value."
+ );
+ }
+
+ await webextension.unload();
+
+ // Uninstall the addon.
+ await addon.startupPromise;
+ await addon.uninstall();
+});
+
+add_task(async function test_signedAddon() {
+ AddonTestUtils.useRealCertChecks = true;
+
+ const ADDON_INSTALL_URL = gDataRoot + "signed-webext.xpi";
+ const ADDON_ID = "tel-signed-webext@tests.mozilla.org";
+ const ADDON_INSTALL_DATE = truncateToDays(Date.now());
+ const EXPECTED_ADDON_DATA = {
+ blocklisted: false,
+ description: "A signed webextension",
+ name: "XPI Telemetry Signed Test",
+ userDisabled: false,
+ appDisabled: false,
+ version: "1.0",
+ scope: 1,
+ type: "extension",
+ foreignInstall: false,
+ hasBinaryComponents: false,
+ installDay: ADDON_INSTALL_DATE,
+ updateDay: ADDON_INSTALL_DATE,
+ signedState: AddonManager.SIGNEDSTATE_SIGNED,
+ quarantineIgnoredByUser: false,
+ // quarantineIgnoredByApp expected to be false because
+ // the test addon is signed as a non-privileged (see signedState),
+ // and it doesn't include any recommendations.
+ quarantineIgnoredByApp: false,
+ };
+
+ let deferred = Promise.withResolvers();
+ TelemetryEnvironment.registerChangeListener(
+ "test_signedAddon",
+ deferred.resolve
+ );
+
+ // Install the addon.
+ let addon = await installXPIFromURL(ADDON_INSTALL_URL);
+
+ await deferred.promise;
+ // Unregister the listener.
+ TelemetryEnvironment.unregisterChangeListener("test_signedAddon");
+
+ let data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+
+ // Check addon data.
+ Assert.ok(
+ ADDON_ID in data.addons.activeAddons,
+ "Add-on should be in the environment."
+ );
+ let targetAddon = data.addons.activeAddons[ADDON_ID];
+ for (let f in EXPECTED_ADDON_DATA) {
+ Assert.equal(
+ targetAddon[f],
+ EXPECTED_ADDON_DATA[f],
+ f + " must have the correct value."
+ );
+ }
+
+ // Make sure quarantineIgnoredByUser property is updated also in the
+ // telemetry environment in response to the user changing it.
+ deferred = Promise.withResolvers();
+ TelemetryEnvironment.registerChangeListener(
+ "test_quarantineIgnoreByUser_changed",
+ deferred.resolve
+ );
+
+ addon.quarantineIgnoredByUser = true;
+ await deferred.promise;
+ // Unregister the listener.
+ TelemetryEnvironment.unregisterChangeListener(
+ "test_quarantineIgnoreByUser_changed"
+ );
+
+ Assert.equal(
+ TelemetryEnvironment.currentEnvironment.addons.activeAddons[ADDON_ID]
+ .quarantineIgnoredByUser,
+ true,
+ "Expect quarantineIgnoredByUser to be set to true"
+ );
+
+ AddonTestUtils.useRealCertChecks = false;
+ await addon.startupPromise;
+ await addon.uninstall();
+});
+
+add_task(async function test_addonsFieldsLimit() {
+ const ADDON_INSTALL_URL = gDataRoot + "long-fields.xpi";
+ const ADDON_ID = "tel-longfields-webext@tests.mozilla.org";
+
+ // Install the addon and wait for the TelemetryEnvironment to pick it up.
+ let deferred = Promise.withResolvers();
+ TelemetryEnvironment.registerChangeListener(
+ "test_longFieldsAddon",
+ deferred.resolve
+ );
+ let addon = await installXPIFromURL(ADDON_INSTALL_URL);
+ await deferred.promise;
+ TelemetryEnvironment.unregisterChangeListener("test_longFieldsAddon");
+
+ let data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+
+ // Check that the addon is available and that the string fields are limited.
+ Assert.ok(
+ ADDON_ID in data.addons.activeAddons,
+ "Add-on should be in the environment."
+ );
+ let targetAddon = data.addons.activeAddons[ADDON_ID];
+
+ // TelemetryEnvironment limits the length of string fields for activeAddons to 100 chars,
+ // to mitigate misbehaving addons.
+ Assert.lessOrEqual(
+ targetAddon.version.length,
+ 100,
+ "The version string must have been limited"
+ );
+ Assert.lessOrEqual(
+ targetAddon.name.length,
+ 100,
+ "The name string must have been limited"
+ );
+ Assert.lessOrEqual(
+ targetAddon.description.length,
+ 100,
+ "The description string must have been limited"
+ );
+
+ await addon.startupPromise;
+ await addon.uninstall();
+});
+
+add_task(async function test_collectionWithbrokenAddonData() {
+ const BROKEN_ADDON_ID = "telemetry-test2.example.com@services.mozilla.org";
+ const BROKEN_MANIFEST = {
+ id: "telemetry-test2.example.com@services.mozilla.org",
+ name: "telemetry broken addon",
+ origin: "https://telemetry-test2.example.com",
+ version: 1, // This is intentionally not a string.
+ signedState: AddonManager.SIGNEDSTATE_SIGNED,
+ type: "extension",
+ };
+
+ const ADDON_INSTALL_URL = gDataRoot + "restartless.xpi";
+ const ADDON_ID = "tel-restartless-webext@tests.mozilla.org";
+ const ADDON_INSTALL_DATE = truncateToDays(Date.now());
+ const EXPECTED_ADDON_DATA = {
+ blocklisted: false,
+ description: "A restartless addon which gets enabled without a reboot.",
+ name: "XPI Telemetry Restartless Test",
+ userDisabled: false,
+ appDisabled: false,
+ version: "1.0",
+ scope: 1,
+ type: "extension",
+ foreignInstall: false,
+ hasBinaryComponents: false,
+ installDay: ADDON_INSTALL_DATE,
+ updateDay: ADDON_INSTALL_DATE,
+ signedState: AddonManager.SIGNEDSTATE_MISSING,
+ };
+
+ let receivedNotifications = 0;
+
+ let registerCheckpointPromise = aExpected => {
+ return new Promise(resolve =>
+ TelemetryEnvironment.registerChangeListener(
+ "testBrokenAddon_collection" + aExpected,
+ (reason, data) => {
+ Assert.equal(reason, "addons-changed");
+ receivedNotifications++;
+ resolve();
+ }
+ )
+ );
+ };
+
+ let assertCheckpoint = aExpected => {
+ Assert.equal(receivedNotifications, aExpected);
+ TelemetryEnvironment.unregisterChangeListener(
+ "testBrokenAddon_collection" + aExpected
+ );
+ };
+
+ // Register the broken provider and install the broken addon.
+ let checkpointPromise = registerCheckpointPromise(1);
+ let brokenAddonProvider = createMockAddonProvider(
+ "Broken Extensions Provider"
+ );
+ AddonManagerPrivate.registerProvider(brokenAddonProvider);
+ brokenAddonProvider.addAddon(BROKEN_MANIFEST);
+ await checkpointPromise;
+ assertCheckpoint(1);
+
+ // Now install an addon which returns the correct information.
+ checkpointPromise = registerCheckpointPromise(2);
+ let addon = await installXPIFromURL(ADDON_INSTALL_URL);
+ await checkpointPromise;
+ assertCheckpoint(2);
+
+ // Check that the new environment contains the info from the broken provider,
+ // despite the addon missing some details.
+ let data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data, {
+ expectBrokenAddons: true,
+ });
+
+ let activeAddons = data.addons.activeAddons;
+ Assert.ok(
+ BROKEN_ADDON_ID in activeAddons,
+ "The addon with the broken manifest must be reported."
+ );
+ Assert.equal(
+ activeAddons[BROKEN_ADDON_ID].version,
+ null,
+ "null should be reported for invalid data."
+ );
+ Assert.ok(ADDON_ID in activeAddons, "The valid addon must be reported.");
+ Assert.equal(
+ activeAddons[ADDON_ID].description,
+ EXPECTED_ADDON_DATA.description,
+ "The description for the valid addon should be correct."
+ );
+
+ // Unregister the broken provider so we don't mess with other tests.
+ AddonManagerPrivate.unregisterProvider(brokenAddonProvider);
+
+ // Uninstall the valid addon.
+ await addon.startupPromise;
+ await addon.uninstall();
+});
+
+add_task(
+ { skip_if: () => AppConstants.MOZ_APP_NAME == "thunderbird" },
+ async function test_delayed_defaultBrowser() {
+ // Skip this test on Thunderbird since it is not a browser, so it cannot
+ // be the default browser.
+
+ // Make sure we don't have anything already cached for this test.
+ await TelemetryEnvironment.testCleanRestart().onInitialized();
+
+ let environmentData = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(environmentData);
+ Assert.equal(
+ environmentData.settings.isDefaultBrowser,
+ null,
+ "isDefaultBrowser must be null before the session is restored."
+ );
+
+ Services.obs.notifyObservers(null, "sessionstore-windows-restored");
+
+ environmentData = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(environmentData);
+ Assert.ok(
+ "isDefaultBrowser" in environmentData.settings,
+ "isDefaultBrowser must be available after the session is restored."
+ );
+ Assert.equal(
+ typeof environmentData.settings.isDefaultBrowser,
+ "boolean",
+ "isDefaultBrowser must be of the right type."
+ );
+
+ // Make sure pref-flipping doesn't overwrite the browser default state.
+ const PREF_TEST = "toolkit.telemetry.test.pref1";
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_STATE }],
+ ]);
+ Services.prefs.clearUserPref(PREF_TEST);
+
+ // Watch the test preference.
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+ let deferred = Promise.withResolvers();
+ TelemetryEnvironment.registerChangeListener(
+ "testDefaultBrowser_pref",
+ deferred.resolve
+ );
+ // Trigger an environment change.
+ Services.prefs.setIntPref(PREF_TEST, 1);
+ await deferred.promise;
+ TelemetryEnvironment.unregisterChangeListener("testDefaultBrowser_pref");
+
+ // Check that the data is still available.
+ environmentData = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(environmentData);
+ Assert.ok(
+ "isDefaultBrowser" in environmentData.settings,
+ "isDefaultBrowser must still be available after a pref is flipped."
+ );
+ }
+);
+
+add_task(async function test_osstrings() {
+ // First test that numbers in sysinfo properties are converted to string fields
+ // in system.os.
+ TelemetryEnvironmentTesting.setSysInfoOverrides({
+ version: 1,
+ name: 2,
+ kernel_version: 3,
+ });
+
+ await TelemetryEnvironment.testCleanRestart().onInitialized();
+ let data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+
+ Assert.equal(data.system.os.version, "1");
+ Assert.equal(data.system.os.name, "2");
+ if (AppConstants.platform == "android") {
+ Assert.equal(data.system.os.kernelVersion, "3");
+ }
+
+ // Check that null values are also handled.
+ TelemetryEnvironmentTesting.setSysInfoOverrides({
+ version: null,
+ name: null,
+ kernel_version: null,
+ });
+
+ await TelemetryEnvironment.testCleanRestart().onInitialized();
+ data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+
+ Assert.equal(data.system.os.version, null);
+ Assert.equal(data.system.os.name, null);
+ if (AppConstants.platform == "android") {
+ Assert.equal(data.system.os.kernelVersion, null);
+ }
+
+ // Clean up.
+ TelemetryEnvironmentTesting.setSysInfoOverrides({});
+ await TelemetryEnvironment.testCleanRestart().onInitialized();
+});
+
+add_task(async function test_experimentsAPI() {
+ const EXPERIMENT1 = "experiment-1";
+ const EXPERIMENT1_BRANCH = "nice-branch";
+ const EXPERIMENT2 = "experiment-2";
+ const EXPERIMENT2_BRANCH = "other-branch";
+
+ let checkExperiment = (environmentData, id, branch, type = null) => {
+ Assert.ok(
+ "experiments" in environmentData,
+ "The current environment must report the experiment annotations."
+ );
+ Assert.ok(
+ id in environmentData.experiments,
+ "The experiments section must contain the expected experiment id."
+ );
+ Assert.equal(
+ environmentData.experiments[id].branch,
+ branch,
+ "The experiment branch must be correct."
+ );
+ };
+
+ // Clean the environment and check that it's reporting the correct info.
+ await TelemetryEnvironment.testCleanRestart().onInitialized();
+ let data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+
+ // We don't expect the experiments section to be there if no annotation
+ // happened.
+ Assert.ok(
+ !("experiments" in data),
+ "No experiments section must be reported if nothing was annotated."
+ );
+
+ // Add a change listener and add an experiment annotation.
+ let deferred = Promise.withResolvers();
+ TelemetryEnvironment.registerChangeListener(
+ "test_experimentsAPI",
+ (reason, env) => {
+ deferred.resolve(env);
+ }
+ );
+ TelemetryEnvironment.setExperimentActive(EXPERIMENT1, EXPERIMENT1_BRANCH);
+ let eventEnvironmentData = await deferred.promise;
+
+ // Check that the old environment does not contain the experiments.
+ TelemetryEnvironmentTesting.checkEnvironmentData(eventEnvironmentData);
+ Assert.ok(
+ !("experiments" in eventEnvironmentData),
+ "No experiments section must be reported in the old environment."
+ );
+
+ // Check that the current environment contains the right experiment.
+ data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+ checkExperiment(data, EXPERIMENT1, EXPERIMENT1_BRANCH);
+
+ TelemetryEnvironment.unregisterChangeListener("test_experimentsAPI");
+
+ // Add a second annotation and check that both experiments are there.
+ deferred = Promise.withResolvers();
+ TelemetryEnvironment.registerChangeListener(
+ "test_experimentsAPI2",
+ (reason, env) => {
+ deferred.resolve(env);
+ }
+ );
+ TelemetryEnvironment.setExperimentActive(EXPERIMENT2, EXPERIMENT2_BRANCH);
+ eventEnvironmentData = await deferred.promise;
+
+ // Check that the current environment contains both the experiment.
+ data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+ checkExperiment(data, EXPERIMENT1, EXPERIMENT1_BRANCH);
+ checkExperiment(data, EXPERIMENT2, EXPERIMENT2_BRANCH);
+
+ // The previous environment should only contain the first experiment.
+ checkExperiment(eventEnvironmentData, EXPERIMENT1, EXPERIMENT1_BRANCH);
+ Assert.ok(
+ !(EXPERIMENT2 in eventEnvironmentData),
+ "The old environment must not contain the new experiment annotation."
+ );
+
+ TelemetryEnvironment.unregisterChangeListener("test_experimentsAPI2");
+
+ // Check that removing an unknown experiment annotation does not trigger
+ // a notification.
+ TelemetryEnvironment.registerChangeListener("test_experimentsAPI3", () => {
+ Assert.ok(
+ false,
+ "Removing an unknown experiment annotation must not trigger a change."
+ );
+ });
+ TelemetryEnvironment.setExperimentInactive("unknown-experiment-id");
+ // Also make sure that passing non-string parameters arguments doesn't throw nor
+ // trigger a notification.
+ TelemetryEnvironment.setExperimentActive({}, "some-branch");
+ TelemetryEnvironment.setExperimentActive("some-id", {});
+ TelemetryEnvironment.unregisterChangeListener("test_experimentsAPI3");
+
+ // Check that removing a known experiment leaves the other in place and triggers
+ // a change.
+ deferred = Promise.withResolvers();
+ TelemetryEnvironment.registerChangeListener(
+ "test_experimentsAPI4",
+ (reason, env) => {
+ deferred.resolve(env);
+ }
+ );
+ TelemetryEnvironment.setExperimentInactive(EXPERIMENT1);
+ eventEnvironmentData = await deferred.promise;
+
+ // Check that the current environment contains just the second experiment.
+ data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+ Assert.ok(
+ !(EXPERIMENT1 in data),
+ "The current environment must not contain the removed experiment annotation."
+ );
+ checkExperiment(data, EXPERIMENT2, EXPERIMENT2_BRANCH);
+
+ // The previous environment should contain both annotations.
+ checkExperiment(eventEnvironmentData, EXPERIMENT1, EXPERIMENT1_BRANCH);
+ checkExperiment(eventEnvironmentData, EXPERIMENT2, EXPERIMENT2_BRANCH);
+
+ // Set an experiment with a type and check that it correctly shows up.
+ TelemetryEnvironment.setExperimentActive(
+ "typed-experiment",
+ "random-branch",
+ { type: "ab-test" }
+ );
+ data = TelemetryEnvironment.currentEnvironment;
+ checkExperiment(data, "typed-experiment", "random-branch", "ab-test");
+});
+
+add_task(async function test_experimentsAPI_limits() {
+ const EXPERIMENT =
+ "experiment-2-experiment-2-experiment-2-experiment-2-experiment-2" +
+ "-experiment-2-experiment-2-experiment-2-experiment-2";
+ const EXPERIMENT_BRANCH =
+ "other-branch-other-branch-other-branch-other-branch-other" +
+ "-branch-other-branch-other-branch-other-branch-other-branch";
+ const EXPERIMENT_TRUNCATED = EXPERIMENT.substring(0, 100);
+ const EXPERIMENT_BRANCH_TRUNCATED = EXPERIMENT_BRANCH.substring(0, 100);
+
+ // Clean the environment and check that it's reporting the correct info.
+ await TelemetryEnvironment.testCleanRestart().onInitialized();
+ let data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+
+ // We don't expect the experiments section to be there if no annotation
+ // happened.
+ Assert.ok(
+ !("experiments" in data),
+ "No experiments section must be reported if nothing was annotated."
+ );
+
+ // Add a change listener and wait for the annotation to happen.
+ let deferred = Promise.withResolvers();
+ TelemetryEnvironment.registerChangeListener("test_experimentsAPI", () =>
+ deferred.resolve()
+ );
+ TelemetryEnvironment.setExperimentActive(EXPERIMENT, EXPERIMENT_BRANCH);
+ await deferred.promise;
+
+ // Check that the current environment contains the truncated values
+ // for the experiment data.
+ data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+ Assert.ok(
+ "experiments" in data,
+ "The environment must contain an experiments section."
+ );
+ Assert.ok(
+ EXPERIMENT_TRUNCATED in data.experiments,
+ "The experiments must be reporting the truncated id."
+ );
+ Assert.ok(
+ !(EXPERIMENT in data.experiments),
+ "The experiments must not be reporting the full id."
+ );
+ Assert.equal(
+ EXPERIMENT_BRANCH_TRUNCATED,
+ data.experiments[EXPERIMENT_TRUNCATED].branch,
+ "The experiments must be reporting the truncated branch."
+ );
+
+ TelemetryEnvironment.unregisterChangeListener("test_experimentsAPI");
+
+ // Check that an overly long type is truncated.
+ const longType = "a0123456678901234567890123456789";
+ TelemetryEnvironment.setExperimentActive("exp", "some-branch", {
+ type: longType,
+ });
+ data = TelemetryEnvironment.currentEnvironment;
+ Assert.equal(data.experiments.exp.type, longType.substring(0, 20));
+});
+
+if (gIsWindows) {
+ add_task(async function test_environmentHDDInfo() {
+ await TelemetryEnvironment.testCleanRestart().onInitialized();
+ let data = TelemetryEnvironment.currentEnvironment;
+ let empty = { model: null, revision: null, type: null };
+ Assert.deepEqual(
+ data.system.hdd,
+ { binary: empty, profile: empty, system: empty },
+ "Should have no data yet."
+ );
+ await TelemetryEnvironment.delayedInit();
+ data = TelemetryEnvironment.currentEnvironment;
+ for (let k of TelemetryEnvironmentTesting.EXPECTED_HDD_FIELDS) {
+ TelemetryEnvironmentTesting.checkString(data.system.hdd[k].model);
+ TelemetryEnvironmentTesting.checkString(data.system.hdd[k].revision);
+ TelemetryEnvironmentTesting.checkString(data.system.hdd[k].type);
+ }
+ });
+
+ add_task(async function test_environmentProcessInfo() {
+ await TelemetryEnvironment.testCleanRestart().onInitialized();
+ let data = TelemetryEnvironment.currentEnvironment;
+ Assert.deepEqual(data.system.isWow64, null, "Should have no data yet.");
+ await TelemetryEnvironment.delayedInit();
+ data = TelemetryEnvironment.currentEnvironment;
+ Assert.equal(
+ typeof data.system.isWow64,
+ "boolean",
+ "isWow64 must be a boolean."
+ );
+ Assert.equal(
+ typeof data.system.isWowARM64,
+ "boolean",
+ "isWowARM64 must be a boolean."
+ );
+ Assert.equal(
+ typeof data.system.hasWinPackageId,
+ "boolean",
+ "hasWinPackageId must be a boolean."
+ );
+ // This is only sent for Mozilla produced MSIX packages
+ Assert.ok(
+ !("winPackageFamilyName" in data.system) ||
+ data.system.winPackageFamilyName === null ||
+ typeof data.system.winPackageFamilyName === "string",
+ "winPackageFamilyName must be a string if non null"
+ );
+ // These should be numbers if they are not null
+ for (let f of [
+ "count",
+ "model",
+ "family",
+ "stepping",
+ "l2cacheKB",
+ "l3cacheKB",
+ "speedMHz",
+ "cores",
+ ]) {
+ Assert.ok(
+ !(f in data.system.cpu) ||
+ data.system.cpu[f] === null ||
+ Number.isFinite(data.system.cpu[f]),
+ f + " must be a number if non null."
+ );
+ }
+ Assert.ok(
+ TelemetryEnvironmentTesting.checkString(data.system.cpu.vendor),
+ "vendor must be a valid string."
+ );
+ });
+
+ add_task(async function test_environmentOSInfo() {
+ await TelemetryEnvironment.testCleanRestart().onInitialized();
+ let data = TelemetryEnvironment.currentEnvironment;
+ Assert.deepEqual(
+ data.system.os.installYear,
+ null,
+ "Should have no data yet."
+ );
+ await TelemetryEnvironment.delayedInit();
+ data = TelemetryEnvironment.currentEnvironment;
+ Assert.ok(
+ Number.isFinite(data.system.os.installYear),
+ "Install year must be a number."
+ );
+ });
+}
+
+add_task(
+ { skip_if: () => AppConstants.MOZ_APP_NAME == "thunderbird" },
+ async function test_environmentServicesInfo() {
+ let cache = TelemetryEnvironment.testCleanRestart();
+ await cache.onInitialized();
+ let oldGetFxaSignedInUser = cache._getFxaSignedInUser;
+ try {
+ // Test the 'yes to both' case.
+
+ // This makes the weave service return that the usere is definitely a sync user
+ Services.prefs.setStringPref(
+ "services.sync.username",
+ "c00lperson123@example.com"
+ );
+ let calledFxa = false;
+ cache._getFxaSignedInUser = () => {
+ calledFxa = true;
+ return null;
+ };
+
+ await cache._updateServicesInfo();
+ ok(
+ !calledFxa,
+ "Shouldn't need to ask FxA if they're definitely signed in"
+ );
+ deepEqual(cache.currentEnvironment.services, {
+ accountEnabled: true,
+ syncEnabled: true,
+ });
+
+ // Test the fxa-but-not-sync case.
+ Services.prefs.clearUserPref("services.sync.username");
+ // We don't actually inspect the returned object, just t
+ cache._getFxaSignedInUser = async () => {
+ return {};
+ };
+ await cache._updateServicesInfo();
+ deepEqual(cache.currentEnvironment.services, {
+ accountEnabled: true,
+ syncEnabled: false,
+ });
+ // Test the "no to both" case.
+ cache._getFxaSignedInUser = async () => {
+ return null;
+ };
+ await cache._updateServicesInfo();
+ deepEqual(cache.currentEnvironment.services, {
+ accountEnabled: false,
+ syncEnabled: false,
+ });
+ // And finally, the 'fxa is in an error state' case.
+ cache._getFxaSignedInUser = () => {
+ throw new Error("You'll never know");
+ };
+ await cache._updateServicesInfo();
+ equal(cache.currentEnvironment.services, null);
+ } finally {
+ cache._getFxaSignedInUser = oldGetFxaSignedInUser;
+ Services.prefs.clearUserPref("services.sync.username");
+ }
+ }
+);
+
+add_task(async function test_normandyTestPrefsGoneAfter91() {
+ const testPrefBool = "app.normandy.test-prefs.bool";
+ const testPrefInteger = "app.normandy.test-prefs.integer";
+ const testPrefString = "app.normandy.test-prefs.string";
+
+ Services.prefs.setBoolPref(testPrefBool, true);
+ Services.prefs.setIntPref(testPrefInteger, 10);
+ Services.prefs.setCharPref(testPrefString, "test-string");
+
+ const data = TelemetryEnvironment.currentEnvironment;
+
+ if (Services.vc.compare(data.build.version, "91") > 0) {
+ Assert.equal(
+ data.settings.userPrefs["app.normandy.test-prefs.bool"],
+ null,
+ "This probe should expire in FX91. bug 1686105 "
+ );
+ Assert.equal(
+ data.settings.userPrefs["app.normandy.test-prefs.integer"],
+ null,
+ "This probe should expire in FX91. bug 1686105 "
+ );
+ Assert.equal(
+ data.settings.userPrefs["app.normandy.test-prefs.string"],
+ null,
+ "This probe should expire in FX91. bug 1686105 "
+ );
+ }
+});
+
+add_task(async function test_environmentShutdown() {
+ // Define and reset the test preference.
+ const PREF_TEST = "toolkit.telemetry.test.pref1";
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_STATE }],
+ ]);
+ Services.prefs.clearUserPref(PREF_TEST);
+
+ // Set up the preferences and listener, then the trigger shutdown
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+ TelemetryEnvironment.registerChangeListener(
+ "test_environmentShutdownChange",
+ () => {
+ // Register a new change listener that asserts if change is propogated
+ Assert.ok(false, "No change should be propagated after shutdown.");
+ }
+ );
+ TelemetryEnvironment.shutdown();
+
+ // Flipping the test preference after shutdown should not trigger the listener
+ Services.prefs.setIntPref(PREF_TEST, 1);
+
+ // Unregister the listener.
+ TelemetryEnvironment.unregisterChangeListener(
+ "test_environmentShutdownChange"
+ );
+});
+
+add_task(async function test_environmentDidntChange() {
+ // Clean the environment and check that it's reporting the correct info.
+ await TelemetryEnvironment.testCleanRestart().onInitialized();
+ let data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+
+ const LISTENER_NAME = "test_environmentDidntChange";
+ TelemetryEnvironment.registerChangeListener(LISTENER_NAME, () => {
+ Assert.ok(false, "The environment didn't actually change.");
+ });
+
+ // Don't actually change the environment, but notify of a compositor abort.
+ const COMPOSITOR_ABORTED_TOPIC = "compositor:process-aborted";
+ Services.obs.notifyObservers(null, COMPOSITOR_ABORTED_TOPIC);
+
+ TelemetryEnvironment.unregisterChangeListener(LISTENER_NAME);
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment_search.js b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment_search.js
new file mode 100644
index 0000000000..0dc3849508
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEnvironment_search.js
@@ -0,0 +1,398 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { TelemetryEnvironment } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryEnvironment.sys.mjs"
+);
+const { SearchTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/SearchTestUtils.sys.mjs"
+);
+const { TelemetryEnvironmentTesting } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryEnvironmentTesting.sys.mjs"
+);
+
+SearchTestUtils.init(this);
+
+function promiseNextTick() {
+ return new Promise(resolve => executeSoon(resolve));
+}
+
+// The webserver hosting the addons.
+var gHttpServer = null;
+// The URL of the webserver root.
+var gHttpRoot = null;
+// The URL of the data directory, on the webserver.
+var gDataRoot = null;
+
+add_task(async function setup() {
+ TelemetryEnvironmentTesting.registerFakeSysInfo();
+ TelemetryEnvironmentTesting.spoofGfxAdapter();
+ do_get_profile();
+
+ // We need to ensure FOG is initialized, otherwise we will panic trying to get test values.
+ Services.fog.initializeFOG();
+
+ // The system add-on must be installed before AddonManager is started.
+ const distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "app0"]);
+ distroDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ do_get_file("system.xpi").copyTo(
+ distroDir,
+ "tel-system-xpi@tests.mozilla.org.xpi"
+ );
+ let system_addon = FileUtils.File(distroDir.path);
+ system_addon.append("tel-system-xpi@tests.mozilla.org.xpi");
+ system_addon.lastModifiedTime = SYSTEM_ADDON_INSTALL_DATE;
+ await loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
+
+ TelemetryEnvironmentTesting.init(gAppInfo);
+
+ // The test runs in a fresh profile so starting the AddonManager causes
+ // the addons database to be created (as does setting new theme).
+ // For test_addonsStartup below, we want to test a "warm" startup where
+ // there is already a database on disk. Simulate that here by just
+ // restarting the AddonManager.
+ await AddonTestUtils.promiseShutdownManager();
+ await AddonTestUtils.overrideBuiltIns({ system: [] });
+ AddonTestUtils.addonStartup.remove(true);
+ await AddonTestUtils.promiseStartupManager();
+
+ // Setup a webserver to serve Addons, etc.
+ gHttpServer = new HttpServer();
+ gHttpServer.start(-1);
+ let port = gHttpServer.identity.primaryPort;
+ gHttpRoot = "http://localhost:" + port + "/";
+ gDataRoot = gHttpRoot + "data/";
+ gHttpServer.registerDirectory("/data/", do_get_cwd());
+ registerCleanupFunction(() => gHttpServer.stop(() => {}));
+
+ // Create the attribution data file, so that settings.attribution will exist.
+ // The attribution functionality only exists in Firefox.
+ if (AppConstants.MOZ_BUILD_APP == "browser") {
+ TelemetryEnvironmentTesting.spoofAttributionData();
+ registerCleanupFunction(async function () {
+ await TelemetryEnvironmentTesting.cleanupAttributionData;
+ });
+ }
+
+ await TelemetryEnvironmentTesting.spoofProfileReset();
+ await TelemetryEnvironment.delayedInit();
+ await SearchTestUtils.useTestEngines("data", "search-extensions");
+
+ // Now continue with startup.
+ let initPromise = TelemetryEnvironment.onInitialized();
+ finishAddonManagerStartup();
+
+ // Fake the delayed startup event for intl data to load.
+ fakeIntlReady();
+
+ let environmentData = await initPromise;
+ TelemetryEnvironmentTesting.checkEnvironmentData(environmentData, {
+ isInitial: true,
+ });
+
+ TelemetryEnvironmentTesting.spoofPartnerInfo();
+ Services.obs.notifyObservers(null, DISTRIBUTION_CUSTOMIZATION_COMPLETE_TOPIC);
+
+ environmentData = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(environmentData, {
+ assertProcessData: true,
+ });
+});
+
+async function checkDefaultSearch(privateOn, reInitSearchService) {
+ // Start off with separate default engine for private browsing turned off.
+ Services.prefs.setBoolPref(
+ "browser.search.separatePrivateDefault.ui.enabled",
+ privateOn
+ );
+ Services.prefs.setBoolPref(
+ "browser.search.separatePrivateDefault",
+ privateOn
+ );
+
+ let data;
+ if (privateOn) {
+ data = await TelemetryEnvironment.testCleanRestart().onInitialized();
+ } else {
+ data = TelemetryEnvironment.currentEnvironment;
+ }
+
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+ Assert.ok(!("defaultSearchEngine" in data.settings));
+ Assert.ok(!("defaultSearchEngineData" in data.settings));
+ Assert.ok(!("defaultPrivateSearchEngine" in data.settings));
+ Assert.ok(!("defaultPrivateSearchEngineData" in data.settings));
+
+ // Load the engines definitions from a xpcshell data: that's needed so that
+ // the search provider reports an engine identifier.
+
+ // Initialize the search service.
+ if (reInitSearchService) {
+ Services.search.wrappedJSObject.reset();
+ }
+ await Services.search.init();
+ await promiseNextTick();
+
+ // Our default engine from the JAR file has an identifier. Check if it is correctly
+ // reported.
+ data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+ Assert.equal(data.settings.defaultSearchEngine, "telemetrySearchIdentifier");
+ let expectedSearchEngineData = {
+ name: "telemetrySearchIdentifier",
+ loadPath: "[addon]telemetrySearchIdentifier@search.mozilla.org",
+ origin: "default",
+ submissionURL:
+ "https://ar.wikipedia.org/wiki/%D8%AE%D8%A7%D8%B5:%D8%A8%D8%AD%D8%AB?search=&sourceId=Mozilla-search",
+ };
+ Assert.deepEqual(
+ data.settings.defaultSearchEngineData,
+ expectedSearchEngineData
+ );
+ if (privateOn) {
+ Assert.equal(
+ data.settings.defaultPrivateSearchEngine,
+ "telemetrySearchIdentifier"
+ );
+ Assert.deepEqual(
+ data.settings.defaultPrivateSearchEngineData,
+ expectedSearchEngineData,
+ "Should have the correct data for the private search engine"
+ );
+ } else {
+ Assert.ok(
+ !("defaultPrivateSearchEngine" in data.settings),
+ "Should not have private name recorded as the pref for separate is off"
+ );
+ Assert.ok(
+ !("defaultPrivateSearchEngineData" in data.settings),
+ "Should not have private data recorded as the pref for separate is off"
+ );
+ }
+
+ // Add a new search engine (this will have no engine identifier).
+ const SEARCH_ENGINE_ID = privateOn
+ ? "telemetry_private"
+ : "telemetry_default";
+ const SEARCH_ENGINE_URL = `https://www.example.org/${
+ privateOn ? "private" : ""
+ }`;
+ await SearchTestUtils.installSearchExtension({
+ id: `${SEARCH_ENGINE_ID}@test.engine`,
+ name: SEARCH_ENGINE_ID,
+ search_url: SEARCH_ENGINE_URL,
+ });
+
+ // Register a new change listener and then wait for the search engine change to be notified.
+ let deferred = Promise.withResolvers();
+ TelemetryEnvironment.registerChangeListener(
+ "testWatch_SearchDefault",
+ deferred.resolve
+ );
+ if (privateOn) {
+ // As we had no default and no search engines, the normal mode engine will
+ // assume the same as the added engine. To ensure the telemetry is different
+ // we enforce a different default here.
+ const engine = await Services.search.getEngineByName(
+ "telemetrySearchIdentifier"
+ );
+ engine.hidden = false;
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await Services.search.setDefaultPrivate(
+ Services.search.getEngineByName(SEARCH_ENGINE_ID),
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ } else {
+ await Services.search.setDefault(
+ Services.search.getEngineByName(SEARCH_ENGINE_ID),
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ }
+ await deferred.promise;
+
+ data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+
+ const EXPECTED_SEARCH_ENGINE = "other-" + SEARCH_ENGINE_ID;
+ const EXPECTED_SEARCH_ENGINE_DATA = {
+ name: SEARCH_ENGINE_ID,
+ loadPath: `[addon]${SEARCH_ENGINE_ID}@test.engine`,
+ origin: "verified",
+ };
+ if (privateOn) {
+ Assert.equal(
+ data.settings.defaultSearchEngine,
+ "telemetrySearchIdentifier"
+ );
+ Assert.deepEqual(
+ data.settings.defaultSearchEngineData,
+ expectedSearchEngineData
+ );
+ Assert.equal(
+ data.settings.defaultPrivateSearchEngine,
+ EXPECTED_SEARCH_ENGINE
+ );
+ Assert.deepEqual(
+ data.settings.defaultPrivateSearchEngineData,
+ EXPECTED_SEARCH_ENGINE_DATA
+ );
+ } else {
+ Assert.equal(data.settings.defaultSearchEngine, EXPECTED_SEARCH_ENGINE);
+ Assert.deepEqual(
+ data.settings.defaultSearchEngineData,
+ EXPECTED_SEARCH_ENGINE_DATA
+ );
+ }
+ TelemetryEnvironment.unregisterChangeListener("testWatch_SearchDefault");
+}
+
+add_task(async function test_defaultSearchEngine() {
+ await checkDefaultSearch(false);
+
+ // Cleanly install an engine from an xml file, and check if origin is
+ // recorded as "verified".
+ let promise = new Promise(resolve => {
+ TelemetryEnvironment.registerChangeListener(
+ "testWatch_SearchDefault",
+ resolve
+ );
+ });
+ let engine = await SearchTestUtils.promiseNewSearchEngine({
+ url: gDataRoot + "engine.xml",
+ setAsDefault: true,
+ skipReset: true,
+ });
+ await promise;
+ TelemetryEnvironment.unregisterChangeListener("testWatch_SearchDefault");
+ let data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+ Assert.deepEqual(data.settings.defaultSearchEngineData, {
+ name: "engine-telemetry",
+ loadPath: "[http]localhost/engine-telemetry.xml",
+ origin: "verified",
+ });
+
+ // Now break this engine's load path hash.
+ promise = new Promise(resolve => {
+ TelemetryEnvironment.registerChangeListener(
+ "testWatch_SearchDefault",
+ resolve
+ );
+ });
+ engine.wrappedJSObject.setAttr("loadPathHash", "broken");
+ Services.obs.notifyObservers(
+ null,
+ "browser-search-engine-modified",
+ "engine-default"
+ );
+ await promise;
+ TelemetryEnvironment.unregisterChangeListener("testWatch_SearchDefault");
+ data = TelemetryEnvironment.currentEnvironment;
+ Assert.equal(data.settings.defaultSearchEngineData.origin, "invalid");
+
+ const SEARCH_ENGINE_ID = "telemetry_default";
+ const EXPECTED_SEARCH_ENGINE = "other-" + SEARCH_ENGINE_ID;
+ // Work around bug 1165341: Intentionally set the default engine.
+ await Services.search.setDefault(
+ Services.search.getEngineByName(SEARCH_ENGINE_ID),
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+
+ // Double-check the default for the next part of the test.
+ data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+ Assert.equal(data.settings.defaultSearchEngine, EXPECTED_SEARCH_ENGINE);
+
+ // Define and reset the test preference.
+ const PREF_TEST = "toolkit.telemetry.test.pref1";
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_STATE }],
+ ]);
+ Services.prefs.clearUserPref(PREF_TEST);
+
+ // Watch the test preference.
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+ let deferred = Promise.withResolvers();
+ TelemetryEnvironment.registerChangeListener(
+ "testSearchEngine_pref",
+ deferred.resolve
+ );
+ // Trigger an environment change.
+ Services.prefs.setIntPref(PREF_TEST, 1);
+ await deferred.promise;
+ TelemetryEnvironment.unregisterChangeListener("testSearchEngine_pref");
+
+ // Check that the search engine information is correctly retained when prefs change.
+ data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+ Assert.equal(data.settings.defaultSearchEngine, EXPECTED_SEARCH_ENGINE);
+});
+
+add_task(async function test_defaultPrivateSearchEngine() {
+ await checkDefaultSearch(true, true);
+});
+
+add_task(async function test_defaultSearchEngine_paramsChanged() {
+ let extension = await SearchTestUtils.installSearchExtension(
+ {
+ name: "TestEngine",
+ search_url: "https://www.google.com/fake1",
+ },
+ { skipUnload: true }
+ );
+
+ let promise = new Promise(resolve => {
+ TelemetryEnvironment.registerChangeListener(
+ "testWatch_SearchDefault",
+ resolve
+ );
+ });
+ let engine = Services.search.getEngineByName("TestEngine");
+ await Services.search.setDefault(
+ engine,
+ Ci.nsISearchService.CHANGE_REASON_UNKNOWN
+ );
+ await promise;
+
+ let data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+ Assert.deepEqual(data.settings.defaultSearchEngineData, {
+ name: "TestEngine",
+ loadPath: "[addon]testengine@tests.mozilla.org",
+ origin: "verified",
+ submissionURL: "https://www.google.com/fake1?q=",
+ });
+
+ promise = new Promise(resolve => {
+ TelemetryEnvironment.registerChangeListener(
+ "testWatch_SearchDefault",
+ resolve
+ );
+ });
+
+ let manifest = SearchTestUtils.createEngineManifest({
+ name: "TestEngine",
+ version: "1.2",
+ search_url: "https://www.google.com/fake2",
+ });
+ await extension.upgrade({
+ useAddonManager: "permanent",
+ manifest,
+ });
+ await AddonTestUtils.waitForSearchProviderStartup(extension);
+ await promise;
+
+ data = TelemetryEnvironment.currentEnvironment;
+ TelemetryEnvironmentTesting.checkEnvironmentData(data);
+ Assert.deepEqual(data.settings.defaultSearchEngineData, {
+ name: "TestEngine",
+ loadPath: "[addon]testengine@tests.mozilla.org",
+ origin: "verified",
+ submissionURL: "https://www.google.com/fake2?q=",
+ });
+
+ await extension.unload();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryEvents.js b/toolkit/components/telemetry/tests/unit/test_TelemetryEvents.js
new file mode 100644
index 0000000000..4369c5a608
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEvents.js
@@ -0,0 +1,1109 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+ChromeUtils.defineESModuleGetters(this, {
+ TestUtils: "resource://testing-common/TestUtils.sys.mjs",
+});
+const { TelemetryTestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TelemetryTestUtils.sys.mjs"
+);
+
+const PRERELEASE_CHANNELS = Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS;
+const ALL_CHANNELS = Ci.nsITelemetry.DATASET_ALL_CHANNELS;
+
+function checkEventFormat(events) {
+ Assert.ok(Array.isArray(events), "Events should be serialized to an array.");
+ for (let e of events) {
+ Assert.ok(Array.isArray(e), "Event should be an array.");
+ Assert.greaterOrEqual(
+ e.length,
+ 4,
+ "Event should have at least 4 elements."
+ );
+ Assert.lessOrEqual(e.length, 6, "Event should have at most 6 elements.");
+
+ Assert.equal(typeof e[0], "number", "Element 0 should be a number.");
+ Assert.equal(typeof e[1], "string", "Element 1 should be a string.");
+ Assert.equal(typeof e[2], "string", "Element 2 should be a string.");
+ Assert.equal(typeof e[3], "string", "Element 3 should be a string.");
+
+ if (e.length > 4) {
+ Assert.ok(
+ e[4] === null || typeof e[4] == "string",
+ "Event element 4 should be null or a string."
+ );
+ }
+ if (e.length > 5) {
+ Assert.ok(
+ e[5] === null || typeof e[5] == "object",
+ "Event element 5 should be null or an object."
+ );
+ }
+
+ let extra = e[5];
+ if (extra) {
+ Assert.ok(
+ Object.keys(extra).every(k => typeof k == "string"),
+ "All extra keys should be strings."
+ );
+ Assert.ok(
+ Object.values(extra).every(v => typeof v == "string"),
+ "All extra values should be strings."
+ );
+ }
+ }
+}
+
+/**
+ * @param summaries is of the form
+ * [{process, [event category, event object, event method], count}]
+ * @param clearScalars - true if you want to clear the scalars
+ */
+function checkEventSummary(summaries, clearScalars) {
+ let scalars = Telemetry.getSnapshotForKeyedScalars("main", clearScalars);
+
+ for (let [process, [category, eObject, method], count] of summaries) {
+ let uniqueEventName = `${category}#${eObject}#${method}`;
+ let summaryCount;
+ if (process === "dynamic") {
+ summaryCount =
+ scalars.dynamic["telemetry.dynamic_event_counts"][uniqueEventName];
+ } else {
+ summaryCount =
+ scalars[process]["telemetry.event_counts"][uniqueEventName];
+ }
+ Assert.equal(
+ summaryCount,
+ count,
+ `${uniqueEventName} had wrong summary count`
+ );
+ }
+}
+
+function checkRegistrationFailure(failureType) {
+ let snapshot = Telemetry.getSnapshotForHistograms("main", true);
+ Assert.ok(
+ "parent" in snapshot,
+ "There should be at least one parent histogram when checking for registration failures."
+ );
+ Assert.ok(
+ "TELEMETRY_EVENT_REGISTRATION_ERROR" in snapshot.parent,
+ "TELEMETRY_EVENT_REGISTRATION_ERROR should exist when checking for registration failures."
+ );
+ let values = snapshot.parent.TELEMETRY_EVENT_REGISTRATION_ERROR.values;
+ Assert.ok(
+ !!values,
+ "TELEMETRY_EVENT_REGISTRATION_ERROR's values should exist when checking for registration failures."
+ );
+ Assert.equal(
+ values[failureType],
+ 1,
+ `Event registration ought to have failed due to type ${failureType}`
+ );
+}
+
+function checkRecordingFailure(failureType) {
+ let snapshot = Telemetry.getSnapshotForHistograms("main", true);
+ Assert.ok(
+ "parent" in snapshot,
+ "There should be at least one parent histogram when checking for recording failures."
+ );
+ Assert.ok(
+ "TELEMETRY_EVENT_RECORDING_ERROR" in snapshot.parent,
+ "TELEMETRY_EVENT_RECORDING_ERROR should exist when checking for recording failures."
+ );
+ let values = snapshot.parent.TELEMETRY_EVENT_RECORDING_ERROR.values;
+ Assert.ok(
+ !!values,
+ "TELEMETRY_EVENT_RECORDING_ERROR's values should exist when checking for recording failures."
+ );
+ Assert.equal(
+ values[failureType],
+ 1,
+ `Event recording ought to have failed due to type ${failureType}`
+ );
+}
+
+add_task(async function test_event_summary_limit() {
+ Telemetry.clearEvents();
+ Telemetry.clearScalars();
+
+ const limit = 500; // matches kMaxEventSummaryKeys in TelemetryScalar.cpp.
+ let objects = [];
+ for (let i = 0; i < limit + 1; i++) {
+ objects.push("object" + i);
+ }
+ // Using "telemetry.test.dynamic" as using "telemetry.test" will enable
+ // the "telemetry.test" category.
+ Telemetry.registerEvents("telemetry.test.dynamic", {
+ test_method: {
+ methods: ["testMethod"],
+ objects,
+ record_on_release: true,
+ },
+ });
+ for (let object of objects) {
+ Telemetry.recordEvent("telemetry.test.dynamic", "testMethod", object);
+ }
+
+ TelemetryTestUtils.assertNumberOfEvents(
+ limit + 1,
+ {},
+ { process: "dynamic" }
+ );
+ let scalarSnapshot = Telemetry.getSnapshotForKeyedScalars("main", true);
+ Assert.equal(
+ Object.keys(scalarSnapshot.dynamic["telemetry.dynamic_event_counts"])
+ .length,
+ limit,
+ "Should not have recorded more than `limit` events"
+ );
+});
+
+add_task(async function test_recording_state() {
+ Telemetry.clearEvents();
+ Telemetry.clearScalars();
+
+ const events = [
+ ["telemetry.test", "test1", "object1"],
+ ["telemetry.test.second", "test", "object1"],
+ ];
+
+ // Both test categories should be off by default.
+ events.forEach(e => Telemetry.recordEvent(...e));
+ TelemetryTestUtils.assertEvents([]);
+ checkEventSummary(
+ events.map(e => ["parent", e, 1]),
+ true
+ );
+
+ // Enable one test category and see that we record correctly.
+ Telemetry.setEventRecordingEnabled("telemetry.test", true);
+ events.forEach(e => Telemetry.recordEvent(...e));
+ TelemetryTestUtils.assertEvents([events[0]]);
+ checkEventSummary(
+ events.map(e => ["parent", e, 1]),
+ true
+ );
+
+ // Also enable the other test category and see that we record correctly.
+ Telemetry.setEventRecordingEnabled("telemetry.test.second", true);
+ events.forEach(e => Telemetry.recordEvent(...e));
+ TelemetryTestUtils.assertEvents(events);
+ checkEventSummary(
+ events.map(e => ["parent", e, 1]),
+ true
+ );
+
+ // Now turn of one category again and check that this works as expected.
+ Telemetry.setEventRecordingEnabled("telemetry.test", false);
+ events.forEach(e => Telemetry.recordEvent(...e));
+ TelemetryTestUtils.assertEvents([events[1]]);
+ checkEventSummary(
+ events.map(e => ["parent", e, 1]),
+ true
+ );
+});
+
+add_task(async function recording_setup() {
+ // Make sure both test categories are enabled for the remaining tests.
+ // Otherwise their event recording won't work.
+ Telemetry.setEventRecordingEnabled("telemetry.test", true);
+ Telemetry.setEventRecordingEnabled("telemetry.test.second", true);
+});
+
+add_task(async function test_recording() {
+ Telemetry.clearScalars();
+ Telemetry.clearEvents();
+
+ // Record some events.
+ let expected = [
+ { optout: false, event: ["telemetry.test", "test1", "object1"] },
+ { optout: false, event: ["telemetry.test", "test2", "object2"] },
+
+ { optout: false, event: ["telemetry.test", "test1", "object1", "value"] },
+ {
+ optout: false,
+ event: ["telemetry.test", "test1", "object1", "value", null],
+ },
+ {
+ optout: false,
+ event: ["telemetry.test", "test1", "object1", null, { key1: "value1" }],
+ },
+ {
+ optout: false,
+ event: [
+ "telemetry.test",
+ "test1",
+ "object1",
+ "value",
+ { key1: "value1", key2: "value2" },
+ ],
+ },
+
+ { optout: true, event: ["telemetry.test", "optout", "object1"] },
+ { optout: false, event: ["telemetry.test.second", "test", "object1"] },
+ {
+ optout: false,
+ event: [
+ "telemetry.test.second",
+ "test",
+ "object1",
+ null,
+ { key1: "value1" },
+ ],
+ },
+ ];
+
+ for (let entry of expected) {
+ entry.tsBefore = Math.floor(Telemetry.msSinceProcessStart());
+ try {
+ Telemetry.recordEvent(...entry.event);
+ } catch (ex) {
+ Assert.ok(
+ false,
+ `Failed to record event ${JSON.stringify(entry.event)}: ${ex}`
+ );
+ }
+ entry.tsAfter = Math.floor(Telemetry.msSinceProcessStart());
+ }
+
+ // Strip off trailing null values to match the serialized events.
+ for (let entry of expected) {
+ let e = entry.event;
+ while (e.length >= 3 && e[e.length - 1] === null) {
+ e.pop();
+ }
+ }
+
+ // Check that the events were summarized properly.
+ let summaries = {};
+ expected.forEach(({ optout, event }) => {
+ let [category, eObject, method] = event;
+ let uniqueEventName = `${category}#${eObject}#${method}`;
+ if (!(uniqueEventName in summaries)) {
+ summaries[uniqueEventName] = ["parent", event, 1];
+ } else {
+ summaries[uniqueEventName][2]++;
+ }
+ });
+ checkEventSummary(Object.values(summaries), true);
+
+ // The following should not result in any recorded events.
+ Telemetry.recordEvent("unknown.category", "test1", "object1");
+ checkRecordingFailure(0 /* UnknownEvent */);
+ Telemetry.recordEvent("telemetry.test", "unknown", "object1");
+ checkRecordingFailure(0 /* UnknownEvent */);
+ Telemetry.recordEvent("telemetry.test", "test1", "unknown");
+ checkRecordingFailure(0 /* UnknownEvent */);
+
+ let checkEvents = (events, expectedEvents) => {
+ checkEventFormat(events);
+ Assert.equal(
+ events.length,
+ expectedEvents.length,
+ "Snapshot should have the right number of events."
+ );
+
+ for (let i = 0; i < events.length; ++i) {
+ let { tsBefore, tsAfter } = expectedEvents[i];
+ let ts = events[i][0];
+ Assert.greaterOrEqual(
+ ts,
+ tsBefore,
+ "The recorded timestamp should be greater than the one before recording."
+ );
+ Assert.lessOrEqual(
+ ts,
+ tsAfter,
+ "The recorded timestamp should be less than the one after recording."
+ );
+
+ let recordedData = events[i].slice(1);
+ let expectedData = expectedEvents[i].event.slice();
+ Assert.deepEqual(
+ recordedData,
+ expectedData,
+ "The recorded event data should match."
+ );
+ }
+ };
+
+ // Check that the expected events were recorded.
+ let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, false);
+ Assert.ok("parent" in snapshot, "Should have entry for main process.");
+ checkEvents(snapshot.parent, expected);
+
+ // Check serializing only opt-out events.
+ snapshot = Telemetry.snapshotEvents(ALL_CHANNELS, false);
+ Assert.ok("parent" in snapshot, "Should have entry for main process.");
+ let filtered = expected.filter(e => !!e.optout);
+ checkEvents(snapshot.parent, filtered);
+});
+
+add_task(async function test_clear() {
+ Telemetry.clearEvents();
+
+ const COUNT = 10;
+ for (let i = 0; i < COUNT; ++i) {
+ Telemetry.recordEvent("telemetry.test", "test1", "object1");
+ Telemetry.recordEvent("telemetry.test.second", "test", "object1");
+ }
+
+ // Check that events were recorded.
+ // The events are cleared by passing the respective flag.
+ let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.ok("parent" in snapshot, "Should have entry for main process.");
+ Assert.equal(
+ snapshot.parent.length,
+ 2 * COUNT,
+ `Should have recorded ${2 * COUNT} events.`
+ );
+
+ // Now the events should be cleared.
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, false);
+ Assert.equal(
+ Object.keys(snapshot).length,
+ 0,
+ `Should have cleared the events.`
+ );
+
+ for (let i = 0; i < COUNT; ++i) {
+ Telemetry.recordEvent("telemetry.test", "test1", "object1");
+ Telemetry.recordEvent("telemetry.test.second", "test", "object1");
+ }
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true, 5);
+ Assert.ok("parent" in snapshot, "Should have entry for main process.");
+ Assert.equal(snapshot.parent.length, 5, "Should have returned 5 events");
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, false);
+ Assert.ok("parent" in snapshot, "Should have entry for main process.");
+ Assert.equal(
+ snapshot.parent.length,
+ 2 * COUNT - 5,
+ `Should have returned ${2 * COUNT - 5} events`
+ );
+
+ Telemetry.recordEvent("telemetry.test", "test1", "object1");
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, false, 5);
+ Assert.ok("parent" in snapshot, "Should have entry for main process.");
+ Assert.equal(snapshot.parent.length, 5, "Should have returned 5 events");
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.ok("parent" in snapshot, "Should have entry for main process.");
+ Assert.equal(
+ snapshot.parent.length,
+ 2 * COUNT - 5 + 1,
+ `Should have returned ${2 * COUNT - 5 + 1} events`
+ );
+});
+
+add_task(async function test_expiry() {
+ Telemetry.clearEvents();
+
+ // Recording call with event that is expired by version.
+ Telemetry.recordEvent("telemetry.test", "expired_version", "object1");
+ checkRecordingFailure(1 /* Expired */);
+ let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.equal(
+ Object.keys(snapshot).length,
+ 0,
+ "Should not record event with expired version."
+ );
+
+ // Recording call with event that has expiry_version set into the future.
+ Telemetry.recordEvent("telemetry.test", "not_expired_optout", "object1");
+ TelemetryTestUtils.assertNumberOfEvents(1);
+});
+
+add_task(async function test_invalidParams() {
+ Telemetry.clearEvents();
+
+ // Recording call with wrong type for value argument.
+ Telemetry.recordEvent("telemetry.test", "test1", "object1", 1);
+ let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.equal(
+ Object.keys(snapshot).length,
+ 0,
+ "Should not record event when value argument with invalid type is passed."
+ );
+ checkRecordingFailure(3 /* Value */);
+
+ // Recording call with wrong type for extra argument.
+ Telemetry.recordEvent("telemetry.test", "test1", "object1", null, "invalid");
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.equal(
+ Object.keys(snapshot).length,
+ 0,
+ "Should not record event when extra argument with invalid type is passed."
+ );
+ checkRecordingFailure(4 /* Extra */);
+
+ // Recording call with unknown extra key.
+ Telemetry.recordEvent("telemetry.test", "test1", "object1", null, {
+ key3: "x",
+ });
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.equal(
+ Object.keys(snapshot).length,
+ 0,
+ "Should not record event when extra argument with invalid key is passed."
+ );
+ checkRecordingFailure(2 /* ExtraKey */);
+
+ // Recording call with invalid value type.
+ Telemetry.recordEvent("telemetry.test", "test1", "object1", null, {
+ key3: 1,
+ });
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.equal(
+ Object.keys(snapshot).length,
+ 0,
+ "Should not record event when extra argument with invalid value type is passed."
+ );
+ checkRecordingFailure(4 /* Extra */);
+});
+
+add_task(async function test_storageLimit() {
+ Telemetry.clearEvents();
+
+ let limitReached = TestUtils.topicObserved(
+ "event-telemetry-storage-limit-reached"
+ );
+ // Record more events than the storage limit allows.
+ let LIMIT = 1000;
+ let COUNT = LIMIT + 10;
+ for (let i = 0; i < COUNT; ++i) {
+ Telemetry.recordEvent("telemetry.test", "test1", "object1", String(i));
+ }
+
+ await limitReached;
+ Assert.ok(true, "Topic was notified when event limit was reached");
+
+ // Check that the right events were recorded.
+ let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.ok("parent" in snapshot, "Should have entry for main process.");
+ let events = snapshot.parent;
+ Assert.equal(
+ events.length,
+ COUNT,
+ `Should have only recorded all ${COUNT} events`
+ );
+ Assert.ok(
+ events.every((e, idx) => e[4] === String(idx)),
+ "Should have recorded all events."
+ );
+});
+
+add_task(async function test_valueLimits() {
+ Telemetry.clearEvents();
+
+ // Record values that are at or over the limits for string lengths.
+ let LIMIT = 80;
+ let expected = [
+ ["telemetry.test", "test1", "object1", "a".repeat(LIMIT - 10), null],
+ ["telemetry.test", "test1", "object1", "a".repeat(LIMIT), null],
+ ["telemetry.test", "test1", "object1", "a".repeat(LIMIT + 1), null],
+ ["telemetry.test", "test1", "object1", "a".repeat(LIMIT + 10), null],
+
+ [
+ "telemetry.test",
+ "test1",
+ "object1",
+ null,
+ { key1: "a".repeat(LIMIT - 10) },
+ ],
+ ["telemetry.test", "test1", "object1", null, { key1: "a".repeat(LIMIT) }],
+ [
+ "telemetry.test",
+ "test1",
+ "object1",
+ null,
+ { key1: "a".repeat(LIMIT + 1) },
+ ],
+ [
+ "telemetry.test",
+ "test1",
+ "object1",
+ null,
+ { key1: "a".repeat(LIMIT + 10) },
+ ],
+ ];
+
+ for (let event of expected) {
+ Telemetry.recordEvent(...event);
+ if (event[3]) {
+ event[3] = event[3].substr(0, LIMIT);
+ } else {
+ event[3] = undefined;
+ }
+ if (event[4]) {
+ event[4].key1 = event[4].key1.substr(0, LIMIT);
+ }
+ }
+
+ // Strip off trailing null values to match the serialized events.
+ for (let e of expected) {
+ while (e.length >= 3 && e[e.length - 1] === null) {
+ e.pop();
+ }
+ }
+
+ // Check that the right events were recorded.
+ TelemetryTestUtils.assertEvents(expected);
+});
+
+add_task(async function test_unicodeValues() {
+ Telemetry.clearEvents();
+
+ // Record string values containing unicode characters.
+ let value = "漢語";
+ Telemetry.recordEvent("telemetry.test", "test1", "object1", value);
+ Telemetry.recordEvent("telemetry.test", "test1", "object1", null, {
+ key1: value,
+ });
+
+ // Check that the values were correctly recorded.
+ TelemetryTestUtils.assertEvents([{ value }, { extra: { key1: value } }]);
+});
+
+add_task(async function test_dynamicEvents() {
+ Telemetry.clearEvents();
+ Telemetry.clearScalars();
+ Telemetry.canRecordExtended = true;
+
+ // Register some test events.
+ Telemetry.registerEvents("telemetry.test.dynamic", {
+ // Event with only required fields.
+ test1: {
+ methods: ["test1"],
+ objects: ["object1"],
+ },
+ // Event with extra_keys.
+ test2: {
+ methods: ["test2", "test2b"],
+ objects: ["object1"],
+ extra_keys: ["key1", "key2"],
+ },
+ // Expired event.
+ test3: {
+ methods: ["test3"],
+ objects: ["object1"],
+ expired: true,
+ },
+ // A release-channel recording event.
+ test4: {
+ methods: ["test4"],
+ objects: ["object1"],
+ record_on_release: true,
+ },
+ });
+
+ // Record some valid events.
+ Telemetry.recordEvent("telemetry.test.dynamic", "test1", "object1");
+ Telemetry.recordEvent("telemetry.test.dynamic", "test2", "object1", null, {
+ key1: "foo",
+ key2: "bar",
+ });
+ Telemetry.recordEvent("telemetry.test.dynamic", "test2b", "object1", null, {
+ key1: "foo",
+ key2: "bar",
+ });
+ Telemetry.recordEvent(
+ "telemetry.test.dynamic",
+ "test3",
+ "object1",
+ "some value"
+ );
+ Telemetry.recordEvent("telemetry.test.dynamic", "test4", "object1", null);
+
+ // Test recording an unknown event.
+ Telemetry.recordEvent("telemetry.test.dynamic", "unknown", "unknown");
+ checkRecordingFailure(0 /* UnknownEvent */);
+
+ // Now check that the snapshot contains the expected data.
+ let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, false);
+ Assert.ok(
+ "dynamic" in snapshot,
+ "Should have dynamic events in the snapshot."
+ );
+
+ let expected = [
+ ["telemetry.test.dynamic", "test1", "object1"],
+ [
+ "telemetry.test.dynamic",
+ "test2",
+ "object1",
+ null,
+ { key1: "foo", key2: "bar" },
+ ],
+ [
+ "telemetry.test.dynamic",
+ "test2b",
+ "object1",
+ null,
+ { key1: "foo", key2: "bar" },
+ ],
+ // "test3" is epxired, so it should not be recorded.
+ ["telemetry.test.dynamic", "test4", "object1"],
+ ];
+ let events = snapshot.dynamic;
+ Assert.equal(
+ events.length,
+ expected.length,
+ "Should have recorded the right amount of events."
+ );
+ for (let i = 0; i < expected.length; ++i) {
+ Assert.deepEqual(
+ events[i].slice(1),
+ expected[i],
+ "Should have recorded the expected event data."
+ );
+ }
+
+ // Check that we've summarized the recorded events
+ checkEventSummary(
+ expected.map(ev => ["dynamic", ev, 1]),
+ true
+ );
+
+ // Check that the opt-out snapshot contains only the one expected event.
+ snapshot = Telemetry.snapshotEvents(ALL_CHANNELS, false);
+ Assert.ok(
+ "dynamic" in snapshot,
+ "Should have dynamic events in the snapshot."
+ );
+ Assert.equal(
+ snapshot.dynamic.length,
+ 1,
+ "Should have one opt-out event in the snapshot."
+ );
+ expected = ["telemetry.test.dynamic", "test4", "object1"];
+ Assert.deepEqual(snapshot.dynamic[0].slice(1), expected);
+
+ // Recording with unknown extra keys should be ignored and print an error.
+ Telemetry.clearEvents();
+ Telemetry.recordEvent("telemetry.test.dynamic", "test1", "object1", null, {
+ key1: "foo",
+ });
+ Telemetry.recordEvent("telemetry.test.dynamic", "test2", "object1", null, {
+ key1: "foo",
+ unknown: "bar",
+ });
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.ok(
+ !("dynamic" in snapshot),
+ "Should have not recorded dynamic events with unknown extra keys."
+ );
+
+ // Other built-in events should not show up in the "dynamic" bucket of the snapshot.
+ Telemetry.recordEvent("telemetry.test", "test1", "object1");
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.ok(
+ !("dynamic" in snapshot),
+ "Should have not recorded built-in event into dynamic bucket."
+ );
+
+ // Test that recording opt-in and opt-out events works as expected.
+ Telemetry.clearEvents();
+ Telemetry.canRecordExtended = false;
+
+ Telemetry.recordEvent("telemetry.test.dynamic", "test1", "object1");
+ Telemetry.recordEvent("telemetry.test.dynamic", "test4", "object1");
+
+ expected = [
+ // Only "test4" should have been recorded.
+ ["telemetry.test.dynamic", "test4", "object1"],
+ ];
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.equal(
+ snapshot.dynamic.length,
+ 1,
+ "Should have one opt-out event in the snapshot."
+ );
+ Assert.deepEqual(
+ snapshot.dynamic.map(e => e.slice(1)),
+ expected
+ );
+});
+
+add_task(async function test_dynamicEventRegistrationValidation() {
+ Telemetry.canRecordExtended = true;
+ Telemetry.clearEvents();
+
+ // Test registration of invalid categories.
+ Telemetry.getSnapshotForHistograms("main", true); // Clear histograms before we begin.
+ Assert.throws(
+ () =>
+ Telemetry.registerEvents("telemetry+test+dynamic", {
+ test1: {
+ methods: ["test1"],
+ objects: ["object1"],
+ },
+ }),
+ /Category parameter should match the identifier pattern\./,
+ "Should throw when registering category names with invalid characters."
+ );
+ checkRegistrationFailure(2 /* Category */);
+ Assert.throws(
+ () =>
+ Telemetry.registerEvents(
+ "telemetry.test.test.test.test.test.test.test.test",
+ {
+ test1: {
+ methods: ["test1"],
+ objects: ["object1"],
+ },
+ }
+ ),
+ /Category parameter should match the identifier pattern\./,
+ "Should throw when registering overly long category names."
+ );
+ checkRegistrationFailure(2 /* Category */);
+
+ // Test registration of invalid event names.
+ Assert.throws(
+ () =>
+ Telemetry.registerEvents("telemetry.test.dynamic1", {
+ "test?1": {
+ methods: ["test1"],
+ objects: ["object1"],
+ },
+ }),
+ /Event names should match the identifier pattern\./,
+ "Should throw when registering event names with invalid characters."
+ );
+ checkRegistrationFailure(1 /* Name */);
+ Assert.throws(
+ () =>
+ Telemetry.registerEvents("telemetry.test.dynamic2", {
+ test1test1test1test1test1test1test1: {
+ methods: ["test1"],
+ objects: ["object1"],
+ },
+ }),
+ /Event names should match the identifier pattern\./,
+ "Should throw when registering overly long event names."
+ );
+ checkRegistrationFailure(1 /* Name */);
+
+ // Test registration of invalid method names.
+ Assert.throws(
+ () =>
+ Telemetry.registerEvents("telemetry.test.dynamic3", {
+ test1: {
+ methods: ["test?1"],
+ objects: ["object1"],
+ },
+ }),
+ /Method names should match the identifier pattern\./,
+ "Should throw when registering method names with invalid characters."
+ );
+ checkRegistrationFailure(3 /* Method */);
+ Assert.throws(
+ () =>
+ Telemetry.registerEvents("telemetry.test.dynamic", {
+ test1: {
+ methods: ["test1test1test1test1test1test1test1"],
+ objects: ["object1"],
+ },
+ }),
+ /Method names should match the identifier pattern\./,
+ "Should throw when registering overly long method names."
+ );
+ checkRegistrationFailure(3 /* Method */);
+
+ // Test registration of invalid object names.
+ Assert.throws(
+ () =>
+ Telemetry.registerEvents("telemetry.test.dynamic4", {
+ test1: {
+ methods: ["test1"],
+ objects: ["object?1"],
+ },
+ }),
+ /Object names should match the identifier pattern\./,
+ "Should throw when registering object names with invalid characters."
+ );
+ checkRegistrationFailure(4 /* Object */);
+ Assert.throws(
+ () =>
+ Telemetry.registerEvents("telemetry.test.dynamic5", {
+ test1: {
+ methods: ["test1"],
+ objects: ["object1object1object1object1object1object1"],
+ },
+ }),
+ /Object names should match the identifier pattern\./,
+ "Should throw when registering overly long object names."
+ );
+ checkRegistrationFailure(4 /* Object */);
+
+ // Test validation of invalid key names.
+ Assert.throws(
+ () =>
+ Telemetry.registerEvents("telemetry.test.dynamic6", {
+ test1: {
+ methods: ["test1"],
+ objects: ["object1"],
+ extra_keys: ["a?1"],
+ },
+ }),
+ /Extra key names should match the identifier pattern\./,
+ "Should throw when registering extra key names with invalid characters."
+ );
+ checkRegistrationFailure(5 /* ExtraKeys */);
+
+ // Test validation of key names that are too long - we allow a maximum of 15 characters.
+ Assert.throws(
+ () =>
+ Telemetry.registerEvents("telemetry.test.dynamic7", {
+ test1: {
+ methods: ["test1"],
+ objects: ["object1"],
+ extra_keys: ["a012345678901234"],
+ },
+ }),
+ /Extra key names should match the identifier pattern\./,
+ "Should throw when registering extra key names which are too long."
+ );
+ checkRegistrationFailure(5 /* ExtraKeys */);
+ Telemetry.registerEvents("telemetry.test.dynamic8", {
+ test1: {
+ methods: ["test1"],
+ objects: ["object1"],
+ extra_keys: ["a01234567890123"],
+ },
+ });
+
+ // Test validation of extra key count - we only allow 10.
+ Assert.throws(
+ () =>
+ Telemetry.registerEvents("telemetry.test.dynamic9", {
+ test1: {
+ methods: ["test1"],
+ objects: ["object1"],
+ extra_keys: [
+ "a1",
+ "a2",
+ "a3",
+ "a4",
+ "a5",
+ "a6",
+ "a7",
+ "a8",
+ "a9",
+ "a10",
+ "a11",
+ ],
+ },
+ }),
+ /No more than 10 extra keys can be registered\./,
+ "Should throw when registering too many extra keys."
+ );
+ checkRegistrationFailure(5 /* ExtraKeys */);
+ Telemetry.registerEvents("telemetry.test.dynamic10", {
+ test1: {
+ methods: ["test1"],
+ objects: ["object1"],
+ extra_keys: ["a1", "a2", "a3", "a4", "a5", "a6", "a7", "a8", "a9", "a10"],
+ },
+ });
+});
+
+// When add-ons update, they may re-register some of the dynamic events.
+// Test through some possible scenarios.
+add_task(async function test_dynamicEventRegisterAgain() {
+ Telemetry.canRecordExtended = true;
+ Telemetry.clearEvents();
+
+ const category = "telemetry.test.register.again";
+ let events = {
+ test1: {
+ methods: ["test1"],
+ objects: ["object1"],
+ },
+ };
+
+ // First register the initial event and make sure it can be recorded.
+ Telemetry.registerEvents(category, events);
+ let expected = [[category, "test1", "object1"]];
+ expected.forEach(e => Telemetry.recordEvent(...e));
+
+ let snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.equal(
+ snapshot.dynamic.length,
+ expected.length,
+ "Should have right number of events in the snapshot."
+ );
+ Assert.deepEqual(
+ snapshot.dynamic.map(e => e.slice(1)),
+ expected
+ );
+
+ // Register the same event again and make sure it can still be recorded.
+ Telemetry.registerEvents(category, events);
+ Telemetry.recordEvent(category, "test1", "object1");
+
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.equal(
+ snapshot.dynamic.length,
+ expected.length,
+ "Should have right number of events in the snapshot."
+ );
+ Assert.deepEqual(
+ snapshot.dynamic.map(e => e.slice(1)),
+ expected
+ );
+
+ // Now register another event in the same category and make sure both events can be recorded.
+ events.test2 = {
+ methods: ["test2"],
+ objects: ["object2"],
+ };
+ Telemetry.registerEvents(category, events);
+
+ expected = [
+ [category, "test1", "object1"],
+ [category, "test2", "object2"],
+ ];
+ expected.forEach(e => Telemetry.recordEvent(...e));
+
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.equal(
+ snapshot.dynamic.length,
+ expected.length,
+ "Should have right number of events in the snapshot."
+ );
+ Assert.deepEqual(
+ snapshot.dynamic.map(e => e.slice(1)),
+ expected
+ );
+
+ // Check that adding a new object to an event entry works.
+ events.test1.methods = ["test1a"];
+ events.test2.objects = ["object2", "object2a"];
+ Telemetry.registerEvents(category, events);
+
+ expected = [
+ [category, "test1", "object1"],
+ [category, "test2", "object2"],
+ [category, "test1a", "object1"],
+ [category, "test2", "object2a"],
+ ];
+ expected.forEach(e => Telemetry.recordEvent(...e));
+
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.equal(
+ snapshot.dynamic.length,
+ expected.length,
+ "Should have right number of events in the snapshot."
+ );
+ Assert.deepEqual(
+ snapshot.dynamic.map(e => e.slice(1)),
+ expected
+ );
+
+ // Make sure that we can expire events that are already registered.
+ events.test2.expired = true;
+ Telemetry.registerEvents(category, events);
+
+ expected = [[category, "test1", "object1"]];
+ expected.forEach(e => Telemetry.recordEvent(...e));
+
+ snapshot = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true);
+ Assert.equal(
+ snapshot.dynamic.length,
+ expected.length,
+ "Should have right number of events in the snapshot."
+ );
+ Assert.deepEqual(
+ snapshot.dynamic.map(e => e.slice(1)),
+ expected
+ );
+});
+
+add_task(
+ {
+ skip_if: () => gIsAndroid,
+ },
+ async function test_productSpecificEvents() {
+ const EVENT_CATEGORY = "telemetry.test";
+ const DEFAULT_PRODUCTS_EVENT = "default_products";
+ const DESKTOP_ONLY_EVENT = "desktop_only";
+ const MULTIPRODUCT_EVENT = "multiproduct";
+ const MOBILE_ONLY_EVENT = "mobile_only";
+
+ Telemetry.clearEvents();
+
+ // Try to record the desktop and multiproduct event
+ Telemetry.recordEvent(EVENT_CATEGORY, DEFAULT_PRODUCTS_EVENT, "object1");
+ Telemetry.recordEvent(EVENT_CATEGORY, DESKTOP_ONLY_EVENT, "object1");
+ Telemetry.recordEvent(EVENT_CATEGORY, MULTIPRODUCT_EVENT, "object1");
+
+ // Try to record the mobile-only event
+ Telemetry.recordEvent(EVENT_CATEGORY, MOBILE_ONLY_EVENT, "object1");
+
+ let events = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true).parent;
+
+ let expected = [
+ [EVENT_CATEGORY, DEFAULT_PRODUCTS_EVENT, "object1"],
+ [EVENT_CATEGORY, DESKTOP_ONLY_EVENT, "object1"],
+ [EVENT_CATEGORY, MULTIPRODUCT_EVENT, "object1"],
+ ];
+ Assert.equal(
+ events.length,
+ expected.length,
+ "Should have recorded the right amount of events."
+ );
+ for (let i = 0; i < expected.length; ++i) {
+ Assert.deepEqual(
+ events[i].slice(1),
+ expected[i],
+ "Should have recorded the expected event data."
+ );
+ }
+ }
+);
+
+add_task(
+ {
+ skip_if: () => !gIsAndroid,
+ },
+ async function test_mobileSpecificEvents() {
+ const EVENT_CATEGORY = "telemetry.test";
+ const DEFAULT_PRODUCTS_EVENT = "default_products";
+ const DESKTOP_ONLY_EVENT = "desktop_only";
+ const MULTIPRODUCT_EVENT = "multiproduct";
+ const MOBILE_ONLY_EVENT = "mobile_only";
+
+ Telemetry.clearEvents();
+
+ // Try to record the mobile-only and multiproduct event
+ Telemetry.recordEvent(EVENT_CATEGORY, DEFAULT_PRODUCTS_EVENT, "object1");
+ Telemetry.recordEvent(EVENT_CATEGORY, MOBILE_ONLY_EVENT, "object1");
+ Telemetry.recordEvent(EVENT_CATEGORY, MULTIPRODUCT_EVENT, "object1");
+
+ // Try to record the mobile-only event
+ Telemetry.recordEvent(EVENT_CATEGORY, DESKTOP_ONLY_EVENT, "object1");
+
+ let events = Telemetry.snapshotEvents(PRERELEASE_CHANNELS, true).parent;
+
+ let expected = [
+ [EVENT_CATEGORY, DEFAULT_PRODUCTS_EVENT, "object1"],
+ [EVENT_CATEGORY, MOBILE_ONLY_EVENT, "object1"],
+ [EVENT_CATEGORY, MULTIPRODUCT_EVENT, "object1"],
+ ];
+ Assert.equal(
+ events.length,
+ expected.length,
+ "Should have recorded the right amount of events."
+ );
+ for (let i = 0; i < expected.length; ++i) {
+ Assert.deepEqual(
+ events[i].slice(1),
+ expected[i],
+ "Should have recorded the expected event data."
+ );
+ }
+ }
+);
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryEvents_buildFaster.js b/toolkit/components/telemetry/tests/unit/test_TelemetryEvents_buildFaster.js
new file mode 100644
index 0000000000..c7e9e5aaba
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEvents_buildFaster.js
@@ -0,0 +1,463 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+/**
+ * Return the path to the definitions file for the events.
+ */
+function getDefinitionsPath() {
+ // Write the event definition to the spec file in the binary directory.
+ let definitionFile = Cc["@mozilla.org/file/local;1"].createInstance(
+ Ci.nsIFile
+ );
+ definitionFile = Services.dirsvc.get("GreD", Ci.nsIFile);
+ definitionFile.append("EventArtifactDefinitions.json");
+ return definitionFile.path;
+}
+
+add_task(async function test_setup() {
+ do_get_profile();
+});
+
+add_task(
+ {
+ // The test needs to write a file, and that fails in tests on Android.
+ // We don't really need the Android coverage, so skip on Android.
+ skip_if: () => AppConstants.platform == "android",
+ },
+ async function test_invalidJSON() {
+ const INVALID_JSON = "{ invalid,JSON { {1}";
+ const FILE_PATH = getDefinitionsPath();
+
+ // Write a corrupted JSON file.
+ await IOUtils.writeUTF8(FILE_PATH, INVALID_JSON, {
+ mode: "overwrite",
+ });
+
+ // Simulate Firefox startup. This should not throw!
+ await TelemetryController.testSetup();
+ await TelemetryController.testPromiseJsProbeRegistration();
+
+ // Cleanup.
+ await TelemetryController.testShutdown();
+ await IOUtils.remove(FILE_PATH);
+ }
+);
+
+add_task(
+ {
+ // The test needs to write a file, and that fails in tests on Android.
+ // We don't really need the Android coverage, so skip on Android.
+ skip_if: () => AppConstants.platform == "android",
+ },
+ async function test_dynamicBuiltin() {
+ const DYNAMIC_EVENT_SPEC = {
+ "telemetry.test.builtin": {
+ test: {
+ objects: ["object1", "object2"],
+ expires: "never",
+ methods: ["test1", "test2"],
+ extra_keys: ["key2", "key1"],
+ record_on_release: false,
+ },
+ },
+ // Test a new, expired event
+ "telemetry.test.expired": {
+ expired: {
+ objects: ["object1"],
+ methods: ["method1"],
+ expires: AppConstants.MOZ_APP_VERSION,
+ record_on_release: false,
+ },
+ },
+ // Test overwriting static expiries
+ "telemetry.test": {
+ expired_version: {
+ objects: ["object1"],
+ methods: ["expired_version"],
+ expires: "never",
+ record_on_release: false,
+ },
+ not_expired_optout: {
+ objects: ["object1"],
+ methods: ["not_expired_optout"],
+ expires: AppConstants.MOZ_APP_VERSION,
+ record_on_release: true,
+ },
+ },
+ };
+
+ Telemetry.clearEvents();
+
+ // Let's write to the definition file to also cover the file
+ // loading part.
+ const FILE_PATH = getDefinitionsPath();
+ await IOUtils.writeJSON(FILE_PATH, DYNAMIC_EVENT_SPEC);
+
+ // Start TelemetryController to trigger loading the specs.
+ await TelemetryController.testReset();
+ await TelemetryController.testPromiseJsProbeRegistration();
+
+ // Record the events
+ const TEST_EVENT_NAME = "telemetry.test.builtin";
+ const DYNAMIC_EVENT_CATEGORY = "telemetry.test.expired";
+ const STATIC_EVENT_CATEGORY = "telemetry.test";
+ Telemetry.setEventRecordingEnabled(TEST_EVENT_NAME, true);
+ Telemetry.setEventRecordingEnabled(DYNAMIC_EVENT_CATEGORY, true);
+ Telemetry.setEventRecordingEnabled(STATIC_EVENT_CATEGORY, true);
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test1", "object1");
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test2", "object1", null, {
+ key1: "foo",
+ key2: "bar",
+ });
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test2", "object2", null, {
+ key2: "bar",
+ });
+ Telemetry.recordEvent(DYNAMIC_EVENT_CATEGORY, "method1", "object1");
+ Telemetry.recordEvent(STATIC_EVENT_CATEGORY, "expired_version", "object1");
+ Telemetry.recordEvent(
+ STATIC_EVENT_CATEGORY,
+ "not_expired_optout",
+ "object1"
+ );
+
+ // Check the values we tried to store.
+ const snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ Assert.ok(
+ "parent" in snapshot,
+ "Should have parent events in the snapshot."
+ );
+
+ let expected = [
+ [TEST_EVENT_NAME, "test1", "object1"],
+ [TEST_EVENT_NAME, "test2", "object1", null, { key1: "foo", key2: "bar" }],
+ [TEST_EVENT_NAME, "test2", "object2", null, { key2: "bar" }],
+ [STATIC_EVENT_CATEGORY, "expired_version", "object1"],
+ ];
+ let events = snapshot.parent;
+ Assert.equal(
+ events.length,
+ expected.length,
+ "Should have recorded the right amount of events."
+ );
+ for (let i = 0; i < expected.length; ++i) {
+ Assert.deepEqual(
+ events[i].slice(1),
+ expected[i],
+ "Should have recorded the expected event data."
+ );
+ }
+
+ // Clean up.
+ await TelemetryController.testShutdown();
+ await IOUtils.remove(FILE_PATH);
+ }
+);
+
+add_task(async function test_dynamicBuiltinEvents() {
+ Telemetry.clearEvents();
+ Telemetry.clearScalars();
+ Telemetry.canRecordExtended = true;
+
+ const TEST_EVENT_NAME = "telemetry.test.dynamicbuiltin";
+
+ // Register some dynamic builtin test events.
+ Telemetry.registerBuiltinEvents(TEST_EVENT_NAME, {
+ // Event with only required fields.
+ test1: {
+ methods: ["test1"],
+ objects: ["object1"],
+ },
+ // Event with extra_keys.
+ test2: {
+ methods: ["test2", "test2b"],
+ objects: ["object1", "object2"],
+ extra_keys: ["key1", "key2"],
+ },
+ });
+
+ // Record some events.
+ Telemetry.setEventRecordingEnabled(TEST_EVENT_NAME, true);
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test1", "object1");
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test2", "object1", null, {
+ key1: "foo",
+ key2: "bar",
+ });
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test2b", "object2", null, {
+ key2: "bar",
+ });
+ // Now check that the snapshot contains the expected data.
+ let snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ Assert.ok("parent" in snapshot, "Should have parent events in the snapshot.");
+
+ // For checking event summaries
+ const scalars = Telemetry.getSnapshotForKeyedScalars("main", true);
+ Assert.ok(
+ "parent" in scalars,
+ "Should have parent scalars in the main snapshot."
+ );
+
+ let expected = [
+ [TEST_EVENT_NAME, "test1", "object1"],
+ [TEST_EVENT_NAME, "test2", "object1", null, { key1: "foo", key2: "bar" }],
+ [TEST_EVENT_NAME, "test2b", "object2", null, { key2: "bar" }],
+ ];
+ let events = snapshot.parent;
+ Assert.equal(
+ events.length,
+ expected.length,
+ "Should have recorded the right amount of events."
+ );
+ for (let i = 0; i < expected.length; ++i) {
+ Assert.deepEqual(
+ events[i].slice(1),
+ expected[i],
+ "Should have recorded the expected event data."
+ );
+
+ const uniqueEventName = `${expected[i][0]}#${expected[i][1]}#${expected[i][2]}`;
+ const summaryCount =
+ scalars.parent["telemetry.event_counts"][uniqueEventName];
+ Assert.equal(1, summaryCount, `${uniqueEventName} had wrong summary count`);
+ }
+});
+
+add_task(async function test_dynamicBuiltinEventsDisabledByDefault() {
+ Telemetry.clearEvents();
+ Telemetry.canRecordExtended = true;
+
+ const TEST_EVENT_NAME = "telemetry.test.offbydefault";
+
+ // Register some dynamic builtin test events.
+ Telemetry.registerBuiltinEvents(TEST_EVENT_NAME, {
+ // Event with only required fields.
+ test1: {
+ methods: ["test1"],
+ objects: ["object1"],
+ },
+ });
+
+ // Record some events.
+ // Explicitely _don't_ enable the category
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test1", "object1");
+
+ // Now check that the snapshot contains the expected data.
+ let snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ Assert.ok(
+ !("parent" in snapshot),
+ "Should not have parent events in the snapshot."
+ );
+
+ // Now enable the category and record again
+ Telemetry.setEventRecordingEnabled(TEST_EVENT_NAME, true);
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test1", "object1");
+
+ snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ Assert.ok("parent" in snapshot, "Should have parent events in the snapshot.");
+
+ let expected = [[TEST_EVENT_NAME, "test1", "object1"]];
+ let events = snapshot.parent;
+ Assert.equal(
+ events.length,
+ expected.length,
+ "Should have recorded the right amount of events."
+ );
+ for (let i = 0; i < expected.length; ++i) {
+ Assert.deepEqual(
+ events[i].slice(1),
+ expected[i],
+ "Should have recorded the expected event data."
+ );
+ }
+});
+
+add_task(async function test_dynamicBuiltinDontOverwriteStaticData() {
+ Telemetry.clearEvents();
+ Telemetry.canRecordExtended = true;
+
+ const TEST_STATIC_EVENT_NAME = "telemetry.test";
+ const TEST_EVENT_NAME = "telemetry.test.nooverwrite";
+
+ // Register some dynamic builtin test events.
+ Telemetry.registerBuiltinEvents(TEST_EVENT_NAME, {
+ dynamic: {
+ methods: ["dynamic"],
+ objects: ["builtin", "anotherone"],
+ },
+ });
+
+ // First enable the categories we're using
+ Telemetry.setEventRecordingEnabled(TEST_STATIC_EVENT_NAME, true);
+ Telemetry.setEventRecordingEnabled(TEST_EVENT_NAME, true);
+
+ // Now record some dynamic-builtin and static events
+ Telemetry.recordEvent(TEST_EVENT_NAME, "dynamic", "builtin");
+ Telemetry.recordEvent(TEST_STATIC_EVENT_NAME, "test1", "object1");
+ Telemetry.recordEvent(TEST_EVENT_NAME, "dynamic", "anotherone");
+
+ let snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ Assert.ok("parent" in snapshot, "Should have parent events in the snapshot.");
+
+ // All events should now be recorded in the right order
+ let expected = [
+ [TEST_EVENT_NAME, "dynamic", "builtin"],
+ [TEST_STATIC_EVENT_NAME, "test1", "object1"],
+ [TEST_EVENT_NAME, "dynamic", "anotherone"],
+ ];
+ let events = snapshot.parent;
+ Assert.equal(
+ events.length,
+ expected.length,
+ "Should have recorded the right amount of events."
+ );
+ for (let i = 0; i < expected.length; ++i) {
+ Assert.deepEqual(
+ events[i].slice(1),
+ expected[i],
+ "Should have recorded the expected event data."
+ );
+ }
+});
+
+add_task(async function test_dynamicBuiltinEventsOverridingStatic() {
+ Telemetry.clearEvents();
+ Telemetry.canRecordExtended = true;
+
+ const TEST_EVENT_NAME = "telemetry.test";
+
+ // Register dynamic builtin test events, overwriting existing one.
+ Telemetry.registerBuiltinEvents(TEST_EVENT_NAME, {
+ // Event with only required fields.
+ test1: {
+ methods: ["test1"],
+ objects: ["object1", "object2"],
+ },
+ // Event with extra_keys.
+ test2: {
+ methods: ["test2"],
+ objects: ["object1", "object2", "object3"],
+ extra_keys: ["key1", "key2", "newdynamickey"],
+ },
+ });
+
+ // Record some events that should be available in the static event already .
+ Telemetry.setEventRecordingEnabled(TEST_EVENT_NAME, true);
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test1", "object1");
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test2", "object1", null, {
+ key1: "foo",
+ key2: "bar",
+ });
+ // Record events with newly added objects and keys.
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test2", "object2", null, {
+ newdynamickey: "foo",
+ });
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test2", "object3", null, {
+ key1: "foo",
+ });
+ // Now check that the snapshot contains the expected data.
+ let snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ Assert.ok("parent" in snapshot, "Should have parent events in the snapshot.");
+
+ let expected = [
+ [TEST_EVENT_NAME, "test1", "object1"],
+ [TEST_EVENT_NAME, "test2", "object1", null, { key1: "foo", key2: "bar" }],
+ [TEST_EVENT_NAME, "test2", "object2", null, { newdynamickey: "foo" }],
+ [TEST_EVENT_NAME, "test2", "object3", null, { key1: "foo" }],
+ ];
+ let events = snapshot.parent;
+ Assert.equal(
+ events.length,
+ expected.length,
+ "Should have recorded the right amount of events."
+ );
+ for (let i = 0; i < expected.length; ++i) {
+ Assert.deepEqual(
+ events[i].slice(1),
+ expected[i],
+ "Should have recorded the expected event data."
+ );
+ }
+});
+
+add_task(async function test_realDynamicDontOverwrite() {
+ // Real dynamic events follow similar code paths internally.
+ // Let's ensure they trigger the right code path and don't overwrite.
+
+ Telemetry.clearEvents();
+ Telemetry.canRecordExtended = true;
+
+ const TEST_EVENT_NAME = "telemetry.test";
+
+ // Register dynamic test events, this should not overwrite existing ones.
+ Telemetry.registerEvents(TEST_EVENT_NAME, {
+ // Event with only required fields.
+ test1: {
+ methods: ["test1"],
+ objects: ["object1", "object2"],
+ },
+ // Event with extra_keys.
+ test2: {
+ methods: ["test2"],
+ objects: ["object1", "object2", "object3"],
+ extra_keys: ["key1", "key2", "realdynamic"],
+ },
+ });
+
+ // Record some events that should be available in the static event already .
+ Telemetry.setEventRecordingEnabled(TEST_EVENT_NAME, true);
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test1", "object1");
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test2", "object1", null, {
+ key1: "foo",
+ key2: "bar",
+ });
+ // Record events with newly added objects and keys.
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test2", "object2", null, {
+ realdynamic: "foo",
+ });
+ Telemetry.recordEvent(TEST_EVENT_NAME, "test2", "object3", null, {
+ key1: "foo",
+ });
+ // Now check that the snapshot contains the expected data.
+ let snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+ Assert.ok("parent" in snapshot, "Should have parent events in the snapshot.");
+
+ let expected = [
+ [TEST_EVENT_NAME, "test1", "object1"],
+ [TEST_EVENT_NAME, "test2", "object1", null, { key1: "foo", key2: "bar" }],
+ [TEST_EVENT_NAME, "test2", "object3", null, { key1: "foo" }],
+ ];
+ let events = snapshot.parent;
+ Assert.equal(
+ events.length,
+ expected.length,
+ "Should have recorded the right amount of events."
+ );
+ for (let i = 0; i < expected.length; ++i) {
+ Assert.deepEqual(
+ events[i].slice(1),
+ expected[i],
+ "Should have recorded the expected event data."
+ );
+ }
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryFlagClear.js b/toolkit/components/telemetry/tests/unit/test_TelemetryFlagClear.js
new file mode 100644
index 0000000000..29ea4c0a1e
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryFlagClear.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ let testFlag = Services.telemetry.getHistogramById("TELEMETRY_TEST_FLAG");
+ deepEqual(
+ testFlag.snapshot().values,
+ { 0: 1, 1: 0 },
+ "Original value is correct"
+ );
+ testFlag.add(1);
+ deepEqual(
+ testFlag.snapshot().values,
+ { 0: 0, 1: 1, 2: 0 },
+ "Value is correct after ping"
+ );
+ testFlag.clear();
+ deepEqual(
+ testFlag.snapshot().values,
+ { 0: 1, 1: 0 },
+ "Value is correct after calling clear()"
+ );
+ testFlag.add(1);
+ deepEqual(
+ testFlag.snapshot().values,
+ { 0: 0, 1: 1, 2: 0 },
+ "Value is correct after ping"
+ );
+}
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryHistograms.js b/toolkit/components/telemetry/tests/unit/test_TelemetryHistograms.js
new file mode 100644
index 0000000000..1ac0c76351
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryHistograms.js
@@ -0,0 +1,2073 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const INT_MAX = 0x7fffffff;
+
+// Return an array of numbers from lower up to, excluding, upper
+function numberRange(lower, upper) {
+ let a = [];
+ for (let i = lower; i < upper; ++i) {
+ a.push(i);
+ }
+ return a;
+}
+
+function check_histogram(histogram_type, name, min, max, bucket_count) {
+ var h = Telemetry.getHistogramById(name);
+ h.add(0);
+ var s = h.snapshot();
+ Assert.equal(0, s.sum);
+
+ var hgrams = Telemetry.getSnapshotForHistograms("main", false).parent;
+ let gh = hgrams[name];
+ Assert.equal(gh.histogram_type, histogram_type);
+
+ Assert.deepEqual(gh.range, [min, max]);
+
+ // Check that booleans work with nonboolean histograms
+ h.add(false);
+ h.add(true);
+ s = Object.values(h.snapshot().values);
+ Assert.deepEqual(s, [2, 1, 0]);
+
+ // Check that clearing works.
+ h.clear();
+ s = h.snapshot();
+ Assert.deepEqual(s.values, {});
+ Assert.equal(s.sum, 0);
+
+ h.add(0);
+ h.add(1);
+ var c = Object.values(h.snapshot().values);
+ Assert.deepEqual(c, [1, 1, 0]);
+}
+
+// This MUST be the very first test of this file.
+add_task(
+ {
+ skip_if: () => gIsAndroid,
+ },
+ function test_instantiate() {
+ const ID = "TELEMETRY_TEST_COUNT";
+ let h = Telemetry.getHistogramById(ID);
+
+ // Instantiate the subsession histogram through |add| and make sure they match.
+ // This MUST be the first use of "TELEMETRY_TEST_COUNT" in this file, otherwise
+ // |add| will not instantiate the histogram.
+ h.add(1);
+ let snapshot = h.snapshot();
+ let subsession = Telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+ Assert.ok(ID in subsession);
+ Assert.equal(
+ snapshot.sum,
+ subsession[ID].sum,
+ "Histogram and subsession histogram sum must match."
+ );
+ // Clear the histogram, so we don't void the assumptions from the other tests.
+ h.clear();
+ }
+);
+
+add_task(async function test_parameterChecks() {
+ let kinds = [Telemetry.HISTOGRAM_EXPONENTIAL, Telemetry.HISTOGRAM_LINEAR];
+ let testNames = ["TELEMETRY_TEST_EXPONENTIAL", "TELEMETRY_TEST_LINEAR"];
+ for (let i = 0; i < kinds.length; i++) {
+ let histogram_type = kinds[i];
+ let test_type = testNames[i];
+ let [min, max, bucket_count] = [1, INT_MAX - 1, 10];
+ check_histogram(histogram_type, test_type, min, max, bucket_count);
+ }
+});
+
+add_task(async function test_parameterCounts() {
+ let histogramIds = [
+ "TELEMETRY_TEST_EXPONENTIAL",
+ "TELEMETRY_TEST_LINEAR",
+ "TELEMETRY_TEST_FLAG",
+ "TELEMETRY_TEST_CATEGORICAL",
+ "TELEMETRY_TEST_BOOLEAN",
+ ];
+
+ for (let id of histogramIds) {
+ let h = Telemetry.getHistogramById(id);
+ h.clear();
+ h.add();
+ Assert.equal(
+ h.snapshot().sum,
+ 0,
+ "Calling add() without a value should only log an error."
+ );
+ h.clear();
+ }
+});
+
+add_task(async function test_parameterCountsKeyed() {
+ let histogramIds = [
+ "TELEMETRY_TEST_KEYED_FLAG",
+ "TELEMETRY_TEST_KEYED_BOOLEAN",
+ "TELEMETRY_TEST_KEYED_EXPONENTIAL",
+ "TELEMETRY_TEST_KEYED_LINEAR",
+ ];
+
+ for (let id of histogramIds) {
+ let h = Telemetry.getKeyedHistogramById(id);
+ h.clear();
+ h.add("key");
+ Assert.deepEqual(
+ h.snapshot(),
+ {},
+ "Calling add('key') without a value should only log an error."
+ );
+ h.clear();
+ }
+});
+
+add_task(async function test_noSerialization() {
+ // Instantiate the storage for this histogram and make sure it doesn't
+ // get reflected into JS, as it has no interesting data in it.
+ Telemetry.getHistogramById("NEWTAB_PAGE_PINNED_SITES_COUNT");
+ let histograms = Telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+ Assert.equal(false, "NEWTAB_PAGE_PINNED_SITES_COUNT" in histograms);
+});
+
+add_task(async function test_boolean_histogram() {
+ var h = Telemetry.getHistogramById("TELEMETRY_TEST_BOOLEAN");
+ var r = h.snapshot().range;
+ // boolean histograms ignore numeric parameters
+ Assert.deepEqual(r, [1, 2]);
+ h.add(0);
+ h.add(1);
+ h.add(2);
+
+ h.add(true);
+ h.add(false);
+ var s = h.snapshot();
+ Assert.equal(s.histogram_type, Telemetry.HISTOGRAM_BOOLEAN);
+ // last bucket should always be 0 since .add parameters are normalized to either 0 or 1
+ Assert.deepEqual(s.values, { 0: 2, 1: 3, 2: 0 });
+ Assert.equal(s.sum, 3);
+});
+
+add_task(async function test_flag_histogram() {
+ var h = Telemetry.getHistogramById("TELEMETRY_TEST_FLAG");
+ var r = h.snapshot().range;
+ // Flag histograms ignore numeric parameters.
+ Assert.deepEqual(r, [1, 2]);
+ // Should already have a 0 counted.
+ var v = h.snapshot().values;
+ var s = h.snapshot().sum;
+ Assert.deepEqual(v, { 0: 1, 1: 0 });
+ Assert.equal(s, 0);
+ // Should switch counts.
+ h.add(1);
+ var v2 = h.snapshot().values;
+ var s2 = h.snapshot().sum;
+ Assert.deepEqual(v2, { 0: 0, 1: 1, 2: 0 });
+ Assert.equal(s2, 1);
+ // Should only switch counts once.
+ h.add(1);
+ var v3 = h.snapshot().values;
+ var s3 = h.snapshot().sum;
+ Assert.deepEqual(v3, { 0: 0, 1: 1, 2: 0 });
+ Assert.equal(s3, 1);
+ Assert.equal(h.snapshot().histogram_type, Telemetry.HISTOGRAM_FLAG);
+});
+
+add_task(async function test_count_histogram() {
+ let h = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT2");
+ let s = h.snapshot();
+ Assert.deepEqual(s.range, [1, 2]);
+ Assert.deepEqual(s.values, {});
+ Assert.equal(s.sum, 0);
+ h.add();
+ s = h.snapshot();
+ Assert.deepEqual(s.values, { 0: 1, 1: 0 });
+ Assert.equal(s.sum, 1);
+ h.add();
+ s = h.snapshot();
+ Assert.deepEqual(s.values, { 0: 2, 1: 0 });
+ Assert.equal(s.sum, 2);
+});
+
+add_task(async function test_categorical_histogram() {
+ let h1 = Telemetry.getHistogramById("TELEMETRY_TEST_CATEGORICAL");
+ for (let v of ["CommonLabel", "Label2", "Label3", "Label3", 0, 0, 1]) {
+ h1.add(v);
+ }
+ for (let s of ["", "Label4", "1234"]) {
+ // The |add| method should not throw for unexpected values, but rather
+ // print an error message in the console.
+ h1.add(s);
+ }
+
+ let snapshot = h1.snapshot();
+ Assert.equal(snapshot.sum, 6);
+ Assert.deepEqual(snapshot.range, [1, 50]);
+ Assert.deepEqual(snapshot.values, { 0: 3, 1: 2, 2: 2, 3: 0 });
+
+ let h2 = Telemetry.getHistogramById("TELEMETRY_TEST_CATEGORICAL_OPTOUT");
+ for (let v of [
+ "CommonLabel",
+ "CommonLabel",
+ "Label4",
+ "Label5",
+ "Label6",
+ 0,
+ 1,
+ ]) {
+ h2.add(v);
+ }
+ for (let s of ["", "Label3", "1234"]) {
+ // The |add| method should not throw for unexpected values, but rather
+ // print an error message in the console.
+ h2.add(s);
+ }
+
+ snapshot = h2.snapshot();
+ Assert.equal(snapshot.sum, 7);
+ Assert.deepEqual(snapshot.range, [1, 50]);
+ Assert.deepEqual(snapshot.values, { 0: 3, 1: 2, 2: 1, 3: 1, 4: 0 });
+
+ // This histogram overrides the default of 50 values to 70.
+ let h3 = Telemetry.getHistogramById("TELEMETRY_TEST_CATEGORICAL_NVALUES");
+ for (let v of ["CommonLabel", "Label7", "Label8"]) {
+ h3.add(v);
+ }
+
+ snapshot = h3.snapshot();
+ Assert.equal(snapshot.sum, 3);
+ Assert.deepEqual(snapshot.range, [1, 70]);
+ Assert.deepEqual(snapshot.values, { 0: 1, 1: 1, 2: 1, 3: 0 });
+});
+
+add_task(async function test_getCategoricalLabels() {
+ let h = Telemetry.getCategoricalLabels();
+
+ Assert.deepEqual(h.TELEMETRY_TEST_CATEGORICAL, [
+ "CommonLabel",
+ "Label2",
+ "Label3",
+ ]);
+ Assert.deepEqual(h.TELEMETRY_TEST_CATEGORICAL_OPTOUT, [
+ "CommonLabel",
+ "Label4",
+ "Label5",
+ "Label6",
+ ]);
+ Assert.deepEqual(h.TELEMETRY_TEST_CATEGORICAL_NVALUES, [
+ "CommonLabel",
+ "Label7",
+ "Label8",
+ ]);
+ Assert.deepEqual(h.TELEMETRY_TEST_KEYED_CATEGORICAL, [
+ "CommonLabel",
+ "Label2",
+ "Label3",
+ ]);
+});
+
+add_task(async function test_add_error_behaviour() {
+ const PLAIN_HISTOGRAMS_TO_TEST = [
+ "TELEMETRY_TEST_FLAG",
+ "TELEMETRY_TEST_EXPONENTIAL",
+ "TELEMETRY_TEST_LINEAR",
+ "TELEMETRY_TEST_BOOLEAN",
+ ];
+
+ const KEYED_HISTOGRAMS_TO_TEST = [
+ "TELEMETRY_TEST_KEYED_FLAG",
+ "TELEMETRY_TEST_KEYED_COUNT",
+ "TELEMETRY_TEST_KEYED_BOOLEAN",
+ ];
+
+ // Check that |add| doesn't throw for plain histograms.
+ for (let hist of PLAIN_HISTOGRAMS_TO_TEST) {
+ const returnValue =
+ Telemetry.getHistogramById(hist).add("unexpected-value");
+ Assert.strictEqual(
+ returnValue,
+ undefined,
+ "Adding to an histogram must return 'undefined'."
+ );
+ }
+
+ // And for keyed histograms.
+ for (let hist of KEYED_HISTOGRAMS_TO_TEST) {
+ const returnValue = Telemetry.getKeyedHistogramById(hist).add(
+ "some-key",
+ "unexpected-value"
+ );
+ Assert.strictEqual(
+ returnValue,
+ undefined,
+ "Adding to a keyed histogram must return 'undefined'."
+ );
+ }
+});
+
+add_task(async function test_API_return_values() {
+ // Check that the plain scalar functions don't allow to crash the browser.
+ // We expect 'undefined' to be returned so that .add(1).add() can't be called.
+ // See bug 1321349 for context.
+ let hist = Telemetry.getHistogramById("TELEMETRY_TEST_LINEAR");
+ let keyedHist = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_COUNT");
+
+ const RETURN_VALUES = [
+ hist.clear(),
+ hist.add(1),
+ keyedHist.clear(),
+ keyedHist.add("some-key", 1),
+ ];
+
+ for (let returnValue of RETURN_VALUES) {
+ Assert.strictEqual(
+ returnValue,
+ undefined,
+ "The function must return undefined"
+ );
+ }
+});
+
+add_task(async function test_getHistogramById() {
+ try {
+ Telemetry.getHistogramById("nonexistent");
+ do_throw("This can't happen");
+ } catch (e) {}
+ var h = Telemetry.getHistogramById("CYCLE_COLLECTOR");
+ var s = h.snapshot();
+ Assert.equal(s.histogram_type, Telemetry.HISTOGRAM_EXPONENTIAL);
+ Assert.deepEqual(s.range, [1, 10000]);
+});
+
+add_task(async function test_getSlowSQL() {
+ var slow = Telemetry.slowSQL;
+ Assert.ok("mainThread" in slow && "otherThreads" in slow);
+});
+
+// Check that telemetry doesn't record in private mode
+add_task(async function test_privateMode() {
+ var h = Telemetry.getHistogramById("TELEMETRY_TEST_BOOLEAN");
+ var orig = h.snapshot();
+ Telemetry.canRecordExtended = false;
+ h.add(1);
+ Assert.deepEqual(orig, h.snapshot());
+ Telemetry.canRecordExtended = true;
+ h.add(1);
+ Assert.notDeepEqual(orig, h.snapshot());
+});
+
+// Check that telemetry records only when it is suppose to.
+add_task(async function test_histogramRecording() {
+ // Check that no histogram is recorded if both base and extended recording are off.
+ Telemetry.canRecordBase = false;
+ Telemetry.canRecordExtended = false;
+
+ let h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTOUT");
+ h.clear();
+ let orig = h.snapshot();
+ h.add(1);
+ Assert.equal(orig.sum, h.snapshot().sum);
+
+ // Check that only base histograms are recorded.
+ Telemetry.canRecordBase = true;
+ h.add(1);
+ Assert.equal(
+ orig.sum + 1,
+ h.snapshot().sum,
+ "Histogram value should have incremented by 1 due to recording."
+ );
+
+ // Extended histograms should not be recorded.
+ h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTIN");
+ orig = h.snapshot();
+ h.add(1);
+ Assert.equal(
+ orig.sum,
+ h.snapshot().sum,
+ "Histograms should be equal after recording."
+ );
+
+ // Runtime created histograms should not be recorded.
+ h = Telemetry.getHistogramById("TELEMETRY_TEST_BOOLEAN");
+ orig = h.snapshot();
+ h.add(1);
+ Assert.equal(
+ orig.sum,
+ h.snapshot().sum,
+ "Histograms should be equal after recording."
+ );
+
+ // Check that extended histograms are recorded when required.
+ Telemetry.canRecordExtended = true;
+
+ h.add(1);
+ Assert.equal(
+ orig.sum + 1,
+ h.snapshot().sum,
+ "Runtime histogram value should have incremented by 1 due to recording."
+ );
+
+ h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTIN");
+ orig = h.snapshot();
+ h.add(1);
+ Assert.equal(
+ orig.sum + 1,
+ h.snapshot().sum,
+ "Histogram value should have incremented by 1 due to recording."
+ );
+
+ // Check that base histograms are still being recorded.
+ h = Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTOUT");
+ h.clear();
+ orig = h.snapshot();
+ h.add(1);
+ Assert.equal(
+ orig.sum + 1,
+ h.snapshot().sum,
+ "Histogram value should have incremented by 1 due to recording."
+ );
+});
+
+add_task(async function test_expired_histogram() {
+ var test_expired_id = "TELEMETRY_TEST_EXPIRED";
+ var dummy = Telemetry.getHistogramById(test_expired_id);
+
+ dummy.add(1);
+
+ for (let process of ["main", "content", "gpu", "extension"]) {
+ let histograms = Telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ );
+ if (!(process in histograms)) {
+ info("Nothing present for process " + process);
+ continue;
+ }
+ Assert.equal(histograms[process].__expired__, undefined);
+ }
+ let parentHgrams = Telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+ Assert.equal(parentHgrams[test_expired_id], undefined);
+});
+
+add_task(async function test_keyed_expired_histogram() {
+ var test_expired_id = "TELEMETRY_TEST_EXPIRED_KEYED";
+ var dummy = Telemetry.getKeyedHistogramById(test_expired_id);
+ dummy.add("someKey", 1);
+
+ const histograms = Telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ false /* clear */
+ );
+ for (let process of ["parent", "content", "gpu", "extension"]) {
+ if (!(process in histograms)) {
+ info("Nothing present for process " + process);
+ continue;
+ }
+ Assert.ok(
+ !(test_expired_id in histograms[process]),
+ "The expired keyed histogram must not be reported"
+ );
+ }
+});
+
+add_task(async function test_keyed_histogram() {
+ // Check that invalid names get rejected.
+
+ let threw = false;
+ try {
+ Telemetry.getKeyedHistogramById(
+ "test::unknown histogram",
+ "never",
+ Telemetry.HISTOGRAM_BOOLEAN
+ );
+ } catch (e) {
+ // This should throw as it is an unknown ID
+ threw = true;
+ }
+ Assert.ok(threw, "getKeyedHistogramById should have thrown");
+});
+
+add_task(async function test_keyed_boolean_histogram() {
+ const KEYED_ID = "TELEMETRY_TEST_KEYED_BOOLEAN";
+ let KEYS = numberRange(0, 2).map(i => "key" + (i + 1));
+ KEYS.push("漢語");
+ let histogramBase = {
+ range: [1, 2],
+ bucket_count: 3,
+ histogram_type: 2,
+ sum: 1,
+ values: { 0: 0, 1: 1, 2: 0 },
+ };
+ let testHistograms = numberRange(0, 3).map(i =>
+ JSON.parse(JSON.stringify(histogramBase))
+ );
+ let testKeys = [];
+ let testSnapShot = {};
+
+ let h = Telemetry.getKeyedHistogramById(KEYED_ID);
+ for (let i = 0; i < 2; ++i) {
+ let key = KEYS[i];
+ h.add(key, true);
+ testSnapShot[key] = testHistograms[i];
+ testKeys.push(key);
+
+ Assert.deepEqual(h.keys().sort(), testKeys);
+ Assert.deepEqual(h.snapshot(), testSnapShot);
+ }
+
+ h = Telemetry.getKeyedHistogramById(KEYED_ID);
+ Assert.deepEqual(h.keys().sort(), testKeys);
+ Assert.deepEqual(h.snapshot(), testSnapShot);
+
+ let key = KEYS[2];
+ h.add(key, false);
+ testKeys.push(key);
+ testSnapShot[key] = testHistograms[2];
+ testSnapShot[key].sum = 0;
+ testSnapShot[key].values = { 0: 1, 1: 0 };
+ Assert.deepEqual(h.keys().sort(), testKeys);
+ Assert.deepEqual(h.snapshot(), testSnapShot);
+
+ let parentHgrams = Telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+ Assert.deepEqual(parentHgrams[KEYED_ID], testSnapShot);
+
+ h.clear();
+ Assert.deepEqual(h.keys(), []);
+ Assert.deepEqual(h.snapshot(), {});
+});
+
+add_task(async function test_keyed_count_histogram() {
+ const KEYED_ID = "TELEMETRY_TEST_KEYED_COUNT";
+ const KEYS = numberRange(0, 5).map(i => "key" + (i + 1));
+ let histogramBase = {
+ range: [1, 2],
+ bucket_count: 3,
+ histogram_type: 4,
+ sum: 0,
+ values: { 0: 1, 1: 0 },
+ };
+ let testHistograms = numberRange(0, 5).map(i =>
+ JSON.parse(JSON.stringify(histogramBase))
+ );
+ let testKeys = [];
+ let testSnapShot = {};
+
+ let h = Telemetry.getKeyedHistogramById(KEYED_ID);
+ h.clear();
+ for (let i = 0; i < 4; ++i) {
+ let key = KEYS[i];
+ let value = i * 2 + 1;
+
+ for (let k = 0; k < value; ++k) {
+ h.add(key);
+ }
+ testHistograms[i].values[0] = value;
+ testHistograms[i].sum = value;
+ testSnapShot[key] = testHistograms[i];
+ testKeys.push(key);
+
+ Assert.deepEqual(h.keys().sort(), testKeys);
+ Assert.deepEqual(h.snapshot()[key], testHistograms[i]);
+ Assert.deepEqual(h.snapshot(), testSnapShot);
+ }
+
+ h = Telemetry.getKeyedHistogramById(KEYED_ID);
+ Assert.deepEqual(h.keys().sort(), testKeys);
+ Assert.deepEqual(h.snapshot(), testSnapShot);
+
+ let key = KEYS[4];
+ h.add(key);
+ testKeys.push(key);
+ testHistograms[4].values[0] = 1;
+ testHistograms[4].sum = 1;
+ testSnapShot[key] = testHistograms[4];
+
+ Assert.deepEqual(h.keys().sort(), testKeys);
+ Assert.deepEqual(h.snapshot(), testSnapShot);
+
+ let parentHgrams = Telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+ Assert.deepEqual(parentHgrams[KEYED_ID], testSnapShot);
+
+ // Test clearing categorical histogram.
+ h.clear();
+ Assert.deepEqual(h.keys(), []);
+ Assert.deepEqual(h.snapshot(), {});
+
+ // Test leaving out the value argument. That should increment by 1.
+ h.add("key");
+ Assert.equal(h.snapshot().key.sum, 1);
+});
+
+add_task(async function test_keyed_categorical_histogram() {
+ const KEYED_ID = "TELEMETRY_TEST_KEYED_CATEGORICAL";
+ const KEYS = numberRange(0, 5).map(i => "key" + (i + 1));
+
+ let h = Telemetry.getKeyedHistogramById(KEYED_ID);
+
+ for (let k of KEYS) {
+ // Test adding both per label and index.
+ for (let v of ["CommonLabel", "Label2", "Label3", "Label3", 0, 0, 1]) {
+ h.add(k, v);
+ }
+
+ // The |add| method should not throw for unexpected values, but rather
+ // print an error message in the console.
+ for (let s of ["", "Label4", "1234"]) {
+ h.add(k, s);
+ }
+ }
+
+ // Check that the set of keys in the snapshot is what we expect.
+ let snapshot = h.snapshot();
+ let snapshotKeys = Object.keys(snapshot);
+ Assert.equal(KEYS.length, snapshotKeys.length);
+ Assert.ok(KEYS.every(k => snapshotKeys.includes(k)));
+
+ // Check the snapshot values.
+ for (let k of KEYS) {
+ Assert.ok(k in snapshot);
+ Assert.equal(snapshot[k].sum, 6);
+ Assert.deepEqual(snapshot[k].range, [1, 50]);
+ Assert.deepEqual(snapshot[k].values, { 0: 3, 1: 2, 2: 2, 3: 0 });
+ }
+});
+
+add_task(async function test_keyed_flag_histogram() {
+ const KEYED_ID = "TELEMETRY_TEST_KEYED_FLAG";
+ let h = Telemetry.getKeyedHistogramById(KEYED_ID);
+
+ const KEY = "default";
+ h.add(KEY, true);
+
+ let testSnapshot = {};
+ testSnapshot[KEY] = {
+ range: [1, 2],
+ bucket_count: 3,
+ histogram_type: 3,
+ sum: 1,
+ values: { 0: 0, 1: 1, 2: 0 },
+ };
+
+ Assert.deepEqual(h.keys().sort(), [KEY]);
+ Assert.deepEqual(h.snapshot(), testSnapshot);
+
+ let parentHgrams = Telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+ Assert.deepEqual(parentHgrams[KEYED_ID], testSnapshot);
+
+ h.clear();
+ Assert.deepEqual(h.keys(), []);
+ Assert.deepEqual(h.snapshot(), {});
+});
+
+add_task(async function test_keyed_histogram_recording() {
+ // Check that no histogram is recorded if both base and extended recording are off.
+ Telemetry.canRecordBase = false;
+ Telemetry.canRecordExtended = false;
+
+ const TEST_KEY = "record_foo";
+ let h = Telemetry.getKeyedHistogramById(
+ "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT"
+ );
+ h.clear();
+ h.add(TEST_KEY, 1);
+ Assert.ok(!(TEST_KEY in h.snapshot()));
+
+ // Check that only base histograms are recorded.
+ Telemetry.canRecordBase = true;
+ h.add(TEST_KEY, 1);
+ Assert.equal(
+ h.snapshot()[TEST_KEY].sum,
+ 1,
+ "The keyed histogram should record the correct value."
+ );
+
+ // Extended set keyed histograms should not be recorded.
+ h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTIN");
+ h.clear();
+ h.add(TEST_KEY, 1);
+ Assert.ok(
+ !(TEST_KEY in h.snapshot()),
+ "The keyed histograms should not record any data."
+ );
+
+ // Check that extended histograms are recorded when required.
+ Telemetry.canRecordExtended = true;
+
+ h.add(TEST_KEY, 1);
+ Assert.equal(
+ h.snapshot()[TEST_KEY].sum,
+ 1,
+ "The runtime keyed histogram should record the correct value."
+ );
+
+ h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTIN");
+ h.clear();
+ h.add(TEST_KEY, 1);
+ Assert.equal(
+ h.snapshot()[TEST_KEY].sum,
+ 1,
+ "The keyed histogram should record the correct value."
+ );
+
+ // Check that base histograms are still being recorded.
+ h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT");
+ h.clear();
+ h.add(TEST_KEY, 1);
+ Assert.equal(h.snapshot()[TEST_KEY].sum, 1);
+});
+
+add_task(async function test_histogram_recording_enabled() {
+ Telemetry.canRecordBase = true;
+ Telemetry.canRecordExtended = true;
+
+ // Check that a "normal" histogram respects recording-enabled on/off
+ var h = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT");
+ var orig = h.snapshot();
+
+ h.add(1);
+ Assert.equal(orig.sum + 1, h.snapshot().sum, "add should record by default.");
+
+ // Check that when recording is disabled - add is ignored
+ Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_COUNT", false);
+ h.add(1);
+ Assert.equal(
+ orig.sum + 1,
+ h.snapshot().sum,
+ "When recording is disabled add should not record."
+ );
+
+ // Check that we're back to normal after recording is enabled
+ Telemetry.setHistogramRecordingEnabled("TELEMETRY_TEST_COUNT", true);
+ h.add(1);
+ Assert.equal(
+ orig.sum + 2,
+ h.snapshot().sum,
+ "When recording is re-enabled add should record."
+ );
+
+ // Check that we're correctly accumulating values other than 1.
+ h.clear();
+ h.add(3);
+ Assert.equal(
+ 3,
+ h.snapshot().sum,
+ "Recording counts greater than 1 should work."
+ );
+
+ // Check that a histogram with recording disabled by default behaves correctly
+ h = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT_INIT_NO_RECORD");
+ orig = h.snapshot();
+
+ h.add(1);
+ Assert.equal(
+ orig.sum,
+ h.snapshot().sum,
+ "When recording is disabled by default, add should not record by default."
+ );
+
+ Telemetry.setHistogramRecordingEnabled(
+ "TELEMETRY_TEST_COUNT_INIT_NO_RECORD",
+ true
+ );
+ h.add(1);
+ Assert.equal(
+ orig.sum + 1,
+ h.snapshot().sum,
+ "When recording is enabled add should record."
+ );
+
+ // Restore to disabled
+ Telemetry.setHistogramRecordingEnabled(
+ "TELEMETRY_TEST_COUNT_INIT_NO_RECORD",
+ false
+ );
+ h.add(1);
+ Assert.equal(
+ orig.sum + 1,
+ h.snapshot().sum,
+ "When recording is disabled add should not record."
+ );
+});
+
+add_task(async function test_keyed_histogram_recording_enabled() {
+ Telemetry.canRecordBase = true;
+ Telemetry.canRecordExtended = true;
+
+ // Check RecordingEnabled for keyed histograms which are recording by default
+ const TEST_KEY = "record_foo";
+ let h = Telemetry.getKeyedHistogramById(
+ "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT"
+ );
+
+ h.clear();
+ h.add(TEST_KEY, 1);
+ Assert.equal(
+ h.snapshot()[TEST_KEY].sum,
+ 1,
+ "Keyed histogram add should record by default"
+ );
+
+ Telemetry.setHistogramRecordingEnabled(
+ "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT",
+ false
+ );
+ h.add(TEST_KEY, 1);
+ Assert.equal(
+ h.snapshot()[TEST_KEY].sum,
+ 1,
+ "Keyed histogram add should not record when recording is disabled"
+ );
+
+ Telemetry.setHistogramRecordingEnabled(
+ "TELEMETRY_TEST_KEYED_RELEASE_OPTOUT",
+ true
+ );
+ h.clear();
+ h.add(TEST_KEY, 1);
+ Assert.equal(
+ h.snapshot()[TEST_KEY].sum,
+ 1,
+ "Keyed histogram add should record when recording is re-enabled"
+ );
+
+ // Check that a histogram with recording disabled by default behaves correctly
+ h = Telemetry.getKeyedHistogramById(
+ "TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD"
+ );
+ h.clear();
+
+ h.add(TEST_KEY, 1);
+ Assert.ok(
+ !(TEST_KEY in h.snapshot()),
+ "Keyed histogram add should not record by default for histograms which don't record by default"
+ );
+
+ Telemetry.setHistogramRecordingEnabled(
+ "TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD",
+ true
+ );
+ h.add(TEST_KEY, 1);
+ Assert.equal(
+ h.snapshot()[TEST_KEY].sum,
+ 1,
+ "Keyed histogram add should record when recording is enabled"
+ );
+
+ // Restore to disabled
+ Telemetry.setHistogramRecordingEnabled(
+ "TELEMETRY_TEST_KEYED_COUNT_INIT_NO_RECORD",
+ false
+ );
+ h.add(TEST_KEY, 1);
+ Assert.equal(
+ h.snapshot()[TEST_KEY].sum,
+ 1,
+ "Keyed histogram add should not record when recording is disabled"
+ );
+});
+
+add_task(async function test_histogramSnapshots() {
+ let keyed = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_COUNT");
+ keyed.add("a", 1);
+
+ // Check that keyed histograms are not returned
+ let parentHgrams = Telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+ Assert.ok(!("TELEMETRY_TEST_KEYED_COUNT" in parentHgrams));
+});
+
+add_task(async function test_datasets() {
+ // Check that datasets work as expected.
+
+ const currentRecordExtended = Telemetry.canRecordExtended;
+
+ // Clear everything out
+ Telemetry.getSnapshotForHistograms("main", true /* clear */);
+ Telemetry.getSnapshotForKeyedHistograms("main", true /* clear */);
+
+ // Empty histograms are filtered. Let's record what we check below.
+ Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTIN").add(1);
+ Telemetry.getHistogramById("TELEMETRY_TEST_RELEASE_OPTOUT").add(1);
+ // Keyed flag histograms are skipped if empty, let's add data
+ Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_FLAG").add("a", 1);
+ Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTIN").add(
+ "a",
+ 1
+ );
+ Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT").add(
+ "a",
+ 1
+ );
+
+ // Check that registeredHistogram works properly
+ Telemetry.canRecordExtended = true;
+ let registered = Telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ );
+ registered = new Set(Object.keys(registered.parent));
+ Assert.ok(registered.has("TELEMETRY_TEST_FLAG"));
+ Assert.ok(registered.has("TELEMETRY_TEST_RELEASE_OPTIN"));
+ Assert.ok(registered.has("TELEMETRY_TEST_RELEASE_OPTOUT"));
+ Telemetry.canRecordExtended = false;
+ registered = Telemetry.getSnapshotForHistograms("main", false /* clear */);
+ registered = new Set(Object.keys(registered.parent));
+ Assert.ok(!registered.has("TELEMETRY_TEST_FLAG"));
+ Assert.ok(!registered.has("TELEMETRY_TEST_RELEASE_OPTIN"));
+ Assert.ok(registered.has("TELEMETRY_TEST_RELEASE_OPTOUT"));
+
+ // Check that registeredKeyedHistograms works properly
+ Telemetry.canRecordExtended = true;
+ registered = Telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ false /* clear */
+ );
+ registered = new Set(Object.keys(registered.parent));
+ Assert.ok(registered.has("TELEMETRY_TEST_KEYED_FLAG"));
+ Assert.ok(registered.has("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT"));
+ Telemetry.canRecordExtended = false;
+ registered = Telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ false /* clear */
+ );
+ registered = new Set(Object.keys(registered.parent));
+ Assert.ok(!registered.has("TELEMETRY_TEST_KEYED_FLAG"));
+ Assert.ok(registered.has("TELEMETRY_TEST_KEYED_RELEASE_OPTOUT"));
+
+ Telemetry.canRecordExtended = currentRecordExtended;
+});
+
+add_task(async function test_keyed_keys() {
+ let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_KEYS");
+ h.clear();
+ Telemetry.clearScalars();
+
+ // The |add| method should not throw for keys that are not allowed.
+ h.add("testkey", true);
+ h.add("thirdKey", false);
+ h.add("not-allowed", true);
+
+ // Check that we have the expected keys.
+ let snap = h.snapshot();
+ Assert.equal(Object.keys(snap).length, 2, "Only 2 keys must be recorded.");
+ Assert.ok("testkey" in snap, "'testkey' must be recorded.");
+ Assert.ok("thirdKey" in snap, "'thirdKey' must be recorded.");
+ Assert.deepEqual(
+ snap.testkey.values,
+ { 0: 0, 1: 1, 2: 0 },
+ "'testkey' must contain the correct value."
+ );
+ Assert.deepEqual(
+ snap.thirdKey.values,
+ { 0: 1, 1: 0 },
+ "'thirdKey' must contain the correct value."
+ );
+
+ // Keys that are not allowed must not be recorded.
+ Assert.ok(!("not-allowed" in snap), "'not-allowed' must not be recorded.");
+
+ // Check that these failures were correctly tracked.
+ const parentScalars = Telemetry.getSnapshotForKeyedScalars(
+ "main",
+ false
+ ).parent;
+ const scalarName = "telemetry.accumulate_unknown_histogram_keys";
+ Assert.ok(
+ scalarName in parentScalars,
+ "Accumulation to unallowed keys must be reported."
+ );
+ Assert.ok(
+ "TELEMETRY_TEST_KEYED_KEYS" in parentScalars[scalarName],
+ "Accumulation to unallowed keys must be recorded with the correct key."
+ );
+ Assert.equal(
+ parentScalars[scalarName].TELEMETRY_TEST_KEYED_KEYS,
+ 1,
+ "Accumulation to unallowed keys must report the correct value."
+ );
+});
+
+add_task(async function test_count_multiple_samples() {
+ let valid = [1, 1, 3, 0];
+ let invalid = ["1", "0", "", "random"];
+
+ let h = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT");
+ h.clear();
+
+ // If the array contains even a single invalid value, no accumulation should take place
+ // Keep the valid values in front of invalid to check if it is simply accumulating as
+ // it's traversing the array and throwing upon first invalid value. That should not happen.
+ h.add(valid.concat(invalid));
+ let s1 = h.snapshot();
+ Assert.equal(s1.sum, 0);
+ // Ensure that no accumulations of 0-like values took place.
+ // These accumulations won't increase the sum.
+ Assert.deepEqual({}, s1.values);
+
+ h.add(valid);
+ let s2 = h.snapshot();
+ Assert.deepEqual(s2.values, { 0: 4, 1: 0 });
+ Assert.equal(s2.sum, 5);
+});
+
+add_task(async function test_categorical_multiple_samples() {
+ let h = Telemetry.getHistogramById("TELEMETRY_TEST_CATEGORICAL");
+ h.clear();
+ let valid = ["CommonLabel", "Label2", "Label3", "Label3", 0, 0, 1];
+ let invalid = ["", "Label4", "1234", "0", "1", 5000];
+
+ // At least one invalid parameter, so no accumulation should happen here
+ // Valid values in front of invalid.
+ h.add(valid.concat(invalid));
+ let s1 = h.snapshot();
+ Assert.equal(s1.sum, 0);
+ Assert.deepEqual({}, s1.values);
+
+ h.add(valid);
+ let snapshot = h.snapshot();
+ Assert.equal(snapshot.sum, 6);
+ Assert.deepEqual(snapshot.values, { 0: 3, 1: 2, 2: 2, 3: 0 });
+});
+
+add_task(async function test_boolean_multiple_samples() {
+ let valid = [true, false, 0, 1, 2];
+ let invalid = ["", "0", "1", ",2", "true", "false", "random"];
+
+ let h = Telemetry.getHistogramById("TELEMETRY_TEST_BOOLEAN");
+ h.clear();
+
+ // At least one invalid parameter, so no accumulation should happen here
+ // Valid values in front of invalid.
+ h.add(valid.concat(invalid));
+ let s1 = h.snapshot();
+ Assert.equal(s1.sum, 0);
+ Assert.deepEqual({}, s1.values);
+
+ h.add(valid);
+ let s = h.snapshot();
+ Assert.deepEqual(s.values, { 0: 2, 1: 3, 2: 0 });
+ Assert.equal(s.sum, 3);
+});
+
+add_task(async function test_linear_multiple_samples() {
+ // According to telemetry.mozilla.org/histogram-simulator, bucket at
+ // index 1 of TELEMETRY_TEST_LINEAR has max value of 268.44M
+ let valid = [0, 1, 5, 10, 268450000, 268450001, Math.pow(2, 31) + 1];
+ let invalid = ["", "0", "1", "random"];
+
+ let h = Telemetry.getHistogramById("TELEMETRY_TEST_LINEAR");
+ h.clear();
+
+ // At least one invalid paramater, so no accumulations.
+ // Valid values in front of invalid.
+ h.add(valid.concat(invalid));
+ let s1 = h.snapshot();
+ Assert.equal(s1.sum, 0);
+ Assert.deepEqual({}, s1.values);
+
+ h.add(valid);
+ let s2 = h.snapshot();
+ // Values >= INT32_MAX are accumulated as INT32_MAX - 1
+ Assert.equal(s2.sum, valid.reduce((acc, cur) => acc + cur) - 3);
+ Assert.deepEqual(Object.values(s2.values), [1, 3, 2, 1]);
+});
+
+add_task(async function test_keyed_no_arguments() {
+ // Test for no accumulation when add is called with no arguments
+ let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_LINEAR");
+ h.clear();
+
+ h.add();
+
+ // No keys should be added due to no accumulation.
+ Assert.equal(h.keys().length, 0);
+});
+
+add_task(async function test_keyed_categorical_invalid_string() {
+ // Test for no accumulation when add is called on a
+ // keyed categorical histogram with an invalid string label.
+ let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_CATEGORICAL");
+ h.clear();
+
+ h.add("someKey", "#notALabel");
+
+ // No keys should be added due to no accumulation.
+ Assert.equal(h.keys().length, 0);
+});
+
+add_task(async function test_keyed_count_multiple_samples() {
+ let valid = [1, 1, 3, 0];
+ let invalid = ["1", "0", "", "random"];
+ let key = "somekeystring";
+
+ let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_COUNT");
+ h.clear();
+
+ // If the array contains even a single invalid value, no accumulation should take place
+ // Keep the valid values in front of invalid to check if it is simply accumulating as
+ // it's traversing the array and throwing upon first invalid value. That should not happen.
+ h.add(key, valid.concat(invalid));
+ let s1 = h.snapshot();
+ Assert.ok(!(key in s1));
+
+ h.add(key, valid);
+ let s2 = h.snapshot()[key];
+ Assert.deepEqual(s2.values, { 0: 4, 1: 0 });
+ Assert.equal(s2.sum, 5);
+});
+
+add_task(async function test_keyed_categorical_multiple_samples() {
+ let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_CATEGORICAL");
+ h.clear();
+ let valid = ["CommonLabel", "Label2", "Label3", "Label3", 0, 0, 1];
+ let invalid = ["", "Label4", "1234", "0", "1", 5000];
+ let key = "somekeystring";
+
+ // At least one invalid parameter, so no accumulation should happen here
+ // Valid values in front of invalid.
+ h.add(key, valid.concat(invalid));
+ let s1 = h.snapshot();
+ Assert.ok(!(key in s1));
+
+ h.add(key, valid);
+ let snapshot = h.snapshot()[key];
+ Assert.equal(snapshot.sum, 6);
+ Assert.deepEqual(Object.values(snapshot.values), [3, 2, 2, 0]);
+});
+
+add_task(async function test_keyed_boolean_multiple_samples() {
+ let valid = [true, false, 0, 1, 2];
+ let invalid = ["", "0", "1", ",2", "true", "false", "random"];
+ let key = "somekey";
+
+ let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_BOOLEAN");
+ h.clear();
+
+ // At least one invalid parameter, so no accumulation should happen here
+ // Valid values in front of invalid.
+ h.add(key, valid.concat(invalid));
+ let s1 = h.snapshot();
+ Assert.ok(!(key in s1));
+
+ h.add(key, valid);
+ let s = h.snapshot()[key];
+ Assert.deepEqual(s.values, { 0: 2, 1: 3, 2: 0 });
+ Assert.equal(s.sum, 3);
+});
+
+add_task(async function test_keyed_linear_multiple_samples() {
+ // According to telemetry.mozilla.org/histogram-simulator, bucket at
+ // index 1 of TELEMETRY_TEST_LINEAR has max value of 3.13K
+ let valid = [0, 1, 5, 10, 268450000, 268450001, Math.pow(2, 31) + 1];
+ let invalid = ["", "0", "1", "random"];
+ let key = "somestring";
+
+ let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_LINEAR");
+ h.clear();
+
+ // At least one invalid paramater, so no accumulations.
+ // Valid values in front of invalid.
+ h.add(key, valid.concat(invalid));
+ let s1 = h.snapshot();
+ Assert.ok(!(key in s1));
+
+ h.add(key, valid);
+ let s2 = h.snapshot()[key];
+ // Values >= INT32_MAX are accumulated as INT32_MAX - 1
+ Assert.equal(s2.sum, valid.reduce((acc, cur) => acc + cur) - 3);
+ Assert.deepEqual(s2.range, [1, 250000]);
+ Assert.deepEqual(s2.values, { 0: 1, 1: 3, 250000: 3 });
+});
+
+add_task(async function test_non_array_non_string_obj() {
+ let invalid_obj = {
+ prop1: "someValue",
+ prop2: "someOtherValue",
+ };
+ let key = "someString";
+
+ let h = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_LINEAR");
+ h.clear();
+
+ h.add(key, invalid_obj);
+ Assert.equal(h.keys().length, 0);
+});
+
+add_task(
+ {
+ skip_if: () => gIsAndroid,
+ },
+ async function test_productSpecificHistograms() {
+ const DEFAULT_PRODUCTS_HISTOGRAM = "TELEMETRY_TEST_DEFAULT_PRODUCTS";
+ const DESKTOP_ONLY_HISTOGRAM = "TELEMETRY_TEST_DESKTOP_ONLY";
+ const MULTIPRODUCT_HISTOGRAM = "TELEMETRY_TEST_MULTIPRODUCT";
+ const MOBILE_ONLY_HISTOGRAM = "TELEMETRY_TEST_MOBILE_ONLY";
+
+ var default_histo = Telemetry.getHistogramById(DEFAULT_PRODUCTS_HISTOGRAM);
+ var desktop_histo = Telemetry.getHistogramById(DESKTOP_ONLY_HISTOGRAM);
+ var multiproduct_histo = Telemetry.getHistogramById(MULTIPRODUCT_HISTOGRAM);
+ var mobile_histo = Telemetry.getHistogramById(MOBILE_ONLY_HISTOGRAM);
+ default_histo.clear();
+ desktop_histo.clear();
+ multiproduct_histo.clear();
+ mobile_histo.clear();
+
+ default_histo.add(42);
+ desktop_histo.add(42);
+ multiproduct_histo.add(42);
+ mobile_histo.add(42);
+
+ let histograms = Telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+
+ Assert.ok(
+ DEFAULT_PRODUCTS_HISTOGRAM in histograms,
+ "Should have recorded default products histogram"
+ );
+ Assert.ok(
+ DESKTOP_ONLY_HISTOGRAM in histograms,
+ "Should have recorded desktop-only histogram"
+ );
+ Assert.ok(
+ MULTIPRODUCT_HISTOGRAM in histograms,
+ "Should have recorded multiproduct histogram"
+ );
+
+ Assert.ok(
+ !(MOBILE_ONLY_HISTOGRAM in histograms),
+ "Should not have recorded mobile-only histogram"
+ );
+ }
+);
+
+add_task(
+ {
+ skip_if: () => !gIsAndroid,
+ },
+ async function test_mobileSpecificHistograms() {
+ const DEFAULT_PRODUCTS_HISTOGRAM = "TELEMETRY_TEST_DEFAULT_PRODUCTS";
+ const DESKTOP_ONLY_HISTOGRAM = "TELEMETRY_TEST_DESKTOP_ONLY";
+ const MULTIPRODUCT_HISTOGRAM = "TELEMETRY_TEST_MULTIPRODUCT";
+ const MOBILE_ONLY_HISTOGRAM = "TELEMETRY_TEST_MOBILE_ONLY";
+
+ var default_histo = Telemetry.getHistogramById(DEFAULT_PRODUCTS_HISTOGRAM);
+ var desktop_histo = Telemetry.getHistogramById(DESKTOP_ONLY_HISTOGRAM);
+ var multiproduct_histo = Telemetry.getHistogramById(MULTIPRODUCT_HISTOGRAM);
+ var mobile_histo = Telemetry.getHistogramById(MOBILE_ONLY_HISTOGRAM);
+ default_histo.clear();
+ desktop_histo.clear();
+ multiproduct_histo.clear();
+ mobile_histo.clear();
+
+ default_histo.add(1);
+ desktop_histo.add(1);
+ multiproduct_histo.add(1);
+ mobile_histo.add(1);
+
+ let histograms = Telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+
+ Assert.ok(
+ DEFAULT_PRODUCTS_HISTOGRAM in histograms,
+ "Should have recorded default products histogram"
+ );
+ Assert.ok(
+ MOBILE_ONLY_HISTOGRAM in histograms,
+ "Should have recorded mobile-only histogram"
+ );
+ Assert.ok(
+ MULTIPRODUCT_HISTOGRAM in histograms,
+ "Should have recorded multiproduct histogram"
+ );
+
+ Assert.ok(
+ !(DESKTOP_ONLY_HISTOGRAM in histograms),
+ "Should not have recorded desktop-only histogram"
+ );
+ }
+);
+
+add_task(async function test_productsOverride() {
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+ );
+ const DEFAULT_PRODUCTS_HISTOGRAM = "TELEMETRY_TEST_DEFAULT_PRODUCTS";
+ const DESKTOP_ONLY_HISTOGRAM = "TELEMETRY_TEST_DESKTOP_ONLY";
+ const MULTIPRODUCT_HISTOGRAM = "TELEMETRY_TEST_MULTIPRODUCT";
+ const MOBILE_ONLY_HISTOGRAM = "TELEMETRY_TEST_MOBILE_ONLY";
+
+ var default_histo = Telemetry.getHistogramById(DEFAULT_PRODUCTS_HISTOGRAM);
+ var desktop_histo = Telemetry.getHistogramById(DESKTOP_ONLY_HISTOGRAM);
+ var multiproduct_histo = Telemetry.getHistogramById(MULTIPRODUCT_HISTOGRAM);
+ var mobile_histo = Telemetry.getHistogramById(MOBILE_ONLY_HISTOGRAM);
+ default_histo.clear();
+ desktop_histo.clear();
+ multiproduct_histo.clear();
+ mobile_histo.clear();
+
+ default_histo.add(1);
+ desktop_histo.add(1);
+ multiproduct_histo.add(1);
+ mobile_histo.add(1);
+
+ let histograms = Telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+
+ Assert.ok(
+ DEFAULT_PRODUCTS_HISTOGRAM in histograms,
+ "Should have recorded default products histogram"
+ );
+ Assert.ok(
+ MOBILE_ONLY_HISTOGRAM in histograms,
+ "Should have recorded mobile-only histogram"
+ );
+ Assert.ok(
+ MULTIPRODUCT_HISTOGRAM in histograms,
+ "Should have recorded multiproduct histogram"
+ );
+
+ Assert.ok(
+ DESKTOP_ONLY_HISTOGRAM in histograms,
+ "Should not have recorded desktop-only histogram"
+ );
+ Services.prefs.clearUserPref(
+ "toolkit.telemetry.testing.overrideProductsCheck"
+ );
+});
+
+add_task(
+ {
+ skip_if: () => gIsAndroid,
+ },
+ async function test_clearHistogramsOnSnapshot() {
+ const COUNT = "TELEMETRY_TEST_COUNT";
+ let h = Telemetry.getHistogramById(COUNT);
+ h.clear();
+ let snapshot;
+
+ // The first snapshot should be empty, nothing recorded.
+ snapshot = Telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+ Assert.ok(!(COUNT in snapshot));
+
+ // After recording into a histogram, the data should be in the snapshot. Don't delete it.
+ h.add(1);
+
+ Assert.equal(h.snapshot().sum, 1);
+ snapshot = Telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+ Assert.ok(COUNT in snapshot);
+ Assert.equal(snapshot[COUNT].sum, 1);
+
+ // After recording into a histogram again, the data should be updated and in the snapshot.
+ // Clean up after.
+ h.add(41);
+
+ Assert.equal(h.snapshot().sum, 42);
+ snapshot = Telemetry.getSnapshotForHistograms(
+ "main",
+ true /* clear */
+ ).parent;
+ Assert.ok(COUNT in snapshot);
+ Assert.equal(snapshot[COUNT].sum, 42);
+
+ // Finally, no data should be in the snapshot.
+ Assert.equal(h.snapshot().sum, 0);
+ snapshot = Telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+ Assert.ok(!(COUNT in snapshot));
+ }
+);
+
+add_task(async function test_valid_os_smoketest() {
+ let nonExistingProbe;
+ let existingProbe;
+
+ switch (AppConstants.platform) {
+ case "linux":
+ nonExistingProbe = "TELEMETRY_TEST_OS_ANDROID_ONLY";
+ existingProbe = "TELEMETRY_TEST_OS_LINUX_ONLY";
+ break;
+ case "macosx":
+ nonExistingProbe = "TELEMETRY_TEST_OS_ANDROID_ONLY";
+ existingProbe = "TELEMETRY_TEST_OS_MAC_ONLY";
+ break;
+ case "win":
+ nonExistingProbe = "TELEMETRY_TEST_OS_ANDROID_ONLY";
+ existingProbe = "TELEMETRY_TEST_OS_WIN_ONLY";
+ break;
+ case "android":
+ nonExistingProbe = "TELEMETRY_TEST_OS_LINUX_ONLY";
+ existingProbe = "TELEMETRY_TEST_OS_ANDROID_ONLY";
+ break;
+ default:
+ /* Unknown OS. Let's not test OS-specific probes */
+ return;
+ }
+
+ Assert.throws(
+ () => Telemetry.getHistogramById(nonExistingProbe),
+ /NS_ERROR_FAILURE/,
+ `Should throw on ${nonExistingProbe} probe that's not available on ${AppConstants.platform}`
+ );
+
+ let h = Telemetry.getHistogramById(existingProbe);
+ h.clear();
+ h.add(1);
+ let snapshot = Telemetry.getSnapshotForHistograms(
+ "main",
+ false /* clear */
+ ).parent;
+ Assert.ok(
+ existingProbe in snapshot,
+ `${existingProbe} should be recorded on ${AppConstants.platform}`
+ );
+ Assert.equal(snapshot[existingProbe].sum, 1);
+});
+
+add_task(async function test_multistore_individual_histogram() {
+ Telemetry.canRecordExtended = true;
+
+ let id;
+ let hist;
+ let snapshot;
+
+ id = "TELEMETRY_TEST_MAIN_ONLY";
+ hist = Telemetry.getHistogramById(id);
+ snapshot = hist.snapshot();
+ Assert.equal(0, snapshot.sum, `Histogram ${id} should be empty.`);
+ hist.add(1);
+ snapshot = hist.snapshot();
+ Assert.equal(
+ 1,
+ snapshot.sum,
+ `Histogram ${id} should have recorded one value.`
+ );
+ hist.clear();
+ snapshot = hist.snapshot();
+ Assert.equal(0, snapshot.sum, `Histogram ${id} should be cleared.`);
+
+ id = "TELEMETRY_TEST_MULTIPLE_STORES";
+ hist = Telemetry.getHistogramById(id);
+ snapshot = hist.snapshot();
+ Assert.equal(0, snapshot.sum, `Histogram ${id} should be empty.`);
+ hist.add(1);
+ snapshot = hist.snapshot();
+ Assert.equal(
+ 1,
+ snapshot.sum,
+ `Histogram ${id} should have recorded one value.`
+ );
+ hist.clear();
+ snapshot = hist.snapshot();
+ Assert.equal(0, snapshot.sum, `Histogram ${id} should be cleared.`);
+
+ // When sync only, then the snapshot will be empty on the main store
+ id = "TELEMETRY_TEST_SYNC_ONLY";
+ hist = Telemetry.getHistogramById(id);
+ snapshot = hist.snapshot();
+ Assert.equal(
+ undefined,
+ snapshot,
+ `Histogram ${id} should not be in the 'main' storage`
+ );
+ hist.add(1);
+ snapshot = hist.snapshot();
+ Assert.equal(
+ undefined,
+ snapshot,
+ `Histogram ${id} should not be in the 'main' storage`
+ );
+ hist.clear();
+ snapshot = hist.snapshot();
+ Assert.equal(
+ undefined,
+ snapshot,
+ `Histogram ${id} should not be in the 'main' storage`
+ );
+
+ id = "TELEMETRY_TEST_KEYED_MULTIPLE_STORES";
+ hist = Telemetry.getKeyedHistogramById(id);
+ snapshot = hist.snapshot();
+ Assert.deepEqual({}, snapshot, `Histogram ${id} should be empty.`);
+ hist.add("key-a", 1);
+ snapshot = hist.snapshot();
+ Assert.equal(
+ 1,
+ snapshot["key-a"].sum,
+ `Histogram ${id} should have recorded one value.`
+ );
+ hist.clear();
+ snapshot = hist.snapshot();
+ Assert.deepEqual({}, snapshot, `Histogram ${id} should be cleared.`);
+
+ // When sync only, then the snapshot will be empty on the main store
+ id = "TELEMETRY_TEST_KEYED_SYNC_ONLY";
+ hist = Telemetry.getKeyedHistogramById(id);
+ snapshot = hist.snapshot();
+ Assert.equal(
+ undefined,
+ snapshot,
+ `Histogram ${id} should not be in the 'main' storage`
+ );
+ hist.add("key-a", 1);
+ snapshot = hist.snapshot();
+ Assert.equal(
+ undefined,
+ snapshot,
+ `Histogram ${id} should not be in the 'main' storage`
+ );
+ hist.clear();
+ snapshot = hist.snapshot();
+ Assert.equal(
+ undefined,
+ snapshot,
+ `Histogram ${id} should not be in the 'main' storage`
+ );
+});
+
+add_task(async function test_multistore_main_snapshot() {
+ Telemetry.canRecordExtended = true;
+ // Clear histograms
+ Telemetry.getSnapshotForHistograms("main", true);
+ Telemetry.getSnapshotForKeyedHistograms("main", true);
+
+ let id;
+ let hist;
+ let snapshot;
+
+ // Plain histograms
+
+ // Fill with data
+ id = "TELEMETRY_TEST_MAIN_ONLY";
+ hist = Telemetry.getHistogramById(id);
+ hist.add(1);
+
+ id = "TELEMETRY_TEST_MULTIPLE_STORES";
+ hist = Telemetry.getHistogramById(id);
+ hist.add(1);
+
+ id = "TELEMETRY_TEST_SYNC_ONLY";
+ hist = Telemetry.getHistogramById(id);
+ hist.add(1);
+
+ // Getting snapshot and NOT clearing (using default values for optional parameters)
+ snapshot = Telemetry.getSnapshotForHistograms().parent;
+ id = "TELEMETRY_TEST_MAIN_ONLY";
+ Assert.ok(id in snapshot, `${id} should be in a main store snapshot`);
+ id = "TELEMETRY_TEST_MULTIPLE_STORES";
+ Assert.ok(id in snapshot, `${id} should be in a main store snapshot`);
+ id = "TELEMETRY_TEST_SYNC_ONLY";
+ Assert.ok(!(id in snapshot), `${id} should not be in a main store snapshot`);
+
+ // Data should still be in, getting snapshot and clearing
+ snapshot = Telemetry.getSnapshotForHistograms(
+ "main",
+ /* clear */ true
+ ).parent;
+ id = "TELEMETRY_TEST_MAIN_ONLY";
+ Assert.ok(id in snapshot, `${id} should be in a main store snapshot`);
+ id = "TELEMETRY_TEST_MULTIPLE_STORES";
+ Assert.ok(id in snapshot, `${id} should be in a main store snapshot`);
+ id = "TELEMETRY_TEST_SYNC_ONLY";
+ Assert.ok(!(id in snapshot), `${id} should not be in a main store snapshot`);
+
+ // Should be empty after clearing
+ snapshot = Telemetry.getSnapshotForHistograms(
+ "main",
+ /* clear */ false
+ ).parent;
+ id = "TELEMETRY_TEST_MAIN_ONLY";
+ Assert.ok(!(id in snapshot), `${id} should not be in a main store snapshot`);
+ id = "TELEMETRY_TEST_MULTIPLE_STORES";
+ Assert.ok(!(id in snapshot), `${id} should not be in a main store snapshot`);
+ id = "TELEMETRY_TEST_SYNC_ONLY";
+ Assert.ok(!(id in snapshot), `${id} should not be in a main store snapshot`);
+
+ // Keyed histograms
+
+ // Fill with data
+ id = "TELEMETRY_TEST_KEYED_MULTIPLE_STORES";
+ hist = Telemetry.getKeyedHistogramById(id);
+ hist.add("key-a", 1);
+
+ id = "TELEMETRY_TEST_KEYED_SYNC_ONLY";
+ hist = Telemetry.getKeyedHistogramById(id);
+ hist.add("key-b", 1);
+
+ // Getting snapshot and NOT clearing (using default values for optional parameters)
+ snapshot = Telemetry.getSnapshotForKeyedHistograms().parent;
+ id = "TELEMETRY_TEST_KEYED_MULTIPLE_STORES";
+ Assert.ok(id in snapshot, `${id} should be in a main store snapshot`);
+ id = "TELEMETRY_TEST_KEYED_SYNC_ONLY";
+ Assert.ok(!(id in snapshot), `${id} should not be in a main store snapshot`);
+
+ // Data should still be in, getting snapshot and clearing
+ snapshot = Telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ /* clear */ true
+ ).parent;
+ id = "TELEMETRY_TEST_KEYED_MULTIPLE_STORES";
+ Assert.ok(id in snapshot, `${id} should be in a main store snapshot`);
+ id = "TELEMETRY_TEST_KEYED_SYNC_ONLY";
+ Assert.ok(!(id in snapshot), `${id} should not be in a main store snapshot`);
+
+ // Should be empty after clearing
+ snapshot = Telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ /* clear */ false
+ ).parent;
+ id = "TELEMETRY_TEST_KEYED_MULTIPLE_STORES";
+ Assert.ok(!(id in snapshot), `${id} should not be in a main store snapshot`);
+ id = "TELEMETRY_TEST_KEYED_SYNC_ONLY";
+ Assert.ok(!(id in snapshot), `${id} should not be in a main store snapshot`);
+});
+
+add_task(async function test_multistore_argument_handling() {
+ Telemetry.canRecordExtended = true;
+ // Clear histograms
+ Telemetry.getSnapshotForHistograms("main", true);
+ Telemetry.getSnapshotForHistograms("sync", true);
+ Telemetry.getSnapshotForKeyedHistograms("main", true);
+ Telemetry.getSnapshotForKeyedHistograms("sync", true);
+
+ let id;
+ let hist;
+ let snapshot;
+
+ // Plain Histograms
+
+ id = "TELEMETRY_TEST_MULTIPLE_STORES";
+ hist = Telemetry.getHistogramById(id);
+ hist.add(37);
+
+ // No argument
+ snapshot = hist.snapshot();
+ Assert.equal(37, snapshot.sum, `${id} should be in a default store snapshot`);
+
+ hist.clear();
+ snapshot = hist.snapshot();
+ Assert.equal(0, snapshot.sum, `${id} should be cleared in the default store`);
+
+ snapshot = hist.snapshot({ store: "sync" });
+ Assert.equal(
+ 37,
+ snapshot.sum,
+ `${id} should not have been cleared in the sync store`
+ );
+
+ Assert.throws(
+ () => hist.snapshot(2, "or", "more", "arguments"),
+ /one argument/,
+ "snapshot should check argument count"
+ );
+ Assert.throws(
+ () => hist.snapshot(2),
+ /object argument/,
+ "snapshot should check argument type"
+ );
+ Assert.throws(
+ () => hist.snapshot({}),
+ /property/,
+ "snapshot should check for object property"
+ );
+ Assert.throws(
+ () => hist.snapshot({ store: 1 }),
+ /string/,
+ "snapshot should check object property's type"
+ );
+
+ Assert.throws(
+ () => hist.clear(2, "or", "more", "arguments"),
+ /one argument/,
+ "clear should check argument count"
+ );
+ Assert.throws(
+ () => hist.clear(2),
+ /object argument/,
+ "clear should check argument type"
+ );
+ Assert.throws(
+ () => hist.clear({}),
+ /property/,
+ "clear should check for object property"
+ );
+ Assert.throws(
+ () => hist.clear({ store: 1 }),
+ /string/,
+ "clear should check object property's type"
+ );
+
+ // Keyed Histogram
+
+ id = "TELEMETRY_TEST_KEYED_MULTIPLE_STORES";
+ hist = Telemetry.getKeyedHistogramById(id);
+ hist.add("key-1", 37);
+
+ // No argument
+ snapshot = hist.snapshot();
+ Assert.equal(
+ 37,
+ snapshot["key-1"].sum,
+ `${id} should be in a default store snapshot`
+ );
+
+ hist.clear();
+ snapshot = hist.snapshot();
+ Assert.ok(
+ !("key-1" in snapshot),
+ `${id} should be cleared in the default store`
+ );
+
+ snapshot = hist.snapshot({ store: "sync" });
+ Assert.equal(
+ 37,
+ snapshot["key-1"].sum,
+ `${id} should not have been cleared in the sync store`
+ );
+
+ Assert.throws(
+ () => hist.snapshot(2, "or", "more", "arguments"),
+ /one argument/,
+ "snapshot should check argument count"
+ );
+ Assert.throws(
+ () => hist.snapshot(2),
+ /object argument/,
+ "snapshot should check argument type"
+ );
+ Assert.throws(
+ () => hist.snapshot({}),
+ /property/,
+ "snapshot should check for object property"
+ );
+ Assert.throws(
+ () => hist.snapshot({ store: 1 }),
+ /string/,
+ "snapshot should check object property's type"
+ );
+
+ Assert.throws(
+ () => hist.clear(2, "or", "more", "arguments"),
+ /one argument/,
+ "clear should check argument count"
+ );
+ Assert.throws(
+ () => hist.clear(2),
+ /object argument/,
+ "clear should check argument type"
+ );
+ Assert.throws(
+ () => hist.clear({}),
+ /property/,
+ "clear should check for object property"
+ );
+ Assert.throws(
+ () => hist.clear({ store: 1 }),
+ /string/,
+ "clear should check object property's type"
+ );
+});
+
+add_task(async function test_multistore_sync_snapshot() {
+ Telemetry.canRecordExtended = true;
+ // Clear histograms
+ Telemetry.getSnapshotForHistograms("main", true);
+ Telemetry.getSnapshotForHistograms("sync", true);
+
+ let id;
+ let hist;
+ let snapshot;
+
+ // Plain histograms
+
+ // Fill with data
+ id = "TELEMETRY_TEST_MAIN_ONLY";
+ hist = Telemetry.getHistogramById(id);
+ hist.add(1);
+
+ id = "TELEMETRY_TEST_MULTIPLE_STORES";
+ hist = Telemetry.getHistogramById(id);
+ hist.add(1);
+
+ id = "TELEMETRY_TEST_SYNC_ONLY";
+ hist = Telemetry.getHistogramById(id);
+ hist.add(1);
+
+ // Getting snapshot and clearing
+ snapshot = Telemetry.getSnapshotForHistograms(
+ "main",
+ /* clear */ true
+ ).parent;
+ id = "TELEMETRY_TEST_MAIN_ONLY";
+ Assert.ok(id in snapshot, `${id} should be in a main store snapshot`);
+ id = "TELEMETRY_TEST_MULTIPLE_STORES";
+ Assert.ok(id in snapshot, `${id} should be in a main store snapshot`);
+ id = "TELEMETRY_TEST_SYNC_ONLY";
+ Assert.ok(!(id in snapshot), `${id} should not be in a main store snapshot`);
+
+ snapshot = Telemetry.getSnapshotForHistograms(
+ "sync",
+ /* clear */ true
+ ).parent;
+ id = "TELEMETRY_TEST_MAIN_ONLY";
+ Assert.ok(!(id in snapshot), `${id} should not be in a sync store snapshot`);
+ id = "TELEMETRY_TEST_MULTIPLE_STORES";
+ Assert.ok(id in snapshot, `${id} should be in a sync store snapshot`);
+ id = "TELEMETRY_TEST_SYNC_ONLY";
+ Assert.ok(id in snapshot, `${id} should be in a sync store snapshot`);
+});
+
+add_task(async function test_multistore_keyed_sync_snapshot() {
+ Telemetry.canRecordExtended = true;
+ // Clear histograms
+ Telemetry.getSnapshotForKeyedHistograms("main", true);
+ Telemetry.getSnapshotForKeyedHistograms("sync", true);
+
+ let id;
+ let hist;
+ let snapshot;
+
+ // Plain histograms
+
+ // Fill with data
+ id = "TELEMETRY_TEST_KEYED_LINEAR";
+ hist = Telemetry.getKeyedHistogramById(id);
+ hist.add("key-1", 1);
+
+ id = "TELEMETRY_TEST_KEYED_MULTIPLE_STORES";
+ hist = Telemetry.getKeyedHistogramById(id);
+ hist.add("key-1", 1);
+
+ id = "TELEMETRY_TEST_KEYED_SYNC_ONLY";
+ hist = Telemetry.getKeyedHistogramById(id);
+ hist.add("key-1", 1);
+
+ // Getting snapshot and clearing
+ snapshot = Telemetry.getSnapshotForKeyedHistograms(
+ "main",
+ /* clear */ true
+ ).parent;
+ id = "TELEMETRY_TEST_KEYED_LINEAR";
+ Assert.ok(id in snapshot, `${id} should be in a main store snapshot`);
+ id = "TELEMETRY_TEST_KEYED_MULTIPLE_STORES";
+ Assert.ok(id in snapshot, `${id} should be in a main store snapshot`);
+ id = "TELEMETRY_TEST_KEYED_SYNC_ONLY";
+ Assert.ok(!(id in snapshot), `${id} should not be in a main store snapshot`);
+
+ snapshot = Telemetry.getSnapshotForKeyedHistograms(
+ "sync",
+ /* clear */ true
+ ).parent;
+ id = "TELEMETRY_TEST_KEYED_LINEAR";
+ Assert.ok(!(id in snapshot), `${id} should not be in a sync store snapshot`);
+ id = "TELEMETRY_TEST_KEYED_MULTIPLE_STORES";
+ Assert.ok(id in snapshot, `${id} should be in a sync store snapshot`);
+ id = "TELEMETRY_TEST_KEYED_SYNC_ONLY";
+ Assert.ok(id in snapshot, `${id} should be in a sync store snapshot`);
+});
+
+add_task(async function test_multistore_plain_individual_snapshot() {
+ Telemetry.canRecordExtended = true;
+ // Clear histograms
+ Telemetry.getSnapshotForHistograms("main", true);
+ Telemetry.getSnapshotForHistograms("sync", true);
+
+ let id;
+ let hist;
+
+ id = "TELEMETRY_TEST_MAIN_ONLY";
+ hist = Telemetry.getHistogramById(id);
+
+ hist.add(37);
+ Assert.deepEqual(37, hist.snapshot({ store: "main" }).sum);
+ Assert.deepEqual(undefined, hist.snapshot({ store: "sync" }));
+
+ hist.clear({ store: "main" });
+ Assert.deepEqual(0, hist.snapshot({ store: "main" }).sum);
+ Assert.deepEqual(undefined, hist.snapshot({ store: "sync" }));
+
+ id = "TELEMETRY_TEST_MULTIPLE_STORES";
+ hist = Telemetry.getHistogramById(id);
+
+ hist.add(37);
+ Assert.deepEqual(37, hist.snapshot({ store: "main" }).sum);
+ Assert.deepEqual(37, hist.snapshot({ store: "sync" }).sum);
+
+ hist.clear({ store: "main" });
+ Assert.deepEqual(0, hist.snapshot({ store: "main" }).sum);
+ Assert.deepEqual(37, hist.snapshot({ store: "sync" }).sum);
+
+ hist.add(3);
+ Assert.deepEqual(3, hist.snapshot({ store: "main" }).sum);
+ Assert.deepEqual(40, hist.snapshot({ store: "sync" }).sum);
+
+ hist.clear({ store: "sync" });
+ Assert.deepEqual(3, hist.snapshot({ store: "main" }).sum);
+ Assert.deepEqual(0, hist.snapshot({ store: "sync" }).sum);
+
+ id = "TELEMETRY_TEST_SYNC_ONLY";
+ hist = Telemetry.getHistogramById(id);
+
+ hist.add(37);
+ Assert.deepEqual(undefined, hist.snapshot({ store: "main" }));
+ Assert.deepEqual(37, hist.snapshot({ store: "sync" }).sum);
+
+ hist.clear({ store: "main" });
+ Assert.deepEqual(undefined, hist.snapshot({ store: "main" }));
+ Assert.deepEqual(37, hist.snapshot({ store: "sync" }).sum);
+
+ hist.add(3);
+ Assert.deepEqual(undefined, hist.snapshot({ store: "main" }));
+ Assert.deepEqual(40, hist.snapshot({ store: "sync" }).sum);
+
+ hist.clear({ store: "sync" });
+ Assert.deepEqual(undefined, hist.snapshot({ store: "main" }));
+ Assert.deepEqual(0, hist.snapshot({ store: "sync" }).sum);
+});
+
+add_task(async function test_multistore_keyed_individual_snapshot() {
+ Telemetry.canRecordExtended = true;
+ // Clear histograms
+ Telemetry.getSnapshotForKeyedHistograms("main", true);
+ Telemetry.getSnapshotForKeyedHistograms("sync", true);
+
+ let id;
+ let hist;
+
+ id = "TELEMETRY_TEST_KEYED_LINEAR";
+ hist = Telemetry.getKeyedHistogramById(id);
+
+ hist.add("key-1", 37);
+ Assert.deepEqual(37, hist.snapshot({ store: "main" })["key-1"].sum);
+ Assert.deepEqual(undefined, hist.snapshot({ store: "sync" }));
+
+ hist.clear({ store: "main" });
+ Assert.deepEqual({}, hist.snapshot({ store: "main" }));
+ Assert.deepEqual(undefined, hist.snapshot({ store: "sync" }));
+
+ hist.add("key-1", 4);
+ hist.clear({ store: "sync" });
+ Assert.deepEqual(4, hist.snapshot({ store: "main" })["key-1"].sum);
+ Assert.deepEqual(undefined, hist.snapshot({ store: "sync" }));
+
+ id = "TELEMETRY_TEST_KEYED_MULTIPLE_STORES";
+ hist = Telemetry.getKeyedHistogramById(id);
+
+ hist.add("key-1", 37);
+ Assert.deepEqual(37, hist.snapshot({ store: "main" })["key-1"].sum);
+ Assert.deepEqual(37, hist.snapshot({ store: "sync" })["key-1"].sum);
+
+ hist.clear({ store: "main" });
+ Assert.deepEqual({}, hist.snapshot({ store: "main" }));
+ Assert.deepEqual(37, hist.snapshot({ store: "sync" })["key-1"].sum);
+
+ hist.add("key-1", 3);
+ Assert.deepEqual(3, hist.snapshot({ store: "main" })["key-1"].sum);
+ Assert.deepEqual(40, hist.snapshot({ store: "sync" })["key-1"].sum);
+
+ hist.clear({ store: "sync" });
+ Assert.deepEqual(3, hist.snapshot({ store: "main" })["key-1"].sum);
+ Assert.deepEqual({}, hist.snapshot({ store: "sync" }));
+
+ id = "TELEMETRY_TEST_KEYED_SYNC_ONLY";
+ hist = Telemetry.getKeyedHistogramById(id);
+
+ hist.add("key-1", 37);
+ Assert.deepEqual(undefined, hist.snapshot({ store: "main" }));
+ Assert.deepEqual(37, hist.snapshot({ store: "sync" })["key-1"].sum);
+
+ hist.clear({ store: "main" });
+ Assert.deepEqual(undefined, hist.snapshot({ store: "main" }));
+ Assert.deepEqual(37, hist.snapshot({ store: "sync" })["key-1"].sum);
+
+ hist.add("key-1", 3);
+ Assert.deepEqual(undefined, hist.snapshot({ store: "main" }));
+ Assert.deepEqual(40, hist.snapshot({ store: "sync" })["key-1"].sum);
+
+ hist.clear({ store: "sync" });
+ Assert.deepEqual(undefined, hist.snapshot({ store: "main" }));
+ Assert.deepEqual({}, hist.snapshot({ store: "sync" }));
+});
+
+add_task(async function test_can_record_in_process_regression_bug_1530361() {
+ Telemetry.getSnapshotForHistograms("main", true);
+
+ // The socket and gpu processes should not have any histograms.
+ // Flag and count histograms have defaults, so if we're accidentally recording them
+ // in these processes they'd show up even immediately after being cleared.
+ let snapshot = Telemetry.getSnapshotForHistograms("main", true);
+
+ Assert.deepEqual(
+ snapshot.gpu,
+ {},
+ "No histograms should have been recorded for the gpu process"
+ );
+ Assert.deepEqual(
+ snapshot.socket,
+ {},
+ "No histograms should have been recorded for the socket process"
+ );
+});
+
+add_task(function test_knows_its_name() {
+ let h;
+
+ // Plain histograms
+ const histNames = [
+ "TELEMETRY_TEST_FLAG",
+ "TELEMETRY_TEST_COUNT",
+ "TELEMETRY_TEST_CATEGORICAL",
+ "TELEMETRY_TEST_EXPIRED",
+ ];
+
+ for (let name of histNames) {
+ h = Telemetry.getHistogramById(name);
+ Assert.equal(name, h.name());
+ }
+
+ // Keyed histograms
+ const keyedHistNames = [
+ "TELEMETRY_TEST_KEYED_EXPONENTIAL",
+ "TELEMETRY_TEST_KEYED_BOOLEAN",
+ "TELEMETRY_TEST_EXPIRED_KEYED",
+ ];
+
+ for (let name of keyedHistNames) {
+ h = Telemetry.getKeyedHistogramById(name);
+ Assert.equal(name, h.name());
+ }
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryLateWrites.js b/toolkit/components/telemetry/tests/unit/test_TelemetryLateWrites.js
new file mode 100644
index 0000000000..00891c36e8
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryLateWrites.js
@@ -0,0 +1,143 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+/* A testcase to make sure reading late writes stacks works. */
+
+// Constants from prio.h for nsIFileOutputStream.init
+const PR_WRONLY = 0x2;
+const PR_CREATE_FILE = 0x8;
+const PR_TRUNCATE = 0x20;
+const RW_OWNER = parseInt("0600", 8);
+
+const STACK_SUFFIX1 = "stack1.txt";
+const STACK_SUFFIX2 = "stack2.txt";
+const STACK_BOGUS_SUFFIX = "bogus.txt";
+const LATE_WRITE_PREFIX = "Telemetry.LateWriteFinal-";
+
+// The names and IDs don't matter, but the format of the IDs does.
+const LOADED_MODULES = {
+ "4759A7E6993548C89CAF716A67EC242D00": "libtest.so",
+ F77AF15BB8D6419FA875954B4A3506CA00: "libxul.so",
+ "1E2F7FB590424E8F93D60BB88D66B8C500": "libc.so",
+ E4D6D70CC09A63EF8B88D532F867858800: "libmodμles.so",
+};
+const N_MODULES = Object.keys(LOADED_MODULES).length;
+
+// Format of individual items is [index, offset-in-library].
+const STACK1 = [
+ [0, 0],
+ [1, 1],
+ [2, 2],
+ [3, 3],
+];
+const STACK2 = [
+ [0, 0],
+ [1, 5],
+ [2, 10],
+ [3, 15],
+];
+// XXX The only error checking is for a zero-sized stack.
+const STACK_BOGUS = [];
+
+function write_string_to_file(file, contents) {
+ let ostream = Cc[
+ "@mozilla.org/network/safe-file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ ostream.init(
+ file,
+ PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE,
+ RW_OWNER,
+ ostream.DEFER_OPEN
+ );
+
+ var bos = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(
+ Ci.nsIBinaryOutputStream
+ );
+ bos.setOutputStream(ostream);
+
+ let utf8 = new TextEncoder().encode(contents);
+ bos.writeByteArray(utf8);
+ ostream.QueryInterface(Ci.nsISafeOutputStream).finish();
+ ostream.close();
+}
+
+function construct_file(suffix) {
+ let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let file = profileDirectory.clone();
+ file.append(LATE_WRITE_PREFIX + suffix);
+ return file;
+}
+
+function write_late_writes_file(stack, suffix) {
+ let file = construct_file(suffix);
+ let contents = N_MODULES + "\n";
+ for (let id in LOADED_MODULES) {
+ contents += id + " " + LOADED_MODULES[id] + "\n";
+ }
+
+ contents += stack.length + "\n";
+ for (let element of stack) {
+ contents += element[0] + " " + element[1].toString(16) + "\n";
+ }
+
+ write_string_to_file(file, contents);
+}
+
+function run_test() {
+ do_get_profile();
+
+ write_late_writes_file(STACK1, STACK_SUFFIX1);
+ write_late_writes_file(STACK2, STACK_SUFFIX2);
+ write_late_writes_file(STACK_BOGUS, STACK_BOGUS_SUFFIX);
+
+ let lateWrites = Telemetry.lateWrites;
+ Assert.ok("memoryMap" in lateWrites);
+ Assert.equal(lateWrites.memoryMap.length, 0);
+ Assert.ok("stacks" in lateWrites);
+ Assert.equal(lateWrites.stacks.length, 0);
+
+ do_test_pending();
+ Telemetry.asyncFetchTelemetryData(function () {
+ actual_test();
+ });
+}
+
+function actual_test() {
+ Assert.ok(!construct_file(STACK_SUFFIX1).exists());
+ Assert.ok(!construct_file(STACK_SUFFIX2).exists());
+ Assert.ok(!construct_file(STACK_BOGUS_SUFFIX).exists());
+
+ let lateWrites = Telemetry.lateWrites;
+
+ Assert.ok("memoryMap" in lateWrites);
+ Assert.equal(lateWrites.memoryMap.length, N_MODULES);
+ for (let id in LOADED_MODULES) {
+ let matchingLibrary = lateWrites.memoryMap.filter(function (
+ library,
+ idx,
+ array
+ ) {
+ return library[1] == id;
+ });
+ Assert.equal(matchingLibrary.length, 1);
+ let library = matchingLibrary[0];
+ let name = library[0];
+ Assert.equal(LOADED_MODULES[id], name);
+ }
+
+ Assert.ok("stacks" in lateWrites);
+ Assert.equal(lateWrites.stacks.length, 2);
+ let uneval_STACKS = [uneval(STACK1), uneval(STACK2)];
+ let first_stack = lateWrites.stacks[0];
+ let second_stack = lateWrites.stacks[1];
+ function stackChecker(canonicalStack) {
+ let unevalCanonicalStack = uneval(canonicalStack);
+ return function (obj, idx, array) {
+ return unevalCanonicalStack == obj;
+ };
+ }
+ Assert.equal(uneval_STACKS.filter(stackChecker(first_stack)).length, 1);
+ Assert.equal(uneval_STACKS.filter(stackChecker(second_stack)).length, 1);
+
+ do_test_finished();
+}
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryLockCount.js b/toolkit/components/telemetry/tests/unit/test_TelemetryLockCount.js
new file mode 100644
index 0000000000..7f9322e37c
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryLockCount.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+/* A testcase to make sure reading the failed profile lock count works. */
+
+const LOCK_FILE_NAME = "Telemetry.FailedProfileLocks.txt";
+const N_FAILED_LOCKS = 10;
+
+// Constants from prio.h for nsIFileOutputStream.init
+const PR_WRONLY = 0x2;
+const PR_CREATE_FILE = 0x8;
+const PR_TRUNCATE = 0x20;
+const RW_OWNER = parseInt("0600", 8);
+
+function write_string_to_file(file, contents) {
+ let ostream = Cc[
+ "@mozilla.org/network/safe-file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ ostream.init(
+ file,
+ PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE,
+ RW_OWNER,
+ ostream.DEFER_OPEN
+ );
+ ostream.write(contents, contents.length);
+ ostream.QueryInterface(Ci.nsISafeOutputStream).finish();
+ ostream.close();
+}
+
+function construct_file() {
+ let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let file = profileDirectory.clone();
+ file.append(LOCK_FILE_NAME);
+ return file;
+}
+
+function run_test() {
+ do_get_profile();
+
+ Assert.equal(Telemetry.failedProfileLockCount, 0);
+
+ write_string_to_file(construct_file(), N_FAILED_LOCKS.toString());
+
+ // Make sure that we're not eagerly reading the count now that the
+ // file exists.
+ Assert.equal(Telemetry.failedProfileLockCount, 0);
+
+ do_test_pending();
+ Telemetry.asyncFetchTelemetryData(actual_test);
+}
+
+function actual_test() {
+ Assert.equal(Telemetry.failedProfileLockCount, N_FAILED_LOCKS);
+ Assert.ok(!construct_file().exists());
+ do_test_finished();
+}
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryReportingPolicy.js b/toolkit/components/telemetry/tests/unit/test_TelemetryReportingPolicy.js
new file mode 100644
index 0000000000..01041172d0
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryReportingPolicy.js
@@ -0,0 +1,399 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test that TelemetryController sends close to shutdown don't lead
+// to AsyncShutdown timeouts.
+
+"use strict";
+
+const { TelemetryReportingPolicy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryReportingPolicy.sys.mjs"
+);
+const { UpdateUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/UpdateUtils.sys.mjs"
+);
+
+const TEST_CHANNEL = "TestChannelABC";
+
+const PREF_MINIMUM_CHANNEL_POLICY_VERSION =
+ TelemetryUtils.Preferences.MinimumPolicyVersion + ".channel-" + TEST_CHANNEL;
+
+function fakeShowPolicyTimeout(set, clear) {
+ let { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryReportingPolicy.sys.mjs"
+ );
+ Policy.setShowInfobarTimeout = set;
+ Policy.clearShowInfobarTimeout = clear;
+}
+
+function fakeResetAcceptedPolicy() {
+ Services.prefs.clearUserPref(TelemetryUtils.Preferences.AcceptedPolicyDate);
+ Services.prefs.clearUserPref(
+ TelemetryUtils.Preferences.AcceptedPolicyVersion
+ );
+}
+
+function setMinimumPolicyVersion(aNewPolicyVersion) {
+ const CHANNEL_NAME = UpdateUtils.getUpdateChannel(false);
+ // We might have channel-dependent minimum policy versions.
+ const CHANNEL_DEPENDENT_PREF =
+ TelemetryUtils.Preferences.MinimumPolicyVersion +
+ ".channel-" +
+ CHANNEL_NAME;
+
+ // Does the channel-dependent pref exist? If so, set its value.
+ if (Services.prefs.getIntPref(CHANNEL_DEPENDENT_PREF, undefined)) {
+ Services.prefs.setIntPref(CHANNEL_DEPENDENT_PREF, aNewPolicyVersion);
+ return;
+ }
+
+ // We don't have a channel specific minimum, so set the common one.
+ Services.prefs.setIntPref(
+ TelemetryUtils.Preferences.MinimumPolicyVersion,
+ aNewPolicyVersion
+ );
+}
+
+add_task(async function test_setup() {
+ // Addon manager needs a profile directory
+ do_get_profile(true);
+ await loadAddonManager(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "1.9.2"
+ );
+ finishAddonManagerStartup();
+ fakeIntlReady();
+
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+
+ // Don't bypass the notifications in this test, we'll fake it.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.BypassNotification,
+ false
+ );
+
+ TelemetryReportingPolicy.setup();
+});
+
+add_task(
+ {
+ // This tests initialises the search service, but that doesn't currently
+ // work on Android.
+ skip_if: () => AppConstants.platform == "android",
+ },
+ async function test_firstRun() {
+ await Services.search.init();
+
+ const FIRST_RUN_TIMEOUT_MSEC = 60 * 1000; // 60s
+ const OTHER_RUNS_TIMEOUT_MSEC = 10 * 1000; // 10s
+
+ Services.prefs.clearUserPref(TelemetryUtils.Preferences.FirstRun);
+
+ let startupTimeout = 0;
+ fakeShowPolicyTimeout(
+ (callback, timeout) => (startupTimeout = timeout),
+ () => {}
+ );
+ TelemetryReportingPolicy.reset();
+
+ Services.obs.notifyObservers(null, "sessionstore-windows-restored");
+ Assert.equal(
+ startupTimeout,
+ FIRST_RUN_TIMEOUT_MSEC,
+ "The infobar display timeout should be 60s on the first run."
+ );
+
+ // Run again, and check that we actually wait only 10 seconds.
+ TelemetryReportingPolicy.reset();
+ Services.obs.notifyObservers(null, "sessionstore-windows-restored");
+ Assert.equal(
+ startupTimeout,
+ OTHER_RUNS_TIMEOUT_MSEC,
+ "The infobar display timeout should be 10s on other runs."
+ );
+ }
+);
+
+add_task(async function test_prefs() {
+ TelemetryReportingPolicy.reset();
+
+ let now = fakeNow(2009, 11, 18);
+
+ // If the date is not valid (earlier than 2012), we don't regard the policy as accepted.
+ TelemetryReportingPolicy.testInfobarShown();
+ Assert.ok(!TelemetryReportingPolicy.testIsUserNotified());
+ Assert.equal(
+ Services.prefs.getStringPref(
+ TelemetryUtils.Preferences.AcceptedPolicyDate,
+ null
+ ),
+ 0,
+ "Invalid dates should not make the policy accepted."
+ );
+
+ // Check that the notification date and version are correctly saved to the prefs.
+ now = fakeNow(2012, 11, 18);
+ TelemetryReportingPolicy.testInfobarShown();
+ Assert.equal(
+ Services.prefs.getStringPref(
+ TelemetryUtils.Preferences.AcceptedPolicyDate,
+ null
+ ),
+ now.getTime(),
+ "A valid date must correctly be saved."
+ );
+
+ // Now that user is notified, check if we are allowed to upload.
+ Assert.ok(
+ TelemetryReportingPolicy.canUpload(),
+ "We must be able to upload after the policy is accepted."
+ );
+
+ // Disable submission and check that we're no longer allowed to upload.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.DataSubmissionEnabled,
+ false
+ );
+ Assert.ok(
+ !TelemetryReportingPolicy.canUpload(),
+ "We must not be able to upload if data submission is disabled."
+ );
+
+ // Turn the submission back on.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.DataSubmissionEnabled,
+ true
+ );
+ Assert.ok(
+ TelemetryReportingPolicy.canUpload(),
+ "We must be able to upload if data submission is enabled and the policy was accepted."
+ );
+
+ // Set a new minimum policy version and check that user is no longer notified.
+ let newMinimum =
+ Services.prefs.getIntPref(
+ TelemetryUtils.Preferences.CurrentPolicyVersion,
+ 1
+ ) + 1;
+ setMinimumPolicyVersion(newMinimum);
+ Assert.ok(
+ !TelemetryReportingPolicy.testIsUserNotified(),
+ "A greater minimum policy version must invalidate the policy and disable upload."
+ );
+
+ // Eventually accept the policy and make sure user is notified.
+ Services.prefs.setIntPref(
+ TelemetryUtils.Preferences.CurrentPolicyVersion,
+ newMinimum
+ );
+ TelemetryReportingPolicy.testInfobarShown();
+ Assert.ok(
+ TelemetryReportingPolicy.testIsUserNotified(),
+ "Accepting the policy again should show the user as notified."
+ );
+ Assert.ok(
+ TelemetryReportingPolicy.canUpload(),
+ "Accepting the policy again should let us upload data."
+ );
+
+ // Set a new, per channel, minimum policy version. Start by setting a test current channel.
+ Services.prefs
+ .getDefaultBranch("")
+ .setStringPref("app.update.channel", TEST_CHANNEL);
+
+ // Increase and set the new minimum version, then check that we're not notified anymore.
+ newMinimum++;
+ Services.prefs.setIntPref(PREF_MINIMUM_CHANNEL_POLICY_VERSION, newMinimum);
+ Assert.ok(
+ !TelemetryReportingPolicy.testIsUserNotified(),
+ "Increasing the minimum policy version should invalidate the policy."
+ );
+
+ // Eventually accept the policy and make sure user is notified.
+ Services.prefs.setIntPref(
+ TelemetryUtils.Preferences.CurrentPolicyVersion,
+ newMinimum
+ );
+ TelemetryReportingPolicy.testInfobarShown();
+ Assert.ok(
+ TelemetryReportingPolicy.testIsUserNotified(),
+ "Accepting the policy again should show the user as notified."
+ );
+ Assert.ok(
+ TelemetryReportingPolicy.canUpload(),
+ "Accepting the policy again should let us upload data."
+ );
+});
+
+add_task(async function test_migratePrefs() {
+ const DEPRECATED_FHR_PREFS = {
+ "datareporting.policy.dataSubmissionPolicyAccepted": true,
+ "datareporting.policy.dataSubmissionPolicyBypassAcceptance": true,
+ "datareporting.policy.dataSubmissionPolicyResponseType": "foxyeah",
+ "datareporting.policy.dataSubmissionPolicyResponseTime":
+ Date.now().toString(),
+ };
+
+ // Make sure the preferences are set before setting up the policy.
+ for (let name in DEPRECATED_FHR_PREFS) {
+ switch (typeof DEPRECATED_FHR_PREFS[name]) {
+ case "string":
+ Services.prefs.setStringPref(name, DEPRECATED_FHR_PREFS[name]);
+ break;
+ case "number":
+ Services.prefs.setIntPref(name, DEPRECATED_FHR_PREFS[name]);
+ break;
+ case "boolean":
+ Services.prefs.setBoolPref(name, DEPRECATED_FHR_PREFS[name]);
+ break;
+ }
+ }
+ // Set up the policy.
+ TelemetryReportingPolicy.reset();
+ // They should have been removed by now.
+ for (let name in DEPRECATED_FHR_PREFS) {
+ Assert.ok(
+ !Services.prefs.prefHasUserValue(name),
+ name + " should have been removed."
+ );
+ }
+});
+
+add_task(async function test_userNotifiedOfCurrentPolicy() {
+ fakeResetAcceptedPolicy();
+ TelemetryReportingPolicy.reset();
+
+ // User should be reported as not notified by default.
+ Assert.ok(
+ !TelemetryReportingPolicy.testIsUserNotified(),
+ "The initial state should be unnotified."
+ );
+
+ // Forcing a policy version should not automatically make the user notified.
+ Services.prefs.setIntPref(
+ TelemetryUtils.Preferences.AcceptedPolicyVersion,
+ TelemetryReportingPolicy.DEFAULT_DATAREPORTING_POLICY_VERSION
+ );
+ Assert.ok(
+ !TelemetryReportingPolicy.testIsUserNotified(),
+ "The default state of the date should have a time of 0 and it should therefore fail"
+ );
+
+ // Showing the notification bar should make the user notified.
+ fakeNow(2012, 11, 11);
+ TelemetryReportingPolicy.testInfobarShown();
+ Assert.ok(
+ TelemetryReportingPolicy.testIsUserNotified(),
+ "Using the proper API causes user notification to report as true."
+ );
+
+ // It is assumed that later versions of the policy will incorporate previous
+ // ones, therefore this should also return true.
+ let newVersion =
+ Services.prefs.getIntPref(
+ TelemetryUtils.Preferences.CurrentPolicyVersion,
+ 1
+ ) + 1;
+ Services.prefs.setIntPref(
+ TelemetryUtils.Preferences.AcceptedPolicyVersion,
+ newVersion
+ );
+ Assert.ok(
+ TelemetryReportingPolicy.testIsUserNotified(),
+ "A future version of the policy should pass."
+ );
+
+ newVersion =
+ Services.prefs.getIntPref(
+ TelemetryUtils.Preferences.CurrentPolicyVersion,
+ 1
+ ) - 1;
+ Services.prefs.setIntPref(
+ TelemetryUtils.Preferences.AcceptedPolicyVersion,
+ newVersion
+ );
+ Assert.ok(
+ !TelemetryReportingPolicy.testIsUserNotified(),
+ "A previous version of the policy should fail."
+ );
+});
+
+add_task(async function test_canSend() {
+ const TEST_PING_TYPE = "test-ping";
+
+ PingServer.start();
+ Services.prefs.setStringPref(
+ TelemetryUtils.Preferences.Server,
+ "http://localhost:" + PingServer.port
+ );
+
+ await TelemetryController.testReset();
+ TelemetryReportingPolicy.reset();
+
+ // User should be reported as not notified by default.
+ Assert.ok(
+ !TelemetryReportingPolicy.testIsUserNotified(),
+ "The initial state should be unnotified."
+ );
+
+ // Assert if we receive any ping before the policy is accepted.
+ PingServer.registerPingHandler(() =>
+ Assert.ok(false, "Should not have received any pings now")
+ );
+ await TelemetryController.submitExternalPing(TEST_PING_TYPE, {});
+
+ // Reset the ping handler.
+ PingServer.resetPingHandler();
+
+ // Fake the infobar: this should also trigger the ping send task.
+ TelemetryReportingPolicy.testInfobarShown();
+ let ping = await PingServer.promiseNextPings(1);
+ Assert.equal(ping.length, 1, "We should have received one ping.");
+ Assert.equal(
+ ping[0].type,
+ TEST_PING_TYPE,
+ "We should have received the previous ping."
+ );
+
+ // Submit another ping, to make sure it gets sent.
+ await TelemetryController.submitExternalPing(TEST_PING_TYPE, {});
+
+ // Get the ping and check its type.
+ ping = await PingServer.promiseNextPings(1);
+ Assert.equal(ping.length, 1, "We should have received one ping.");
+ Assert.equal(
+ ping[0].type,
+ TEST_PING_TYPE,
+ "We should have received the new ping."
+ );
+
+ // Fake a restart with a pending ping.
+ await TelemetryController.addPendingPing(TEST_PING_TYPE, {});
+ await TelemetryController.testReset();
+
+ // We should be immediately sending the ping out.
+ ping = await PingServer.promiseNextPings(1);
+ Assert.equal(ping.length, 1, "We should have received one ping.");
+ Assert.equal(
+ ping[0].type,
+ TEST_PING_TYPE,
+ "We should have received the pending ping."
+ );
+
+ // Submit another ping, to make sure it gets sent.
+ await TelemetryController.submitExternalPing(TEST_PING_TYPE, {});
+
+ // Get the ping and check its type.
+ ping = await PingServer.promiseNextPings(1);
+ Assert.equal(ping.length, 1, "We should have received one ping.");
+ Assert.equal(
+ ping[0].type,
+ TEST_PING_TYPE,
+ "We should have received the new ping."
+ );
+
+ await PingServer.stop();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryScalars.js b/toolkit/components/telemetry/tests/unit/test_TelemetryScalars.js
new file mode 100644
index 0000000000..46f1ca9058
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryScalars.js
@@ -0,0 +1,1088 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+ChromeUtils.defineESModuleGetters(this, {
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+});
+
+const UINT_SCALAR = "telemetry.test.unsigned_int_kind";
+const STRING_SCALAR = "telemetry.test.string_kind";
+const BOOLEAN_SCALAR = "telemetry.test.boolean_kind";
+const KEYED_UINT_SCALAR = "telemetry.test.keyed_unsigned_int";
+const KEYED_EXCEED_SCALAR = "telemetry.keyed_scalars_exceed_limit";
+
+function getProcessScalars(aProcessName, aKeyed = false, aClear = false) {
+ const scalars = aKeyed
+ ? Telemetry.getSnapshotForKeyedScalars("main", aClear)[aProcessName]
+ : Telemetry.getSnapshotForScalars("main", aClear)[aProcessName];
+ return scalars || {};
+}
+
+add_task(async function test_serializationFormat() {
+ Telemetry.clearScalars();
+
+ // Set the scalars to a known value.
+ const expectedUint = 3785;
+ const expectedString = "some value";
+ Telemetry.scalarSet(UINT_SCALAR, expectedUint);
+ Telemetry.scalarSet(STRING_SCALAR, expectedString);
+ Telemetry.scalarSet(BOOLEAN_SCALAR, true);
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, "first_key", 1234);
+
+ // Get a snapshot of the scalars for the main process (internally called "default").
+ const scalars = TelemetryTestUtils.getProcessScalars("parent");
+
+ // Check that they are serialized to the correct format.
+ Assert.equal(
+ typeof scalars[UINT_SCALAR],
+ "number",
+ UINT_SCALAR + " must be serialized to the correct format."
+ );
+ Assert.ok(
+ Number.isInteger(scalars[UINT_SCALAR]),
+ UINT_SCALAR + " must be a finite integer."
+ );
+ Assert.equal(
+ scalars[UINT_SCALAR],
+ expectedUint,
+ UINT_SCALAR + " must have the correct value."
+ );
+ Assert.equal(
+ typeof scalars[STRING_SCALAR],
+ "string",
+ STRING_SCALAR + " must be serialized to the correct format."
+ );
+ Assert.equal(
+ scalars[STRING_SCALAR],
+ expectedString,
+ STRING_SCALAR + " must have the correct value."
+ );
+ Assert.equal(
+ typeof scalars[BOOLEAN_SCALAR],
+ "boolean",
+ BOOLEAN_SCALAR + " must be serialized to the correct format."
+ );
+ Assert.equal(
+ scalars[BOOLEAN_SCALAR],
+ true,
+ BOOLEAN_SCALAR + " must have the correct value."
+ );
+ Assert.ok(
+ !(KEYED_UINT_SCALAR in scalars),
+ "Keyed scalars must be reported in a separate section."
+ );
+});
+
+add_task(async function test_keyedSerializationFormat() {
+ Telemetry.clearScalars();
+
+ const expectedKey = "first_key";
+ const expectedOtherKey = "漢語";
+ const expectedUint = 3785;
+ const expectedOtherValue = 1107;
+
+ Telemetry.scalarSet(UINT_SCALAR, expectedUint);
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, expectedKey, expectedUint);
+ Telemetry.keyedScalarSet(
+ KEYED_UINT_SCALAR,
+ expectedOtherKey,
+ expectedOtherValue
+ );
+
+ // Get a snapshot of the scalars.
+ const keyedScalars = TelemetryTestUtils.getProcessScalars("parent", true);
+
+ Assert.ok(
+ !(UINT_SCALAR in keyedScalars),
+ UINT_SCALAR + " must not be serialized with the keyed scalars."
+ );
+ Assert.ok(
+ KEYED_UINT_SCALAR in keyedScalars,
+ KEYED_UINT_SCALAR + " must be serialized with the keyed scalars."
+ );
+ Assert.equal(
+ Object.keys(keyedScalars[KEYED_UINT_SCALAR]).length,
+ 2,
+ "The keyed scalar must contain exactly 2 keys."
+ );
+ Assert.ok(
+ expectedKey in keyedScalars[KEYED_UINT_SCALAR],
+ KEYED_UINT_SCALAR + " must contain the expected keys."
+ );
+ Assert.ok(
+ expectedOtherKey in keyedScalars[KEYED_UINT_SCALAR],
+ KEYED_UINT_SCALAR + " must contain the expected keys."
+ );
+ Assert.ok(
+ Number.isInteger(keyedScalars[KEYED_UINT_SCALAR][expectedKey]),
+ KEYED_UINT_SCALAR + "." + expectedKey + " must be a finite integer."
+ );
+ Assert.equal(
+ keyedScalars[KEYED_UINT_SCALAR][expectedKey],
+ expectedUint,
+ KEYED_UINT_SCALAR + "." + expectedKey + " must have the correct value."
+ );
+ Assert.equal(
+ keyedScalars[KEYED_UINT_SCALAR][expectedOtherKey],
+ expectedOtherValue,
+ KEYED_UINT_SCALAR + "." + expectedOtherKey + " must have the correct value."
+ );
+});
+
+add_task(async function test_nonexistingScalar() {
+ const NON_EXISTING_SCALAR = "telemetry.test.non_existing";
+
+ Telemetry.clearScalars();
+
+ // The JS API must not throw when used incorrectly but rather print
+ // a message to the console.
+ Telemetry.scalarAdd(NON_EXISTING_SCALAR, 11715);
+ Telemetry.scalarSet(NON_EXISTING_SCALAR, 11715);
+ Telemetry.scalarSetMaximum(NON_EXISTING_SCALAR, 11715);
+
+ // Make sure we do not throw on any operation for non-existing scalars.
+ Telemetry.keyedScalarAdd(NON_EXISTING_SCALAR, "some_key", 11715);
+ Telemetry.keyedScalarSet(NON_EXISTING_SCALAR, "some_key", 11715);
+ Telemetry.keyedScalarSetMaximum(NON_EXISTING_SCALAR, "some_key", 11715);
+
+ // Get a snapshot of the scalars.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent");
+
+ Assert.ok(
+ !(NON_EXISTING_SCALAR in scalars),
+ "The non existing scalar must not be persisted."
+ );
+
+ const keyedScalars = TelemetryTestUtils.getProcessScalars("parent", true);
+
+ Assert.ok(
+ !(NON_EXISTING_SCALAR in keyedScalars),
+ "The non existing keyed scalar must not be persisted."
+ );
+});
+
+add_task(async function test_expiredScalar() {
+ const EXPIRED_SCALAR = "telemetry.test.expired";
+ const EXPIRED_KEYED_SCALAR = "telemetry.test.keyed_expired";
+ const UNEXPIRED_SCALAR = "telemetry.test.unexpired";
+
+ Telemetry.clearScalars();
+
+ // Try to set the expired scalar to some value. We will not be recording the value,
+ // but we shouldn't throw.
+ Telemetry.scalarAdd(EXPIRED_SCALAR, 11715);
+ Telemetry.scalarSet(EXPIRED_SCALAR, 11715);
+ Telemetry.scalarSetMaximum(EXPIRED_SCALAR, 11715);
+ Telemetry.keyedScalarAdd(EXPIRED_KEYED_SCALAR, "some_key", 11715);
+ Telemetry.keyedScalarSet(EXPIRED_KEYED_SCALAR, "some_key", 11715);
+ Telemetry.keyedScalarSetMaximum(EXPIRED_KEYED_SCALAR, "some_key", 11715);
+
+ // The unexpired scalar has an expiration version, but far away in the future.
+ const expectedValue = 11716;
+ Telemetry.scalarSet(UNEXPIRED_SCALAR, expectedValue);
+
+ // Get a snapshot of the scalars.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent");
+ const keyedScalars = TelemetryTestUtils.getProcessScalars("parent");
+
+ Assert.ok(
+ !(EXPIRED_SCALAR in scalars),
+ "The expired scalar must not be persisted."
+ );
+ Assert.equal(
+ scalars[UNEXPIRED_SCALAR],
+ expectedValue,
+ "The unexpired scalar must be persisted with the correct value."
+ );
+ Assert.ok(
+ !(EXPIRED_KEYED_SCALAR in keyedScalars),
+ "The expired keyed scalar must not be persisted."
+ );
+});
+
+add_task(async function test_unsignedIntScalar() {
+ let checkScalar = expectedValue => {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent");
+ Assert.equal(
+ scalars[UINT_SCALAR],
+ expectedValue,
+ UINT_SCALAR + " must contain the expected value."
+ );
+ };
+
+ Telemetry.clearScalars();
+
+ // Let's start with an accumulation without a prior set.
+ Telemetry.scalarAdd(UINT_SCALAR, 1);
+ Telemetry.scalarAdd(UINT_SCALAR, 2);
+ // Do we get what we expect?
+ checkScalar(3);
+
+ // Let's test setting the scalar to a value.
+ Telemetry.scalarSet(UINT_SCALAR, 3785);
+ checkScalar(3785);
+ Telemetry.scalarAdd(UINT_SCALAR, 1);
+ checkScalar(3786);
+
+ // Does setMaximum work?
+ Telemetry.scalarSet(UINT_SCALAR, 2);
+ checkScalar(2);
+ Telemetry.scalarSetMaximum(UINT_SCALAR, 5);
+ checkScalar(5);
+ // The value of the probe should still be 5, as the previous value
+ // is greater than the one we want to set.
+ Telemetry.scalarSetMaximum(UINT_SCALAR, 3);
+ checkScalar(5);
+
+ // Check that non-integer numbers get truncated and set.
+ Telemetry.scalarSet(UINT_SCALAR, 3.785);
+ checkScalar(3);
+
+ // Setting or adding a negative number must report an error through
+ // the console and drop the change (shouldn't throw).
+ Telemetry.scalarAdd(UINT_SCALAR, -5);
+ Telemetry.scalarSet(UINT_SCALAR, -5);
+ Telemetry.scalarSetMaximum(UINT_SCALAR, -1);
+ checkScalar(3);
+
+ // If we try to set a value of a different type, the JS API should not
+ // throw but rather print a console message.
+ Telemetry.scalarSet(UINT_SCALAR, 1);
+ Telemetry.scalarSet(UINT_SCALAR, "unexpected value");
+ Telemetry.scalarAdd(UINT_SCALAR, "unexpected value");
+ Telemetry.scalarSetMaximum(UINT_SCALAR, "unexpected value");
+ // The stored value must not be compromised.
+ checkScalar(1);
+});
+
+add_task(async function test_stringScalar() {
+ let checkExpectedString = expectedString => {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent");
+ Assert.equal(
+ scalars[STRING_SCALAR],
+ expectedString,
+ STRING_SCALAR + " must contain the expected string value."
+ );
+ };
+
+ Telemetry.clearScalars();
+
+ // Let's check simple strings...
+ let expected = "test string";
+ Telemetry.scalarSet(STRING_SCALAR, expected);
+ checkExpectedString(expected);
+ expected = "漢語";
+ Telemetry.scalarSet(STRING_SCALAR, expected);
+ checkExpectedString(expected);
+
+ // We have some unsupported operations for strings.
+ Telemetry.scalarAdd(STRING_SCALAR, 1);
+ Telemetry.scalarAdd(STRING_SCALAR, "string value");
+ Telemetry.scalarSetMaximum(STRING_SCALAR, 1);
+ Telemetry.scalarSetMaximum(STRING_SCALAR, "string value");
+ Telemetry.scalarSet(STRING_SCALAR, 1);
+
+ // Try to set the scalar to a string longer than the maximum length limit.
+ const LONG_STRING =
+ "browser.qaxfiuosnzmhlg.rpvxicawolhtvmbkpnludhedobxvkjwqyeyvmv";
+ Telemetry.scalarSet(STRING_SCALAR, LONG_STRING);
+ checkExpectedString(LONG_STRING.substr(0, 50));
+});
+
+add_task(async function test_booleanScalar() {
+ let checkExpectedBool = expectedBoolean => {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent");
+ Assert.equal(
+ scalars[BOOLEAN_SCALAR],
+ expectedBoolean,
+ BOOLEAN_SCALAR + " must contain the expected boolean value."
+ );
+ };
+
+ Telemetry.clearScalars();
+
+ // Set a test boolean value.
+ let expected = false;
+ Telemetry.scalarSet(BOOLEAN_SCALAR, expected);
+ checkExpectedBool(expected);
+ expected = true;
+ Telemetry.scalarSet(BOOLEAN_SCALAR, expected);
+ checkExpectedBool(expected);
+
+ // Check that setting a numeric value implicitly converts to boolean.
+ Telemetry.scalarSet(BOOLEAN_SCALAR, 1);
+ checkExpectedBool(true);
+ Telemetry.scalarSet(BOOLEAN_SCALAR, 0);
+ checkExpectedBool(false);
+ Telemetry.scalarSet(BOOLEAN_SCALAR, 1.0);
+ checkExpectedBool(true);
+ Telemetry.scalarSet(BOOLEAN_SCALAR, 0.0);
+ checkExpectedBool(false);
+
+ // Check that unsupported operations for booleans do not throw.
+ Telemetry.scalarAdd(BOOLEAN_SCALAR, 1);
+ Telemetry.scalarAdd(BOOLEAN_SCALAR, "string value");
+ Telemetry.scalarSetMaximum(BOOLEAN_SCALAR, 1);
+ Telemetry.scalarSetMaximum(BOOLEAN_SCALAR, "string value");
+ Telemetry.scalarSet(BOOLEAN_SCALAR, "true");
+});
+
+add_task(async function test_scalarRecording() {
+ const OPTIN_SCALAR = "telemetry.test.release_optin";
+ const OPTOUT_SCALAR = "telemetry.test.release_optout";
+
+ let checkValue = (scalarName, expectedValue) => {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent");
+ Assert.equal(
+ scalars[scalarName],
+ expectedValue,
+ scalarName + " must contain the expected value."
+ );
+ };
+
+ let checkNotSerialized = scalarName => {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent");
+ Assert.ok(!(scalarName in scalars), scalarName + " was not recorded.");
+ };
+
+ Telemetry.canRecordBase = false;
+ Telemetry.canRecordExtended = false;
+ Telemetry.clearScalars();
+
+ // Check that no scalar is recorded if both base and extended recording are off.
+ Telemetry.scalarSet(OPTOUT_SCALAR, 3);
+ Telemetry.scalarSet(OPTIN_SCALAR, 3);
+ checkNotSerialized(OPTOUT_SCALAR);
+ checkNotSerialized(OPTIN_SCALAR);
+
+ // Check that opt-out scalars are recorded, while opt-in are not.
+ Telemetry.canRecordBase = true;
+ Telemetry.scalarSet(OPTOUT_SCALAR, 3);
+ Telemetry.scalarSet(OPTIN_SCALAR, 3);
+ checkValue(OPTOUT_SCALAR, 3);
+ checkNotSerialized(OPTIN_SCALAR);
+
+ // Check that both opt-out and opt-in scalars are recorded.
+ Telemetry.canRecordExtended = true;
+ Telemetry.scalarSet(OPTOUT_SCALAR, 5);
+ Telemetry.scalarSet(OPTIN_SCALAR, 6);
+ checkValue(OPTOUT_SCALAR, 5);
+ checkValue(OPTIN_SCALAR, 6);
+});
+
+add_task(async function test_keyedScalarRecording() {
+ const OPTIN_SCALAR = "telemetry.test.keyed_release_optin";
+ const OPTOUT_SCALAR = "telemetry.test.keyed_release_optout";
+ const testKey = "policy_key";
+
+ let checkValue = (scalarName, expectedValue) => {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true);
+ Assert.equal(
+ scalars[scalarName][testKey],
+ expectedValue,
+ scalarName + " must contain the expected value."
+ );
+ };
+
+ let checkNotSerialized = scalarName => {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true);
+ Assert.ok(!(scalarName in scalars), scalarName + " was not recorded.");
+ };
+
+ Telemetry.canRecordBase = false;
+ Telemetry.canRecordExtended = false;
+ Telemetry.clearScalars();
+
+ // Check that no scalar is recorded if both base and extended recording are off.
+ Telemetry.keyedScalarSet(OPTOUT_SCALAR, testKey, 3);
+ Telemetry.keyedScalarSet(OPTIN_SCALAR, testKey, 3);
+ checkNotSerialized(OPTOUT_SCALAR);
+ checkNotSerialized(OPTIN_SCALAR);
+
+ // Check that opt-out scalars are recorded, while opt-in are not.
+ Telemetry.canRecordBase = true;
+ Telemetry.keyedScalarSet(OPTOUT_SCALAR, testKey, 3);
+ Telemetry.keyedScalarSet(OPTIN_SCALAR, testKey, 3);
+ checkValue(OPTOUT_SCALAR, 3);
+ checkNotSerialized(OPTIN_SCALAR);
+
+ // Check that both opt-out and opt-in scalars are recorded.
+ Telemetry.canRecordExtended = true;
+ Telemetry.keyedScalarSet(OPTOUT_SCALAR, testKey, 5);
+ Telemetry.keyedScalarSet(OPTIN_SCALAR, testKey, 6);
+ checkValue(OPTOUT_SCALAR, 5);
+ checkValue(OPTIN_SCALAR, 6);
+});
+
+add_task(async function test_subsession() {
+ Telemetry.clearScalars();
+
+ // Set the scalars to a known value.
+ Telemetry.scalarSet(UINT_SCALAR, 3785);
+ Telemetry.scalarSet(STRING_SCALAR, "some value");
+ Telemetry.scalarSet(BOOLEAN_SCALAR, false);
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, "some_random_key", 12);
+
+ // Get a snapshot and reset the subsession. The value we set must be there.
+ let scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ let keyedScalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+
+ Assert.equal(
+ scalars[UINT_SCALAR],
+ 3785,
+ UINT_SCALAR + " must contain the expected value."
+ );
+ Assert.equal(
+ scalars[STRING_SCALAR],
+ "some value",
+ STRING_SCALAR + " must contain the expected value."
+ );
+ Assert.equal(
+ scalars[BOOLEAN_SCALAR],
+ false,
+ BOOLEAN_SCALAR + " must contain the expected value."
+ );
+ Assert.equal(
+ keyedScalars[KEYED_UINT_SCALAR].some_random_key,
+ 12,
+ KEYED_UINT_SCALAR + " must contain the expected value."
+ );
+
+ // Get a new snapshot and reset the subsession again. Since no new value
+ // was set, the scalars should not be reported.
+ scalars = TelemetryTestUtils.getProcessScalars("parent", false, true);
+ keyedScalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+
+ Assert.ok(
+ !(UINT_SCALAR in scalars),
+ UINT_SCALAR + " must be empty and not reported."
+ );
+ Assert.ok(
+ !(STRING_SCALAR in scalars),
+ STRING_SCALAR + " must be empty and not reported."
+ );
+ Assert.ok(
+ !(BOOLEAN_SCALAR in scalars),
+ BOOLEAN_SCALAR + " must be empty and not reported."
+ );
+ Assert.ok(
+ !(KEYED_UINT_SCALAR in keyedScalars),
+ KEYED_UINT_SCALAR + " must be empty and not reported."
+ );
+});
+
+add_task(async function test_keyed_uint() {
+ Telemetry.clearScalars();
+
+ const KEYS = ["a_key", "another_key", "third_key"];
+ let expectedValues = [1, 1, 1];
+
+ // Set all the keys to a baseline value.
+ for (let key of KEYS) {
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, key, 1);
+ }
+
+ // Increment only one key.
+ Telemetry.keyedScalarAdd(KEYED_UINT_SCALAR, KEYS[1], 1);
+ expectedValues[1]++;
+
+ // Use SetMaximum on the third key.
+ Telemetry.keyedScalarSetMaximum(KEYED_UINT_SCALAR, KEYS[2], 37);
+ expectedValues[2] = 37;
+
+ // Get a snapshot of the scalars and make sure the keys contain
+ // the correct values.
+ const keyedScalars = TelemetryTestUtils.getProcessScalars("parent", true);
+
+ for (let k = 0; k < 3; k++) {
+ const keyName = KEYS[k];
+ Assert.equal(
+ keyedScalars[KEYED_UINT_SCALAR][keyName],
+ expectedValues[k],
+ KEYED_UINT_SCALAR + "." + keyName + " must contain the correct value."
+ );
+ }
+
+ // Do not throw when doing unsupported things on uint keyed scalars.
+ // Just test one single unsupported operation, the other are covered in the plain
+ // unsigned scalar test.
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, "new_key", "unexpected value");
+});
+
+add_task(async function test_keyed_boolean() {
+ Telemetry.clearScalars();
+
+ const KEYED_BOOLEAN_TYPE = "telemetry.test.keyed_boolean_kind";
+ const first_key = "first_key";
+ const second_key = "second_key";
+
+ // Set the initial values.
+ Telemetry.keyedScalarSet(KEYED_BOOLEAN_TYPE, first_key, true);
+ Telemetry.keyedScalarSet(KEYED_BOOLEAN_TYPE, second_key, false);
+
+ // Get a snapshot of the scalars and make sure the keys contain
+ // the correct values.
+ let keyedScalars = TelemetryTestUtils.getProcessScalars("parent", true);
+ Assert.equal(
+ keyedScalars[KEYED_BOOLEAN_TYPE][first_key],
+ true,
+ "The key must contain the expected value."
+ );
+ Assert.equal(
+ keyedScalars[KEYED_BOOLEAN_TYPE][second_key],
+ false,
+ "The key must contain the expected value."
+ );
+
+ // Now flip the values and make sure we get the expected values back.
+ Telemetry.keyedScalarSet(KEYED_BOOLEAN_TYPE, first_key, false);
+ Telemetry.keyedScalarSet(KEYED_BOOLEAN_TYPE, second_key, true);
+
+ keyedScalars = TelemetryTestUtils.getProcessScalars("parent", true);
+ Assert.equal(
+ keyedScalars[KEYED_BOOLEAN_TYPE][first_key],
+ false,
+ "The key must contain the expected value."
+ );
+ Assert.equal(
+ keyedScalars[KEYED_BOOLEAN_TYPE][second_key],
+ true,
+ "The key must contain the expected value."
+ );
+
+ // Do not throw when doing unsupported things on a boolean keyed scalars.
+ // Just test one single unsupported operation, the other are covered in the plain
+ // boolean scalar test.
+ Telemetry.keyedScalarAdd(KEYED_BOOLEAN_TYPE, "somehey", 1);
+});
+
+add_task(async function test_keyed_keys_length() {
+ Telemetry.clearScalars();
+
+ const LONG_KEY_STRING =
+ "browser.qaxfiuosnzmhlg.rpvxicawolhtvmbkpnludhedobxvkjwqyeyvmv.somemoresowereach70chars";
+ const NORMAL_KEY = "a_key";
+
+ // Set the value for a key within the length limits.
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, NORMAL_KEY, 1);
+
+ // Now try to set and modify the value for a very long key (must not throw).
+ Telemetry.keyedScalarAdd(KEYED_UINT_SCALAR, LONG_KEY_STRING, 10);
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, LONG_KEY_STRING, 1);
+ Telemetry.keyedScalarSetMaximum(KEYED_UINT_SCALAR, LONG_KEY_STRING, 10);
+
+ // Also attempt to set the value for an empty key.
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, "", 1);
+
+ // Make sure the key with the right length contains the expected value.
+ let keyedScalars = TelemetryTestUtils.getProcessScalars("parent", true);
+ Assert.equal(
+ Object.keys(keyedScalars[KEYED_UINT_SCALAR]).length,
+ 1,
+ "The keyed scalar must contain exactly 1 key."
+ );
+ Assert.ok(
+ NORMAL_KEY in keyedScalars[KEYED_UINT_SCALAR],
+ "The keyed scalar must contain the expected key."
+ );
+ Assert.equal(
+ keyedScalars[KEYED_UINT_SCALAR][NORMAL_KEY],
+ 1,
+ "The key must contain the expected value."
+ );
+ Assert.ok(
+ !(LONG_KEY_STRING in keyedScalars[KEYED_UINT_SCALAR]),
+ "The data for the long key should not have been recorded."
+ );
+ Assert.ok(
+ !("" in keyedScalars[KEYED_UINT_SCALAR]),
+ "The data for the empty key should not have been recorded."
+ );
+});
+
+add_task(async function test_keyed_max_keys() {
+ Telemetry.clearScalars();
+
+ // Generate the names for the first 100 keys.
+ let keyNamesSet = new Set();
+ for (let k = 0; k < 100; k++) {
+ keyNamesSet.add("key_" + k);
+ }
+
+ // Add 100 keys to an histogram and set their initial value.
+ let valueToSet = 0;
+ keyNamesSet.forEach(keyName => {
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, keyName, valueToSet++);
+ });
+
+ // Perform some operations on the 101th key. This should throw, as
+ // we're not allowed to have more than 100 keys.
+ const LAST_KEY_NAME = "overflowing_key";
+ Telemetry.keyedScalarAdd(KEYED_UINT_SCALAR, LAST_KEY_NAME, 10);
+ Telemetry.keyedScalarSet(KEYED_UINT_SCALAR, LAST_KEY_NAME, 1);
+ Telemetry.keyedScalarSetMaximum(KEYED_UINT_SCALAR, LAST_KEY_NAME, 10);
+
+ // Make sure all the keys except the last one are available and have the correct
+ // values.
+ let keyedScalars = TelemetryTestUtils.getProcessScalars("parent", true);
+
+ // Check that the keyed scalar only contain the first 100 keys.
+ const reportedKeysSet = new Set(Object.keys(keyedScalars[KEYED_UINT_SCALAR]));
+ Assert.ok(
+ [...keyNamesSet].filter(x => reportedKeysSet.has(x)) &&
+ [...reportedKeysSet].filter(x => keyNamesSet.has(x)),
+ "The keyed scalar must contain all the 100 keys, and drop the others."
+ );
+
+ // Check that all the keys recorded the expected values.
+ let expectedValue = 0;
+ keyNamesSet.forEach(keyName => {
+ Assert.equal(
+ keyedScalars[KEYED_UINT_SCALAR][keyName],
+ expectedValue++,
+ "The key must contain the expected value."
+ );
+ });
+
+ // Check that KEYED_EXCEED_SCALAR is in keyedScalars
+ Assert.ok(
+ KEYED_EXCEED_SCALAR in keyedScalars,
+ "We have exceeded maximum number of Keys."
+ );
+
+ // Generate the names for the exceeded keys
+ let keyNamesSet2 = new Set();
+ for (let k = 0; k < 100; k++) {
+ keyNamesSet2.add("key2_" + k);
+ }
+
+ // Add 100 keys to the keyed exceed scalar and set their initial value.
+ valueToSet = 0;
+ keyNamesSet2.forEach(keyName2 => {
+ Telemetry.keyedScalarSet(KEYED_EXCEED_SCALAR, keyName2, valueToSet++);
+ });
+
+ // Check that there are exactly 100 keys in KEYED_EXCEED_SCALAR
+ let snapshot = Telemetry.getSnapshotForKeyedScalars("main", false);
+ Assert.equal(
+ 100,
+ Object.keys(snapshot.parent[KEYED_UINT_SCALAR]).length,
+ "The keyed scalar must contain all the 100 keys."
+ );
+
+ // Check that KEYED_UINT_SCALAR is in keyedScalars and its value equals 3
+ Assert.ok(
+ KEYED_UINT_SCALAR in keyedScalars[KEYED_EXCEED_SCALAR],
+ "The keyed Scalar is in the keyed exceeded scalar"
+ );
+ Assert.equal(
+ keyedScalars[KEYED_EXCEED_SCALAR][KEYED_UINT_SCALAR],
+ 3,
+ "We have exactly 3 keys over the limit"
+ );
+});
+
+add_task(async function test_dynamicScalars_registration() {
+ Telemetry.clearScalars();
+
+ const TEST_CASES = [
+ {
+ category: "telemetry.test",
+ data: {
+ missing_kind: {
+ keyed: false,
+ record_on_release: true,
+ },
+ },
+ evaluation: /missing 'kind'/,
+ description: "Registration must fail if required fields are missing",
+ },
+ {
+ category: "telemetry.test",
+ data: {
+ invalid_collection: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ record_on_release: "opt-in",
+ },
+ },
+ evaluation: /Invalid 'record_on_release'/,
+ description:
+ "Registration must fail if 'record_on_release' is of the wrong type",
+ },
+ {
+ category: "telemetry.test",
+ data: {
+ invalid_kind: {
+ kind: "12",
+ },
+ },
+ evaluation: /Invalid or missing 'kind'/,
+ description: "Registration must fail if 'kind' is of the wrong type",
+ },
+ {
+ category: "telemetry.test",
+ data: {
+ invalid_expired: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ expired: "never",
+ },
+ },
+ evaluation: /Invalid 'expired'/,
+ description: "Registration must fail if 'expired' is of the wrong type",
+ },
+ {
+ category: "telemetry.test",
+ data: {
+ valid_scalar: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ keyed: false,
+ record_on_release: true,
+ },
+ invalid_scalar: {
+ expired: false,
+ },
+ },
+ evaluation: /Invalid or missing 'kind'/,
+ description:
+ "No scalar must be registered if the batch contains an invalid one",
+ },
+ {
+ category: "telemetry.test",
+ data: {
+ invalid_stores: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ keyed: false,
+ stores: true,
+ },
+ },
+ evaluation: /Invalid 'stores'/,
+ description: "Registration must fail if 'stores' is of the wrong type",
+ },
+ {
+ category: "telemetry.test",
+ data: {
+ invalid_stores: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ keyed: false,
+ stores: {},
+ },
+ },
+ evaluation: /Invalid 'stores'/,
+ description: "Registration must fail if 'stores' is of the wrong type",
+ },
+ {
+ category: "telemetry.test",
+ data: {
+ invalid_stores: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ keyed: false,
+ stores: [{}],
+ },
+ },
+ evaluation: /'stores' array isn't a string./,
+ description:
+ "Registration must fail if element in 'stores' is of the wrong type",
+ },
+ ];
+
+ for (let testCase of TEST_CASES) {
+ Assert.throws(
+ () => Telemetry.registerScalars(testCase.category, testCase.data),
+ testCase.evaluation,
+ testCase.description
+ );
+ }
+});
+
+add_task(async function test_dynamicScalars_doubleRegistration() {
+ Telemetry.clearScalars();
+
+ // Register a test scalar.
+ Telemetry.registerScalars("telemetry.test.dynamic", {
+ double_registration_1: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ record_on_release: true,
+ },
+ });
+
+ // Verify that we can record the scalar.
+ Telemetry.scalarSet("telemetry.test.dynamic.double_registration_1", 1);
+
+ // Register the same scalar again, along with a second scalar.
+ // This must not throw.
+ Telemetry.registerScalars("telemetry.test.dynamic", {
+ double_registration_1: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ record_on_release: true,
+ },
+ double_registration_2: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ record_on_release: true,
+ },
+ });
+
+ // Set the dynamic scalars to some test values.
+ Telemetry.scalarAdd("telemetry.test.dynamic.double_registration_1", 1);
+ Telemetry.scalarSet("telemetry.test.dynamic.double_registration_2", 3);
+
+ // Get a snapshot of the scalars and check that the dynamic ones were correctly set.
+ let scalars = getProcessScalars("dynamic", false, false);
+
+ Assert.equal(
+ scalars["telemetry.test.dynamic.double_registration_1"],
+ 2,
+ "The recorded scalar must contain the right value."
+ );
+ Assert.equal(
+ scalars["telemetry.test.dynamic.double_registration_2"],
+ 3,
+ "The recorded scalar must contain the right value."
+ );
+
+ // Register an existing scalar again, only change the definition
+ // to make it expire.
+ Telemetry.registerScalars("telemetry.test.dynamic", {
+ double_registration_2: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ record_on_release: true,
+ expired: true,
+ },
+ });
+
+ // Attempt to record and make sure that no recording happens.
+ Telemetry.scalarAdd("telemetry.test.dynamic.double_registration_2", 1);
+ scalars = getProcessScalars("dynamic", false, false);
+ Assert.equal(
+ scalars["telemetry.test.dynamic.double_registration_2"],
+ 3,
+ "The recorded scalar must contain the right value."
+ );
+});
+
+add_task(async function test_dynamicScalars_recording() {
+ Telemetry.clearScalars();
+
+ // Disable extended recording so that we will just record opt-out.
+ Telemetry.canRecordExtended = false;
+
+ // Register some test scalars.
+ Telemetry.registerScalars("telemetry.test.dynamic", {
+ record_optout: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ record_on_release: true,
+ },
+ record_keyed: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ keyed: true,
+ record_on_release: true,
+ },
+ record_optin: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_BOOLEAN,
+ record_on_release: false,
+ },
+ record_expired: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_STRING,
+ expired: true,
+ record_on_release: true,
+ },
+ });
+
+ // Set the dynamic scalars to some test values.
+ Telemetry.scalarSet("telemetry.test.dynamic.record_optout", 1);
+ Telemetry.keyedScalarSet("telemetry.test.dynamic.record_keyed", "someKey", 5);
+ Telemetry.scalarSet("telemetry.test.dynamic.record_optin", false);
+ Telemetry.scalarSet("telemetry.test.dynamic.record_expired", "test");
+
+ // Get a snapshot of the scalars and check that the dynamic ones were correctly set.
+ let scalars = getProcessScalars("dynamic", false, false);
+ let keyedScalars = getProcessScalars("dynamic", true, true);
+
+ Assert.ok(
+ !("telemetry.test.dynamic.record_optin" in scalars),
+ "Dynamic opt-in scalars must not be recorded."
+ );
+ Assert.ok(
+ "telemetry.test.dynamic.record_keyed" in keyedScalars,
+ "Dynamic opt-out keyed scalars must be recorded."
+ );
+ Assert.ok(
+ !("telemetry.test.dynamic.record_expired" in scalars),
+ "Dynamic expired scalars must not be recorded."
+ );
+ Assert.ok(
+ "telemetry.test.dynamic.record_optout" in scalars,
+ "Dynamic opt-out scalars must be recorded."
+ );
+ Assert.equal(
+ scalars["telemetry.test.dynamic.record_optout"],
+ 1,
+ "The recorded scalar must contain the right value."
+ );
+ Assert.equal(
+ keyedScalars["telemetry.test.dynamic.record_keyed"].someKey,
+ 5,
+ "The recorded keyed scalar must contain the right value."
+ );
+
+ // Enable extended recording.
+ Telemetry.canRecordExtended = true;
+
+ // Set the dynamic scalars to some test values.
+ Telemetry.scalarSet("telemetry.test.dynamic.record_optin", true);
+ Telemetry.scalarSet("telemetry.test.dynamic.record_expired", "test");
+
+ // Get a snapshot of the scalars and check that the dynamic ones were correctly set.
+ scalars = getProcessScalars("dynamic", false, true);
+
+ Assert.ok(
+ !("telemetry.test.dynamic.record_expired" in scalars),
+ "Dynamic expired scalars must not be recorded."
+ );
+ Assert.ok(
+ "telemetry.test.dynamic.record_optin" in scalars,
+ "Dynamic opt-in scalars must be recorded."
+ );
+ Assert.equal(
+ scalars["telemetry.test.dynamic.record_optin"],
+ true,
+ "The recorded scalar must contain the right value."
+ );
+});
+
+add_task(
+ {
+ skip_if: () => gIsAndroid,
+ },
+ async function test_productSpecificScalar() {
+ const DEFAULT_PRODUCT_SCALAR = "telemetry.test.default_products";
+ const DESKTOP_ONLY_SCALAR = "telemetry.test.desktop_only";
+ const MULTIPRODUCT_SCALAR = "telemetry.test.multiproduct";
+ const MOBILE_ONLY_SCALAR = "telemetry.test.mobile_only";
+ const MOBILE_ONLY_KEYED_SCALAR = "telemetry.test.keyed_mobile_only";
+
+ Telemetry.clearScalars();
+
+ // Try to set the desktop scalars
+ let expectedValue = 11714;
+ Telemetry.scalarAdd(DEFAULT_PRODUCT_SCALAR, expectedValue);
+ Telemetry.scalarAdd(DESKTOP_ONLY_SCALAR, expectedValue);
+ Telemetry.scalarAdd(MULTIPRODUCT_SCALAR, expectedValue);
+
+ // Try to set the mobile-only scalar to some value. We will not be recording the value,
+ // but we shouldn't throw.
+ let expectedKey = "some_key";
+ Telemetry.scalarSet(MOBILE_ONLY_SCALAR, 11715);
+ Telemetry.scalarSetMaximum(MOBILE_ONLY_SCALAR, 11715);
+ Telemetry.keyedScalarAdd(MOBILE_ONLY_KEYED_SCALAR, expectedKey, 11715);
+ Telemetry.keyedScalarSet(MOBILE_ONLY_KEYED_SCALAR, expectedKey, 11715);
+ Telemetry.keyedScalarSetMaximum(
+ MOBILE_ONLY_KEYED_SCALAR,
+ expectedKey,
+ 11715
+ );
+
+ // Get a snapshot of the scalars.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent");
+ const keyedScalars = TelemetryTestUtils.getProcessScalars("parent");
+
+ Assert.equal(
+ scalars[DEFAULT_PRODUCT_SCALAR],
+ expectedValue,
+ "The default platfomrs scalar must contain the right value"
+ );
+ Assert.equal(
+ scalars[DESKTOP_ONLY_SCALAR],
+ expectedValue,
+ "The desktop-only scalar must contain the right value"
+ );
+ Assert.equal(
+ scalars[MULTIPRODUCT_SCALAR],
+ expectedValue,
+ "The multiproduct scalar must contain the right value"
+ );
+
+ Assert.ok(
+ !(MOBILE_ONLY_SCALAR in scalars),
+ "The mobile-only scalar must not be persisted."
+ );
+ Assert.ok(
+ !(MOBILE_ONLY_KEYED_SCALAR in keyedScalars),
+ "The mobile-only keyed scalar must not be persisted."
+ );
+ }
+);
+
+add_task(
+ {
+ skip_if: () => !gIsAndroid,
+ },
+ async function test_mobileSpecificScalar() {
+ const DEFAULT_PRODUCT_SCALAR = "telemetry.test.default_products";
+ const DESKTOP_ONLY_SCALAR = "telemetry.test.desktop_only";
+ const DESKTOP_ONLY_KEYED_SCALAR = "telemetry.test.keyed_desktop_only";
+ const MULTIPRODUCT_SCALAR = "telemetry.test.multiproduct";
+ const MOBILE_ONLY_SCALAR = "telemetry.test.mobile_only";
+ const MOBILE_ONLY_KEYED_SCALAR = "telemetry.test.keyed_mobile_only";
+
+ Telemetry.clearScalars();
+
+ // Try to set the mobile and multiproduct scalars
+ let expectedValue = 11714;
+ let expectedKey = "some_key";
+ Telemetry.scalarAdd(DEFAULT_PRODUCT_SCALAR, expectedValue);
+ Telemetry.scalarAdd(MOBILE_ONLY_SCALAR, expectedValue);
+ Telemetry.keyedScalarSet(
+ MOBILE_ONLY_KEYED_SCALAR,
+ expectedKey,
+ expectedValue
+ );
+ Telemetry.scalarAdd(MULTIPRODUCT_SCALAR, expectedValue);
+
+ // Try to set the desktop-only scalar to some value. We will not be recording the value,
+ // but we shouldn't throw.
+ Telemetry.scalarSet(DESKTOP_ONLY_SCALAR, 11715);
+ Telemetry.scalarSetMaximum(DESKTOP_ONLY_SCALAR, 11715);
+ Telemetry.keyedScalarAdd(DESKTOP_ONLY_KEYED_SCALAR, expectedKey, 11715);
+ Telemetry.keyedScalarSet(DESKTOP_ONLY_KEYED_SCALAR, expectedKey, 11715);
+ Telemetry.keyedScalarSetMaximum(
+ DESKTOP_ONLY_KEYED_SCALAR,
+ expectedKey,
+ 11715
+ );
+
+ // Get a snapshot of the scalars.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent");
+ const keyedScalars = TelemetryTestUtils.getProcessScalars("parent", true);
+
+ Assert.equal(
+ scalars[DEFAULT_PRODUCT_SCALAR],
+ expectedValue,
+ "The default products scalar must contain the right value"
+ );
+ Assert.equal(
+ scalars[MOBILE_ONLY_SCALAR],
+ expectedValue,
+ "The mobile-only scalar must contain the right value"
+ );
+ Assert.equal(
+ keyedScalars[MOBILE_ONLY_KEYED_SCALAR][expectedKey],
+ expectedValue,
+ "The mobile-only keyed scalar must contain the right value"
+ );
+ Assert.equal(
+ scalars[MULTIPRODUCT_SCALAR],
+ expectedValue,
+ "The multiproduct scalar must contain the right value"
+ );
+
+ Assert.ok(
+ !(DESKTOP_ONLY_SCALAR in scalars),
+ "The desktop-only scalar must not be persisted."
+ );
+ Assert.ok(
+ !(DESKTOP_ONLY_KEYED_SCALAR in keyedScalars),
+ "The desktop-only keyed scalar must not be persisted."
+ );
+ }
+);
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryScalars_buildFaster.js b/toolkit/components/telemetry/tests/unit/test_TelemetryScalars_buildFaster.js
new file mode 100644
index 0000000000..909025b91a
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryScalars_buildFaster.js
@@ -0,0 +1,226 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+/**
+ * Return the path to the definitions file for the scalars.
+ */
+function getDefinitionsPath() {
+ // Write the scalar definition to the spec file in the binary directory.
+ let definitionFile = Cc["@mozilla.org/file/local;1"].createInstance(
+ Ci.nsIFile
+ );
+ definitionFile = Services.dirsvc.get("GreD", Ci.nsIFile);
+ definitionFile.append("ScalarArtifactDefinitions.json");
+ return definitionFile.path;
+}
+
+add_task(async function test_setup() {
+ do_get_profile();
+});
+
+add_task(
+ {
+ // The test needs to write a file, and that fails in tests on Android.
+ // We don't really need the Android coverage, so skip on Android.
+ skip_if: () => AppConstants.platform == "android",
+ },
+ async function test_invalidJSON() {
+ const INVALID_JSON = "{ invalid,JSON { {1}";
+ const FILE_PATH = getDefinitionsPath();
+
+ // Write a corrupted JSON file.
+ await IOUtils.writeUTF8(FILE_PATH, INVALID_JSON, {
+ mode: "overwrite",
+ });
+
+ // Simulate Firefox startup. This should not throw!
+ await TelemetryController.testSetup();
+ await TelemetryController.testPromiseJsProbeRegistration();
+
+ // Cleanup.
+ await TelemetryController.testShutdown();
+ await IOUtils.remove(FILE_PATH);
+ }
+);
+
+add_task(
+ {
+ // The test needs to write a file, and that fails in tests on Android.
+ // We don't really need the Android coverage, so skip on Android.
+ skip_if: () => AppConstants.platform == "android",
+ },
+ async function test_dynamicBuiltin() {
+ const DYNAMIC_SCALAR_SPEC = {
+ "telemetry.test": {
+ builtin_dynamic: {
+ kind: "nsITelemetry::SCALAR_TYPE_COUNT",
+ expires: "never",
+ record_on_release: false,
+ keyed: false,
+ },
+ builtin_dynamic_other: {
+ kind: "nsITelemetry::SCALAR_TYPE_BOOLEAN",
+ expires: "never",
+ record_on_release: false,
+ keyed: false,
+ },
+ builtin_dynamic_expired: {
+ kind: "nsITelemetry::SCALAR_TYPE_BOOLEAN",
+ expires: AppConstants.MOZ_APP_VERSION,
+ record_on_release: false,
+ keyed: false,
+ },
+ builtin_dynamic_multi: {
+ kind: "nsITelemetry::SCALAR_TYPE_COUNT",
+ expired: false,
+ record_on_release: false,
+ keyed: false,
+ stores: ["main", "sync"],
+ },
+ builtin_dynamic_sync_only: {
+ kind: "nsITelemetry::SCALAR_TYPE_COUNT",
+ expired: false,
+ record_on_release: false,
+ keyed: false,
+ stores: ["sync"],
+ },
+ },
+ };
+
+ Telemetry.clearScalars();
+
+ // Let's write to the definition file to also cover the file
+ // loading part.
+ const FILE_PATH = getDefinitionsPath();
+ await IOUtils.writeJSON(FILE_PATH, DYNAMIC_SCALAR_SPEC);
+
+ // Start TelemetryController to trigger loading the specs.
+ await TelemetryController.testReset();
+ await TelemetryController.testPromiseJsProbeRegistration();
+
+ // Store to that scalar.
+ const TEST_SCALAR1 = "telemetry.test.builtin_dynamic";
+ const TEST_SCALAR2 = "telemetry.test.builtin_dynamic_other";
+ const TEST_SCALAR3 = "telemetry.test.builtin_dynamic_multi";
+ const TEST_SCALAR4 = "telemetry.test.builtin_dynamic_sync_only";
+ const TEST_SCALAR5 = "telemetry.test.builtin_dynamic_expired";
+ Telemetry.scalarSet(TEST_SCALAR1, 3785);
+ Telemetry.scalarSet(TEST_SCALAR2, true);
+ Telemetry.scalarSet(TEST_SCALAR3, 1337);
+ Telemetry.scalarSet(TEST_SCALAR4, 31337);
+ Telemetry.scalarSet(TEST_SCALAR5, true);
+
+ // Check the values we tried to store.
+ const scalars = Telemetry.getSnapshotForScalars("main", false).parent;
+ const syncScalars = Telemetry.getSnapshotForScalars("sync", false).parent;
+
+ // Check that they are serialized to the correct format.
+ Assert.equal(
+ typeof scalars[TEST_SCALAR1],
+ "number",
+ TEST_SCALAR1 + " must be serialized to the correct format."
+ );
+ Assert.ok(
+ Number.isInteger(scalars[TEST_SCALAR1]),
+ TEST_SCALAR1 + " must be a finite integer."
+ );
+ Assert.equal(
+ scalars[TEST_SCALAR1],
+ 3785,
+ TEST_SCALAR1 + " must have the correct value."
+ );
+ Assert.equal(
+ typeof scalars[TEST_SCALAR2],
+ "boolean",
+ TEST_SCALAR2 + " must be serialized to the correct format."
+ );
+ Assert.equal(
+ scalars[TEST_SCALAR2],
+ true,
+ TEST_SCALAR2 + " must have the correct value."
+ );
+
+ Assert.equal(
+ typeof scalars[TEST_SCALAR3],
+ "number",
+ `${TEST_SCALAR3} must be serialized to the correct format.`
+ );
+ Assert.equal(
+ scalars[TEST_SCALAR3],
+ 1337,
+ `${TEST_SCALAR3} must have the correct value.`
+ );
+ Assert.equal(
+ typeof syncScalars[TEST_SCALAR3],
+ "number",
+ `${TEST_SCALAR3} must be serialized in the sync store to the correct format.`
+ );
+ Assert.equal(
+ syncScalars[TEST_SCALAR3],
+ 1337,
+ `${TEST_SCALAR3} must have the correct value in the sync snapshot.`
+ );
+
+ Assert.ok(
+ !(TEST_SCALAR4 in scalars),
+ `${TEST_SCALAR4} must not be in the main store.`
+ );
+ Assert.equal(
+ typeof syncScalars[TEST_SCALAR4],
+ "number",
+ `${TEST_SCALAR4} must be in the sync snapshot.`
+ );
+ Assert.equal(
+ syncScalars[TEST_SCALAR4],
+ 31337,
+ `${TEST_SCALAR4} must have the correct value.`
+ );
+
+ // Clean up.
+ await TelemetryController.testShutdown();
+ await IOUtils.remove(FILE_PATH);
+ }
+);
+
+add_task(async function test_keyedDynamicBuiltin() {
+ Telemetry.clearScalars();
+
+ // Register the built-in scalars (let's not take the I/O hit).
+ Telemetry.registerBuiltinScalars("telemetry.test", {
+ builtin_dynamic_keyed: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ expired: false,
+ record_on_release: false,
+ keyed: true,
+ },
+ });
+
+ // Store to that scalar.
+ const TEST_SCALAR1 = "telemetry.test.builtin_dynamic_keyed";
+ Telemetry.keyedScalarSet(TEST_SCALAR1, "test-key", 3785);
+
+ // Check the values we tried to store.
+ const scalars = Telemetry.getSnapshotForKeyedScalars("main", false).parent;
+
+ // Check that they are serialized to the correct format.
+ Assert.equal(
+ typeof scalars[TEST_SCALAR1],
+ "object",
+ TEST_SCALAR1 + " must be a keyed scalar."
+ );
+ Assert.equal(
+ typeof scalars[TEST_SCALAR1]["test-key"],
+ "number",
+ TEST_SCALAR1 + " must be serialized to the correct format."
+ );
+ Assert.ok(
+ Number.isInteger(scalars[TEST_SCALAR1]["test-key"]),
+ TEST_SCALAR1 + " must be a finite integer."
+ );
+ Assert.equal(
+ scalars[TEST_SCALAR1]["test-key"],
+ 3785,
+ TEST_SCALAR1 + " must have the correct value."
+ );
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryScalars_impressionId.js b/toolkit/components/telemetry/tests/unit/test_TelemetryScalars_impressionId.js
new file mode 100644
index 0000000000..8717d34501
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryScalars_impressionId.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+const CATEGORY = "telemetry.test";
+const MAIN_ONLY = `${CATEGORY}.main_only`;
+const IMPRESSION_ID_ONLY = `${CATEGORY}.impression_id_only`;
+
+add_task(async function test_multistore_basics() {
+ Telemetry.clearScalars();
+
+ const expectedUint = 3785;
+ const expectedString = "{some_impression_id}";
+ Telemetry.scalarSet(MAIN_ONLY, expectedUint);
+ Telemetry.scalarSet(IMPRESSION_ID_ONLY, expectedString);
+
+ const mainScalars = Telemetry.getSnapshotForScalars("main").parent;
+ const impressionIdScalars =
+ Telemetry.getSnapshotForScalars("deletion-request").parent;
+
+ Assert.ok(
+ MAIN_ONLY in mainScalars,
+ `Main-store scalar ${MAIN_ONLY} must be in main snapshot.`
+ );
+ Assert.ok(
+ !(MAIN_ONLY in impressionIdScalars),
+ `Main-store scalar ${MAIN_ONLY} must not be in deletion-request snapshot.`
+ );
+ Assert.equal(
+ mainScalars[MAIN_ONLY],
+ expectedUint,
+ `Main-store scalar ${MAIN_ONLY} must have correct value.`
+ );
+
+ Assert.ok(
+ IMPRESSION_ID_ONLY in impressionIdScalars,
+ `Deletion-request store scalar ${IMPRESSION_ID_ONLY} must be in deletion-request snapshot.`
+ );
+ Assert.ok(
+ !(IMPRESSION_ID_ONLY in mainScalars),
+ `Deletion-request scalar ${IMPRESSION_ID_ONLY} must not be in main snapshot.`
+ );
+ Assert.equal(
+ impressionIdScalars[IMPRESSION_ID_ONLY],
+ expectedString,
+ `Deletion-request store scalar ${IMPRESSION_ID_ONLY} must have correct value.`
+ );
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryScalars_multistore.js b/toolkit/components/telemetry/tests/unit/test_TelemetryScalars_multistore.js
new file mode 100644
index 0000000000..841caa4f1d
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryScalars_multistore.js
@@ -0,0 +1,415 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+const CATEGORY = "telemetry.test";
+const MAIN_ONLY = `${CATEGORY}.main_only`;
+const SYNC_ONLY = `${CATEGORY}.sync_only`;
+const MULTIPLE_STORES = `${CATEGORY}.multiple_stores`;
+const MULTIPLE_STORES_STRING = `${CATEGORY}.multiple_stores_string`;
+const MULTIPLE_STORES_BOOL = `${CATEGORY}.multiple_stores_bool`;
+const MULTIPLE_STORES_KEYED = `${CATEGORY}.multiple_stores_keyed`;
+
+function getParentSnapshot(store, keyed = false, clear = false) {
+ return keyed
+ ? Telemetry.getSnapshotForKeyedScalars(store, clear).parent
+ : Telemetry.getSnapshotForScalars(store, clear).parent;
+}
+
+add_task(async function test_multistore_basics() {
+ Telemetry.clearScalars();
+
+ const expectedUint = 3785;
+ const expectedBool = true;
+ const expectedString = "some value";
+ const expectedKey = "some key";
+ Telemetry.scalarSet(MAIN_ONLY, expectedUint);
+ Telemetry.scalarSet(SYNC_ONLY, expectedUint);
+ Telemetry.scalarSet(MULTIPLE_STORES, expectedUint);
+ Telemetry.scalarSet(MULTIPLE_STORES_STRING, expectedString);
+ Telemetry.scalarSet(MULTIPLE_STORES_BOOL, expectedBool);
+ Telemetry.keyedScalarSet(MULTIPLE_STORES_KEYED, expectedKey, expectedUint);
+
+ const mainScalars = getParentSnapshot("main");
+ const syncScalars = getParentSnapshot("sync");
+ const mainKeyedScalars = getParentSnapshot("main", true /* keyed */);
+ const syncKeyedScalars = getParentSnapshot("sync", true /* keyed */);
+
+ Assert.ok(
+ MAIN_ONLY in mainScalars,
+ `Main-store scalar ${MAIN_ONLY} must be in main snapshot.`
+ );
+ Assert.ok(
+ !(MAIN_ONLY in syncScalars),
+ `Main-store scalar ${MAIN_ONLY} must not be in sync snapshot.`
+ );
+ Assert.equal(
+ mainScalars[MAIN_ONLY],
+ expectedUint,
+ `Main-store scalar ${MAIN_ONLY} must have correct value.`
+ );
+
+ Assert.ok(
+ SYNC_ONLY in syncScalars,
+ `Sync-store scalar ${SYNC_ONLY} must be in sync snapshot.`
+ );
+ Assert.ok(
+ !(SYNC_ONLY in mainScalars),
+ `Sync-store scalar ${SYNC_ONLY} must not be in main snapshot.`
+ );
+ Assert.equal(
+ syncScalars[SYNC_ONLY],
+ expectedUint,
+ `Sync-store scalar ${SYNC_ONLY} must have correct value.`
+ );
+
+ Assert.ok(
+ MULTIPLE_STORES in mainScalars && MULTIPLE_STORES in syncScalars,
+ `Multi-store scalar ${MULTIPLE_STORES} must be in both main and sync snapshots.`
+ );
+ Assert.equal(
+ mainScalars[MULTIPLE_STORES],
+ expectedUint,
+ `Multi-store scalar ${MULTIPLE_STORES} must have correct value in main store.`
+ );
+ Assert.equal(
+ syncScalars[MULTIPLE_STORES],
+ expectedUint,
+ `Multi-store scalar ${MULTIPLE_STORES} must have correct value in sync store.`
+ );
+
+ Assert.ok(
+ MULTIPLE_STORES_STRING in mainScalars &&
+ MULTIPLE_STORES_STRING in syncScalars,
+ `Multi-store scalar ${MULTIPLE_STORES_STRING} must be in both main and sync snapshots.`
+ );
+ Assert.equal(
+ mainScalars[MULTIPLE_STORES_STRING],
+ expectedString,
+ `Multi-store scalar ${MULTIPLE_STORES_STRING} must have correct value in main store.`
+ );
+ Assert.equal(
+ syncScalars[MULTIPLE_STORES_STRING],
+ expectedString,
+ `Multi-store scalar ${MULTIPLE_STORES_STRING} must have correct value in sync store.`
+ );
+
+ Assert.ok(
+ MULTIPLE_STORES_BOOL in mainScalars && MULTIPLE_STORES_BOOL in syncScalars,
+ `Multi-store scalar ${MULTIPLE_STORES_BOOL} must be in both main and sync snapshots.`
+ );
+ Assert.equal(
+ mainScalars[MULTIPLE_STORES_BOOL],
+ expectedBool,
+ `Multi-store scalar ${MULTIPLE_STORES_BOOL} must have correct value in main store.`
+ );
+ Assert.equal(
+ syncScalars[MULTIPLE_STORES_BOOL],
+ expectedBool,
+ `Multi-store scalar ${MULTIPLE_STORES_BOOL} must have correct value in sync store.`
+ );
+
+ Assert.ok(
+ MULTIPLE_STORES_KEYED in mainKeyedScalars &&
+ MULTIPLE_STORES_KEYED in syncKeyedScalars,
+ `Multi-store scalar ${MULTIPLE_STORES_KEYED} must be in both main and sync snapshots.`
+ );
+ Assert.ok(
+ expectedKey in mainKeyedScalars[MULTIPLE_STORES_KEYED] &&
+ expectedKey in syncKeyedScalars[MULTIPLE_STORES_KEYED],
+ `Multi-store scalar ${MULTIPLE_STORES_KEYED} must have key ${expectedKey} in both snapshots.`
+ );
+ Assert.equal(
+ mainKeyedScalars[MULTIPLE_STORES_KEYED][expectedKey],
+ expectedUint,
+ `Multi-store scalar ${MULTIPLE_STORES_KEYED} must have correct value in main store.`
+ );
+ Assert.equal(
+ syncKeyedScalars[MULTIPLE_STORES_KEYED][expectedKey],
+ expectedUint,
+ `Multi-store scalar ${MULTIPLE_STORES_KEYED} must have correct value in sync store.`
+ );
+});
+
+add_task(async function test_multistore_uint() {
+ Telemetry.clearScalars();
+
+ // Uint scalars are the only kind with an implicit default value of 0.
+ // They shouldn't report any value until set, but if you Add or SetMaximum
+ // they pretend that they have been set to 0 for the purposes of that operation.
+
+ function assertNotIn() {
+ let mainScalars = getParentSnapshot("main");
+ let syncScalars = getParentSnapshot("sync");
+
+ if (!mainScalars && !syncScalars) {
+ Assert.ok(true, "No scalars at all");
+ } else {
+ Assert.ok(
+ !(MULTIPLE_STORES in mainScalars) && !(MULTIPLE_STORES in syncScalars),
+ `Multi-store scalar ${MULTIPLE_STORES} must not have an initial value in either store.`
+ );
+ }
+ }
+ assertNotIn();
+
+ // Test that Add operates on implicit 0.
+ Telemetry.scalarAdd(MULTIPLE_STORES, 1);
+
+ function assertBothEqual(val, clear = false) {
+ let mainScalars = getParentSnapshot("main", false, clear);
+ let syncScalars = getParentSnapshot("sync", false, clear);
+
+ Assert.ok(
+ MULTIPLE_STORES in mainScalars && MULTIPLE_STORES in syncScalars,
+ `Multi-store scalar ${MULTIPLE_STORES} must be in both main and sync snapshots.`
+ );
+ Assert.equal(
+ mainScalars[MULTIPLE_STORES],
+ val,
+ `Multi-store scalar ${MULTIPLE_STORES} must have the correct value in main store.`
+ );
+ Assert.equal(
+ syncScalars[MULTIPLE_STORES],
+ val,
+ `Multi-store scalar ${MULTIPLE_STORES} must have the correct value in sync store.`
+ );
+ }
+
+ assertBothEqual(1, true /* clear */);
+
+ assertNotIn();
+
+ // Test that SetMaximum operates on implicit 0.
+ Telemetry.scalarSetMaximum(MULTIPLE_STORES, 1337);
+ assertBothEqual(1337);
+
+ // Test that Add works, since we're in the neighbourhood.
+ Telemetry.scalarAdd(MULTIPLE_STORES, 1);
+ assertBothEqual(1338, true /* clear */);
+
+ assertNotIn();
+
+ // Test that clearing individual stores works
+ // and that afterwards the values are managed independently.
+ Telemetry.scalarAdd(MULTIPLE_STORES, 1234);
+ assertBothEqual(1234);
+ let syncScalars = getParentSnapshot(
+ "sync",
+ false /* keyed */,
+ true /* clear */
+ );
+ Assert.equal(
+ syncScalars[MULTIPLE_STORES],
+ 1234,
+ `Multi-store scalar ${MULTIPLE_STORES} must be present in a second snapshot.`
+ );
+ syncScalars = getParentSnapshot("sync");
+ Assert.equal(
+ syncScalars,
+ undefined,
+ `Multi-store scalar ${MULTIPLE_STORES} must not be present after clearing.`
+ );
+ let mainScalars = getParentSnapshot("main");
+ Assert.equal(
+ mainScalars[MULTIPLE_STORES],
+ 1234,
+ `Multi-store scalar ${MULTIPLE_STORES} must maintain value in main store after sync store is cleared.`
+ );
+
+ Telemetry.scalarSetMaximum(MULTIPLE_STORES, 1);
+ syncScalars = getParentSnapshot("sync");
+ Assert.equal(
+ syncScalars[MULTIPLE_STORES],
+ 1,
+ `Multi-store scalar ${MULTIPLE_STORES} must return to using implicit 0 for setMax operation.`
+ );
+ mainScalars = getParentSnapshot("main");
+ Assert.equal(
+ mainScalars[MULTIPLE_STORES],
+ 1234,
+ `Multi-store scalar ${MULTIPLE_STORES} must retain old value.`
+ );
+
+ Telemetry.scalarAdd(MULTIPLE_STORES, 1);
+ syncScalars = getParentSnapshot("sync");
+ Assert.equal(
+ syncScalars[MULTIPLE_STORES],
+ 2,
+ `Multi-store scalar ${MULTIPLE_STORES} must manage independently for add operations.`
+ );
+ mainScalars = getParentSnapshot("main");
+ Assert.equal(
+ mainScalars[MULTIPLE_STORES],
+ 1235,
+ `Multi-store scalar ${MULTIPLE_STORES} must add properly.`
+ );
+
+ Telemetry.scalarSet(MULTIPLE_STORES, 9876);
+ assertBothEqual(9876);
+});
+
+add_task(async function test_empty_absence() {
+ // Current semantics are we don't snapshot empty things.
+ // So no {parent: {}, ...}. Instead {...}.
+
+ Telemetry.clearScalars();
+
+ Telemetry.scalarSet(MULTIPLE_STORES, 1);
+ let snapshot = getParentSnapshot("main", false /* keyed */, true /* clear */);
+
+ Assert.ok(
+ MULTIPLE_STORES in snapshot,
+ `${MULTIPLE_STORES} must be in the snapshot.`
+ );
+ Assert.equal(
+ snapshot[MULTIPLE_STORES],
+ 1,
+ `${MULTIPLE_STORES} must have the correct value.`
+ );
+
+ snapshot = getParentSnapshot("main", false /* keyed */, true /* clear */);
+ Assert.equal(
+ snapshot,
+ undefined,
+ `Parent snapshot must be empty if no data.`
+ );
+
+ snapshot = getParentSnapshot("sync", false /* keyed */, true /* clear */);
+ Assert.ok(
+ MULTIPLE_STORES in snapshot,
+ `${MULTIPLE_STORES} must be in the sync snapshot.`
+ );
+ Assert.equal(
+ snapshot[MULTIPLE_STORES],
+ 1,
+ `${MULTIPLE_STORES} must have the correct value in the sync snapshot.`
+ );
+});
+
+add_task(async function test_empty_absence_keyed() {
+ // Current semantics are we don't snapshot empty things.
+ // So no {parent: {}, ...}. Instead {...}.
+ // And for Keyed Scalars, no {parent: { keyed_scalar: {} }, ...}. Just {...}.
+
+ Telemetry.clearScalars();
+
+ const key = "just a key, y'know";
+ Telemetry.keyedScalarSet(MULTIPLE_STORES_KEYED, key, 1);
+ let snapshot = getParentSnapshot("main", true /* keyed */, true /* clear */);
+
+ Assert.ok(
+ MULTIPLE_STORES_KEYED in snapshot,
+ `${MULTIPLE_STORES_KEYED} must be in the snapshot.`
+ );
+ Assert.ok(
+ key in snapshot[MULTIPLE_STORES_KEYED],
+ `${MULTIPLE_STORES_KEYED} must have the stored key.`
+ );
+ Assert.equal(
+ snapshot[MULTIPLE_STORES_KEYED][key],
+ 1,
+ `${MULTIPLE_STORES_KEYED}[${key}] should have the correct value.`
+ );
+
+ snapshot = getParentSnapshot("main", true /* keyed */);
+ Assert.equal(
+ snapshot,
+ undefined,
+ `Parent snapshot should be empty if no data.`
+ );
+ snapshot = getParentSnapshot("sync", true /* keyed */);
+
+ Assert.ok(
+ MULTIPLE_STORES_KEYED in snapshot,
+ `${MULTIPLE_STORES_KEYED} must be in the sync snapshot.`
+ );
+ Assert.ok(
+ key in snapshot[MULTIPLE_STORES_KEYED],
+ `${MULTIPLE_STORES_KEYED} must have the stored key.`
+ );
+ Assert.equal(
+ snapshot[MULTIPLE_STORES_KEYED][key],
+ 1,
+ `${MULTIPLE_STORES_KEYED}[${key}] should have the correct value.`
+ );
+});
+
+add_task(async function test_multistore_default_values() {
+ Telemetry.clearScalars();
+
+ const expectedUint = 3785;
+ const expectedKey = "some key";
+ Telemetry.scalarSet(MAIN_ONLY, expectedUint);
+ Telemetry.scalarSet(SYNC_ONLY, expectedUint);
+ Telemetry.scalarSet(MULTIPLE_STORES, expectedUint);
+ Telemetry.keyedScalarSet(MULTIPLE_STORES_KEYED, expectedKey, expectedUint);
+
+ let mainScalars;
+ let mainKeyedScalars;
+
+ // Getting snapshot and NOT clearing (using default values for optional parameters)
+ mainScalars = Telemetry.getSnapshotForScalars().parent;
+ mainKeyedScalars = Telemetry.getSnapshotForKeyedScalars().parent;
+
+ Assert.equal(
+ mainScalars[MAIN_ONLY],
+ expectedUint,
+ `Main-store scalar ${MAIN_ONLY} must have correct value.`
+ );
+ Assert.ok(
+ !(SYNC_ONLY in mainScalars),
+ `Sync-store scalar ${SYNC_ONLY} must not be in main snapshot.`
+ );
+ Assert.equal(
+ mainScalars[MULTIPLE_STORES],
+ expectedUint,
+ `Multi-store scalar ${MULTIPLE_STORES} must have correct value in main store.`
+ );
+ Assert.equal(
+ mainKeyedScalars[MULTIPLE_STORES_KEYED][expectedKey],
+ expectedUint,
+ `Multi-store scalar ${MULTIPLE_STORES_KEYED} must have correct value in main store.`
+ );
+
+ // Getting snapshot and clearing
+ mainScalars = Telemetry.getSnapshotForScalars("main", true).parent;
+ mainKeyedScalars = Telemetry.getSnapshotForKeyedScalars("main", true).parent;
+
+ Assert.equal(
+ mainScalars[MAIN_ONLY],
+ expectedUint,
+ `Main-store scalar ${MAIN_ONLY} must have correct value.`
+ );
+ Assert.ok(
+ !(SYNC_ONLY in mainScalars),
+ `Sync-store scalar ${SYNC_ONLY} must not be in main snapshot.`
+ );
+ Assert.equal(
+ mainScalars[MULTIPLE_STORES],
+ expectedUint,
+ `Multi-store scalar ${MULTIPLE_STORES} must have correct value in main store.`
+ );
+ Assert.equal(
+ mainKeyedScalars[MULTIPLE_STORES_KEYED][expectedKey],
+ expectedUint,
+ `Multi-store scalar ${MULTIPLE_STORES_KEYED} must have correct value in main store.`
+ );
+
+ // Getting snapshot (with default values), should be empty now
+ mainScalars = Telemetry.getSnapshotForScalars().parent || {};
+ mainKeyedScalars = Telemetry.getSnapshotForKeyedScalars().parent || {};
+
+ Assert.ok(
+ !(MAIN_ONLY in mainScalars),
+ `Main-store scalar ${MAIN_ONLY} must not be in main snapshot.`
+ );
+ Assert.ok(
+ !(MULTIPLE_STORES in mainScalars),
+ `Multi-store scalar ${MULTIPLE_STORES} must not be in main snapshot.`
+ );
+ Assert.ok(
+ !(MULTIPLE_STORES_KEYED in mainKeyedScalars),
+ `Multi-store scalar ${MULTIPLE_STORES_KEYED} must not be in main snapshot.`
+ );
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetrySend.js b/toolkit/components/telemetry/tests/unit/test_TelemetrySend.js
new file mode 100644
index 0000000000..5bcb69d5a0
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySend.js
@@ -0,0 +1,1110 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+// This tests the public Telemetry API for submitting pings.
+
+"use strict";
+
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { MockRegistrar } = ChromeUtils.importESModule(
+ "resource://testing-common/MockRegistrar.sys.mjs"
+);
+const { TelemetrySend } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySend.sys.mjs"
+);
+const { TelemetryStorage } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryStorage.sys.mjs"
+);
+const { TelemetryUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ TelemetryHealthPing: "resource://gre/modules/HealthPing.sys.mjs",
+});
+
+const MS_IN_A_MINUTE = 60 * 1000;
+
+function countPingTypes(pings) {
+ let countByType = new Map();
+ for (let p of pings) {
+ countByType.set(p.type, 1 + (countByType.get(p.type) || 0));
+ }
+ return countByType;
+}
+
+function setPingLastModified(id, timestamp) {
+ const path = PathUtils.join(TelemetryStorage.pingDirectoryPath, id);
+ return IOUtils.setModificationTime(path, timestamp);
+}
+
+// Mock out the send timer activity.
+function waitForTimer() {
+ return new Promise(resolve => {
+ fakePingSendTimer(
+ (callback, timeout) => {
+ resolve([callback, timeout]);
+ },
+ () => {}
+ );
+ });
+}
+
+function sendPing(aSendClientId, aSendEnvironment) {
+ const TEST_PING_TYPE = "test-ping-type";
+
+ if (PingServer.started) {
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+ } else {
+ TelemetrySend.setServer("http://doesnotexist");
+ }
+
+ let options = {
+ addClientId: aSendClientId,
+ addEnvironment: aSendEnvironment,
+ };
+ return TelemetryController.submitExternalPing(TEST_PING_TYPE, {}, options);
+}
+
+// Allow easy faking of readable ping ids.
+// This helps with debugging issues with e.g. ordering in the send logic.
+function fakePingId(type, number) {
+ const HEAD = "93bd0011-2c8f-4e1c-bee0-";
+ const TAIL = "000000000000";
+ const N = String(number);
+ const id = HEAD + type + TAIL.slice(type.length, -N.length) + N;
+ fakeGeneratePingId(() => id);
+ return id;
+}
+
+var checkPingsSaved = async function (pingIds) {
+ let allFound = true;
+ for (let id of pingIds) {
+ const path = PathUtils.join(TelemetryStorage.pingDirectoryPath, id);
+ let exists = false;
+ try {
+ exists = await IOUtils.exists(path);
+ } catch (ex) {}
+
+ if (!exists) {
+ dump("checkPingsSaved - failed to find ping: " + path + "\n");
+ allFound = false;
+ }
+ }
+
+ return allFound;
+};
+
+function histogramValueCount(h) {
+ return Object.values(h.values).reduce((a, b) => a + b, 0);
+}
+
+add_task(async function test_setup() {
+ // Trigger a proper telemetry init.
+ do_get_profile(true);
+
+ // Addon manager needs a profile directory.
+ await loadAddonManager(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "1.9.2"
+ );
+ finishAddonManagerStartup();
+ fakeIntlReady();
+
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.HealthPingEnabled,
+ true
+ );
+ TelemetryStopwatch.setTestModeEnabled(true);
+});
+
+// Test the ping sending logic.
+add_task(async function test_sendPendingPings() {
+ const TYPE_PREFIX = "test-sendPendingPings-";
+ const TEST_TYPE_A = TYPE_PREFIX + "A";
+ const TEST_TYPE_B = TYPE_PREFIX + "B";
+
+ const TYPE_A_COUNT = 20;
+ const TYPE_B_COUNT = 5;
+
+ let histSuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
+ let histSendTimeSuccess = Telemetry.getHistogramById(
+ "TELEMETRY_SEND_SUCCESS"
+ );
+ let histSendTimeFail = Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE");
+ histSuccess.clear();
+ histSendTimeSuccess.clear();
+ histSendTimeFail.clear();
+
+ // Fake a current date.
+ let now = TelemetryUtils.truncateToDays(new Date());
+ now = fakeNow(futureDate(now, 10 * 60 * MS_IN_A_MINUTE));
+
+ // Enable test-mode for TelemetrySend, otherwise we won't store pending pings
+ // before the module is fully initialized later.
+ TelemetrySend.setTestModeEnabled(true);
+
+ // Submit some pings without the server and telemetry started yet.
+ for (let i = 0; i < TYPE_A_COUNT; ++i) {
+ fakePingId("a", i);
+ const id = await TelemetryController.submitExternalPing(TEST_TYPE_A, {});
+ await setPingLastModified(id, now.getTime() + i * 1000);
+ }
+
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ TYPE_A_COUNT,
+ "Should have correct pending ping count"
+ );
+
+ // Submit some more pings of a different type.
+ now = fakeNow(futureDate(now, 5 * MS_IN_A_MINUTE));
+ for (let i = 0; i < TYPE_B_COUNT; ++i) {
+ fakePingId("b", i);
+ const id = await TelemetryController.submitExternalPing(TEST_TYPE_B, {});
+ await setPingLastModified(id, now.getTime() + i * 1000);
+ }
+
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ TYPE_A_COUNT + TYPE_B_COUNT,
+ "Should have correct pending ping count"
+ );
+
+ Assert.deepEqual(
+ histSuccess.snapshot().values,
+ {},
+ "Should not have recorded any sending in histograms yet."
+ );
+ Assert.equal(
+ histSendTimeSuccess.snapshot().sum,
+ 0,
+ "Should not have recorded any sending in histograms yet."
+ );
+ Assert.equal(
+ histSendTimeFail.snapshot().sum,
+ 0,
+ "Should not have recorded any sending in histograms yet."
+ );
+
+ // Now enable sending to the ping server.
+ now = fakeNow(futureDate(now, MS_IN_A_MINUTE));
+ PingServer.start();
+ Services.prefs.setStringPref(
+ TelemetryUtils.Preferences.Server,
+ "http://localhost:" + PingServer.port
+ );
+
+ let timerPromise = waitForTimer();
+ await TelemetryController.testReset();
+ let [pingSendTimerCallback, pingSendTimeout] = await timerPromise;
+ Assert.ok(!!pingSendTimerCallback, "Should have a timer callback");
+
+ // We should have received 10 pings from the first send batch:
+ // 5 of type B and 5 of type A, as sending is newest-first.
+ // The other pings should be delayed by the 10-pings-per-minute limit.
+ let pings = await PingServer.promiseNextPings(10);
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ TYPE_A_COUNT - 5,
+ "Should have correct pending ping count"
+ );
+ PingServer.registerPingHandler(() =>
+ Assert.ok(false, "Should not have received any pings now")
+ );
+ let countByType = countPingTypes(pings);
+
+ Assert.equal(
+ countByType.get(TEST_TYPE_B),
+ TYPE_B_COUNT,
+ "Should have received the correct amount of type B pings"
+ );
+ Assert.equal(
+ countByType.get(TEST_TYPE_A),
+ 10 - TYPE_B_COUNT,
+ "Should have received the correct amount of type A pings"
+ );
+
+ Assert.deepEqual(
+ histSuccess.snapshot().values,
+ { 0: 0, 1: 10, 2: 0 },
+ "Should have recorded sending success in histograms."
+ );
+ Assert.equal(
+ histogramValueCount(histSendTimeSuccess.snapshot()),
+ 10,
+ "Should have recorded successful send times in histograms."
+ );
+ Assert.equal(
+ histogramValueCount(histSendTimeFail.snapshot()),
+ 0,
+ "Should not have recorded any failed sending in histograms yet."
+ );
+
+ // As we hit the ping send limit and still have pending pings, a send tick should
+ // be scheduled in a minute.
+ Assert.ok(!!pingSendTimerCallback, "Timer callback should be set");
+ Assert.equal(
+ pingSendTimeout,
+ MS_IN_A_MINUTE,
+ "Send tick timeout should be correct"
+ );
+
+ // Trigger the next tick - we should receive the next 10 type A pings.
+ PingServer.resetPingHandler();
+ now = fakeNow(futureDate(now, pingSendTimeout));
+ timerPromise = waitForTimer();
+ pingSendTimerCallback();
+ [pingSendTimerCallback, pingSendTimeout] = await timerPromise;
+
+ pings = await PingServer.promiseNextPings(10);
+ PingServer.registerPingHandler(() =>
+ Assert.ok(false, "Should not have received any pings now")
+ );
+ countByType = countPingTypes(pings);
+
+ Assert.equal(
+ countByType.get(TEST_TYPE_A),
+ 10,
+ "Should have received the correct amount of type A pings"
+ );
+
+ // We hit the ping send limit again and still have pending pings, a send tick should
+ // be scheduled in a minute.
+ Assert.equal(
+ pingSendTimeout,
+ MS_IN_A_MINUTE,
+ "Send tick timeout should be correct"
+ );
+
+ // Trigger the next tick - we should receive the remaining type A pings.
+ PingServer.resetPingHandler();
+ now = fakeNow(futureDate(now, pingSendTimeout));
+ await pingSendTimerCallback();
+
+ pings = await PingServer.promiseNextPings(5);
+ PingServer.registerPingHandler(() =>
+ Assert.ok(false, "Should not have received any pings now")
+ );
+ countByType = countPingTypes(pings);
+
+ Assert.equal(
+ countByType.get(TEST_TYPE_A),
+ 5,
+ "Should have received the correct amount of type A pings"
+ );
+
+ await TelemetrySend.testWaitOnOutgoingPings();
+ PingServer.resetPingHandler();
+ // Restore the default ping id generator.
+ fakeGeneratePingId(() => TelemetryUtils.generateUUID());
+});
+
+add_task(async function test_sendDateHeader() {
+ fakeNow(new Date(Date.UTC(2011, 1, 1, 11, 0, 0)));
+ await TelemetrySend.reset();
+
+ let pingId = await TelemetryController.submitExternalPing(
+ "test-send-date-header",
+ {}
+ );
+ let req = await PingServer.promiseNextRequest();
+ let ping = decodeRequestPayload(req);
+ Assert.equal(
+ req.getHeader("Date"),
+ "Tue, 01 Feb 2011 11:00:00 GMT",
+ "Telemetry should send the correct Date header with requests."
+ );
+ Assert.equal(ping.id, pingId, "Should have received the correct ping id.");
+});
+
+// Test the backoff timeout behavior after send failures.
+add_task(async function test_backoffTimeout() {
+ const TYPE_PREFIX = "test-backoffTimeout-";
+ const TEST_TYPE_C = TYPE_PREFIX + "C";
+ const TEST_TYPE_D = TYPE_PREFIX + "D";
+ const TEST_TYPE_E = TYPE_PREFIX + "E";
+
+ let histSuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
+ let histSendTimeSuccess = Telemetry.getHistogramById(
+ "TELEMETRY_SEND_SUCCESS"
+ );
+ let histSendTimeFail = Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE");
+
+ // Failing a ping send now should trigger backoff behavior.
+ let now = fakeNow(2010, 1, 1, 11, 0, 0);
+ await TelemetrySend.reset();
+ PingServer.stop();
+
+ histSuccess.clear();
+ histSendTimeSuccess.clear();
+ histSendTimeFail.clear();
+
+ fakePingId("c", 0);
+ now = fakeNow(futureDate(now, MS_IN_A_MINUTE));
+ let sendAttempts = 0;
+ let timerPromise = waitForTimer();
+ await TelemetryController.submitExternalPing(TEST_TYPE_C, {});
+ let [pingSendTimerCallback, pingSendTimeout] = await timerPromise;
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ 1,
+ "Should have one pending ping."
+ );
+ ++sendAttempts;
+
+ const MAX_BACKOFF_TIMEOUT = 120 * MS_IN_A_MINUTE;
+ for (
+ let timeout = 2 * MS_IN_A_MINUTE;
+ timeout <= MAX_BACKOFF_TIMEOUT;
+ timeout *= 2
+ ) {
+ Assert.ok(!!pingSendTimerCallback, "Should have received a timer callback");
+ Assert.equal(
+ pingSendTimeout,
+ timeout,
+ "Send tick timeout should be correct"
+ );
+
+ let callback = pingSendTimerCallback;
+ now = fakeNow(futureDate(now, pingSendTimeout));
+ timerPromise = waitForTimer();
+ await callback();
+ [pingSendTimerCallback, pingSendTimeout] = await timerPromise;
+ ++sendAttempts;
+ }
+
+ timerPromise = waitForTimer();
+ await pingSendTimerCallback();
+ [pingSendTimerCallback, pingSendTimeout] = await timerPromise;
+ Assert.equal(
+ pingSendTimeout,
+ MAX_BACKOFF_TIMEOUT,
+ "Tick timeout should be capped"
+ );
+ ++sendAttempts;
+
+ Assert.deepEqual(
+ histSuccess.snapshot().values,
+ { 0: sendAttempts, 1: 0 },
+ "Should have recorded sending failure in histograms."
+ );
+ Assert.equal(
+ histSendTimeSuccess.snapshot().sum,
+ 0,
+ "Should not have recorded any sending success in histograms yet."
+ );
+ Assert.greaterOrEqual(
+ histSendTimeFail.snapshot().sum,
+ 0,
+ "Should have recorded send failure times in histograms."
+ );
+ Assert.equal(
+ histogramValueCount(histSendTimeFail.snapshot()),
+ sendAttempts,
+ "Should have recorded send failure times in histograms."
+ );
+
+ // Submitting a new ping should reset the backoff behavior.
+ fakePingId("d", 0);
+ now = fakeNow(futureDate(now, MS_IN_A_MINUTE));
+ timerPromise = waitForTimer();
+ await TelemetryController.submitExternalPing(TEST_TYPE_D, {});
+ [pingSendTimerCallback, pingSendTimeout] = await timerPromise;
+ Assert.equal(
+ pingSendTimeout,
+ 2 * MS_IN_A_MINUTE,
+ "Send tick timeout should be correct"
+ );
+ sendAttempts += 2;
+
+ // With the server running again, we should send out the pending pings immediately
+ // when a new ping is submitted.
+ PingServer.start();
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+ fakePingId("e", 0);
+ now = fakeNow(futureDate(now, MS_IN_A_MINUTE));
+ timerPromise = waitForTimer();
+ await TelemetryController.submitExternalPing(TEST_TYPE_E, {});
+
+ let pings = await PingServer.promiseNextPings(3);
+ let countByType = countPingTypes(pings);
+
+ Assert.equal(
+ countByType.get(TEST_TYPE_C),
+ 1,
+ "Should have received the correct amount of type C pings"
+ );
+ Assert.equal(
+ countByType.get(TEST_TYPE_D),
+ 1,
+ "Should have received the correct amount of type D pings"
+ );
+ Assert.equal(
+ countByType.get(TEST_TYPE_E),
+ 1,
+ "Should have received the correct amount of type E pings"
+ );
+
+ await TelemetrySend.testWaitOnOutgoingPings();
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ 0,
+ "Should have no pending pings left"
+ );
+
+ Assert.deepEqual(
+ histSuccess.snapshot().values,
+ { 0: sendAttempts, 1: 3, 2: 0 },
+ "Should have recorded sending failure in histograms."
+ );
+ Assert.greaterOrEqual(
+ histSendTimeSuccess.snapshot().sum,
+ 0,
+ "Should have recorded sending success in histograms."
+ );
+ Assert.equal(
+ histogramValueCount(histSendTimeSuccess.snapshot()),
+ 3,
+ "Should have recorded sending success in histograms."
+ );
+ Assert.equal(
+ histogramValueCount(histSendTimeFail.snapshot()),
+ sendAttempts,
+ "Should have recorded send failure times in histograms."
+ );
+
+ // Restore the default ping id generator.
+ fakeGeneratePingId(() => TelemetryUtils.generateUUID());
+});
+
+add_task(async function test_discardBigPings() {
+ const TEST_PING_TYPE = "test-ping-type";
+
+ let histSizeExceeded = Telemetry.getHistogramById(
+ "TELEMETRY_PING_SIZE_EXCEEDED_SEND"
+ );
+ let histDiscardedSize = Telemetry.getHistogramById(
+ "TELEMETRY_DISCARDED_SEND_PINGS_SIZE_MB"
+ );
+ let histSuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
+ let histSendTimeSuccess = Telemetry.getHistogramById(
+ "TELEMETRY_SEND_SUCCESS"
+ );
+ let histSendTimeFail = Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE");
+ for (let h of [
+ histSizeExceeded,
+ histDiscardedSize,
+ histSuccess,
+ histSendTimeSuccess,
+ histSendTimeFail,
+ ]) {
+ h.clear();
+ }
+
+ // Submit a ping of a normal size and check that we don't count it in the histogram.
+ await TelemetryController.submitExternalPing(TEST_PING_TYPE, {
+ test: "test",
+ });
+ await TelemetrySend.testWaitOnOutgoingPings();
+ await PingServer.promiseNextPing();
+
+ Assert.equal(
+ histSizeExceeded.snapshot().sum,
+ 0,
+ "Telemetry must report no oversized ping submitted."
+ );
+ Assert.equal(
+ histDiscardedSize.snapshot().sum,
+ 0,
+ "Telemetry must report no oversized pings."
+ );
+ Assert.deepEqual(
+ histSuccess.snapshot().values,
+ { 0: 0, 1: 1, 2: 0 },
+ "Should have recorded sending success."
+ );
+ Assert.equal(
+ histogramValueCount(histSendTimeSuccess.snapshot()),
+ 1,
+ "Should have recorded send success time."
+ );
+ Assert.greaterOrEqual(
+ histSendTimeSuccess.snapshot().sum,
+ 0,
+ "Should have recorded send success time."
+ );
+ Assert.equal(
+ histogramValueCount(histSendTimeFail.snapshot()),
+ 0,
+ "Should not have recorded send failure time."
+ );
+
+ // Submit an oversized ping and check that it gets discarded.
+ TelemetryHealthPing.testReset();
+ // Ensure next ping has a 2 MB gzipped payload.
+ fakeGzipCompressStringForNextPing(2 * 1024 * 1024);
+ const OVERSIZED_PAYLOAD = {
+ data: "empty on purpose - policy takes care of size",
+ };
+ await TelemetryController.submitExternalPing(
+ TEST_PING_TYPE,
+ OVERSIZED_PAYLOAD
+ );
+ await TelemetrySend.testWaitOnOutgoingPings();
+ let ping = await PingServer.promiseNextPing();
+
+ Assert.equal(
+ ping.type,
+ TelemetryHealthPing.HEALTH_PING_TYPE,
+ "Should have received a health ping."
+ );
+ Assert.equal(
+ ping.payload.reason,
+ TelemetryHealthPing.Reason.IMMEDIATE,
+ "Health ping should have the right reason."
+ );
+ Assert.deepEqual(
+ ping.payload[TelemetryHealthPing.FailureType.DISCARDED_FOR_SIZE],
+ { [TEST_PING_TYPE]: 1 },
+ "Should have recorded correct type of oversized ping."
+ );
+ Assert.deepEqual(
+ ping.payload.os,
+ TelemetryHealthPing.OsInfo,
+ "Should have correct os info."
+ );
+
+ Assert.equal(
+ histSizeExceeded.snapshot().sum,
+ 1,
+ "Telemetry must report 1 oversized ping submitted."
+ );
+ Assert.equal(
+ histDiscardedSize.snapshot().values[2],
+ 1,
+ "Telemetry must report a 2MB, oversized, ping submitted."
+ );
+
+ Assert.deepEqual(
+ histSuccess.snapshot().values,
+ { 0: 0, 1: 2, 2: 0 },
+ "Should have recorded sending success."
+ );
+ Assert.equal(
+ histogramValueCount(histSendTimeSuccess.snapshot()),
+ 2,
+ "Should have recorded send success time."
+ );
+ Assert.greaterOrEqual(
+ histSendTimeSuccess.snapshot().sum,
+ 0,
+ "Should have recorded send success time."
+ );
+ Assert.equal(
+ histogramValueCount(histSendTimeFail.snapshot()),
+ 0,
+ "Should not have recorded send failure time."
+ );
+});
+
+add_task(async function test_largeButWithinLimit() {
+ const TEST_PING_TYPE = "test-ping-type";
+
+ let histSuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
+ histSuccess.clear();
+
+ // Next ping will have a 900KB gzip payload.
+ fakeGzipCompressStringForNextPing(900 * 1024);
+ const LARGE_PAYLOAD = {
+ data: "empty on purpose - policy takes care of size",
+ };
+
+ await TelemetryController.submitExternalPing(TEST_PING_TYPE, LARGE_PAYLOAD);
+ await TelemetrySend.testWaitOnOutgoingPings();
+ await PingServer.promiseNextRequest();
+
+ Assert.deepEqual(
+ histSuccess.snapshot().values,
+ { 0: 0, 1: 1, 2: 0 },
+ "Should have sent large ping."
+ );
+});
+
+add_task(async function test_evictedOnServerErrors() {
+ const TEST_TYPE = "test-evicted";
+
+ await TelemetrySend.reset();
+
+ let histEvicted = Telemetry.getHistogramById(
+ "TELEMETRY_PING_EVICTED_FOR_SERVER_ERRORS"
+ );
+ let histSuccess = Telemetry.getHistogramById("TELEMETRY_SUCCESS");
+ let histSendTimeSuccess = Telemetry.getHistogramById(
+ "TELEMETRY_SEND_SUCCESS"
+ );
+ let histSendTimeFail = Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE");
+ for (let h of [
+ histEvicted,
+ histSuccess,
+ histSendTimeSuccess,
+ histSendTimeFail,
+ ]) {
+ h.clear();
+ }
+
+ // Write a custom ping handler which will return 403. This will trigger ping eviction
+ // on client side.
+ PingServer.registerPingHandler((req, res) => {
+ res.setStatusLine(null, 403, "Forbidden");
+ res.processAsync();
+ res.finish();
+ });
+
+ // Clear the histogram and submit a ping.
+ let pingId = await TelemetryController.submitExternalPing(TEST_TYPE, {});
+ await TelemetrySend.testWaitOnOutgoingPings();
+
+ Assert.equal(
+ histEvicted.snapshot().sum,
+ 1,
+ "Telemetry must report a ping evicted due to server errors"
+ );
+ Assert.deepEqual(histSuccess.snapshot().values, { 0: 0, 1: 1, 2: 0 });
+ Assert.equal(histogramValueCount(histSendTimeSuccess.snapshot()), 1);
+ Assert.greaterOrEqual(histSendTimeSuccess.snapshot().sum, 0);
+ Assert.equal(histogramValueCount(histSendTimeFail.snapshot()), 0);
+
+ // The ping should not be persisted.
+ await Assert.rejects(
+ TelemetryStorage.loadPendingPing(pingId),
+ /TelemetryStorage.loadPendingPing - no ping with id/,
+ "The ping must not be persisted."
+ );
+
+ // Reset the ping handler and submit a new ping.
+ PingServer.resetPingHandler();
+ pingId = await TelemetryController.submitExternalPing(TEST_TYPE, {});
+
+ let ping = await PingServer.promiseNextPings(1);
+ Assert.equal(ping[0].id, pingId, "The correct ping must be received");
+
+ // We should not have updated the error histogram.
+ await TelemetrySend.testWaitOnOutgoingPings();
+ Assert.equal(
+ histEvicted.snapshot().sum,
+ 1,
+ "Telemetry must report only one ping evicted due to server errors"
+ );
+ Assert.deepEqual(histSuccess.snapshot().values, { 0: 0, 1: 2, 2: 0 });
+ Assert.equal(histogramValueCount(histSendTimeSuccess.snapshot()), 2);
+ Assert.equal(histogramValueCount(histSendTimeFail.snapshot()), 0);
+});
+
+add_task(async function test_tooLateToSend() {
+ Assert.ok(true, "TEST BEGIN");
+ const TEST_TYPE = "test-too-late-to-send";
+
+ await TelemetrySend.reset();
+ PingServer.start();
+ PingServer.registerPingHandler(() =>
+ Assert.ok(false, "Should not have received any pings now")
+ );
+
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ 0,
+ "Should have no pending pings yet"
+ );
+
+ TelemetrySend.testTooLateToSend(true);
+
+ const id = await TelemetryController.submitExternalPing(TEST_TYPE, {});
+
+ // Triggering a shutdown should persist the pings
+ await TelemetrySend.shutdown();
+ const pendingPings = TelemetryStorage.getPendingPingList();
+ Assert.equal(pendingPings.length, 1, "Should have a pending ping in storage");
+ Assert.equal(pendingPings[0].id, id, "Should have pended our test's ping");
+
+ Assert.equal(
+ Telemetry.getHistogramById("TELEMETRY_SEND_FAILURE_TYPE").snapshot()
+ .values[7],
+ 1,
+ "Should have registered the failed attempt to send"
+ );
+ Assert.equal(
+ Telemetry.getKeyedHistogramById(
+ "TELEMETRY_SEND_FAILURE_TYPE_PER_PING"
+ ).snapshot()[TEST_TYPE].values[7],
+ 1,
+ "Should have registered the failed attempt to send TEST_TYPE ping"
+ );
+ await TelemetryStorage.reset();
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ 0,
+ "Should clean up after yourself"
+ );
+});
+
+add_task(
+ { skip_if: () => gIsAndroid },
+ async function test_pingSenderShutdownBatch() {
+ const TEST_TYPE = "test-ping-sender-batch";
+
+ await TelemetrySend.reset();
+ PingServer.start();
+ PingServer.registerPingHandler(() =>
+ Assert.ok(false, "Should not have received any pings at this time.")
+ );
+
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ 0,
+ "Should have no pending pings yet"
+ );
+
+ TelemetrySend.testTooLateToSend(true);
+
+ const id = await TelemetryController.submitExternalPing(
+ TEST_TYPE,
+ { payload: false },
+ { usePingSender: true }
+ );
+ const id2 = await TelemetryController.submitExternalPing(
+ TEST_TYPE,
+ { payload: false },
+ { usePingSender: true }
+ );
+
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ 2,
+ "Should have stored these two pings in pending land."
+ );
+
+ // Permit pings to be received.
+ PingServer.resetPingHandler();
+
+ TelemetrySend.flushPingSenderBatch();
+
+ // Pings don't have to be sent in the order they're submitted.
+ const ping = await PingServer.promiseNextPing();
+ const ping2 = await PingServer.promiseNextPing();
+ Assert.ok(
+ (ping.id == id && ping2.id == id2) || (ping.id == id2 && ping2.id == id)
+ );
+ Assert.equal(ping.type, TEST_TYPE);
+ Assert.equal(ping2.type, TEST_TYPE);
+
+ await TelemetryStorage.reset();
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ 0,
+ "Should clean up after yourself"
+ );
+ }
+);
+
+// Test that the current, non-persisted pending pings are properly saved on shutdown.
+add_task(async function test_persistCurrentPingsOnShutdown() {
+ const TEST_TYPE = "test-persistCurrentPingsOnShutdown";
+ const PING_COUNT = 5;
+ await TelemetrySend.reset();
+ PingServer.stop();
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ 0,
+ "Should have no pending pings yet"
+ );
+
+ // Submit new pings that shouldn't be persisted yet.
+ let ids = [];
+ for (let i = 0; i < 5; ++i) {
+ ids.push(fakePingId("f", i));
+ TelemetryController.submitExternalPing(TEST_TYPE, {});
+ }
+
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ PING_COUNT,
+ "Should have the correct pending ping count"
+ );
+
+ // Triggering a shutdown should persist the pings.
+ await TelemetrySend.shutdown();
+ Assert.ok(
+ await checkPingsSaved(ids),
+ "All pending pings should have been persisted"
+ );
+
+ // After a restart the pings should have been found when scanning.
+ await TelemetrySend.reset();
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ PING_COUNT,
+ "Should have the correct pending ping count"
+ );
+
+ // Restore the default ping id generator.
+ fakeGeneratePingId(() => TelemetryUtils.generateUUID());
+});
+
+add_task(async function test_sendCheckOverride() {
+ const TEST_PING_TYPE = "test-sendCheckOverride";
+
+ // Disable "health" ping. It can sneak into the test.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.HealthPingEnabled,
+ false
+ );
+
+ // Clear any pending pings.
+ await TelemetryController.testShutdown();
+ await TelemetryStorage.testClearPendingPings();
+
+ // Enable the ping server.
+ PingServer.start();
+ Services.prefs.setStringPref(
+ TelemetryUtils.Preferences.Server,
+ "http://localhost:" + PingServer.port
+ );
+
+ // Start Telemetry and disable the test-mode so pings don't get
+ // sent unless we enable the override.
+ await TelemetryController.testReset();
+
+ // Submit a test ping and make sure it doesn't get sent. We only do
+ // that if we're on unofficial builds: pings will always get sent otherwise.
+ if (!Services.telemetry.isOfficialTelemetry) {
+ TelemetrySend.setTestModeEnabled(false);
+ PingServer.registerPingHandler(() =>
+ Assert.ok(false, "Should not have received any pings now")
+ );
+
+ await TelemetryController.submitExternalPing(TEST_PING_TYPE, {
+ test: "test",
+ });
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ 0,
+ "Should have no pending pings"
+ );
+ }
+
+ // Enable the override and try to send again.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.OverrideOfficialCheck,
+ true
+ );
+ PingServer.resetPingHandler();
+ await TelemetrySend.reset();
+ await TelemetryController.submitExternalPing(TEST_PING_TYPE, {
+ test: "test",
+ });
+
+ // Make sure we received the ping.
+ const ping = await PingServer.promiseNextPing();
+ Assert.equal(
+ ping.type,
+ TEST_PING_TYPE,
+ "Must receive a ping of the expected type"
+ );
+
+ // Restore the test mode and disable the override.
+ TelemetrySend.setTestModeEnabled(true);
+ Services.prefs.clearUserPref(
+ TelemetryUtils.Preferences.OverrideOfficialCheck
+ );
+});
+
+add_task(async function test_submissionPath() {
+ const PING_FORMAT_VERSION = 4;
+ const TEST_PING_TYPE = "test-ping-type";
+
+ await TelemetrySend.reset();
+ PingServer.clearRequests();
+
+ await sendPing(false, false);
+
+ // Fetch the request from the server.
+ let request = await PingServer.promiseNextRequest();
+
+ // Get the payload.
+ let ping = decodeRequestPayload(request);
+ checkPingFormat(ping, TEST_PING_TYPE, false, false);
+
+ let app = ping.application;
+ let pathComponents = [
+ ping.id,
+ ping.type,
+ app.name,
+ app.version,
+ app.channel,
+ app.buildId,
+ ];
+
+ let urlComponents = request.path.split("/");
+
+ for (let i = 0; i < pathComponents.length; i++) {
+ Assert.ok(
+ urlComponents.includes(pathComponents[i]),
+ `Path should include ${pathComponents[i]}`
+ );
+ }
+
+ // Check that we have a version query parameter in the URL.
+ Assert.notEqual(request.queryString, "");
+
+ // Make sure the version in the query string matches the new ping format version.
+ let params = request.queryString.split("&");
+ Assert.ok(params.find(p => p == "v=" + PING_FORMAT_VERSION));
+});
+
+add_task(async function testCookies() {
+ const TEST_TYPE = "test-cookies";
+
+ await TelemetrySend.reset();
+ PingServer.clearRequests();
+
+ let uri = Services.io.newURI("http://localhost:" + PingServer.port);
+ let channel = NetUtil.newChannel({
+ uri,
+ loadUsingSystemPrincipal: true,
+ contentPolicyType: Ci.nsIContentPolicy.TYPE_DOCUMENT,
+ });
+ Services.cookies.QueryInterface(Ci.nsICookieService);
+ Services.cookies.setCookieStringFromHttp(uri, "cookie-time=yes", channel);
+
+ const id = await TelemetryController.submitExternalPing(TEST_TYPE, {});
+ let foundit = false;
+ while (!foundit) {
+ var request = await PingServer.promiseNextRequest();
+ var ping = decodeRequestPayload(request);
+ foundit = id === ping.id;
+ }
+ Assert.equal(id, ping.id, "We're testing the right ping's request, right?");
+ Assert.equal(
+ false,
+ request.hasHeader("Cookie"),
+ "Request should not have Cookie header"
+ );
+});
+
+add_task(async function test_pref_observer() {
+ // This test requires the presence of the crash reporter component.
+ let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ if (
+ !registrar.isContractIDRegistered("@mozilla.org/toolkit/crash-reporter;1")
+ ) {
+ return;
+ }
+
+ await TelemetrySend.setup(true);
+
+ const IS_UNIFIED_TELEMETRY = Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.Unified,
+ false
+ );
+
+ let origTelemetryEnabled = Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.TelemetryEnabled
+ );
+ let origFhrUploadEnabled = Services.prefs.getBoolPref(
+ TelemetryUtils.Preferences.FhrUploadEnabled
+ );
+
+ if (!IS_UNIFIED_TELEMETRY) {
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.TelemetryEnabled,
+ true
+ );
+ }
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+
+ function waitAnnotateCrashReport(expectedValue, trigger) {
+ return new Promise(function (resolve, reject) {
+ let keys = new Set(["TelemetryClientId", "TelemetryServerURL"]);
+
+ let crs = {
+ QueryInterface: ChromeUtils.generateQI(["nsICrashReporter"]),
+ annotateCrashReport(key, value) {
+ if (!keys.delete(key)) {
+ MockRegistrar.unregister(gMockCrs);
+ reject(
+ Error(`Crash report annotation with unexpected key: "${key}".`)
+ );
+ }
+
+ if (expectedValue && value == "") {
+ MockRegistrar.unregister(gMockCrs);
+ reject(Error("Crash report annotation without expected value."));
+ }
+
+ if (keys.size == 0) {
+ MockRegistrar.unregister(gMockCrs);
+ resolve();
+ }
+ },
+ removeCrashReportAnnotation(key) {
+ if (!keys.delete(key)) {
+ MockRegistrar.unregister(gMockCrs);
+ }
+
+ if (keys.size == 0) {
+ MockRegistrar.unregister(gMockCrs);
+ resolve();
+ }
+ },
+ UpdateCrashEventsDir() {},
+ };
+
+ let gMockCrs = MockRegistrar.register(
+ "@mozilla.org/toolkit/crash-reporter;1",
+ crs
+ );
+ registerCleanupFunction(function () {
+ MockRegistrar.unregister(gMockCrs);
+ });
+
+ trigger();
+ });
+ }
+
+ await waitAnnotateCrashReport(!IS_UNIFIED_TELEMETRY, () =>
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ false
+ )
+ );
+
+ await waitAnnotateCrashReport(true, () =>
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ true
+ )
+ );
+
+ if (!IS_UNIFIED_TELEMETRY) {
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.TelemetryEnabled,
+ origTelemetryEnabled
+ );
+ }
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ origFhrUploadEnabled
+ );
+});
+
+add_task(async function cleanup() {
+ await PingServer.stop();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js b/toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js
new file mode 100644
index 0000000000..7b7b0f674d
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySendOldPings.js
@@ -0,0 +1,586 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+
+/**
+ * This test case populates the profile with some fake stored
+ * pings, and checks that pending pings are immediatlely sent
+ * after delayed init.
+ */
+
+"use strict";
+
+const PING_SAVE_FOLDER = "saved-telemetry-pings";
+const OLD_FORMAT_PINGS = 4;
+const RECENT_PINGS = 4;
+
+var gSeenPings = 0;
+
+/**
+ * Creates some Telemetry pings for the and saves them to disk. Each ping gets a
+ * unique ID based on an incrementor.
+ *
+ * @param {Array} aPingInfos An array of ping type objects. Each entry must be an
+ * object containing a "num" field for the number of pings to create and
+ * an "age" field. The latter representing the age in milliseconds to offset
+ * from now. A value of 10 would make the ping 10ms older than now, for
+ * example.
+ * @returns Promise
+ * @resolve an Array with the created pings ids.
+ */
+var createSavedPings = async function (aPingInfos) {
+ let pingIds = [];
+ let now = Date.now();
+
+ for (let type in aPingInfos) {
+ let num = aPingInfos[type].num;
+ let age = now - (aPingInfos[type].age || 0);
+ for (let i = 0; i < num; ++i) {
+ let pingId = await TelemetryController.addPendingPing(
+ "test-ping",
+ {},
+ { overwrite: true }
+ );
+ if (aPingInfos[type].age) {
+ // savePing writes to the file synchronously, so we're good to
+ // modify the lastModifedTime now.
+ let filePath = getSavePathForPingId(pingId);
+ await IOUtils.setModificationTime(filePath, age);
+ }
+ pingIds.push(pingId);
+ }
+ }
+
+ return pingIds;
+};
+
+/**
+ * Fakes the pending pings storage quota.
+ * @param {Integer} aPendingQuota The new quota, in bytes.
+ */
+function fakePendingPingsQuota(aPendingQuota) {
+ let { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryStorage.sys.mjs"
+ );
+ Policy.getPendingPingsQuota = () => aPendingQuota;
+}
+
+/**
+ * Returns a path for the file that a ping should be
+ * stored in locally.
+ *
+ * @returns path
+ */
+function getSavePathForPingId(aPingId) {
+ return PathUtils.join(PathUtils.profileDir, PING_SAVE_FOLDER, aPingId);
+}
+
+/**
+ * Check if the number of Telemetry pings received by the HttpServer is not equal
+ * to aExpectedNum.
+ *
+ * @param aExpectedNum the number of pings we expect to receive.
+ */
+function assertReceivedPings(aExpectedNum) {
+ Assert.equal(gSeenPings, aExpectedNum);
+}
+
+/**
+ * Our handler function for the HttpServer that simply
+ * increments the gSeenPings global when it successfully
+ * receives and decodes a Telemetry payload.
+ *
+ * @param aRequest the HTTP request sent from HttpServer.
+ */
+function pingHandler(aRequest) {
+ gSeenPings++;
+}
+
+add_task(async function test_setup() {
+ PingServer.start();
+ PingServer.registerPingHandler(pingHandler);
+ do_get_profile();
+ await loadAddonManager(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "1",
+ "1.9.2"
+ );
+ finishAddonManagerStartup();
+ fakeIntlReady();
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+
+ Services.prefs.setCharPref(
+ TelemetryUtils.Preferences.Server,
+ "http://localhost:" + PingServer.port
+ );
+});
+
+/**
+ * Setup the tests by making sure the ping storage directory is available, otherwise
+ * |TelemetryController.testSaveDirectoryToFile| could fail.
+ */
+add_task(async function setupEnvironment() {
+ // The following tests assume this pref to be true by default.
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+
+ await TelemetryController.testSetup();
+
+ let directory = TelemetryStorage.pingDirectoryPath;
+ await IOUtils.makeDirectory(directory, {
+ ignoreExisting: true,
+ permissions: 0o700,
+ });
+
+ await TelemetryStorage.testClearPendingPings();
+});
+
+/**
+ * Test that really recent pings are sent on Telemetry initialization.
+ */
+add_task(async function test_recent_pings_sent() {
+ let pingTypes = [{ num: RECENT_PINGS }];
+ await createSavedPings(pingTypes);
+
+ await TelemetryController.testReset();
+ await TelemetrySend.testWaitOnOutgoingPings();
+ assertReceivedPings(RECENT_PINGS);
+
+ await TelemetryStorage.testClearPendingPings();
+});
+
+/**
+ * Create an overdue ping in the old format and try to send it.
+ */
+add_task(async function test_old_formats() {
+ // A test ping in the old, standard format.
+ const PING_OLD_FORMAT = {
+ slug: "1234567abcd",
+ reason: "test-ping",
+ payload: {
+ info: {
+ reason: "test-ping",
+ OS: "XPCShell",
+ appID: "SomeId",
+ appVersion: "1.0",
+ appName: "XPCShell",
+ appBuildID: "123456789",
+ appUpdateChannel: "Test",
+ platformBuildID: "987654321",
+ },
+ },
+ };
+
+ // A ping with no info section, but with a slug.
+ const PING_NO_INFO = {
+ slug: "1234-no-info-ping",
+ reason: "test-ping",
+ payload: {},
+ };
+
+ // A ping with no payload.
+ const PING_NO_PAYLOAD = {
+ slug: "5678-no-payload",
+ reason: "test-ping",
+ };
+
+ // A ping with no info and no slug.
+ const PING_NO_SLUG = {
+ reason: "test-ping",
+ payload: {},
+ };
+
+ const PING_FILES_PATHS = [
+ getSavePathForPingId(PING_OLD_FORMAT.slug),
+ getSavePathForPingId(PING_NO_INFO.slug),
+ getSavePathForPingId(PING_NO_PAYLOAD.slug),
+ getSavePathForPingId("no-slug-file"),
+ ];
+
+ // Write the ping to file
+ await TelemetryStorage.savePing(PING_OLD_FORMAT, true);
+ await TelemetryStorage.savePing(PING_NO_INFO, true);
+ await TelemetryStorage.savePing(PING_NO_PAYLOAD, true);
+ await TelemetryStorage.savePingToFile(
+ PING_NO_SLUG,
+ PING_FILES_PATHS[3],
+ true
+ );
+
+ gSeenPings = 0;
+ await TelemetryController.testReset();
+ await TelemetrySend.testWaitOnOutgoingPings();
+ assertReceivedPings(OLD_FORMAT_PINGS);
+
+ // |TelemetryStorage.cleanup| doesn't know how to remove a ping with no slug or id,
+ // so remove it manually so that the next test doesn't fail.
+ await IOUtils.remove(PING_FILES_PATHS[3]);
+
+ await TelemetryStorage.testClearPendingPings();
+});
+
+add_task(async function test_corrupted_pending_pings() {
+ const TEST_TYPE = "test_corrupted";
+
+ Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_READ").clear();
+ Telemetry.getHistogramById("TELEMETRY_PENDING_LOAD_FAILURE_PARSE").clear();
+
+ // Save a pending ping and get its id.
+ let pendingPingId = await TelemetryController.addPendingPing(
+ TEST_TYPE,
+ {},
+ {}
+ );
+
+ // Try to load it: there should be no error.
+ await TelemetryStorage.loadPendingPing(pendingPingId);
+
+ let h = Telemetry.getHistogramById(
+ "TELEMETRY_PENDING_LOAD_FAILURE_READ"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ 0,
+ "Telemetry must not report a pending ping load failure"
+ );
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_PENDING_LOAD_FAILURE_PARSE"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ 0,
+ "Telemetry must not report a pending ping parse failure"
+ );
+
+ // Delete it from the disk, so that its id will be kept in the cache but it will
+ // fail loading the file.
+ await IOUtils.remove(getSavePathForPingId(pendingPingId));
+
+ // Try to load a pending ping which isn't there anymore.
+ await Assert.rejects(
+ TelemetryStorage.loadPendingPing(pendingPingId),
+ /PingReadError/,
+ "Telemetry must fail loading a ping which isn't there"
+ );
+
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_PENDING_LOAD_FAILURE_READ"
+ ).snapshot();
+ Assert.equal(h.sum, 1, "Telemetry must report a pending ping load failure");
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_PENDING_LOAD_FAILURE_PARSE"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ 0,
+ "Telemetry must not report a pending ping parse failure"
+ );
+
+ // Save a new ping, so that it gets in the pending pings cache.
+ pendingPingId = await TelemetryController.addPendingPing(TEST_TYPE, {}, {});
+ // Overwrite it with a corrupted JSON file and then try to load it.
+ const INVALID_JSON = "{ invalid,JSON { {1}";
+ await IOUtils.writeUTF8(getSavePathForPingId(pendingPingId), INVALID_JSON);
+
+ // Try to load the ping with the corrupted JSON content.
+ await Assert.rejects(
+ TelemetryStorage.loadPendingPing(pendingPingId),
+ /PingParseError/,
+ "Telemetry must fail loading a corrupted ping"
+ );
+
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_PENDING_LOAD_FAILURE_READ"
+ ).snapshot();
+ Assert.equal(h.sum, 1, "Telemetry must report a pending ping load failure");
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_PENDING_LOAD_FAILURE_PARSE"
+ ).snapshot();
+ Assert.equal(h.sum, 1, "Telemetry must report a pending ping parse failure");
+
+ let exists = await IOUtils.exists(getSavePathForPingId(pendingPingId));
+ Assert.ok(!exists, "The unparseable ping should have been removed");
+
+ await TelemetryStorage.testClearPendingPings();
+});
+
+/**
+ * Create a ping in the old format, send it, and make sure the request URL contains
+ * the correct version query parameter.
+ */
+add_task(async function test_overdue_old_format() {
+ // A test ping in the old, standard format.
+ const PING_OLD_FORMAT = {
+ slug: "1234567abcd",
+ reason: "test-ping",
+ payload: {
+ info: {
+ reason: "test-ping",
+ OS: "XPCShell",
+ appID: "SomeId",
+ appVersion: "1.0",
+ appName: "XPCShell",
+ appBuildID: "123456789",
+ appUpdateChannel: "Test",
+ platformBuildID: "987654321",
+ },
+ },
+ };
+
+ // Write the ping to file
+ await TelemetryStorage.savePing(PING_OLD_FORMAT, true);
+
+ let receivedPings = 0;
+ // Register a new prefix handler to validate the URL.
+ PingServer.registerPingHandler(request => {
+ // Check that we have a version query parameter in the URL.
+ Assert.notEqual(request.queryString, "");
+
+ // Make sure the version in the query string matches the old ping format version.
+ let params = request.queryString.split("&");
+ Assert.ok(params.find(p => p == "v=1"));
+
+ receivedPings++;
+ });
+
+ await TelemetryController.testReset();
+ await TelemetrySend.testWaitOnOutgoingPings();
+ Assert.equal(receivedPings, 1, "We must receive a ping in the old format.");
+
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.resetPingHandler();
+});
+
+add_task(async function test_pendingPingsQuota() {
+ const PING_TYPE = "foo";
+
+ // Disable upload so pings don't get sent and removed from the pending pings directory.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ false
+ );
+
+ // Remove all the pending pings then startup and wait for the cleanup task to complete.
+ // There should be nothing to remove.
+ await TelemetryStorage.testClearPendingPings();
+ await TelemetryController.testReset();
+ await TelemetrySend.testWaitOnOutgoingPings();
+ await TelemetryStorage.testPendingQuotaTaskPromise();
+
+ // Remove the pending optout ping generated when flipping FHR upload off.
+ await TelemetryStorage.testClearPendingPings();
+
+ let expectedPrunedPings = [];
+ let expectedNotPrunedPings = [];
+
+ let checkPendingPings = async function () {
+ // Check that the pruned pings are not on disk anymore.
+ for (let prunedPingId of expectedPrunedPings) {
+ await Assert.rejects(
+ TelemetryStorage.loadPendingPing(prunedPingId),
+ /TelemetryStorage.loadPendingPing - no ping with id/,
+ "Ping " + prunedPingId + " should have been pruned."
+ );
+ const pingPath = getSavePathForPingId(prunedPingId);
+ Assert.ok(
+ !(await IOUtils.exists(pingPath)),
+ "The ping should not be on the disk anymore."
+ );
+ }
+
+ // Check that the expected pings are there.
+ for (let expectedPingId of expectedNotPrunedPings) {
+ Assert.ok(
+ await TelemetryStorage.loadPendingPing(expectedPingId),
+ "Ping" + expectedPingId + " should be among the pending pings."
+ );
+ }
+ };
+
+ let pendingPingsInfo = [];
+ let pingsSizeInBytes = 0;
+
+ // Create 10 pings to test the pending pings quota.
+ for (let days = 1; days < 11; days++) {
+ const date = fakeNow(2010, 1, days, 1, 1, 0);
+ const pingId = await TelemetryController.addPendingPing(PING_TYPE, {}, {});
+
+ // Find the size of the ping.
+ const pingFilePath = getSavePathForPingId(pingId);
+ const pingSize = (await IOUtils.stat(pingFilePath)).size;
+ // Add the info at the beginning of the array, so that most recent pings come first.
+ pendingPingsInfo.unshift({
+ id: pingId,
+ size: pingSize,
+ timestamp: date.getTime(),
+ });
+
+ // Set the last modification date.
+ await IOUtils.setModificationTime(pingFilePath, date.getTime());
+
+ // Add it to the pending ping directory size.
+ pingsSizeInBytes += pingSize;
+ }
+
+ // We need to test the pending pings size before we hit the quota, otherwise a special
+ // value is recorded.
+ Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_SIZE_MB").clear();
+ Telemetry.getHistogramById(
+ "TELEMETRY_PENDING_PINGS_EVICTED_OVER_QUOTA"
+ ).clear();
+ Telemetry.getHistogramById(
+ "TELEMETRY_PENDING_EVICTING_OVER_QUOTA_MS"
+ ).clear();
+
+ await TelemetryController.testReset();
+ await TelemetryStorage.testPendingQuotaTaskPromise();
+
+ // Check that the correct values for quota probes are reported when no quota is hit.
+ let h = Telemetry.getHistogramById(
+ "TELEMETRY_PENDING_PINGS_SIZE_MB"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ Math.round(pingsSizeInBytes / 1024 / 1024),
+ "Telemetry must report the correct pending pings directory size."
+ );
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_PENDING_PINGS_EVICTED_OVER_QUOTA"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ 0,
+ "Telemetry must report 0 evictions if quota is not hit."
+ );
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_PENDING_EVICTING_OVER_QUOTA_MS"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ 0,
+ "Telemetry must report a null elapsed time if quota is not hit."
+ );
+
+ // Set the quota to 80% of the space.
+ const testQuotaInBytes = pingsSizeInBytes * 0.8;
+ fakePendingPingsQuota(testQuotaInBytes);
+
+ // The storage prunes pending pings until we reach 90% of the requested storage quota.
+ // Based on that, find how many pings should be kept.
+ const safeQuotaSize = Math.round(testQuotaInBytes * 0.9);
+ let sizeInBytes = 0;
+ let pingsWithinQuota = [];
+ let pingsOutsideQuota = [];
+
+ for (let pingInfo of pendingPingsInfo) {
+ sizeInBytes += pingInfo.size;
+ if (sizeInBytes >= safeQuotaSize) {
+ pingsOutsideQuota.push(pingInfo.id);
+ continue;
+ }
+ pingsWithinQuota.push(pingInfo.id);
+ }
+
+ expectedNotPrunedPings = pingsWithinQuota;
+ expectedPrunedPings = pingsOutsideQuota;
+
+ // Reset TelemetryController to start the pending pings cleanup.
+ await TelemetryController.testReset();
+ await TelemetryStorage.testPendingQuotaTaskPromise();
+ await checkPendingPings();
+
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_PENDING_PINGS_EVICTED_OVER_QUOTA"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ pingsOutsideQuota.length,
+ "Telemetry must correctly report the over quota pings evicted from the pending pings directory."
+ );
+ h = Telemetry.getHistogramById("TELEMETRY_PENDING_PINGS_SIZE_MB").snapshot();
+ Assert.equal(
+ h.sum,
+ 17,
+ "Pending pings quota was hit, a special size must be reported."
+ );
+
+ // Trigger a cleanup again and make sure we're not removing anything.
+ await TelemetryController.testReset();
+ await TelemetryStorage.testPendingQuotaTaskPromise();
+ await checkPendingPings();
+
+ const OVERSIZED_PING_ID = "9b21ec8f-f762-4d28-a2c1-44e1c4694f24";
+ // Create a pending oversized ping.
+ const OVERSIZED_PING = {
+ id: OVERSIZED_PING_ID,
+ type: PING_TYPE,
+ creationDate: new Date().toISOString(),
+ // Generate a 2MB string to use as the ping payload.
+ payload: generateRandomString(2 * 1024 * 1024),
+ };
+ await TelemetryStorage.savePendingPing(OVERSIZED_PING);
+
+ // Reset the histograms.
+ Telemetry.getHistogramById("TELEMETRY_PING_SIZE_EXCEEDED_PENDING").clear();
+ Telemetry.getHistogramById(
+ "TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB"
+ ).clear();
+
+ // Try to manually load the oversized ping.
+ await Assert.rejects(
+ TelemetryStorage.loadPendingPing(OVERSIZED_PING_ID),
+ /loadPendingPing - exceeded the maximum ping size/,
+ "The oversized ping should have been pruned."
+ );
+ Assert.ok(
+ !(await IOUtils.exists(getSavePathForPingId(OVERSIZED_PING_ID))),
+ "The ping should not be on the disk anymore."
+ );
+
+ // Make sure we're correctly updating the related histograms.
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_PING_SIZE_EXCEEDED_PENDING"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ 1,
+ "Telemetry must report 1 oversized ping in the pending pings directory."
+ );
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB"
+ ).snapshot();
+ Assert.equal(h.values[2], 1, "Telemetry must report a 2MB, oversized, ping.");
+
+ // Save the ping again to check if it gets pruned when scanning the pings directory.
+ await TelemetryStorage.savePendingPing(OVERSIZED_PING);
+ expectedPrunedPings.push(OVERSIZED_PING_ID);
+
+ // Scan the pending pings directory.
+ await TelemetryController.testReset();
+ await TelemetryStorage.testPendingQuotaTaskPromise();
+ await checkPendingPings();
+
+ // Make sure we're correctly updating the related histograms.
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_PING_SIZE_EXCEEDED_PENDING"
+ ).snapshot();
+ Assert.equal(
+ h.sum,
+ 2,
+ "Telemetry must report 1 oversized ping in the pending pings directory."
+ );
+ h = Telemetry.getHistogramById(
+ "TELEMETRY_DISCARDED_PENDING_PINGS_SIZE_MB"
+ ).snapshot();
+ Assert.equal(
+ h.values[2],
+ 2,
+ "Telemetry must report two 2MB, oversized, pings."
+ );
+
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+});
+
+add_task(async function teardown() {
+ await PingServer.stop();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
new file mode 100644
index 0000000000..9267c96ce1
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession.js
@@ -0,0 +1,2389 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+/* This testcase triggers two telemetry pings.
+ *
+ * Telemetry code keeps histograms of past telemetry pings. The first
+ * ping populates these histograms. One of those histograms is then
+ * checked in the second request.
+ */
+
+const { ClientID } = ChromeUtils.importESModule(
+ "resource://gre/modules/ClientID.sys.mjs"
+);
+const { TelemetrySession } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySession.sys.mjs"
+);
+const { TelemetryEnvironment } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryEnvironment.sys.mjs"
+);
+const { TelemetryReportingPolicy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryReportingPolicy.sys.mjs"
+);
+
+const PING_FORMAT_VERSION = 4;
+const PING_TYPE_MAIN = "main";
+const PING_TYPE_SAVED_SESSION = "saved-session";
+
+const REASON_ABORTED_SESSION = "aborted-session";
+const REASON_SAVED_SESSION = "saved-session";
+const REASON_SHUTDOWN = "shutdown";
+const REASON_TEST_PING = "test-ping";
+const REASON_DAILY = "daily";
+const REASON_ENVIRONMENT_CHANGE = "environment-change";
+
+const IGNORE_CLONED_HISTOGRAM = "test::ignore_me_also";
+// Add some unicode characters here to ensure that sending them works correctly.
+const SHUTDOWN_TIME = 10000;
+const FAILED_PROFILE_LOCK_ATTEMPTS = 2;
+
+// Constants from prio.h for nsIFileOutputStream.init
+const PR_WRONLY = 0x2;
+const PR_CREATE_FILE = 0x8;
+const PR_TRUNCATE = 0x20;
+const RW_OWNER = parseInt("0600", 8);
+
+const MS_IN_ONE_HOUR = 60 * 60 * 1000;
+const MS_IN_ONE_DAY = 24 * MS_IN_ONE_HOUR;
+
+const DATAREPORTING_DIR = "datareporting";
+const ABORTED_PING_FILE_NAME = "aborted-session-ping";
+const ABORTED_SESSION_UPDATE_INTERVAL_MS = 5 * 60 * 1000;
+
+ChromeUtils.defineLazyGetter(this, "DATAREPORTING_PATH", function () {
+ return PathUtils.join(PathUtils.profileDir, DATAREPORTING_DIR);
+});
+
+var gClientID = null;
+var gMonotonicNow = 0;
+
+function sendPing() {
+ TelemetrySession.gatherStartup();
+ if (PingServer.started) {
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+ return TelemetrySession.testPing();
+ }
+ TelemetrySend.setServer("http://doesnotexist");
+ return TelemetrySession.testPing();
+}
+
+function fakeGenerateUUID(sessionFunc, subsessionFunc) {
+ const { Policy } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySession.sys.mjs"
+ );
+ Policy.generateSessionUUID = sessionFunc;
+ Policy.generateSubsessionUUID = subsessionFunc;
+}
+
+function fakeIdleNotification(topic) {
+ return TelemetryScheduler.observe(null, topic, null);
+}
+
+function setupTestData() {
+ Services.startup.interrupted = true;
+ let h2 = Telemetry.getHistogramById("TELEMETRY_TEST_COUNT");
+ h2.add();
+
+ let k1 = Telemetry.getKeyedHistogramById("TELEMETRY_TEST_KEYED_COUNT");
+ k1.add("a");
+ k1.add("a");
+ k1.add("b");
+}
+
+function checkPingFormat(aPing, aType, aHasClientId, aHasEnvironment) {
+ const MANDATORY_PING_FIELDS = [
+ "type",
+ "id",
+ "creationDate",
+ "version",
+ "application",
+ "payload",
+ ];
+
+ const APPLICATION_TEST_DATA = {
+ buildId: gAppInfo.appBuildID,
+ name: APP_NAME,
+ version: APP_VERSION,
+ vendor: "Mozilla",
+ platformVersion: PLATFORM_VERSION,
+ xpcomAbi: "noarch-spidermonkey",
+ };
+
+ // Check that the ping contains all the mandatory fields.
+ for (let f of MANDATORY_PING_FIELDS) {
+ Assert.ok(f in aPing, f + " must be available.");
+ }
+
+ Assert.equal(aPing.type, aType, "The ping must have the correct type.");
+ Assert.equal(
+ aPing.version,
+ PING_FORMAT_VERSION,
+ "The ping must have the correct version."
+ );
+
+ // Test the application section.
+ for (let f in APPLICATION_TEST_DATA) {
+ Assert.equal(
+ aPing.application[f],
+ APPLICATION_TEST_DATA[f],
+ f + " must have the correct value."
+ );
+ }
+
+ // We can't check the values for channel and architecture. Just make
+ // sure they are in.
+ Assert.ok(
+ "architecture" in aPing.application,
+ "The application section must have an architecture field."
+ );
+ Assert.ok(
+ "channel" in aPing.application,
+ "The application section must have a channel field."
+ );
+
+ // Check the clientId and environment fields, as needed.
+ Assert.equal("clientId" in aPing, aHasClientId);
+ Assert.equal("environment" in aPing, aHasEnvironment);
+}
+
+function checkPayloadInfo(data, reason) {
+ const ALLOWED_REASONS = [
+ "environment-change",
+ "shutdown",
+ "daily",
+ "saved-session",
+ "test-ping",
+ ];
+ let numberCheck = arg => {
+ return typeof arg == "number";
+ };
+ let positiveNumberCheck = arg => {
+ return numberCheck(arg) && arg >= 0;
+ };
+ let stringCheck = arg => {
+ return typeof arg == "string" && arg != "";
+ };
+ let revisionCheck = arg => {
+ return AppConstants.MOZILLA_OFFICIAL
+ ? stringCheck(arg)
+ : typeof arg == "string";
+ };
+ let uuidCheck = arg => {
+ return UUID_REGEX.test(arg);
+ };
+ let isoDateCheck = arg => {
+ // We expect use of this version of the ISO format:
+ // 2015-04-12T18:51:19.1+00:00
+ const isoDateRegEx =
+ /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+[+-]\d{2}:\d{2}$/;
+ return (
+ stringCheck(arg) &&
+ !Number.isNaN(Date.parse(arg)) &&
+ isoDateRegEx.test(arg)
+ );
+ };
+
+ const EXPECTED_INFO_FIELDS_TYPES = {
+ reason: stringCheck,
+ revision: revisionCheck,
+ timezoneOffset: numberCheck,
+ sessionId: uuidCheck,
+ subsessionId: uuidCheck,
+ // Special cases: previousSessionId and previousSubsessionId are null on first run.
+ previousSessionId: arg => {
+ return arg ? uuidCheck(arg) : true;
+ },
+ previousSubsessionId: arg => {
+ return arg ? uuidCheck(arg) : true;
+ },
+ subsessionCounter: positiveNumberCheck,
+ profileSubsessionCounter: positiveNumberCheck,
+ sessionStartDate: isoDateCheck,
+ subsessionStartDate: isoDateCheck,
+ subsessionLength: positiveNumberCheck,
+ };
+
+ for (let f in EXPECTED_INFO_FIELDS_TYPES) {
+ Assert.ok(f in data, f + " must be available.");
+
+ let checkFunc = EXPECTED_INFO_FIELDS_TYPES[f];
+ Assert.ok(
+ checkFunc(data[f]),
+ f + " must have the correct type and valid data " + data[f]
+ );
+ }
+
+ // Check for a valid revision.
+ if (data.revision != "") {
+ const revisionUrlRegEx =
+ /^http[s]?:\/\/hg.mozilla.org(\/[a-z\S]+)+(\/rev\/[0-9a-z]+)$/g;
+ Assert.ok(revisionUrlRegEx.test(data.revision));
+ }
+
+ // Previous buildId is not mandatory.
+ if (data.previousBuildId) {
+ Assert.ok(stringCheck(data.previousBuildId));
+ }
+
+ Assert.ok(
+ ALLOWED_REASONS.find(r => r == data.reason),
+ "Payload must contain an allowed reason."
+ );
+ Assert.equal(data.reason, reason, "Payload reason must match expected.");
+
+ Assert.ok(
+ Date.parse(data.subsessionStartDate) >= Date.parse(data.sessionStartDate)
+ );
+ Assert.ok(data.profileSubsessionCounter >= data.subsessionCounter);
+
+ // According to https://en.wikipedia.org/wiki/List_of_UTC_time_offsets,
+ // UTC offsets range from -12 to +14 hours.
+ // Don't think the extremes of the range are affected by further
+ // daylight-savings adjustments, but it is possible.
+ Assert.ok(
+ data.timezoneOffset >= -12 * 60,
+ "The timezone must be in a valid range."
+ );
+ Assert.ok(
+ data.timezoneOffset <= 14 * 60,
+ "The timezone must be in a valid range."
+ );
+}
+
+function checkScalars(processes) {
+ // Check that the scalars section is available in the ping payload.
+ const parentProcess = processes.parent;
+ Assert.ok(
+ "scalars" in parentProcess,
+ "The scalars section must be available in the parent process."
+ );
+ Assert.ok(
+ "keyedScalars" in parentProcess,
+ "The keyedScalars section must be available in the parent process."
+ );
+ Assert.equal(
+ typeof parentProcess.scalars,
+ "object",
+ "The scalars entry must be an object."
+ );
+ Assert.equal(
+ typeof parentProcess.keyedScalars,
+ "object",
+ "The keyedScalars entry must be an object."
+ );
+
+ let checkScalar = function (scalar, name) {
+ // Check if the value is of a supported type.
+ const valueType = typeof scalar;
+ switch (valueType) {
+ case "string":
+ Assert.ok(
+ scalar.length <= 50,
+ "String values can't have more than 50 characters"
+ );
+ break;
+ case "number":
+ Assert.ok(
+ scalar >= 0,
+ "We only support unsigned integer values in scalars."
+ );
+ break;
+ case "boolean":
+ Assert.ok(true, "Boolean scalar found.");
+ break;
+ default:
+ Assert.ok(
+ false,
+ name + " contains an unsupported value type (" + valueType + ")"
+ );
+ }
+ };
+
+ // Check that we have valid scalar entries.
+ const scalars = parentProcess.scalars;
+ for (let name in scalars) {
+ Assert.equal(typeof name, "string", "Scalar names must be strings.");
+ checkScalar(scalars[name], name);
+ }
+
+ // Check that we have valid keyed scalar entries.
+ const keyedScalars = parentProcess.keyedScalars;
+ for (let name in keyedScalars) {
+ Assert.equal(typeof name, "string", "Scalar names must be strings.");
+ Assert.ok(
+ Object.keys(keyedScalars[name]).length,
+ "The reported keyed scalars must contain at least 1 key."
+ );
+ for (let key in keyedScalars[name]) {
+ Assert.equal(typeof key, "string", "Keyed scalar keys must be strings.");
+ Assert.ok(
+ key.length <= 70,
+ "Keyed scalar keys can't have more than 70 characters."
+ );
+ checkScalar(scalars[name][key], name);
+ }
+ }
+}
+
+function checkPayload(payload, reason, successfulPings) {
+ Assert.ok("info" in payload, "Payload must contain an info section.");
+ checkPayloadInfo(payload.info, reason);
+
+ Assert.ok(payload.simpleMeasurements.totalTime >= 0);
+ Assert.equal(payload.simpleMeasurements.startupInterrupted, 1);
+ Assert.equal(payload.simpleMeasurements.shutdownDuration, SHUTDOWN_TIME);
+
+ let activeTicks = payload.simpleMeasurements.activeTicks;
+ Assert.ok(activeTicks >= 0);
+
+ if ("browser.timings.last_shutdown" in payload.processes.parent.scalars) {
+ Assert.equal(
+ payload.processes.parent.scalars["browser.timings.last_shutdown"],
+ SHUTDOWN_TIME
+ );
+ }
+
+ Assert.equal(
+ payload.simpleMeasurements.failedProfileLockCount,
+ FAILED_PROFILE_LOCK_ATTEMPTS
+ );
+ let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let failedProfileLocksFile = profileDirectory.clone();
+ failedProfileLocksFile.append("Telemetry.FailedProfileLocks.txt");
+ Assert.ok(!failedProfileLocksFile.exists());
+
+ let isWindows = "@mozilla.org/windows-registry-key;1" in Cc;
+ if (isWindows) {
+ Assert.ok(payload.simpleMeasurements.startupSessionRestoreReadBytes > 0);
+ Assert.ok(payload.simpleMeasurements.startupSessionRestoreWriteBytes > 0);
+ }
+
+ const TELEMETRY_SEND_SUCCESS = "TELEMETRY_SEND_SUCCESS";
+ const TELEMETRY_SUCCESS = "TELEMETRY_SUCCESS";
+ const TELEMETRY_TEST_FLAG = "TELEMETRY_TEST_FLAG";
+ const TELEMETRY_TEST_COUNT = "TELEMETRY_TEST_COUNT";
+ const TELEMETRY_TEST_KEYED_FLAG = "TELEMETRY_TEST_KEYED_FLAG";
+ const TELEMETRY_TEST_KEYED_COUNT = "TELEMETRY_TEST_KEYED_COUNT";
+
+ if (successfulPings > 0) {
+ Assert.ok(TELEMETRY_SEND_SUCCESS in payload.histograms);
+ }
+ Assert.ok(TELEMETRY_TEST_FLAG in payload.histograms);
+ Assert.ok(TELEMETRY_TEST_COUNT in payload.histograms);
+
+ Assert.ok(!(IGNORE_CLONED_HISTOGRAM in payload.histograms));
+
+ // Flag histograms should automagically spring to life.
+ const expected_flag = {
+ range: [1, 2],
+ bucket_count: 3,
+ histogram_type: 3,
+ values: { 0: 1, 1: 0 },
+ sum: 0,
+ };
+ let flag = payload.histograms[TELEMETRY_TEST_FLAG];
+ Assert.deepEqual(flag, expected_flag);
+
+ // We should have a test count.
+ const expected_count = {
+ range: [1, 2],
+ bucket_count: 3,
+ histogram_type: 4,
+ values: { 0: 1, 1: 0 },
+ sum: 1,
+ };
+ let count = payload.histograms[TELEMETRY_TEST_COUNT];
+ Assert.deepEqual(count, expected_count);
+
+ // There should be one successful report from the previous telemetry ping.
+ if (successfulPings > 0) {
+ const expected_tc = {
+ range: [1, 2],
+ bucket_count: 3,
+ histogram_type: 2,
+ values: { 0: 2, 1: successfulPings, 2: 0 },
+ sum: successfulPings,
+ };
+ let tc = payload.histograms[TELEMETRY_SUCCESS];
+ Assert.deepEqual(tc, expected_tc);
+ }
+
+ // The ping should include data from memory reporters. We can't check that
+ // this data is correct, because we can't control the values returned by the
+ // memory reporters. But we can at least check that the data is there.
+ //
+ // It's important to check for the presence of reporters with a mix of units,
+ // because MemoryTelemetry has separate logic for each one. But we can't
+ // currently check UNITS_COUNT_CUMULATIVE or UNITS_PERCENTAGE because
+ // Telemetry doesn't touch a memory reporter with these units that's
+ // available on all platforms.
+
+ Assert.ok("MEMORY_TOTAL" in payload.histograms); // UNITS_BYTES
+ Assert.ok("MEMORY_JS_GC_HEAP" in payload.histograms); // UNITS_BYTES
+ Assert.ok("MEMORY_JS_COMPARTMENTS_SYSTEM" in payload.histograms); // UNITS_COUNT
+
+ Assert.ok(
+ "mainThread" in payload.slowSQL && "otherThreads" in payload.slowSQL
+ );
+
+ // Check keyed histogram payload.
+
+ Assert.ok("keyedHistograms" in payload);
+ let keyedHistograms = payload.keyedHistograms;
+ Assert.ok(!(TELEMETRY_TEST_KEYED_FLAG in keyedHistograms));
+ Assert.ok(TELEMETRY_TEST_KEYED_COUNT in keyedHistograms);
+
+ const expected_keyed_count = {
+ a: {
+ range: [1, 2],
+ bucket_count: 3,
+ histogram_type: 4,
+ values: { 0: 2, 1: 0 },
+ sum: 2,
+ },
+ b: {
+ range: [1, 2],
+ bucket_count: 3,
+ histogram_type: 4,
+ values: { 0: 1, 1: 0 },
+ sum: 1,
+ },
+ };
+ Assert.deepEqual(
+ expected_keyed_count,
+ keyedHistograms[TELEMETRY_TEST_KEYED_COUNT]
+ );
+
+ Assert.ok(
+ "processes" in payload,
+ "The payload must have a processes section."
+ );
+ Assert.ok(
+ "parent" in payload.processes,
+ "There must be at least a parent process."
+ );
+
+ checkScalars(payload.processes);
+}
+
+function writeStringToFile(file, contents) {
+ let ostream = Cc[
+ "@mozilla.org/network/safe-file-output-stream;1"
+ ].createInstance(Ci.nsIFileOutputStream);
+ ostream.init(
+ file,
+ PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE,
+ RW_OWNER,
+ ostream.DEFER_OPEN
+ );
+ ostream.write(contents, contents.length);
+ ostream.QueryInterface(Ci.nsISafeOutputStream).finish();
+ ostream.close();
+}
+
+function write_fake_shutdown_file() {
+ let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let file = profileDirectory.clone();
+ file.append("Telemetry.ShutdownTime.txt");
+ let contents = "" + SHUTDOWN_TIME;
+ writeStringToFile(file, contents);
+}
+
+function write_fake_failedprofilelocks_file() {
+ let profileDirectory = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ let file = profileDirectory.clone();
+ file.append("Telemetry.FailedProfileLocks.txt");
+ let contents = "" + FAILED_PROFILE_LOCK_ATTEMPTS;
+ writeStringToFile(file, contents);
+}
+
+add_task(async function test_setup() {
+ // Addon manager needs a profile directory
+ do_get_profile();
+ await loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
+ finishAddonManagerStartup();
+ fakeIntlReady();
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+
+ // Make it look like we've previously failed to lock a profile a couple times.
+ write_fake_failedprofilelocks_file();
+
+ // Make it look like we've shutdown before.
+ write_fake_shutdown_file();
+
+ await new Promise(resolve =>
+ Telemetry.asyncFetchTelemetryData(wrapWithExceptionHandler(resolve))
+ );
+});
+
+add_task(async function asyncSetup() {
+ await TelemetryController.testSetup();
+ // Load the client ID from the client ID provider to check for pings sanity.
+ gClientID = await ClientID.getClientID();
+});
+
+// Ensures that expired histograms are not part of the payload.
+add_task(async function test_expiredHistogram() {
+ let dummy = Telemetry.getHistogramById("TELEMETRY_TEST_EXPIRED");
+
+ dummy.add(1);
+
+ Assert.equal(
+ TelemetrySession.getPayload().histograms.TELEMETRY_TEST_EXPIRED,
+ undefined
+ );
+});
+
+add_task(async function sessionTimeExcludingAndIncludingSuspend() {
+ if (gIsAndroid) {
+ // We don't support this new probe on android at the moment.
+ return;
+ }
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+ );
+ await TelemetryController.testReset();
+ let subsession = TelemetrySession.getPayload("environment-change", true);
+ let parentScalars = subsession.processes.parent.scalars;
+
+ let withSuspend =
+ parentScalars["browser.engagement.session_time_including_suspend"];
+ let withoutSuspend =
+ parentScalars["browser.engagement.session_time_excluding_suspend"];
+
+ Assert.ok(
+ withSuspend > 0,
+ "The session time including suspend should be positive"
+ );
+
+ Assert.ok(
+ withoutSuspend > 0,
+ "The session time excluding suspend should be positive"
+ );
+
+ // Two things about the next assertion:
+ // 1. The two calls to get the two different uptime values are made
+ // separately, so we can't guarantee equality, even if we know the machine
+ // has not been suspended (for example because it's running in infra and
+ // was just booted). In this case the value should be close to each other.
+ // 2. This test will fail if the device running this has been suspended in
+ // between booting the Firefox process running this test, and doing the
+ // following assertion test, but that's unlikely in practice.
+ const max_delta_ms = 100;
+
+ Assert.ok(
+ withSuspend - withoutSuspend <= max_delta_ms,
+ "In test condition, the two uptimes should be close to each other"
+ );
+
+ // This however should always hold.
+ Assert.greaterOrEqual(
+ withSuspend,
+ withoutSuspend,
+ `The uptime with suspend must always been greater or equal to the uptime
+ without suspend`
+ );
+
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ false
+ );
+});
+
+// Sends a ping to a non existing server. If we remove this test, we won't get
+// all the histograms we need in the main ping.
+add_task(async function test_noServerPing() {
+ await sendPing();
+ // We need two pings in order to make sure STARTUP_MEMORY_STORAGE_SQLIE histograms
+ // are initialised. See bug 1131585.
+ await sendPing();
+ // Allowing Telemetry to persist unsent pings as pending. If omitted may cause
+ // problems to the consequent tests.
+ await TelemetryController.testShutdown();
+});
+
+// Checks that a sent ping is correctly received by a dummy http server.
+add_task(async function test_simplePing() {
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.start();
+ Services.prefs.setStringPref(
+ TelemetryUtils.Preferences.Server,
+ "http://localhost:" + PingServer.port
+ );
+
+ let now = new Date(2020, 1, 1, 12, 5, 6);
+ let expectedDate = new Date(2020, 1, 1, 12, 0, 0);
+ fakeNow(now);
+ gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 5000);
+
+ const expectedSessionUUID = "bd314d15-95bf-4356-b682-b6c4a8942202";
+ const expectedSubsessionUUID = "3e2e5f6c-74ba-4e4d-a93f-a48af238a8c7";
+ fakeGenerateUUID(
+ () => expectedSessionUUID,
+ () => expectedSubsessionUUID
+ );
+ await TelemetryController.testReset();
+
+ // Session and subsession start dates are faked during TelemetrySession setup. We can
+ // now fake the session duration.
+ const SESSION_DURATION_IN_MINUTES = 15;
+ fakeNow(new Date(2020, 1, 1, 12, SESSION_DURATION_IN_MINUTES, 0));
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + SESSION_DURATION_IN_MINUTES * 60 * 1000
+ );
+
+ await sendPing();
+ let ping = await PingServer.promiseNextPing();
+
+ checkPingFormat(ping, PING_TYPE_MAIN, true, true);
+
+ // Check that we get the data we expect.
+ let payload = ping.payload;
+ Assert.equal(payload.info.sessionId, expectedSessionUUID);
+ Assert.equal(payload.info.subsessionId, expectedSubsessionUUID);
+ let sessionStartDate = new Date(payload.info.sessionStartDate);
+ Assert.equal(sessionStartDate.toISOString(), expectedDate.toISOString());
+ let subsessionStartDate = new Date(payload.info.subsessionStartDate);
+ Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString());
+ Assert.equal(payload.info.subsessionLength, SESSION_DURATION_IN_MINUTES * 60);
+
+ // Restore the UUID generator so we don't mess with other tests.
+ fakeGenerateUUID(TelemetryUtils.generateUUID, TelemetryUtils.generateUUID);
+
+ await TelemetryController.testShutdown();
+});
+
+// Saves the current session histograms, reloads them, performs a ping
+// and checks that the dummy http server received both the previously
+// saved ping and the new one.
+add_task(async function test_saveLoadPing() {
+ // Let's start out with a defined state.
+ await TelemetryStorage.testClearPendingPings();
+ await TelemetryController.testReset();
+ PingServer.clearRequests();
+
+ // Setup test data and trigger pings.
+ setupTestData();
+ await TelemetrySession.testSavePendingPing();
+ await sendPing();
+
+ // Get requests received by dummy server.
+ const requests = await PingServer.promiseNextRequests(2);
+
+ for (let req of requests) {
+ Assert.equal(
+ req.getHeader("content-type"),
+ "application/json; charset=UTF-8",
+ "The request must have the correct content-type."
+ );
+ }
+
+ // We decode both requests to check for the |reason|.
+ let pings = Array.from(requests, decodeRequestPayload);
+
+ // Check we have the correct two requests. Ordering is not guaranteed. The ping type
+ // is encoded in the URL.
+ if (pings[0].type != PING_TYPE_MAIN) {
+ pings.reverse();
+ }
+
+ checkPingFormat(pings[0], PING_TYPE_MAIN, true, true);
+ checkPayload(pings[0].payload, REASON_TEST_PING, 0);
+ checkPingFormat(pings[1], PING_TYPE_SAVED_SESSION, true, true);
+ checkPayload(pings[1].payload, REASON_SAVED_SESSION, 0);
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_checkSubsessionScalars() {
+ if (gIsAndroid) {
+ // We don't support subsessions yet on Android.
+ return;
+ }
+
+ // Clear the scalars.
+ Telemetry.clearScalars();
+ await TelemetryController.testReset();
+
+ // Set some scalars.
+ const UINT_SCALAR = "telemetry.test.unsigned_int_kind";
+ const STRING_SCALAR = "telemetry.test.string_kind";
+ let expectedUint = 37;
+ let expectedString = "Test value. Yay.";
+ Telemetry.scalarSet(UINT_SCALAR, expectedUint);
+ Telemetry.scalarSet(STRING_SCALAR, expectedString);
+
+ // Check that scalars are not available in classic pings but are in subsession
+ // pings. Also clear the subsession.
+ let classic = TelemetrySession.getPayload();
+ let subsession = TelemetrySession.getPayload("environment-change", true);
+
+ const TEST_SCALARS = [UINT_SCALAR, STRING_SCALAR];
+ for (let name of TEST_SCALARS) {
+ // Scalar must be reported in subsession pings (e.g. main).
+ Assert.ok(
+ name in subsession.processes.parent.scalars,
+ name + " must be reported in a subsession ping."
+ );
+ }
+ // No scalar must be reported in classic pings (e.g. saved-session).
+ Assert.ok(
+ !Object.keys(classic.processes.parent.scalars).length,
+ "Scalars must not be reported in a classic ping."
+ );
+
+ // And make sure that we're getting the right values in the
+ // subsession ping.
+ Assert.equal(
+ subsession.processes.parent.scalars[UINT_SCALAR],
+ expectedUint,
+ UINT_SCALAR + " must contain the expected value."
+ );
+ Assert.equal(
+ subsession.processes.parent.scalars[STRING_SCALAR],
+ expectedString,
+ STRING_SCALAR + " must contain the expected value."
+ );
+
+ // Since we cleared the subsession in the last getPayload(), check that
+ // breaking subsessions clears the scalars.
+ subsession = TelemetrySession.getPayload("environment-change");
+ for (let name of TEST_SCALARS) {
+ Assert.ok(
+ !(name in subsession.processes.parent.scalars),
+ name + " must be cleared with the new subsession."
+ );
+ }
+
+ // Check if setting the scalars again works as expected.
+ expectedUint = 85;
+ expectedString = "A creative different value";
+ Telemetry.scalarSet(UINT_SCALAR, expectedUint);
+ Telemetry.scalarSet(STRING_SCALAR, expectedString);
+ subsession = TelemetrySession.getPayload("environment-change");
+ Assert.equal(
+ subsession.processes.parent.scalars[UINT_SCALAR],
+ expectedUint,
+ UINT_SCALAR + " must contain the expected value."
+ );
+ Assert.equal(
+ subsession.processes.parent.scalars[STRING_SCALAR],
+ expectedString,
+ STRING_SCALAR + " must contain the expected value."
+ );
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_dailyCollection() {
+ if (gIsAndroid) {
+ // We don't do daily collections yet on Android.
+ return;
+ }
+
+ let now = new Date(2030, 1, 1, 12, 0, 0);
+ let nowHour = new Date(2030, 1, 1, 12, 0, 0);
+ let schedulerTickCallback = null;
+
+ PingServer.clearRequests();
+
+ fakeNow(now);
+
+ // Fake scheduler functions to control daily collection flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+
+ // Init and check timer.
+ await TelemetryStorage.testClearPendingPings();
+ await TelemetryController.testReset();
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+
+ // Set histograms to expected state.
+ const COUNT_ID = "TELEMETRY_TEST_COUNT";
+ const KEYED_ID = "TELEMETRY_TEST_KEYED_COUNT";
+ const count = Telemetry.getHistogramById(COUNT_ID);
+ const keyed = Telemetry.getKeyedHistogramById(KEYED_ID);
+
+ count.clear();
+ keyed.clear();
+ count.add(1);
+ keyed.add("a", 1);
+ keyed.add("b", 1);
+ keyed.add("b", 1);
+
+ // Make sure the daily ping gets triggered.
+ let expectedDate = nowHour;
+ now = futureDate(nowHour, MS_IN_ONE_DAY);
+ fakeNow(now);
+
+ Assert.ok(!!schedulerTickCallback);
+ // Run a scheduler tick: it should trigger the daily ping.
+ await schedulerTickCallback();
+
+ // Collect the daily ping.
+ let ping = await PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.payload.info.reason, REASON_DAILY);
+ let subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
+ Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString());
+
+ Assert.equal(ping.payload.histograms[COUNT_ID].sum, 1);
+ Assert.equal(ping.payload.keyedHistograms[KEYED_ID].a.sum, 1);
+ Assert.equal(ping.payload.keyedHistograms[KEYED_ID].b.sum, 2);
+
+ // The daily ping is rescheduled for "tomorrow".
+ expectedDate = futureDate(expectedDate, MS_IN_ONE_DAY);
+ now = futureDate(now, MS_IN_ONE_DAY);
+ fakeNow(now);
+
+ // Run a scheduler tick. Trigger and collect another ping. The histograms should be reset.
+ await schedulerTickCallback();
+
+ ping = await PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.payload.info.reason, REASON_DAILY);
+ subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
+ Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString());
+
+ Assert.ok(!(COUNT_ID in ping.payload.histograms));
+ Assert.ok(!(KEYED_ID in ping.payload.keyedHistograms));
+
+ // Trigger and collect another daily ping, with the histograms being set again.
+ count.add(1);
+ keyed.add("a", 1);
+ keyed.add("b", 1);
+
+ // The daily ping is rescheduled for "tomorrow".
+ expectedDate = futureDate(expectedDate, MS_IN_ONE_DAY);
+ now = futureDate(now, MS_IN_ONE_DAY);
+ fakeNow(now);
+
+ await schedulerTickCallback();
+ ping = await PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.payload.info.reason, REASON_DAILY);
+ subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
+ Assert.equal(subsessionStartDate.toISOString(), expectedDate.toISOString());
+
+ Assert.equal(ping.payload.histograms[COUNT_ID].sum, 1);
+ Assert.equal(ping.payload.keyedHistograms[KEYED_ID].a.sum, 1);
+ Assert.equal(ping.payload.keyedHistograms[KEYED_ID].b.sum, 1);
+
+ // Shutdown to cleanup the aborted-session if it gets created.
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_dailyDuplication() {
+ if (gIsAndroid) {
+ // We don't do daily collections yet on Android.
+ return;
+ }
+
+ await TelemetrySend.reset();
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+
+ let schedulerTickCallback = null;
+ let now = new Date(2030, 1, 1, 0, 0, 0);
+ fakeNow(now);
+ // Fake scheduler functions to control daily collection flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+
+ // Make sure the daily ping gets triggered at midnight.
+ // We need to make sure that we trigger this after the period where we wait for
+ // the user to become idle.
+ let firstDailyDue = new Date(2030, 1, 2, 0, 0, 0);
+ fakeNow(firstDailyDue);
+
+ // Run a scheduler tick: it should trigger the daily ping.
+ Assert.ok(!!schedulerTickCallback);
+ await schedulerTickCallback();
+
+ // Get the first daily ping.
+ let ping = await PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.payload.info.reason, REASON_DAILY);
+
+ // We don't expect to receive any other daily ping in this test, so assert if we do.
+ PingServer.registerPingHandler((req, res) => {
+ Assert.ok(
+ false,
+ "No more daily pings should be sent/received in this test."
+ );
+ });
+
+ // Set the current time to a bit after midnight.
+ let secondDailyDue = new Date(firstDailyDue);
+ secondDailyDue.setHours(0);
+ secondDailyDue.setMinutes(15);
+ fakeNow(secondDailyDue);
+
+ // Run a scheduler tick: it should NOT trigger the daily ping.
+ Assert.ok(!!schedulerTickCallback);
+ await schedulerTickCallback();
+
+ // Shutdown to cleanup the aborted-session if it gets created.
+ PingServer.resetPingHandler();
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_dailyOverdue() {
+ if (gIsAndroid) {
+ // We don't do daily collections yet on Android.
+ return;
+ }
+
+ let schedulerTickCallback = null;
+ let now = new Date(2030, 1, 1, 11, 0, 0);
+ fakeNow(now);
+ // Fake scheduler functions to control daily collection flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryStorage.testClearPendingPings();
+ await TelemetryController.testReset();
+
+ // Skip one hour ahead: nothing should be due.
+ now.setHours(now.getHours() + 1);
+ fakeNow(now);
+
+ // Assert if we receive something!
+ PingServer.registerPingHandler((req, res) => {
+ Assert.ok(false, "No daily ping should be received if not overdue!.");
+ });
+
+ // This tick should not trigger any daily ping.
+ Assert.ok(!!schedulerTickCallback);
+ await schedulerTickCallback();
+
+ // Restore the non asserting ping handler.
+ PingServer.resetPingHandler();
+ PingServer.clearRequests();
+
+ // Simulate an overdue ping: we're not close to midnight, but the last daily ping
+ // time is too long ago.
+ let dailyOverdue = new Date(2030, 1, 2, 13, 0, 0);
+ fakeNow(dailyOverdue);
+
+ // Run a scheduler tick: it should trigger the daily ping.
+ Assert.ok(!!schedulerTickCallback);
+ await schedulerTickCallback();
+
+ // Get the first daily ping.
+ let ping = await PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.payload.info.reason, REASON_DAILY);
+
+ // Shutdown to cleanup the aborted-session if it gets created.
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_environmentChange() {
+ if (gIsAndroid) {
+ // We don't split subsessions on environment changes yet on Android.
+ return;
+ }
+
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+
+ let now = fakeNow(2040, 1, 1, 12, 0, 0);
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+
+ const PREF_TEST = "toolkit.telemetry.test.pref1";
+ Services.prefs.clearUserPref(PREF_TEST);
+
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_VALUE }],
+ ]);
+
+ // Setup.
+ await TelemetryController.testReset();
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+
+ // Set histograms to expected state.
+ const COUNT_ID = "TELEMETRY_TEST_COUNT";
+ const KEYED_ID = "TELEMETRY_TEST_KEYED_COUNT";
+ const count = Telemetry.getHistogramById(COUNT_ID);
+ const keyed = Telemetry.getKeyedHistogramById(KEYED_ID);
+
+ count.clear();
+ keyed.clear();
+ count.add(1);
+ keyed.add("a", 1);
+ keyed.add("b", 1);
+
+ // Trigger and collect environment-change ping.
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+ let startHour = TelemetryUtils.truncateToHours(now);
+ now = fakeNow(futureDate(now, 10 * MILLISECONDS_PER_MINUTE));
+
+ Services.prefs.setIntPref(PREF_TEST, 1);
+ let ping = await PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.environment.settings.userPrefs[PREF_TEST], undefined);
+ Assert.equal(ping.payload.info.reason, REASON_ENVIRONMENT_CHANGE);
+ let subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
+ Assert.equal(subsessionStartDate.toISOString(), startHour.toISOString());
+
+ Assert.equal(ping.payload.histograms[COUNT_ID].sum, 1);
+ Assert.equal(ping.payload.keyedHistograms[KEYED_ID].a.sum, 1);
+
+ // Trigger and collect another ping. The histograms should be reset.
+ startHour = TelemetryUtils.truncateToHours(now);
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+ now = fakeNow(futureDate(now, 10 * MILLISECONDS_PER_MINUTE));
+
+ Services.prefs.setIntPref(PREF_TEST, 2);
+ ping = await PingServer.promiseNextPing();
+ Assert.ok(!!ping);
+
+ Assert.equal(ping.type, PING_TYPE_MAIN);
+ Assert.equal(ping.environment.settings.userPrefs[PREF_TEST], 1);
+ Assert.equal(ping.payload.info.reason, REASON_ENVIRONMENT_CHANGE);
+ subsessionStartDate = new Date(ping.payload.info.subsessionStartDate);
+ Assert.equal(subsessionStartDate.toISOString(), startHour.toISOString());
+
+ Assert.ok(!(COUNT_ID in ping.payload.histograms));
+ Assert.ok(!(KEYED_ID in ping.payload.keyedHistograms));
+
+ // Trigger and collect another ping. The histograms should be reset.
+ startHour = TelemetryUtils.truncateToHours(now);
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+ now = fakeNow(futureDate(now, 10 * MILLISECONDS_PER_MINUTE));
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_experimentAnnotations_subsession() {
+ if (gIsAndroid) {
+ // We don't split subsessions on environment changes yet on Android.
+ return;
+ }
+
+ const EXPERIMENT1 = "experiment-1";
+ const EXPERIMENT1_BRANCH = "nice-branch";
+ const EXPERIMENT2 = "experiment-2";
+ const EXPERIMENT2_BRANCH = "other-branch";
+
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+
+ let now = fakeNow(2040, 1, 1, 12, 0, 0);
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+
+ // Setup.
+ await TelemetryController.testReset();
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+ Assert.equal(TelemetrySession.getPayload().info.subsessionCounter, 1);
+
+ // Trigger a subsession split with a telemetry annotation.
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+ let futureTestDate = futureDate(now, 10 * MILLISECONDS_PER_MINUTE);
+ now = fakeNow(futureTestDate);
+ TelemetryEnvironment.setExperimentActive(EXPERIMENT1, EXPERIMENT1_BRANCH);
+
+ let ping = await PingServer.promiseNextPing();
+ Assert.ok(!!ping, "A ping must be received.");
+
+ Assert.equal(
+ ping.type,
+ PING_TYPE_MAIN,
+ "The received ping must be a 'main' ping."
+ );
+ Assert.equal(
+ ping.payload.info.reason,
+ REASON_ENVIRONMENT_CHANGE,
+ "The 'main' ping must be triggered by a change in the environment."
+ );
+ // We expect the current experiments to be reported in the next ping, not this
+ // one.
+ Assert.ok(
+ !("experiments" in ping.environment),
+ "The old environment must contain no active experiments."
+ );
+ // Since this change wasn't throttled, the subsession counter must increase.
+ Assert.equal(
+ TelemetrySession.getPayload().info.subsessionCounter,
+ 2,
+ "The experiment annotation must trigger a new subsession."
+ );
+
+ // Add another annotation to the environment. We're not advancing the fake
+ // timer, so no subsession split should happen due to throttling.
+ TelemetryEnvironment.setExperimentActive(EXPERIMENT2, EXPERIMENT2_BRANCH);
+ Assert.equal(
+ TelemetrySession.getPayload().info.subsessionCounter,
+ 2,
+ "The experiment annotation must not trigger a new subsession " +
+ "if throttling happens."
+ );
+ let oldExperiments = TelemetryEnvironment.getActiveExperiments();
+
+ // Fake the timer and remove an annotation, we expect a new subsession split.
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+ now = fakeNow(futureDate(now, 10 * MILLISECONDS_PER_MINUTE));
+ TelemetryEnvironment.setExperimentInactive(EXPERIMENT1, EXPERIMENT1_BRANCH);
+
+ ping = await PingServer.promiseNextPing();
+ Assert.ok(!!ping, "A ping must be received.");
+
+ Assert.equal(
+ ping.type,
+ PING_TYPE_MAIN,
+ "The received ping must be a 'main' ping."
+ );
+ Assert.equal(
+ ping.payload.info.reason,
+ REASON_ENVIRONMENT_CHANGE,
+ "The 'main' ping must be triggered by a change in the environment."
+ );
+ // We expect both experiments to be in this environment.
+ Assert.deepEqual(
+ ping.environment.experiments,
+ oldExperiments,
+ "The environment must contain both the experiments."
+ );
+ Assert.equal(
+ TelemetrySession.getPayload().info.subsessionCounter,
+ 3,
+ "The removing an experiment annotation must trigger a new subsession."
+ );
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_savedPingsOnShutdown() {
+ await TelemetryController.testReset();
+
+ // Assure that we store the ping properly when saving sessions on shutdown.
+ // We make the TelemetryController shutdown to trigger a session save.
+ const dir = TelemetryStorage.pingDirectoryPath;
+ await IOUtils.remove(dir, { ignoreAbsent: true, recursive: true });
+ await IOUtils.makeDirectory(dir);
+ await TelemetryController.testShutdown();
+
+ PingServer.clearRequests();
+ await TelemetryController.testReset();
+
+ const ping = await PingServer.promiseNextPing();
+
+ let expectedType = gIsAndroid ? PING_TYPE_SAVED_SESSION : PING_TYPE_MAIN;
+ let expectedReason = gIsAndroid ? REASON_SAVED_SESSION : REASON_SHUTDOWN;
+
+ checkPingFormat(ping, expectedType, true, true);
+ Assert.equal(ping.payload.info.reason, expectedReason);
+ Assert.equal(ping.clientId, gClientID);
+});
+
+add_task(async function test_sendShutdownPing() {
+ if (
+ gIsAndroid ||
+ (AppConstants.platform == "linux" && !Services.appinfo.is64Bit)
+ ) {
+ // We don't support the pingsender on Android, yet, see bug 1335917.
+ // We also don't suppor the pingsender testing on Treeherder for
+ // Linux 32 bit (due to missing libraries). So skip it there too.
+ // See bug 1310703 comment 78.
+ return;
+ }
+
+ let checkPendingShutdownPing = async function () {
+ let pendingPings = await TelemetryStorage.loadPendingPingList();
+ Assert.equal(pendingPings.length, 1, "We expect 1 pending ping: shutdown.");
+ // Load the pings off the disk.
+ const shutdownPing = await TelemetryStorage.loadPendingPing(
+ pendingPings[0].id
+ );
+ Assert.ok(shutdownPing, "The 'shutdown' ping must be saved to disk.");
+ Assert.equal(
+ "shutdown",
+ shutdownPing.payload.info.reason,
+ "The 'shutdown' ping must be saved to disk."
+ );
+ };
+
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.ShutdownPingSender,
+ true
+ );
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FirstRun, false);
+ // Make sure the reporting policy picks up the updated pref.
+ TelemetryReportingPolicy.testUpdateFirstRun();
+ PingServer.clearRequests();
+ Telemetry.clearScalars();
+
+ // Shutdown telemetry and wait for an incoming ping.
+ let nextPing = PingServer.promiseNextPing();
+ await TelemetryController.testShutdown();
+ let ping = await nextPing;
+
+ // Check that we received a shutdown ping.
+ checkPingFormat(ping, ping.type, true, true);
+ Assert.equal(ping.payload.info.reason, REASON_SHUTDOWN);
+ Assert.equal(ping.clientId, gClientID);
+ // Try again, this time disable ping upload. The PingSender
+ // should not be sending any ping!
+ PingServer.registerPingHandler(() =>
+ Assert.ok(false, "Telemetry must not send pings if not allowed to.")
+ );
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.FhrUploadEnabled,
+ false
+ );
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+
+ // Make sure we have no pending pings between the runs.
+ await TelemetryStorage.testClearPendingPings();
+
+ // Enable ping upload and signal an OS shutdown. The pingsender
+ // will not be spawned and no ping will be sent.
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FhrUploadEnabled, true);
+ await TelemetryController.testReset();
+ Services.obs.notifyObservers(null, "quit-application-forced");
+ await TelemetryController.testShutdown();
+ // After re-enabling FHR, wait for the new client ID
+ gClientID = await ClientID.getClientID();
+
+ // Check that the "shutdown" ping was correctly saved to disk.
+ await checkPendingShutdownPing();
+
+ // Make sure we have no pending pings between the runs.
+ await TelemetryStorage.testClearPendingPings();
+ Telemetry.clearScalars();
+
+ await TelemetryController.testReset();
+ Services.obs.notifyObservers(
+ null,
+ "quit-application-granted",
+ "syncShutdown"
+ );
+ await TelemetryController.testShutdown();
+ await checkPendingShutdownPing();
+
+ // Make sure we have no pending pings between the runs.
+ await TelemetryStorage.testClearPendingPings();
+
+ // Disable the "submission policy". The shutdown ping must not be sent.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.BypassNotification,
+ false
+ );
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+
+ // Make sure we have no pending pings between the runs.
+ await TelemetryStorage.testClearPendingPings();
+
+ // We cannot reset the BypassNotification pref, as we need it to be
+ // |true| in tests.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.BypassNotification,
+ true
+ );
+
+ // With both upload enabled and the policy shown, make sure we don't
+ // send the shutdown ping using the pingsender on the first
+ // subsession.
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FirstRun, true);
+ // Make sure the reporting policy picks up the updated pref.
+ TelemetryReportingPolicy.testUpdateFirstRun();
+
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+
+ // Clear the state and prepare for the next test.
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+ PingServer.resetPingHandler();
+
+ // Check that we're able to send the shutdown ping using the pingsender
+ // from the first session if the related pref is on.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.ShutdownPingSenderFirstSession,
+ true
+ );
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FirstRun, true);
+ TelemetryReportingPolicy.testUpdateFirstRun();
+
+ // Restart/shutdown telemetry and wait for an incoming ping.
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ ping = await PingServer.promiseNextPing();
+
+ // Check that we received a shutdown ping.
+ checkPingFormat(ping, ping.type, true, true);
+ Assert.equal(ping.payload.info.reason, REASON_SHUTDOWN);
+ Assert.equal(ping.clientId, gClientID);
+
+ // Reset the pref and restart Telemetry.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.ShutdownPingSender,
+ false
+ );
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.ShutdownPingSenderFirstSession,
+ false
+ );
+ Services.prefs.clearUserPref(TelemetryUtils.Preferences.FirstRun);
+ PingServer.resetPingHandler();
+});
+
+add_task(async function test_sendFirstShutdownPing() {
+ if (
+ gIsAndroid ||
+ (AppConstants.platform == "linux" && !Services.appinfo.is64Bit)
+ ) {
+ // We don't support the pingsender on Android, yet, see bug 1335917.
+ // We also don't suppor the pingsender testing on Treeherder for
+ // Linux 32 bit (due to missing libraries). So skip it there too.
+ // See bug 1310703 comment 78.
+ return;
+ }
+
+ let storageContainsFirstShutdown = async function () {
+ let pendingPings = await TelemetryStorage.loadPendingPingList();
+ let pings = await Promise.all(
+ pendingPings.map(async p => {
+ return TelemetryStorage.loadPendingPing(p.id);
+ })
+ );
+ return pings.find(p => p.type == "first-shutdown");
+ };
+
+ let checkShutdownNotSent = async function () {
+ // The failure-mode of the ping-sender is used to check that a ping was
+ // *not* sent. This can be combined with the state of the storage to infer
+ // the appropriate behavior from the preference flags.
+
+ // Assert failure if we recive a ping.
+ PingServer.registerPingHandler((req, res) => {
+ const receivedPing = decodeRequestPayload(req);
+ Assert.ok(
+ false,
+ `No ping should be received in this test (got ${receivedPing.id}).`
+ );
+ });
+
+ // Assert that pings are sent on first run, forcing a forced application
+ // quit. This should be equivalent to the first test in this suite.
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FirstRun, true);
+ TelemetryReportingPolicy.testUpdateFirstRun();
+
+ await TelemetryController.testReset();
+ Services.obs.notifyObservers(null, "quit-application-forced");
+ await TelemetryController.testShutdown();
+ Assert.ok(
+ await storageContainsFirstShutdown(),
+ "The 'first-shutdown' ping must be saved to disk."
+ );
+
+ await TelemetryStorage.testClearPendingPings();
+
+ // Assert that it's not sent during subsequent runs
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FirstRun, false);
+ TelemetryReportingPolicy.testUpdateFirstRun();
+
+ await TelemetryController.testReset();
+ Services.obs.notifyObservers(null, "quit-application-forced");
+ await TelemetryController.testShutdown();
+ Assert.ok(
+ !(await storageContainsFirstShutdown()),
+ "The 'first-shutdown' ping should only be written during first run."
+ );
+
+ await TelemetryStorage.testClearPendingPings();
+
+ // Assert that the the ping is only sent if the flag is enabled.
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FirstRun, true);
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.FirstShutdownPingEnabled,
+ false
+ );
+ TelemetryReportingPolicy.testUpdateFirstRun();
+
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ Assert.ok(
+ !(await storageContainsFirstShutdown()),
+ "The 'first-shutdown' ping should only be written if enabled"
+ );
+
+ await TelemetryStorage.testClearPendingPings();
+
+ // Assert that the the ping is not collected when the ping-sender is disabled.
+ // The information would be made irrelevant by the main-ping in the second session.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.FirstShutdownPingEnabled,
+ true
+ );
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.ShutdownPingSender,
+ false
+ );
+ TelemetryReportingPolicy.testUpdateFirstRun();
+
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ Assert.ok(
+ !(await storageContainsFirstShutdown()),
+ "The 'first-shutdown' ping should only be written if ping-sender is enabled"
+ );
+
+ // Clear the state and prepare for the next test.
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+ PingServer.resetPingHandler();
+ };
+
+ // Remove leftover pending pings from other tests
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+ Telemetry.clearScalars();
+
+ // Set testing invariants for FirstShutdownPingEnabled
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.ShutdownPingSender,
+ true
+ );
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.ShutdownPingSenderFirstSession,
+ false
+ );
+
+ // Set primary conditions of the 'first-shutdown' ping
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.FirstShutdownPingEnabled,
+ true
+ );
+ Services.prefs.setBoolPref(TelemetryUtils.Preferences.FirstRun, true);
+ TelemetryReportingPolicy.testUpdateFirstRun();
+
+ // Assert general 'first-shutdown' use-case.
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ let ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, "first-shutdown", true, true);
+ Assert.equal(ping.payload.info.reason, REASON_SHUTDOWN);
+ Assert.equal(ping.clientId, gClientID);
+
+ await TelemetryStorage.testClearPendingPings();
+
+ // Assert that the shutdown is not sent under various conditions
+ await checkShutdownNotSent();
+
+ // Reset the pref and restart Telemetry.
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.ShutdownPingSender,
+ false
+ );
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.ShutdownPingSenderFirstSession,
+ false
+ );
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.FirstShutdownPingEnabled,
+ false
+ );
+ Services.prefs.clearUserPref(TelemetryUtils.Preferences.FirstRun);
+ PingServer.resetPingHandler();
+});
+
+add_task(async function test_savedSessionData() {
+ // Create the directory which will contain the data file, if it doesn't already
+ // exist.
+ await IOUtils.makeDirectory(DATAREPORTING_PATH);
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_LOAD").clear();
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_PARSE").clear();
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").clear();
+
+ // Write test data to the session data file.
+ const dataFilePath = PathUtils.join(DATAREPORTING_PATH, "session-state.json");
+ const sessionState = {
+ sessionId: null,
+ subsessionId: null,
+ profileSubsessionCounter: 3785,
+ };
+ await IOUtils.writeJSON(dataFilePath, sessionState);
+
+ const PREF_TEST = "toolkit.telemetry.test.pref1";
+ Services.prefs.clearUserPref(PREF_TEST);
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_VALUE }],
+ ]);
+
+ // We expect one new subsession when starting TelemetrySession and one after triggering
+ // an environment change.
+ const expectedSubsessions = sessionState.profileSubsessionCounter + 2;
+ const expectedSessionUUID = "ff602e52-47a1-b7e8-4c1a-ffffffffc87a";
+ const expectedSubsessionUUID = "009fd1ad-b85e-4817-b3e5-000000003785";
+ fakeGenerateUUID(
+ () => expectedSessionUUID,
+ () => expectedSubsessionUUID
+ );
+
+ if (gIsAndroid) {
+ // We don't support subsessions yet on Android, so skip the next checks.
+ return;
+ }
+
+ // Start TelemetrySession so that it loads the session data file.
+ await TelemetryController.testReset();
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);
+
+ // Watch a test preference, trigger and environment change and wait for it to propagate.
+ // _watchPreferences triggers a subsession notification
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+ fakeNow(new Date(2050, 1, 1, 12, 0, 0));
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+ let changePromise = new Promise(resolve =>
+ TelemetryEnvironment.registerChangeListener("test_fake_change", resolve)
+ );
+ Services.prefs.setIntPref(PREF_TEST, 1);
+ await changePromise;
+ TelemetryEnvironment.unregisterChangeListener("test_fake_change");
+
+ let payload = TelemetrySession.getPayload();
+ Assert.equal(payload.info.profileSubsessionCounter, expectedSubsessions);
+ await TelemetryController.testShutdown();
+
+ // Restore the UUID generator so we don't mess with other tests.
+ fakeGenerateUUID(TelemetryUtils.generateUUID, TelemetryUtils.generateUUID);
+
+ // Load back the serialised session data.
+ let data = await IOUtils.readJSON(dataFilePath);
+ Assert.equal(data.profileSubsessionCounter, expectedSubsessions);
+ Assert.equal(data.sessionId, expectedSessionUUID);
+ Assert.equal(data.subsessionId, expectedSubsessionUUID);
+});
+
+add_task(async function test_sessionData_ShortSession() {
+ if (gIsAndroid) {
+ // We don't support subsessions yet on Android, so skip the next checks.
+ return;
+ }
+
+ const SESSION_STATE_PATH = PathUtils.join(
+ DATAREPORTING_PATH,
+ "session-state.json"
+ );
+
+ // Remove the session state file.
+ await IOUtils.remove(SESSION_STATE_PATH, { ignoreAbsent: true });
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_LOAD").clear();
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_PARSE").clear();
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").clear();
+
+ const expectedSessionUUID = "ff602e52-47a1-b7e8-4c1a-ffffffffc87a";
+ const expectedSubsessionUUID = "009fd1ad-b85e-4817-b3e5-000000003785";
+ fakeGenerateUUID(
+ () => expectedSessionUUID,
+ () => expectedSubsessionUUID
+ );
+
+ // We intentionally don't wait for the setup to complete and shut down to simulate
+ // short sessions. We expect the profile subsession counter to be 1.
+ TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+
+ Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);
+
+ // Restore the UUID generation functions.
+ fakeGenerateUUID(TelemetryUtils.generateUUID, TelemetryUtils.generateUUID);
+
+ // Start TelemetryController so that it loads the session data file. We expect the profile
+ // subsession counter to be incremented by 1 again.
+ await TelemetryController.testReset();
+
+ // We expect 2 profile subsession counter updates.
+ let payload = TelemetrySession.getPayload();
+ Assert.equal(payload.info.profileSubsessionCounter, 2);
+ Assert.equal(payload.info.previousSessionId, expectedSessionUUID);
+ Assert.equal(payload.info.previousSubsessionId, expectedSubsessionUUID);
+ Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_invalidSessionData() {
+ // Create the directory which will contain the data file, if it doesn't already
+ // exist.
+ await IOUtils.makeDirectory(DATAREPORTING_PATH);
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_LOAD").clear();
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_PARSE").clear();
+ getHistogram("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").clear();
+
+ // Write test data to the session data file. This should fail to parse.
+ const dataFilePath = PathUtils.join(DATAREPORTING_PATH, "session-state.json");
+ const unparseableData = "{asdf:@äü";
+ await IOUtils.writeUTF8(dataFilePath, unparseableData, {
+ tmpPath: `${dataFilePath}.tmp`,
+ });
+
+ // Start TelemetryController so that it loads the session data file.
+ await TelemetryController.testReset();
+
+ // The session data file should not load. Only expect the current subsession.
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
+ Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);
+
+ // Write test data to the session data file. This should fail validation.
+ const sessionState = {
+ profileSubsessionCounter: "not-a-number?",
+ someOtherField: 12,
+ };
+ await IOUtils.writeJSON(dataFilePath, sessionState);
+
+ // The session data file should not load. Only expect the current subsession.
+ const expectedSubsessions = 1;
+ const expectedSessionUUID = "ff602e52-47a1-b7e8-4c1a-ffffffffc87a";
+ const expectedSubsessionUUID = "009fd1ad-b85e-4817-b3e5-000000003785";
+ fakeGenerateUUID(
+ () => expectedSessionUUID,
+ () => expectedSubsessionUUID
+ );
+
+ // Start TelemetryController so that it loads the session data file.
+ await TelemetryController.testShutdown();
+ await TelemetryController.testReset();
+
+ let payload = TelemetrySession.getPayload();
+ Assert.equal(payload.info.profileSubsessionCounter, expectedSubsessions);
+ Assert.equal(0, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_LOAD").sum);
+ Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_PARSE").sum);
+ Assert.equal(1, getSnapshot("TELEMETRY_SESSIONDATA_FAILED_VALIDATION").sum);
+
+ await TelemetryController.testShutdown();
+
+ // Restore the UUID generator so we don't mess with other tests.
+ fakeGenerateUUID(TelemetryUtils.generateUUID, TelemetryUtils.generateUUID);
+
+ // Load back the serialised session data.
+ let data = await IOUtils.readJSON(dataFilePath);
+ Assert.equal(data.profileSubsessionCounter, expectedSubsessions);
+ Assert.equal(data.sessionId, expectedSessionUUID);
+ Assert.equal(data.subsessionId, expectedSubsessionUUID);
+});
+
+add_task(async function test_abortedSession() {
+ if (gIsAndroid) {
+ // We don't have the aborted session ping here.
+ return;
+ }
+
+ const ABORTED_FILE = PathUtils.join(
+ DATAREPORTING_PATH,
+ ABORTED_PING_FILE_NAME
+ );
+
+ // Make sure the aborted sessions directory does not exist to test its creation.
+ await IOUtils.remove(DATAREPORTING_PATH, {
+ ignoreAbsent: true,
+ recursive: true,
+ });
+
+ let schedulerTickCallback = null;
+ let now = new Date(2040, 1, 1, 0, 0, 0);
+ fakeNow(now);
+ // Fake scheduler functions to control aborted-session flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+
+ Assert.ok(
+ await IOUtils.exists(DATAREPORTING_PATH),
+ "Telemetry must create the aborted session directory when starting."
+ );
+
+ // Fake now again so that the scheduled aborted-session save takes place.
+ now = futureDate(now, ABORTED_SESSION_UPDATE_INTERVAL_MS);
+ fakeNow(now);
+ // The first aborted session checkpoint must take place right after the initialisation.
+ Assert.ok(!!schedulerTickCallback);
+ // Execute one scheduler tick.
+ await schedulerTickCallback();
+ // Check that the aborted session is due at the correct time.
+ Assert.ok(
+ await IOUtils.exists(ABORTED_FILE),
+ "There must be an aborted session ping."
+ );
+
+ // This ping is not yet in the pending pings folder, so we can't access it using
+ // TelemetryStorage.popPendingPings().
+ let abortedSessionPing = await IOUtils.readJSON(ABORTED_FILE);
+
+ // Validate the ping.
+ checkPingFormat(abortedSessionPing, PING_TYPE_MAIN, true, true);
+ Assert.equal(abortedSessionPing.payload.info.reason, REASON_ABORTED_SESSION);
+
+ // Trigger a another aborted-session ping and check that it overwrites the previous one.
+ now = futureDate(now, ABORTED_SESSION_UPDATE_INTERVAL_MS);
+ fakeNow(now);
+ await schedulerTickCallback();
+
+ let updatedAbortedSessionPing = await IOUtils.readJSON(ABORTED_FILE);
+ checkPingFormat(updatedAbortedSessionPing, PING_TYPE_MAIN, true, true);
+ Assert.equal(
+ updatedAbortedSessionPing.payload.info.reason,
+ REASON_ABORTED_SESSION
+ );
+ Assert.notEqual(abortedSessionPing.id, updatedAbortedSessionPing.id);
+ Assert.notEqual(
+ abortedSessionPing.creationDate,
+ updatedAbortedSessionPing.creationDate
+ );
+
+ await TelemetryController.testShutdown();
+ Assert.ok(
+ !(await IOUtils.exists(ABORTED_FILE)),
+ "No aborted session ping must be available after a shutdown."
+ );
+});
+
+add_task(async function test_abortedSession_Shutdown() {
+ if (gIsAndroid) {
+ // We don't have the aborted session ping here.
+ return;
+ }
+
+ const ABORTED_FILE = PathUtils.join(
+ DATAREPORTING_PATH,
+ ABORTED_PING_FILE_NAME
+ );
+
+ let schedulerTickCallback = null;
+ let now = fakeNow(2040, 1, 1, 0, 0, 0);
+ // Fake scheduler functions to control aborted-session flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+
+ Assert.ok(
+ await IOUtils.exists(DATAREPORTING_PATH),
+ "Telemetry must create the aborted session directory when starting."
+ );
+
+ // Fake now again so that the scheduled aborted-session save takes place.
+ fakeNow(futureDate(now, ABORTED_SESSION_UPDATE_INTERVAL_MS));
+ // The first aborted session checkpoint must take place right after the initialisation.
+ Assert.ok(!!schedulerTickCallback);
+ // Execute one scheduler tick.
+ await schedulerTickCallback();
+ // Check that the aborted session is due at the correct time.
+ Assert.ok(
+ await IOUtils.exists(ABORTED_FILE),
+ "There must be an aborted session ping."
+ );
+
+ // Remove the aborted session file and then shut down to make sure exceptions (e.g file
+ // not found) do not compromise the shutdown.
+ await IOUtils.remove(ABORTED_FILE);
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_abortedDailyCoalescing() {
+ if (gIsAndroid) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ const ABORTED_FILE = PathUtils.join(
+ DATAREPORTING_PATH,
+ ABORTED_PING_FILE_NAME
+ );
+
+ // Make sure the aborted sessions directory does not exist to test its creation.
+ await IOUtils.remove(DATAREPORTING_PATH, {
+ ignoreAbsent: true,
+ recursive: true,
+ });
+
+ let schedulerTickCallback = null;
+ PingServer.clearRequests();
+
+ let nowDate = new Date(2009, 10, 18, 0, 0, 0);
+ fakeNow(nowDate);
+
+ // Fake scheduler functions to control aborted-session flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+ await TelemetryController.testReset();
+
+ Assert.ok(
+ await IOUtils.exists(DATAREPORTING_PATH),
+ "Telemetry must create the aborted session directory when starting."
+ );
+
+ // Delay the callback around midnight so that the aborted-session ping gets merged with the
+ // daily ping.
+ let dailyDueDate = futureDate(nowDate, MS_IN_ONE_DAY);
+ fakeNow(dailyDueDate);
+ // Trigger both the daily ping and the saved-session.
+ Assert.ok(!!schedulerTickCallback);
+ // Execute one scheduler tick.
+ await schedulerTickCallback();
+
+ // Wait for the daily ping.
+ let dailyPing = await PingServer.promiseNextPing();
+ Assert.equal(dailyPing.payload.info.reason, REASON_DAILY);
+
+ // Check that an aborted session ping was also written to disk.
+ Assert.ok(
+ await IOUtils.exists(ABORTED_FILE),
+ "There must be an aborted session ping."
+ );
+
+ // Read aborted session ping and check that the session/subsession ids equal the
+ // ones in the daily ping.
+ let abortedSessionPing = await IOUtils.readJSON(ABORTED_FILE);
+ Assert.equal(
+ abortedSessionPing.payload.info.sessionId,
+ dailyPing.payload.info.sessionId
+ );
+ Assert.equal(
+ abortedSessionPing.payload.info.subsessionId,
+ dailyPing.payload.info.subsessionId
+ );
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_schedulerComputerSleep() {
+ if (gIsAndroid) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ const ABORTED_FILE = PathUtils.join(
+ DATAREPORTING_PATH,
+ ABORTED_PING_FILE_NAME
+ );
+
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+
+ // Remove any aborted-session ping from the previous tests.
+ await IOUtils.remove(DATAREPORTING_PATH, {
+ ignoreAbsent: true,
+ recursive: true,
+ });
+
+ // Set a fake current date and start Telemetry.
+ let nowDate = fakeNow(2009, 10, 18, 0, 0, 0);
+ let schedulerTickCallback = null;
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+
+ // Set the current time 3 days in the future at midnight, before running the callback.
+ nowDate = fakeNow(futureDate(nowDate, 3 * MS_IN_ONE_DAY));
+ Assert.ok(!!schedulerTickCallback);
+ // Execute one scheduler tick.
+ await schedulerTickCallback();
+
+ let dailyPing = await PingServer.promiseNextPing();
+ Assert.equal(
+ dailyPing.payload.info.reason,
+ REASON_DAILY,
+ "The wake notification should have triggered a daily ping."
+ );
+ Assert.equal(
+ dailyPing.creationDate,
+ nowDate.toISOString(),
+ "The daily ping date should be correct."
+ );
+
+ Assert.ok(
+ await IOUtils.exists(ABORTED_FILE),
+ "There must be an aborted session ping."
+ );
+
+ // Now also test if we are sending a daily ping if we wake up on the next
+ // day even when the timer doesn't trigger.
+ // This can happen due to timeouts not running out during sleep times,
+ // see bug 1262386, bug 1204823 et al.
+ // Note that we don't get wake notifications on Linux due to bug 758848.
+ nowDate = fakeNow(futureDate(nowDate, 1 * MS_IN_ONE_DAY));
+
+ // We emulate the mentioned timeout behavior by sending the wake notification
+ // instead of triggering the timeout callback.
+ // This should trigger a daily ping, because we passed midnight.
+ Services.obs.notifyObservers(null, "wake_notification");
+
+ dailyPing = await PingServer.promiseNextPing();
+ Assert.equal(
+ dailyPing.payload.info.reason,
+ REASON_DAILY,
+ "The wake notification should have triggered a daily ping."
+ );
+ Assert.equal(
+ dailyPing.creationDate,
+ nowDate.toISOString(),
+ "The daily ping date should be correct."
+ );
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_schedulerEnvironmentReschedules() {
+ if (gIsAndroid) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ // Reset the test preference.
+ const PREF_TEST = "toolkit.telemetry.test.pref1";
+ Services.prefs.clearUserPref(PREF_TEST);
+ const PREFS_TO_WATCH = new Map([
+ [PREF_TEST, { what: TelemetryEnvironment.RECORD_PREF_VALUE }],
+ ]);
+
+ await TelemetryController.testReset();
+ await TelemetryController.testShutdown();
+ await TelemetryStorage.testClearPendingPings();
+ // bug 1829855 - Sometimes the idle dispatch from a previous test interferes.
+ TelemetryScheduler.testReset();
+ PingServer.clearRequests();
+
+ // Set a fake current date and start Telemetry.
+ let nowDate = fakeNow(2060, 10, 18, 0, 0, 0);
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+ let schedulerTickCallback = null;
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+ await TelemetryEnvironment.testWatchPreferences(PREFS_TO_WATCH);
+
+ // Set the current time at midnight.
+ fakeNow(futureDate(nowDate, MS_IN_ONE_DAY));
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+
+ // Trigger the environment change.
+ Services.prefs.setIntPref(PREF_TEST, 1);
+
+ // Wait for the environment-changed ping.
+ let ping = await PingServer.promiseNextPing();
+ Assert.equal(ping.type, "main", `Expected 'main' ping on ${ping.id}`);
+ Assert.equal(
+ ping.payload.info.reason,
+ "environment-change",
+ `Expected 'environment-change' reason on ${ping.id}`
+ );
+
+ // We don't expect to receive any daily ping in this test, so assert if we do.
+ PingServer.registerPingHandler((req, res) => {
+ const receivedPing = decodeRequestPayload(req);
+ Assert.ok(
+ false,
+ `No ping should be received in this test (got ${receivedPing.id} type: ${receivedPing.type} reason: ${receivedPing.payload.info.reason}).`
+ );
+ });
+
+ // Execute one scheduler tick. It should not trigger a daily ping.
+ Assert.ok(!!schedulerTickCallback);
+ await schedulerTickCallback();
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_schedulerNothingDue() {
+ if (gIsAndroid) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ const ABORTED_FILE = PathUtils.join(
+ DATAREPORTING_PATH,
+ ABORTED_PING_FILE_NAME
+ );
+
+ // Remove any aborted-session ping from the previous tests.
+ await IOUtils.remove(DATAREPORTING_PATH, {
+ ignoreAbsent: true,
+ recursive: true,
+ });
+ await TelemetryStorage.testClearPendingPings();
+ await TelemetryController.testReset();
+
+ // We don't expect to receive any ping in this test, so assert if we do.
+ PingServer.registerPingHandler((req, res) => {
+ const receivedPing = decodeRequestPayload(req);
+ Assert.ok(
+ false,
+ `No ping should be received in this test (got ${receivedPing.id}).`
+ );
+ });
+
+ // Set a current date/time away from midnight, so that the daily ping doesn't get
+ // sent.
+ let nowDate = new Date(2009, 10, 18, 11, 0, 0);
+ fakeNow(nowDate);
+ let schedulerTickCallback = null;
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+
+ // Delay the callback execution to a time when no ping should be due.
+ let nothingDueDate = futureDate(
+ nowDate,
+ ABORTED_SESSION_UPDATE_INTERVAL_MS / 2
+ );
+ fakeNow(nothingDueDate);
+ Assert.ok(!!schedulerTickCallback);
+ // Execute one scheduler tick.
+ await schedulerTickCallback();
+
+ // Check that no aborted session ping was written to disk.
+ Assert.ok(!(await IOUtils.exists(ABORTED_FILE)));
+
+ await TelemetryController.testShutdown();
+ PingServer.resetPingHandler();
+});
+
+add_task(async function test_pingExtendedStats() {
+ const EXTENDED_PAYLOAD_FIELDS = [
+ "log",
+ "slowSQL",
+ "fileIOReports",
+ "lateWrites",
+ "addonDetails",
+ ];
+
+ // Reset telemetry and disable sending extended statistics.
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+ await TelemetryController.testReset();
+ Telemetry.canRecordExtended = false;
+
+ await sendPing();
+
+ let ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, PING_TYPE_MAIN, true, true);
+
+ // Check that the payload does not contain extended statistics fields.
+ for (let f in EXTENDED_PAYLOAD_FIELDS) {
+ Assert.ok(
+ !(EXTENDED_PAYLOAD_FIELDS[f] in ping.payload),
+ EXTENDED_PAYLOAD_FIELDS[f] +
+ " must not be in the payload if the extended set is off."
+ );
+ }
+
+ // We check this one separately so that we can reuse EXTENDED_PAYLOAD_FIELDS below, since
+ // slowSQLStartup might not be there.
+ Assert.ok(
+ !("slowSQLStartup" in ping.payload),
+ "slowSQLStartup must not be sent if the extended set is off"
+ );
+
+ Assert.ok(
+ !("addonManager" in ping.payload.simpleMeasurements),
+ "addonManager must not be sent if the extended set is off."
+ );
+ Assert.ok(
+ !("UITelemetry" in ping.payload.simpleMeasurements),
+ "UITelemetry must not be sent."
+ );
+
+ // Restore the preference.
+ Telemetry.canRecordExtended = true;
+
+ // Send a new ping that should contain the extended data.
+ await sendPing();
+ ping = await PingServer.promiseNextPing();
+ checkPingFormat(ping, PING_TYPE_MAIN, true, true);
+
+ // Check that the payload now contains extended statistics fields.
+ for (let f in EXTENDED_PAYLOAD_FIELDS) {
+ Assert.ok(
+ EXTENDED_PAYLOAD_FIELDS[f] in ping.payload,
+ EXTENDED_PAYLOAD_FIELDS[f] +
+ " must be in the payload if the extended set is on."
+ );
+ }
+
+ Assert.ok(
+ "addonManager" in ping.payload.simpleMeasurements,
+ "addonManager must be sent if the extended set is on."
+ );
+ Assert.ok(
+ !("UITelemetry" in ping.payload.simpleMeasurements),
+ "UITelemetry must not be sent."
+ );
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_schedulerUserIdle() {
+ if (gIsAndroid) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ const SCHEDULER_TICK_INTERVAL_MS = 5 * 60 * 1000;
+ const SCHEDULER_TICK_IDLE_INTERVAL_MS = 60 * 60 * 1000;
+
+ let now = new Date(2010, 1, 1, 11, 0, 0);
+ fakeNow(now);
+
+ let schedulerTimeout = 0;
+ fakeSchedulerTimer(
+ (callback, timeout) => {
+ schedulerTimeout = timeout;
+ },
+ () => {}
+ );
+ await TelemetryController.testReset();
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+
+ // When not idle, the scheduler should have a 5 minutes tick interval.
+ Assert.equal(schedulerTimeout, SCHEDULER_TICK_INTERVAL_MS);
+
+ // Send an "idle" notification to the scheduler.
+ fakeIdleNotification("idle");
+
+ // When idle, the scheduler should have a 1hr tick interval.
+ Assert.equal(schedulerTimeout, SCHEDULER_TICK_IDLE_INTERVAL_MS);
+
+ // Send an "active" notification to the scheduler.
+ await fakeIdleNotification("active");
+
+ // When user is back active, the scheduler tick should be 5 minutes again.
+ Assert.equal(schedulerTimeout, SCHEDULER_TICK_INTERVAL_MS);
+
+ // We should not miss midnight when going to idle.
+ now.setHours(23);
+ now.setMinutes(50);
+ fakeNow(now);
+ fakeIdleNotification("idle");
+ Assert.equal(schedulerTimeout, 10 * 60 * 1000);
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_DailyDueAndIdle() {
+ if (gIsAndroid) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+
+ let receivedPingRequest = null;
+ // Register a ping handler that will assert when receiving multiple daily pings.
+ PingServer.registerPingHandler(req => {
+ Assert.ok(!receivedPingRequest, "Telemetry must only send one daily ping.");
+ receivedPingRequest = req;
+ });
+
+ // Faking scheduler timer has to happen before resetting TelemetryController
+ // to be effective.
+ let schedulerTickCallback = null;
+ let now = new Date(2030, 1, 1, 0, 0, 0);
+ fakeNow(now);
+ // Fake scheduler functions to control daily collection flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+
+ // Trigger the daily ping.
+ let firstDailyDue = new Date(2030, 1, 2, 0, 0, 0);
+ fakeNow(firstDailyDue);
+
+ // Run a scheduler tick: it should trigger the daily ping.
+ Assert.ok(!!schedulerTickCallback);
+ let tickPromise = schedulerTickCallback();
+
+ // Send an idle and then an active user notification.
+ fakeIdleNotification("idle");
+ fakeIdleNotification("active");
+
+ // Wait on the tick promise.
+ await tickPromise;
+
+ await TelemetrySend.testWaitOnOutgoingPings();
+
+ // Decode the ping contained in the request and check that's a daily ping.
+ Assert.ok(receivedPingRequest, "Telemetry must send one daily ping.");
+ const receivedPing = decodeRequestPayload(receivedPingRequest);
+ checkPingFormat(receivedPing, PING_TYPE_MAIN, true, true);
+ Assert.equal(receivedPing.payload.info.reason, REASON_DAILY);
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_userIdleAndSchedlerTick() {
+ if (gIsAndroid) {
+ // We don't have the aborted session or the daily ping here.
+ return;
+ }
+
+ let receivedPingRequest = null;
+ // Register a ping handler that will assert when receiving multiple daily pings.
+ PingServer.registerPingHandler(req => {
+ Assert.ok(!receivedPingRequest, "Telemetry must only send one daily ping.");
+ receivedPingRequest = req;
+ });
+
+ let schedulerTickCallback = null;
+ let now = new Date(2030, 1, 1, 0, 0, 0);
+ fakeNow(now);
+ // Fake scheduler functions to control daily collection flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryStorage.testClearPendingPings();
+ await TelemetryController.testReset();
+ PingServer.clearRequests();
+
+ // Move the current date/time to midnight.
+ let firstDailyDue = new Date(2030, 1, 2, 0, 0, 0);
+ fakeNow(firstDailyDue);
+
+ // The active notification should trigger a scheduler tick. The latter will send the
+ // due daily ping.
+ fakeIdleNotification("active");
+
+ // Immediately running another tick should not send a daily ping again.
+ Assert.ok(!!schedulerTickCallback);
+ await schedulerTickCallback();
+
+ // A new "idle" notification should not send a new daily ping.
+ fakeIdleNotification("idle");
+
+ await TelemetrySend.testWaitOnOutgoingPings();
+
+ // Decode the ping contained in the request and check that's a daily ping.
+ Assert.ok(receivedPingRequest, "Telemetry must send one daily ping.");
+ const receivedPing = decodeRequestPayload(receivedPingRequest);
+ checkPingFormat(receivedPing, PING_TYPE_MAIN, true, true);
+ Assert.equal(receivedPing.payload.info.reason, REASON_DAILY);
+
+ PingServer.resetPingHandler();
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function test_changeThrottling() {
+ if (gIsAndroid) {
+ // We don't support subsessions yet on Android.
+ return;
+ }
+
+ let getSubsessionCount = () => {
+ return TelemetrySession.getPayload().info.subsessionCounter;
+ };
+
+ let now = fakeNow(2050, 1, 2, 0, 0, 0);
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 10 * MILLISECONDS_PER_MINUTE
+ );
+ await TelemetryController.testReset();
+ Assert.equal(getSubsessionCount(), 1);
+
+ // The first pref change should not trigger a notification.
+ TelemetrySession.testOnEnvironmentChange("test", {});
+ Assert.equal(getSubsessionCount(), 1);
+
+ // We should get a change notification after the 5min throttling interval.
+ fakeNow(futureDate(now, 5 * MILLISECONDS_PER_MINUTE + 1));
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 5 * MILLISECONDS_PER_MINUTE + 1
+ );
+ TelemetrySession.testOnEnvironmentChange("test", {});
+ Assert.equal(getSubsessionCount(), 2);
+
+ // After that, changes should be throttled again.
+ now = fakeNow(futureDate(now, 1 * MILLISECONDS_PER_MINUTE));
+ gMonotonicNow = fakeMonotonicNow(gMonotonicNow + 1 * MILLISECONDS_PER_MINUTE);
+ TelemetrySession.testOnEnvironmentChange("test", {});
+ Assert.equal(getSubsessionCount(), 2);
+
+ // ... for 5min.
+ now = fakeNow(futureDate(now, 4 * MILLISECONDS_PER_MINUTE + 1));
+ gMonotonicNow = fakeMonotonicNow(
+ gMonotonicNow + 4 * MILLISECONDS_PER_MINUTE + 1
+ );
+ TelemetrySession.testOnEnvironmentChange("test", {});
+ Assert.equal(getSubsessionCount(), 3);
+});
+
+add_task(async function stopServer() {
+ // It is important to shut down the TelemetryController first as, due to test
+ // environment changes, failure to upload pings here during shutdown results
+ // in an infinite loop of send failures and retries.
+ await TelemetryController.testShutdown();
+ await PingServer.stop();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetrySession_abortedSessionQueued.js b/toolkit/components/telemetry/tests/unit/test_TelemetrySession_abortedSessionQueued.js
new file mode 100644
index 0000000000..80807469fc
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession_abortedSessionQueued.js
@@ -0,0 +1,197 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+/**
+ * This file only contains the |test_abortedSessionQueued| test. This needs
+ * to be in a separate, stand-alone file since we're initializing Telemetry
+ * twice, in a non-standard way to simulate incorrect shutdowns. Doing this
+ * in other files might interfere with the other tests.
+ */
+
+const { TelemetryStorage } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryStorage.sys.mjs"
+);
+
+const DATAREPORTING_DIR = "datareporting";
+const ABORTED_PING_FILE_NAME = "aborted-session-ping";
+const ABORTED_SESSION_UPDATE_INTERVAL_MS = 5 * 60 * 1000;
+
+const PING_TYPE_MAIN = "main";
+const REASON_ABORTED_SESSION = "aborted-session";
+const TEST_PING_TYPE = "test-ping-type";
+
+ChromeUtils.defineLazyGetter(this, "DATAREPORTING_PATH", function () {
+ return PathUtils.join(PathUtils.profileDir, DATAREPORTING_DIR);
+});
+
+function sendPing() {
+ if (PingServer.started) {
+ TelemetrySend.setServer("http://localhost:" + PingServer.port);
+ } else {
+ TelemetrySend.setServer("http://doesnotexist");
+ }
+
+ let options = {
+ addClientId: true,
+ addEnvironment: true,
+ };
+ return TelemetryController.submitExternalPing(TEST_PING_TYPE, {}, options);
+}
+
+add_task(async function test_setup() {
+ do_get_profile();
+ PingServer.start();
+ Services.prefs.setCharPref(
+ TelemetryUtils.Preferences.Server,
+ "http://localhost:" + PingServer.port
+ );
+});
+
+add_task(async function test_abortedSessionQueued() {
+ const ABORTED_FILE = PathUtils.join(
+ DATAREPORTING_PATH,
+ ABORTED_PING_FILE_NAME
+ );
+
+ // Make sure the aborted sessions directory does not exist to test its creation.
+ await IOUtils.remove(DATAREPORTING_PATH, {
+ ignoreAbsent: true,
+ recursive: true,
+ });
+
+ let schedulerTickCallback = null;
+ let now = new Date(2040, 1, 1, 0, 0, 0);
+ fakeNow(now);
+ // Fake scheduler functions to control aborted-session flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+
+ Assert.ok(
+ await IOUtils.exists(DATAREPORTING_PATH),
+ "Telemetry must create the aborted session directory when starting."
+ );
+
+ // Fake now again so that the scheduled aborted-session save takes place.
+ now = futureDate(now, ABORTED_SESSION_UPDATE_INTERVAL_MS);
+ fakeNow(now);
+ // The first aborted session checkpoint must take place right after the initialisation.
+ Assert.ok(!!schedulerTickCallback);
+ // Execute one scheduler tick.
+ await schedulerTickCallback();
+ // Check that the aborted session is due at the correct time.
+ Assert.ok(
+ await IOUtils.exists(ABORTED_FILE),
+ "There must be an aborted session ping."
+ );
+
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+ await TelemetryController.testReset();
+
+ Assert.ok(
+ !(await IOUtils.exists(ABORTED_FILE)),
+ "The aborted session ping must be removed from the aborted session ping directory."
+ );
+
+ // Restarting Telemetry again to trigger sending pings in TelemetrySend.
+ await TelemetryController.testReset();
+
+ // We should have received an aborted-session ping.
+ const receivedPing = await PingServer.promiseNextPing();
+ Assert.equal(
+ receivedPing.type,
+ PING_TYPE_MAIN,
+ "Should have the correct type"
+ );
+ Assert.equal(
+ receivedPing.payload.info.reason,
+ REASON_ABORTED_SESSION,
+ "Ping should have the correct reason"
+ );
+
+ await TelemetryController.testShutdown();
+});
+
+/*
+ * An aborted-session ping might have been written when Telemetry upload was disabled and
+ * the profile had a canary client ID.
+ * These pings should not be sent out at a later point when Telemetry is enabled again.
+ */
+add_task(async function test_abortedSession_canary_clientid() {
+ const ABORTED_FILE = PathUtils.join(
+ DATAREPORTING_PATH,
+ ABORTED_PING_FILE_NAME
+ );
+
+ // Make sure the aborted sessions directory does not exist to test its creation.
+ await IOUtils.remove(DATAREPORTING_PATH, {
+ ignoreAbsent: true,
+ recursive: true,
+ });
+
+ let schedulerTickCallback = null;
+ let now = new Date(2040, 1, 1, 0, 0, 0);
+ fakeNow(now);
+ // Fake scheduler functions to control aborted-session flow in tests.
+ fakeSchedulerTimer(
+ callback => (schedulerTickCallback = callback),
+ () => {}
+ );
+ await TelemetryController.testReset();
+
+ Assert.ok(
+ await IOUtils.exists(DATAREPORTING_PATH),
+ "Telemetry must create the aborted session directory when starting."
+ );
+
+ // Fake now again so that the scheduled aborted-session save takes place.
+ now = futureDate(now, ABORTED_SESSION_UPDATE_INTERVAL_MS);
+ fakeNow(now);
+ // The first aborted session checkpoint must take place right after the initialisation.
+ Assert.ok(!!schedulerTickCallback);
+ // Execute one scheduler tick.
+ await schedulerTickCallback();
+ // Check that the aborted session is due at the correct time.
+ Assert.ok(
+ await IOUtils.exists(ABORTED_FILE),
+ "There must be an aborted session ping."
+ );
+
+ // Set clientID in aborted-session ping to canary value
+ let abortedPing = await IOUtils.readJSON(ABORTED_FILE);
+ abortedPing.clientId = TelemetryUtils.knownClientID;
+ await IOUtils.writeJSON(ABORTED_FILE, abortedPing);
+
+ await TelemetryStorage.testClearPendingPings();
+ PingServer.clearRequests();
+ await TelemetryController.testReset();
+
+ Assert.ok(
+ !(await IOUtils.exists(ABORTED_FILE)),
+ "The aborted session ping must be removed from the aborted session ping directory."
+ );
+
+ // Restarting Telemetry again to trigger sending pings in TelemetrySend.
+ await TelemetryController.testReset();
+
+ // Trigger a test ping, so we can verify the server received something.
+ sendPing();
+
+ // We should have received an aborted-session ping.
+ const receivedPing = await PingServer.promiseNextPing();
+ Assert.equal(
+ receivedPing.type,
+ TEST_PING_TYPE,
+ "Should have received test ping"
+ );
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(async function stopServer() {
+ await PingServer.stop();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetrySession_activeTicks.js b/toolkit/components/telemetry/tests/unit/test_TelemetrySession_activeTicks.js
new file mode 100644
index 0000000000..23e476a028
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetrySession_activeTicks.js
@@ -0,0 +1,126 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { TelemetrySession } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySession.sys.mjs"
+);
+
+function tick(aHowMany) {
+ for (let i = 0; i < aHowMany; i++) {
+ Services.obs.notifyObservers(null, "user-interaction-active");
+ }
+}
+
+function checkSessionTicks(aExpected) {
+ let payload = TelemetrySession.getPayload();
+ Assert.equal(
+ payload.simpleMeasurements.activeTicks,
+ aExpected,
+ "Should record the expected number of active ticks for the session."
+ );
+}
+
+function checkSubsessionTicks(aExpected, aClearSubsession) {
+ let payload = TelemetrySession.getPayload("main", aClearSubsession);
+ Assert.equal(
+ payload.simpleMeasurements.activeTicks,
+ aExpected,
+ "Should record the expected number of active ticks for the subsession."
+ );
+ if (aExpected > 0) {
+ Assert.equal(
+ payload.processes.parent.scalars["browser.engagement.active_ticks"],
+ aExpected,
+ "Should record the expected number of active ticks for the subsession, in a scalar."
+ );
+ }
+}
+
+add_task(async function test_setup() {
+ do_get_profile();
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+ // Ensure FOG's init
+ Services.fog.initializeFOG();
+});
+
+add_task(async function test_record_activeTicks() {
+ await TelemetryController.testSetup();
+
+ let checkActiveTicks = expected => {
+ // Scalars are only present in subsession payloads.
+ let payload = TelemetrySession.getPayload("main");
+ Assert.equal(
+ payload.simpleMeasurements.activeTicks,
+ expected,
+ "TelemetrySession must record the expected number of active ticks (in simpleMeasurements)."
+ );
+ // Subsessions are not yet supported on Android.
+ if (!gIsAndroid) {
+ Assert.equal(
+ payload.processes.parent.scalars["browser.engagement.active_ticks"],
+ expected,
+ "TelemetrySession must record the expected number of active ticks (in scalars)."
+ );
+ }
+ Assert.equal(Glean.browserEngagement.activeTicks.testGetValue(), expected);
+ };
+
+ for (let i = 0; i < 3; i++) {
+ Services.obs.notifyObservers(null, "user-interaction-active");
+ }
+ checkActiveTicks(3);
+
+ // Now send inactive. This must not increment the active ticks.
+ Services.obs.notifyObservers(null, "user-interaction-inactive");
+ checkActiveTicks(3);
+
+ // If we send active again, this should be counted as inactive.
+ Services.obs.notifyObservers(null, "user-interaction-active");
+ checkActiveTicks(3);
+
+ // If we send active again, this should be counted as active.
+ Services.obs.notifyObservers(null, "user-interaction-active");
+ checkActiveTicks(4);
+
+ Services.obs.notifyObservers(null, "user-interaction-active");
+ checkActiveTicks(5);
+
+ await TelemetryController.testShutdown();
+});
+
+add_task(
+ {
+ skip_if: () => gIsAndroid,
+ },
+ async function test_subsession_activeTicks() {
+ await TelemetryController.testReset();
+ Telemetry.clearScalars();
+
+ tick(5);
+ checkSessionTicks(5);
+ checkSubsessionTicks(5, true);
+
+ // After clearing the subsession, subsession ticks should be 0 but session
+ // ticks should still be 5.
+ checkSubsessionTicks(0);
+ checkSessionTicks(5);
+
+ tick(1);
+ checkSessionTicks(6);
+ checkSubsessionTicks(1, true);
+
+ checkSubsessionTicks(0);
+ checkSessionTicks(6);
+
+ tick(2);
+ checkSessionTicks(8);
+ checkSubsessionTicks(2);
+
+ await TelemetryController.testShutdown();
+ }
+);
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryStopwatch.js b/toolkit/components/telemetry/tests/unit/test_TelemetryStopwatch.js
new file mode 100644
index 0000000000..d9e5e08625
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryStopwatch.js
@@ -0,0 +1,196 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const HIST_NAME = "TELEMETRY_SEND_SUCCESS";
+const HIST_NAME2 = "RANGE_CHECKSUM_ERRORS";
+const KEYED_HIST = { id: "TELEMETRY_INVALID_PING_TYPE_SUBMITTED", key: "TEST" };
+
+var refObj = {},
+ refObj2 = {};
+
+var originalCount1, originalCount2, originalCount3;
+
+function run_test() {
+ let histogram = Telemetry.getHistogramById(HIST_NAME);
+ let snapshot = histogram.snapshot();
+ originalCount1 = Object.values(snapshot.values).reduce((a, b) => (a += b), 0);
+
+ histogram = Telemetry.getHistogramById(HIST_NAME2);
+ snapshot = histogram.snapshot();
+ originalCount2 = Object.values(snapshot.values).reduce((a, b) => (a += b), 0);
+
+ histogram = Telemetry.getKeyedHistogramById(KEYED_HIST.id);
+ snapshot = histogram.snapshot()[KEYED_HIST.key] || { values: [] };
+ originalCount3 = Object.values(snapshot.values).reduce((a, b) => (a += b), 0);
+
+ Assert.ok(TelemetryStopwatch.start("mark1"));
+ Assert.ok(TelemetryStopwatch.start("mark2"));
+
+ Assert.ok(TelemetryStopwatch.start("mark1", refObj));
+ Assert.ok(TelemetryStopwatch.start("mark2", refObj));
+
+ // Same timer can't be re-started before being stopped
+ Assert.ok(!TelemetryStopwatch.start("mark1"));
+ Assert.ok(!TelemetryStopwatch.start("mark1", refObj));
+
+ // Can't stop a timer that was accidentaly started twice
+ Assert.ok(!TelemetryStopwatch.finish("mark1"));
+ Assert.ok(!TelemetryStopwatch.finish("mark1", refObj));
+
+ Assert.ok(TelemetryStopwatch.start("NON-EXISTENT_HISTOGRAM"));
+ Assert.ok(!TelemetryStopwatch.finish("NON-EXISTENT_HISTOGRAM"));
+
+ Assert.ok(TelemetryStopwatch.start("NON-EXISTENT_HISTOGRAM", refObj));
+ Assert.ok(!TelemetryStopwatch.finish("NON-EXISTENT_HISTOGRAM", refObj));
+
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME));
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME2));
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME, refObj));
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME2, refObj));
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME, refObj2));
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME2, refObj2));
+
+ Assert.ok(TelemetryStopwatch.start(HIST_NAME));
+ Assert.ok(TelemetryStopwatch.start(HIST_NAME2));
+ Assert.ok(TelemetryStopwatch.start(HIST_NAME, refObj));
+ Assert.ok(TelemetryStopwatch.start(HIST_NAME2, refObj));
+ Assert.ok(TelemetryStopwatch.start(HIST_NAME, refObj2));
+ Assert.ok(TelemetryStopwatch.start(HIST_NAME2, refObj2));
+
+ Assert.ok(TelemetryStopwatch.running(HIST_NAME));
+ Assert.ok(TelemetryStopwatch.running(HIST_NAME));
+ Assert.ok(TelemetryStopwatch.running(HIST_NAME2));
+ Assert.ok(TelemetryStopwatch.running(HIST_NAME, refObj));
+ Assert.ok(TelemetryStopwatch.running(HIST_NAME2, refObj));
+ Assert.ok(TelemetryStopwatch.running(HIST_NAME, refObj2));
+ Assert.ok(TelemetryStopwatch.running(HIST_NAME2, refObj2));
+
+ Assert.ok(TelemetryStopwatch.finish(HIST_NAME));
+ Assert.ok(TelemetryStopwatch.finish(HIST_NAME2));
+ Assert.ok(TelemetryStopwatch.finish(HIST_NAME, refObj));
+ Assert.ok(TelemetryStopwatch.finish(HIST_NAME2, refObj));
+ Assert.ok(TelemetryStopwatch.finish(HIST_NAME, refObj2));
+ Assert.ok(TelemetryStopwatch.finish(HIST_NAME2, refObj2));
+
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME));
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME));
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME2));
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME, refObj));
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME2, refObj));
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME, refObj2));
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME2, refObj2));
+
+ // Verify that TS.finish deleted the timers
+ Assert.ok(!TelemetryStopwatch.finish(HIST_NAME));
+ Assert.ok(!TelemetryStopwatch.finish(HIST_NAME, refObj));
+
+ // Verify that they can be used again
+ Assert.ok(TelemetryStopwatch.start(HIST_NAME));
+ Assert.ok(TelemetryStopwatch.start(HIST_NAME, refObj));
+ Assert.ok(TelemetryStopwatch.finish(HIST_NAME));
+ Assert.ok(TelemetryStopwatch.finish(HIST_NAME, refObj));
+
+ Assert.ok(!TelemetryStopwatch.finish("unknown-mark")); // Unknown marker
+ Assert.ok(!TelemetryStopwatch.finish("unknown-mark", {})); // Unknown object
+ Assert.ok(!TelemetryStopwatch.finish(HIST_NAME, {})); // Known mark on unknown object
+
+ // Test cancel
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME));
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME, refObj));
+ Assert.ok(TelemetryStopwatch.start(HIST_NAME));
+ Assert.ok(TelemetryStopwatch.start(HIST_NAME, refObj));
+ Assert.ok(TelemetryStopwatch.running(HIST_NAME));
+ Assert.ok(TelemetryStopwatch.running(HIST_NAME, refObj));
+ Assert.ok(TelemetryStopwatch.cancel(HIST_NAME));
+ Assert.ok(TelemetryStopwatch.cancel(HIST_NAME, refObj));
+
+ // Verify that can not cancel twice
+ Assert.ok(!TelemetryStopwatch.cancel(HIST_NAME));
+ Assert.ok(!TelemetryStopwatch.cancel(HIST_NAME, refObj));
+
+ // Verify that cancel removes the timers
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME));
+ Assert.ok(!TelemetryStopwatch.running(HIST_NAME, refObj));
+ Assert.ok(!TelemetryStopwatch.finish(HIST_NAME));
+ Assert.ok(!TelemetryStopwatch.finish(HIST_NAME, refObj));
+
+ // Verify that keyed histograms can be started.
+ Assert.ok(!TelemetryStopwatch.runningKeyed("HISTOGRAM", "KEY1"));
+ Assert.ok(!TelemetryStopwatch.runningKeyed("HISTOGRAM", "KEY2"));
+ Assert.ok(!TelemetryStopwatch.runningKeyed("HISTOGRAM", "KEY1", refObj));
+ Assert.ok(!TelemetryStopwatch.runningKeyed("HISTOGRAM", "KEY2", refObj));
+
+ Assert.ok(TelemetryStopwatch.startKeyed("HISTOGRAM", "KEY1"));
+ Assert.ok(TelemetryStopwatch.startKeyed("HISTOGRAM", "KEY2"));
+ Assert.ok(TelemetryStopwatch.startKeyed("HISTOGRAM", "KEY1", refObj));
+ Assert.ok(TelemetryStopwatch.startKeyed("HISTOGRAM", "KEY2", refObj));
+
+ Assert.ok(TelemetryStopwatch.runningKeyed("HISTOGRAM", "KEY1"));
+ Assert.ok(TelemetryStopwatch.runningKeyed("HISTOGRAM", "KEY2"));
+ Assert.ok(TelemetryStopwatch.runningKeyed("HISTOGRAM", "KEY1", refObj));
+ Assert.ok(TelemetryStopwatch.runningKeyed("HISTOGRAM", "KEY2", refObj));
+
+ // Restarting keyed histograms should fail.
+ Assert.ok(!TelemetryStopwatch.startKeyed("HISTOGRAM", "KEY1"));
+ Assert.ok(!TelemetryStopwatch.startKeyed("HISTOGRAM", "KEY1", refObj));
+
+ // Finishing a stopwatch of a non existing histogram should return false.
+ Assert.ok(!TelemetryStopwatch.finishKeyed("HISTOGRAM", "KEY2"));
+ Assert.ok(!TelemetryStopwatch.finishKeyed("HISTOGRAM", "KEY2", refObj));
+
+ // Starting & finishing a keyed stopwatch for an existing histogram should work.
+ Assert.ok(TelemetryStopwatch.startKeyed(KEYED_HIST.id, KEYED_HIST.key));
+ Assert.ok(TelemetryStopwatch.finishKeyed(KEYED_HIST.id, KEYED_HIST.key));
+ // Verify that TS.finish deleted the timers
+ Assert.ok(!TelemetryStopwatch.runningKeyed(KEYED_HIST.id, KEYED_HIST.key));
+
+ // Verify that they can be used again
+ Assert.ok(TelemetryStopwatch.startKeyed(KEYED_HIST.id, KEYED_HIST.key));
+ Assert.ok(TelemetryStopwatch.finishKeyed(KEYED_HIST.id, KEYED_HIST.key));
+
+ Assert.ok(!TelemetryStopwatch.finishKeyed("unknown-mark", "unknown-key"));
+ Assert.ok(!TelemetryStopwatch.finishKeyed(KEYED_HIST.id, "unknown-key"));
+
+ // Verify that keyed histograms can only be canceled through "keyed" API.
+ Assert.ok(TelemetryStopwatch.startKeyed(KEYED_HIST.id, KEYED_HIST.key));
+ Assert.throws(
+ () => TelemetryStopwatch.cancel(KEYED_HIST.id, KEYED_HIST.key),
+ /is not an object/
+ );
+ Assert.ok(TelemetryStopwatch.cancelKeyed(KEYED_HIST.id, KEYED_HIST.key));
+ Assert.ok(!TelemetryStopwatch.cancelKeyed(KEYED_HIST.id, KEYED_HIST.key));
+
+ finishTest();
+}
+
+function finishTest() {
+ let histogram = Telemetry.getHistogramById(HIST_NAME);
+ let snapshot = histogram.snapshot();
+ let newCount = Object.values(snapshot.values).reduce((a, b) => (a += b), 0);
+
+ Assert.equal(
+ newCount - originalCount1,
+ 5,
+ "The correct number of histograms were added for histogram 1."
+ );
+
+ histogram = Telemetry.getHistogramById(HIST_NAME2);
+ snapshot = histogram.snapshot();
+ newCount = Object.values(snapshot.values).reduce((a, b) => (a += b), 0);
+
+ Assert.equal(
+ newCount - originalCount2,
+ 3,
+ "The correct number of histograms were added for histogram 2."
+ );
+
+ histogram = Telemetry.getKeyedHistogramById(KEYED_HIST.id);
+ snapshot = histogram.snapshot()[KEYED_HIST.key];
+ newCount = Object.values(snapshot.values).reduce((a, b) => (a += b), 0);
+
+ Assert.equal(
+ newCount - originalCount3,
+ 2,
+ "The correct number of histograms were added for histogram 3."
+ );
+}
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryTimestamps.js b/toolkit/components/telemetry/tests/unit/test_TelemetryTimestamps.js
new file mode 100644
index 0000000000..37524fbb91
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryTimestamps.js
@@ -0,0 +1,79 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { TelemetrySession } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetrySession.sys.mjs"
+);
+
+// The @mozilla/xre/app-info;1 XPCOM object provided by the xpcshell test harness doesn't
+// implement the nsIXULAppInfo interface, which is needed by Services and
+// TelemetrySession.sys.mjs. updateAppInfo() creates and registers a minimal mock app-info.
+const { updateAppInfo } = ChromeUtils.importESModule(
+ "resource://testing-common/AppInfo.sys.mjs"
+);
+updateAppInfo();
+
+function getSimpleMeasurementsFromTelemetryController() {
+ return TelemetrySession.getPayload().simpleMeasurements;
+}
+
+add_task(async function test_setup() {
+ // Telemetry needs the AddonManager.
+ await loadAddonManager();
+ finishAddonManagerStartup();
+ fakeIntlReady();
+ // Make profile available for |TelemetryController.testShutdown()|.
+ do_get_profile();
+
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+
+ await new Promise(resolve =>
+ Services.telemetry.asyncFetchTelemetryData(resolve)
+ );
+});
+
+add_task(async function actualTest() {
+ await TelemetryController.testSetup();
+
+ // Test the module logic
+ let { TelemetryTimestamps } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryTimestamps.sys.mjs"
+ );
+ let now = Date.now();
+ TelemetryTimestamps.add("foo");
+ Assert.ok(TelemetryTimestamps.get().foo != null); // foo was added
+ Assert.ok(TelemetryTimestamps.get().foo >= now); // foo has a reasonable value
+
+ // Add timestamp with value
+ // Use a value far in the future since TelemetryController substracts the time of
+ // process initialization.
+ const YEAR_4000_IN_MS = 64060588800000;
+ TelemetryTimestamps.add("bar", YEAR_4000_IN_MS);
+ Assert.equal(TelemetryTimestamps.get().bar, YEAR_4000_IN_MS); // bar has the right value
+
+ // Can't add the same timestamp twice
+ TelemetryTimestamps.add("bar", 2);
+ Assert.equal(TelemetryTimestamps.get().bar, YEAR_4000_IN_MS); // bar wasn't overwritten
+
+ let threw = false;
+ try {
+ TelemetryTimestamps.add("baz", "this isn't a number");
+ } catch (ex) {
+ threw = true;
+ }
+ Assert.ok(threw); // adding non-number threw
+ Assert.equal(null, TelemetryTimestamps.get().baz); // no baz was added
+
+ // Test that the data gets added to the telemetry ping properly
+ let simpleMeasurements = getSimpleMeasurementsFromTelemetryController();
+ Assert.ok(simpleMeasurements != null); // got simple measurements from ping data
+ Assert.ok(simpleMeasurements.foo > 1); // foo was included
+ Assert.ok(simpleMeasurements.bar > 1); // bar was included
+ Assert.equal(undefined, simpleMeasurements.baz); // baz wasn't included since it wasn't added
+
+ await TelemetryController.testShutdown();
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_TelemetryUtils.js b/toolkit/components/telemetry/tests/unit/test_TelemetryUtils.js
new file mode 100644
index 0000000000..6ac7944a0a
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryUtils.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { TelemetryUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryUtils.sys.mjs"
+);
+const { UpdateUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/UpdateUtils.sys.mjs"
+);
+
+add_task(async function testUpdateChannelOverride() {
+ if (
+ Services.prefs.prefHasDefaultValue(
+ TelemetryUtils.Preferences.OverrideUpdateChannel
+ ) ||
+ Services.prefs.prefHasUserValue(
+ TelemetryUtils.Preferences.OverrideUpdateChannel
+ )
+ ) {
+ // If the pref is already set at this point, the test is running in a build
+ // that makes use of the override pref. For testing purposes, unset the pref.
+ Services.prefs.setStringPref(
+ TelemetryUtils.Preferences.OverrideUpdateChannel,
+ ""
+ );
+ }
+
+ // Check that we return the same channel as UpdateUtils, by default
+ Assert.equal(
+ TelemetryUtils.getUpdateChannel(),
+ UpdateUtils.getUpdateChannel(false),
+ "The telemetry reported channel must match the one from UpdateChannel, by default."
+ );
+
+ // Now set the override pref and check that we return the correct channel
+ const OVERRIDE_TEST_CHANNEL = "nightly-test";
+ Services.prefs.setStringPref(
+ TelemetryUtils.Preferences.OverrideUpdateChannel,
+ OVERRIDE_TEST_CHANNEL
+ );
+ Assert.equal(
+ TelemetryUtils.getUpdateChannel(),
+ OVERRIDE_TEST_CHANNEL,
+ "The telemetry reported channel must match the override pref when pref is set."
+ );
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_ThirdPartyModulesPing.js b/toolkit/components/telemetry/tests/unit/test_ThirdPartyModulesPing.js
new file mode 100644
index 0000000000..6e07975687
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_ThirdPartyModulesPing.js
@@ -0,0 +1,282 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { ctypes } = ChromeUtils.importESModule(
+ "resource://gre/modules/ctypes.sys.mjs"
+);
+const { setTimeout } = ChromeUtils.importESModule(
+ "resource://gre/modules/Timer.sys.mjs"
+);
+
+const kDllName = "modules-test.dll";
+
+let gCurrentPidStr;
+
+async function load_and_free(name) {
+ // Dynamically load a DLL which we have hard-coded as untrusted; this should
+ // appear in the payload.
+ let dllHandle = ctypes.open(do_get_file(name).path);
+ if (dllHandle) {
+ dllHandle.close();
+ dllHandle = null;
+ }
+ // Give the thread some cycles to process a loading event.
+ // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
+ await new Promise(resolve => setTimeout(resolve, 50));
+}
+
+add_task(async function setup() {
+ do_get_profile();
+
+ // Dynamically load a DLL which we have hard-coded as untrusted; this should
+ // appear in the payload.
+ await load_and_free(kDllName);
+
+ // Force the timer to fire (using a small interval).
+ Cc["@mozilla.org/updates/timer-manager;1"]
+ .getService(Ci.nsIObserver)
+ .observe(null, "utm-test-init", "");
+ Services.prefs.setIntPref(
+ "toolkit.telemetry.untrustedModulesPing.frequency",
+ 0
+ );
+ Services.prefs.setStringPref("app.update.url", "http://localhost");
+
+ let currentPid = Services.appinfo.processID;
+ gCurrentPidStr = "browser.0x" + currentPid.toString(16);
+
+ // Start the local ping server and setup Telemetry to use it during the tests.
+ PingServer.start();
+ Services.prefs.setStringPref(
+ TelemetryUtils.Preferences.Server,
+ "http://localhost:" + PingServer.port
+ );
+
+ return TelemetryController.testSetup();
+});
+
+registerCleanupFunction(function () {
+ return PingServer.stop();
+});
+
+// This tests basic end-to-end functionality of the untrusted modules
+// telemetry ping. We force the ping to fire, capture the result, and test for:
+// - Basic payload structure validity.
+// - Expected results for a few specific DLLs
+add_task(async function test_send_ping() {
+ let expectedModules = [
+ // This checks that a DLL loaded during runtime is evaluated properly.
+ // This is hard-coded as untrusted in toolkit/xre/UntrustedModules.cpp for
+ // testing purposes.
+ {
+ nameMatch: new RegExp(kDllName, "i"),
+ expectedTrusted: false,
+ wasFound: false,
+ },
+ {
+ nameMatch: /kernelbase.dll/i,
+ expectedTrusted: true,
+ wasFound: false,
+ },
+ ];
+
+ // There is a tiny chance some other ping is being sent legitimately before
+ // the one we care about. Spin until we find the correct ping type.
+ let found;
+ while (true) {
+ found = await PingServer.promiseNextPing();
+ if (found.type == "third-party-modules") {
+ break;
+ }
+ }
+
+ // Test the ping payload's validity.
+ Assert.ok(found, "Untrusted modules ping submitted");
+ Assert.ok(found.environment, "Ping has an environment");
+ Assert.ok(typeof found.clientId != "undefined", "Ping has a client ID");
+
+ Assert.equal(found.payload.structVersion, 1, "Version is correct");
+ Assert.ok(found.payload.modules, "'modules' object exists");
+ Assert.ok(Array.isArray(found.payload.modules), "'modules' is an array");
+ Assert.ok(found.payload.blockedModules, "'blockedModules' object exists");
+ Assert.ok(
+ Array.isArray(found.payload.blockedModules),
+ "'blockedModules' is an array"
+ );
+ // Unfortunately, the way this test is run it doesn't usually get a launcher
+ // process, so the blockedModules member doesn't get populated. This is the
+ // same structure that's used in the about:third-party page, though, so we
+ // have coverage in browser_aboutthirdparty.js that this is correct.
+ Assert.ok(found.payload.processes, "'processes' object exists");
+ Assert.ok(
+ gCurrentPidStr in found.payload.processes,
+ `Current process "${gCurrentPidStr}" is included in payload`
+ );
+
+ let ourProcInfo = found.payload.processes[gCurrentPidStr];
+ Assert.equal(ourProcInfo.processType, "browser", "'processType' is correct");
+ Assert.ok(typeof ourProcInfo.elapsed == "number", "'elapsed' exists");
+ Assert.equal(
+ ourProcInfo.sanitizationFailures,
+ 0,
+ "'sanitizationFailures' is 0"
+ );
+ Assert.equal(ourProcInfo.trustTestFailures, 0, "'trustTestFailures' is 0");
+
+ Assert.equal(
+ ourProcInfo.combinedStacks.stacks.length,
+ ourProcInfo.events.length,
+ "combinedStacks.stacks.length == events.length"
+ );
+
+ for (let event of ourProcInfo.events) {
+ Assert.ok(
+ typeof event.processUptimeMS == "number",
+ "'processUptimeMS' exists"
+ );
+ Assert.ok(typeof event.threadID == "number", "'threadID' exists");
+ Assert.ok(typeof event.baseAddress == "string", "'baseAddress' exists");
+
+ Assert.ok(typeof event.moduleIndex == "number", "'moduleIndex' exists");
+ Assert.ok(event.moduleIndex >= 0, "'moduleIndex' is non-negative");
+
+ Assert.ok(typeof event.isDependent == "boolean", "'isDependent' exists");
+ Assert.ok(!event.isDependent, "'isDependent' is false");
+
+ Assert.ok(typeof event.loadStatus == "number", "'loadStatus' exists");
+ Assert.ok(event.loadStatus == 0, "'loadStatus' is 0 (Loaded)");
+
+ let modRecord = found.payload.modules[event.moduleIndex];
+ Assert.ok(modRecord, "module record for this event exists");
+ Assert.ok(
+ typeof modRecord.resolvedDllName == "string",
+ "'resolvedDllName' exists"
+ );
+ Assert.ok(typeof modRecord.trustFlags == "number", "'trustFlags' exists");
+
+ let mod = expectedModules.find(function (elem) {
+ return elem.nameMatch.test(modRecord.resolvedDllName);
+ });
+
+ if (mod) {
+ mod.wasFound = true;
+ }
+ }
+
+ for (let x of expectedModules) {
+ Assert.equal(
+ !x.wasFound,
+ x.expectedTrusted,
+ `Trustworthiness == expected for module: ${x.nameMatch.source}`
+ );
+ }
+});
+
+// This tests the flags INCLUDE_OLD_LOADEVENTS and KEEP_LOADEVENTS_NEW
+// controls the method's return value and the internal storages
+// "Staging" and "Settled" correctly.
+add_task(async function test_new_old_instances() {
+ const kIncludeOld = Telemetry.INCLUDE_OLD_LOADEVENTS;
+ const kKeepNew = Telemetry.KEEP_LOADEVENTS_NEW;
+ const get_events_count = data => data.processes[gCurrentPidStr].events.length;
+
+ // Make sure |baseline| has at least one instance.
+ await load_and_free(kDllName);
+
+ // Make sure all instances are "old"
+ const baseline = await Telemetry.getUntrustedModuleLoadEvents(kIncludeOld);
+ const baseline_count = get_events_count(baseline);
+ print("baseline_count = " + baseline_count);
+ print("baseline = " + JSON.stringify(baseline));
+
+ await Assert.rejects(
+ Telemetry.getUntrustedModuleLoadEvents(),
+ e => e.result == Cr.NS_ERROR_NOT_AVAILABLE,
+ "New instances should not exist!"
+ );
+
+ await load_and_free(kDllName); // A
+
+ // Passing kIncludeOld and kKeepNew is unsupported. A is kept new.
+ await Assert.rejects(
+ Telemetry.getUntrustedModuleLoadEvents(kIncludeOld | kKeepNew),
+ e => e.result == Cr.NS_ERROR_INVALID_ARG,
+ "Passing unsupported flag combination should throw an exception!"
+ );
+
+ await load_and_free(kDllName); // B
+
+ // After newly loading B, the new instances we have is {A, B}
+ // Both A and B are still kept new.
+ let payload = await Telemetry.getUntrustedModuleLoadEvents(kKeepNew);
+ print("payload = " + JSON.stringify(payload));
+ Assert.equal(get_events_count(payload), 2);
+
+ await load_and_free(kDllName); // C
+
+ // After newly loading C, the new instances we have is {A, B, C}
+ // All of A, B, and C are now marked as old.
+ payload = await Telemetry.getUntrustedModuleLoadEvents();
+ Assert.equal(get_events_count(payload), 3);
+
+ payload = await Telemetry.getUntrustedModuleLoadEvents(kIncludeOld);
+ // payload is {baseline, A, B, C}
+ Assert.equal(get_events_count(payload), baseline_count + 3);
+});
+
+// This tests the flag INCLUDE_PRIVATE_FIELDS_IN_LOADEVENTS returns
+// data including private fields.
+add_task(async function test_private_fields() {
+ await load_and_free(kDllName);
+ const data = await Telemetry.getUntrustedModuleLoadEvents(
+ Telemetry.KEEP_LOADEVENTS_NEW |
+ Telemetry.INCLUDE_PRIVATE_FIELDS_IN_LOADEVENTS
+ );
+
+ for (const module of data.modules) {
+ Assert.ok(!("resolvedDllName" in module));
+ Assert.ok("dllFile" in module);
+ Assert.ok(module.dllFile.QueryInterface);
+ Assert.ok(module.dllFile.QueryInterface(Ci.nsIFile));
+ }
+});
+
+// This tests the flag EXCLUDE_STACKINFO_FROM_LOADEVENTS correctly
+// merges "Staging" and "Settled" on a JS object correctly, and
+// the "combinedStacks" field is really excluded.
+add_task(async function test_exclude_stack() {
+ const baseline = await Telemetry.getUntrustedModuleLoadEvents(
+ Telemetry.EXCLUDE_STACKINFO_FROM_LOADEVENTS |
+ Telemetry.INCLUDE_OLD_LOADEVENTS
+ );
+ Assert.ok(!("combinedStacks" in baseline.processes[gCurrentPidStr]));
+ const baseSet = baseline.processes[gCurrentPidStr].events.map(
+ x => x.processUptimeMS
+ );
+
+ await load_and_free(kDllName);
+ await load_and_free(kDllName);
+ const newLoadsWithStack = await Telemetry.getUntrustedModuleLoadEvents(
+ Telemetry.KEEP_LOADEVENTS_NEW
+ );
+ Assert.ok("combinedStacks" in newLoadsWithStack.processes[gCurrentPidStr]);
+ const newSet = newLoadsWithStack.processes[gCurrentPidStr].events.map(
+ x => x.processUptimeMS
+ );
+
+ const merged = baseSet.concat(newSet);
+
+ const allData = await Telemetry.getUntrustedModuleLoadEvents(
+ Telemetry.KEEP_LOADEVENTS_NEW |
+ Telemetry.EXCLUDE_STACKINFO_FROM_LOADEVENTS |
+ Telemetry.INCLUDE_OLD_LOADEVENTS
+ );
+ Assert.ok(!("combinedStacks" in allData.processes[gCurrentPidStr]));
+ const allSet = allData.processes[gCurrentPidStr].events.map(
+ x => x.processUptimeMS
+ );
+
+ Assert.deepEqual(allSet.sort(), merged.sort());
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_UninstallPing.js b/toolkit/components/telemetry/tests/unit/test_UninstallPing.js
new file mode 100644
index 0000000000..d619ebb10e
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_UninstallPing.js
@@ -0,0 +1,126 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+"use strict";
+
+const { BasePromiseWorker } = ChromeUtils.importESModule(
+ "resource://gre/modules/PromiseWorker.sys.mjs"
+);
+const { TelemetryStorage } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryStorage.sys.mjs"
+);
+const { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+
+const gFakeInstallPathHash = "0123456789ABCDEF";
+let gFakeVendorDirectory;
+let gFakeGetUninstallPingPath;
+
+add_setup(async function setup() {
+ do_get_profile();
+
+ let fakeVendorDirectoryNSFile = new FileUtils.File(
+ PathUtils.join(PathUtils.profileDir, "uninstall-ping-test")
+ );
+ fakeVendorDirectoryNSFile.createUnique(
+ Ci.nsIFile.DIRECTORY_TYPE,
+ FileUtils.PERMS_DIRECTORY
+ );
+ gFakeVendorDirectory = fakeVendorDirectoryNSFile.path;
+
+ gFakeGetUninstallPingPath = id => ({
+ directory: fakeVendorDirectoryNSFile.clone(),
+ file: `uninstall_ping_${gFakeInstallPathHash}_${id}.json`,
+ });
+
+ fakeUninstallPingPath(gFakeGetUninstallPingPath);
+
+ registerCleanupFunction(async () => {
+ await IOUtils.remove(gFakeVendorDirectory, { recursive: true });
+ });
+});
+
+function ping_path(ping) {
+ let { directory: pingFile, file } = gFakeGetUninstallPingPath(ping.id);
+ pingFile.append(file);
+ return pingFile.path;
+}
+
+add_task(async function test_store_ping() {
+ // Remove shouldn't throw on an empty dir.
+ await TelemetryStorage.removeUninstallPings();
+
+ // Write ping
+ const ping1 = {
+ id: "58b63aac-999e-4efb-9d5a-20f368670721",
+ payload: { some: "thing" },
+ };
+ const ping1Path = ping_path(ping1);
+ await TelemetryStorage.saveUninstallPing(ping1);
+
+ // Check the ping
+ Assert.ok(await IOUtils.exists(ping1Path));
+ const readPing1 = await IOUtils.readJSON(ping1Path);
+ Assert.deepEqual(ping1, readPing1);
+
+ // Write another file that shouldn't match the pattern
+ const otherFilePath = PathUtils.join(gFakeVendorDirectory, "other_file.json");
+ await IOUtils.writeUTF8(otherFilePath, "");
+ Assert.ok(await IOUtils.exists(otherFilePath));
+
+ // Write another ping, should remove the earlier one
+ const ping2 = {
+ id: "7202c564-8f23-41b4-8a50-1744e9549260",
+ payload: { another: "thing" },
+ };
+ const ping2Path = ping_path(ping2);
+ await TelemetryStorage.saveUninstallPing(ping2);
+
+ Assert.ok(!(await IOUtils.exists(ping1Path)));
+ Assert.ok(await IOUtils.exists(ping2Path));
+ Assert.ok(await IOUtils.exists(otherFilePath));
+
+ // Write an additional file manually so there are multiple matching pings to remove
+ const ping3 = { id: "yada-yada" };
+ const ping3Path = ping_path(ping3);
+
+ await IOUtils.writeUTF8(ping3Path, "");
+ Assert.ok(await IOUtils.exists(ping3Path));
+
+ // Remove pings
+ await TelemetryStorage.removeUninstallPings();
+
+ // Check our pings are removed but other file isn't
+ Assert.ok(!(await IOUtils.exists(ping1Path)));
+ Assert.ok(!(await IOUtils.exists(ping2Path)));
+ Assert.ok(!(await IOUtils.exists(ping3Path)));
+ Assert.ok(await IOUtils.exists(otherFilePath));
+
+ // Remove again, confirming that the remove doesn't cause an error if nothing to remove
+ await TelemetryStorage.removeUninstallPings();
+
+ const ping4 = {
+ id: "1f113673-753c-4fbe-9143-fe197f936036",
+ payload: { any: "thing" },
+ };
+ const ping4Path = ping_path(ping4);
+ await TelemetryStorage.saveUninstallPing(ping4);
+
+ // Use a worker to keep the ping file open, so a delete should fail.
+ const worker = new BasePromiseWorker(
+ "resource://test/file_UninstallPing.worker.js"
+ );
+ await worker.post("open", [ping4Path]);
+
+ // Check that there is no error if the file can't be removed.
+ await TelemetryStorage.removeUninstallPings();
+
+ // And file should still exist.
+ Assert.ok(await IOUtils.exists(ping4Path));
+
+ // Close the file, so it should be possible to remove now.
+ await worker.post("close");
+ await TelemetryStorage.removeUninstallPings();
+ Assert.ok(!(await IOUtils.exists(ping4Path)));
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_UserInteraction.js b/toolkit/components/telemetry/tests/unit/test_UserInteraction.js
new file mode 100644
index 0000000000..5fc3c5ecd1
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_UserInteraction.js
@@ -0,0 +1,134 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_USER_INTERACTION_ID = "testing.interaction";
+const TEST_VALUE_1 = "some value";
+const TEST_VALUE_2 = "some other value";
+const TEST_INVALID_VALUE =
+ "This is a value that is far too long - it has too many characters.";
+const TEST_ADDITIONAL_TEXT_1 = "some additional text";
+const TEST_ADDITIONAL_TEXT_2 = "some other additional text";
+
+function run_test() {
+ let obj1 = {};
+ let obj2 = {};
+
+ Assert.ok(UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1));
+ Assert.ok(
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1, obj1)
+ );
+ Assert.ok(
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1, obj2)
+ );
+
+ Assert.ok(UserInteraction.running(TEST_USER_INTERACTION_ID));
+ Assert.ok(UserInteraction.running(TEST_USER_INTERACTION_ID, obj1));
+ Assert.ok(UserInteraction.running(TEST_USER_INTERACTION_ID, obj2));
+
+ // Unlike TelemetryStopwatch, we can clobber UserInteractions.
+ Assert.ok(UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1));
+ Assert.ok(
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1, obj1)
+ );
+ Assert.ok(
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1, obj2)
+ );
+
+ Assert.ok(UserInteraction.running(TEST_USER_INTERACTION_ID));
+ Assert.ok(UserInteraction.running(TEST_USER_INTERACTION_ID, obj1));
+ Assert.ok(UserInteraction.running(TEST_USER_INTERACTION_ID, obj2));
+
+ // Ensure that we can finish a UserInteraction that was accidentally started
+ // twice
+ Assert.ok(UserInteraction.finish(TEST_USER_INTERACTION_ID));
+ Assert.ok(UserInteraction.finish(TEST_USER_INTERACTION_ID, obj1));
+ Assert.ok(UserInteraction.finish(TEST_USER_INTERACTION_ID, obj2));
+
+ // Make sure we can't start or finish non-existent UserInteractions.
+ Assert.ok(!UserInteraction.start("non-existent.interaction", TEST_VALUE_1));
+ Assert.ok(
+ !UserInteraction.start("non-existent.interaction", TEST_VALUE_1, obj1)
+ );
+ Assert.ok(
+ !UserInteraction.start("non-existent.interaction", TEST_VALUE_1, obj2)
+ );
+ Assert.ok(!UserInteraction.running("non-existent.interaction"));
+ Assert.ok(!UserInteraction.running("non-existent.interaction", obj1));
+ Assert.ok(!UserInteraction.running("non-existent.interaction", obj2));
+ Assert.ok(!UserInteraction.finish("non-existent.interaction"));
+ Assert.ok(!UserInteraction.finish("non-existent.interaction", obj1));
+ Assert.ok(!UserInteraction.finish("non-existent.interaction", obj2));
+
+ // Ensure that we enforce the character limit on value strings.
+ Assert.ok(
+ !UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_INVALID_VALUE)
+ );
+ Assert.ok(
+ !UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_INVALID_VALUE, obj1)
+ );
+ Assert.ok(
+ !UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_INVALID_VALUE, obj2)
+ );
+ Assert.ok(!UserInteraction.running(TEST_USER_INTERACTION_ID));
+ Assert.ok(!UserInteraction.running(TEST_USER_INTERACTION_ID, obj1));
+ Assert.ok(!UserInteraction.running(TEST_USER_INTERACTION_ID, obj2));
+
+ // Verify that they can be used again
+ Assert.ok(UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_2));
+ Assert.ok(
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_2, obj1)
+ );
+ Assert.ok(
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_2, obj2)
+ );
+ Assert.ok(UserInteraction.running(TEST_USER_INTERACTION_ID));
+ Assert.ok(UserInteraction.running(TEST_USER_INTERACTION_ID, obj1));
+ Assert.ok(UserInteraction.running(TEST_USER_INTERACTION_ID, obj2));
+ Assert.ok(UserInteraction.finish(TEST_USER_INTERACTION_ID));
+ Assert.ok(UserInteraction.finish(TEST_USER_INTERACTION_ID, obj1));
+ Assert.ok(UserInteraction.finish(TEST_USER_INTERACTION_ID, obj2));
+
+ Assert.ok(!UserInteraction.finish(TEST_USER_INTERACTION_ID));
+ Assert.ok(!UserInteraction.finish(TEST_USER_INTERACTION_ID, obj1));
+ Assert.ok(!UserInteraction.finish(TEST_USER_INTERACTION_ID, obj2));
+ Assert.ok(!UserInteraction.running(TEST_USER_INTERACTION_ID));
+ Assert.ok(!UserInteraction.running(TEST_USER_INTERACTION_ID, obj1));
+ Assert.ok(!UserInteraction.running(TEST_USER_INTERACTION_ID, obj2));
+
+ // Verify that they can be used again with different values.
+ Assert.ok(UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1));
+ Assert.ok(
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_2, obj1)
+ );
+ Assert.ok(
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1, obj2)
+ );
+ Assert.ok(UserInteraction.running(TEST_USER_INTERACTION_ID));
+ Assert.ok(UserInteraction.running(TEST_USER_INTERACTION_ID, obj1));
+ Assert.ok(UserInteraction.running(TEST_USER_INTERACTION_ID, obj2));
+ Assert.ok(UserInteraction.finish(TEST_USER_INTERACTION_ID));
+ Assert.ok(
+ UserInteraction.finish(
+ TEST_USER_INTERACTION_ID,
+ obj1,
+ TEST_ADDITIONAL_TEXT_1
+ )
+ );
+ Assert.ok(
+ UserInteraction.finish(
+ TEST_USER_INTERACTION_ID,
+ obj2,
+ TEST_ADDITIONAL_TEXT_2
+ )
+ );
+
+ // Test that they can be cancelled
+ Assert.ok(UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1));
+ Assert.ok(UserInteraction.cancel(TEST_USER_INTERACTION_ID));
+ Assert.ok(!UserInteraction.running(TEST_USER_INTERACTION_ID));
+ Assert.ok(!UserInteraction.finish(TEST_USER_INTERACTION_ID));
+
+ // Test that they cannot be cancelled twice
+ Assert.ok(!UserInteraction.cancel(TEST_USER_INTERACTION_ID));
+ Assert.ok(!UserInteraction.cancel(TEST_USER_INTERACTION_ID));
+}
diff --git a/toolkit/components/telemetry/tests/unit/test_UserInteraction_annotations.js b/toolkit/components/telemetry/tests/unit/test_UserInteraction_annotations.js
new file mode 100644
index 0000000000..a10021e088
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_UserInteraction_annotations.js
@@ -0,0 +1,470 @@
+/* Any copyright is dedicated to the Public Domain.
+http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+const { TelemetryUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryUtils.sys.mjs"
+);
+
+const HANG_TIME = 1000; // ms
+const TEST_USER_INTERACTION_ID = "testing.interaction";
+const TEST_CLOBBERED_USER_INTERACTION_ID = `${TEST_USER_INTERACTION_ID} (clobbered)`;
+const TEST_VALUE_1 = "some value";
+const TEST_VALUE_2 = "some other value";
+const TEST_ADDITIONAL_TEXT_1 = "some additional text";
+const TEST_ADDITIONAL_TEXT_2 = "some other additional text";
+
+/**
+ * Intentionally hangs the main thread in the parent process for
+ * HANG_TIME, and then returns the BHR hang report generated for
+ * that hang.
+ *
+ * @returns {Promise}
+ * @resolves {nsIHangDetails}
+ * The hang report that was created.
+ */
+async function hangAndWaitForReport(expectTestAnnotation) {
+ let hangPromise = TestUtils.topicObserved("bhr-thread-hang", subject => {
+ let hang = subject.QueryInterface(Ci.nsIHangDetails);
+ if (hang.thread != "Gecko") {
+ return false;
+ }
+
+ if (expectTestAnnotation) {
+ return hang.annotations.some(annotation =>
+ annotation[0].startsWith(TEST_USER_INTERACTION_ID)
+ );
+ }
+
+ return hang.annotations.every(
+ annotation => annotation[0] != TEST_USER_INTERACTION_ID
+ );
+ });
+
+ executeSoon(() => {
+ let startTime = Date.now();
+ // eslint-disable-next-line no-empty
+ while (Date.now() - startTime < HANG_TIME) {}
+ });
+
+ let [report] = await hangPromise;
+ return report;
+}
+
+/**
+ * Makes sure that the profiler is initialized. This has the added side-effect
+ * of making sure that BHR is initialized as well.
+ */
+function ensureProfilerInitialized() {
+ startProfiler();
+ stopProfiler();
+}
+
+function stopProfiler() {
+ Services.profiler.StopProfiler();
+}
+
+function startProfiler() {
+ // Starting and stopping the profiler with the "stackwalk" flag will cause the
+ // profiler's stackwalking features to be synchronously initialized. This
+ // should prevent us from not initializing BHR quickly enough.
+ Services.profiler.StartProfiler(1000, 10, ["stackwalk"]);
+}
+
+/**
+ * Given a performance profile object, returns a count of how many
+ * markers matched the value (and optional additionalText) that
+ * the UserInteraction backend added. This function only checks
+ * markers on thread 0.
+ *
+ * @param {Object} profile
+ * A profile returned from Services.profiler.getProfileData();
+ * @param {String} value
+ * The value that the marker is expected to have.
+ * @param {String} additionalText
+ * (Optional) If additionalText was provided when finishing the
+ * UserInteraction, then markerCount will check for a marker with
+ * text in the form of "value,additionalText".
+ * @returns {Number}
+ * A count of how many markers appear that match the criteria.
+ */
+function markerCount(profile, value, additionalText) {
+ let expectedName = value;
+ if (additionalText) {
+ expectedName = [value, additionalText].join(",");
+ }
+
+ let thread0 = profile.threads[0];
+ let stringTable = thread0.stringTable;
+ let markerStringIndex = stringTable.indexOf(TEST_USER_INTERACTION_ID);
+
+ let markers = thread0.markers.data.filter(markerData => {
+ return (
+ markerData[0] == markerStringIndex && markerData[5].name == expectedName
+ );
+ });
+
+ return markers.length;
+}
+
+/**
+ * Given an nsIHangReport, returns true if there are one or more annotations
+ * with the TEST_USER_INTERACTION_ID name, and the passed value.
+ *
+ * @param {nsIHangReport} report
+ * The hang report to check the annotations of.
+ * @param {String} value
+ * The value that the annotation should have.
+ * @returns {boolean}
+ * True if the annotation was found.
+ */
+function hasHangAnnotation(report, value) {
+ return report.annotations.some(annotation => {
+ return annotation[0] == TEST_USER_INTERACTION_ID && annotation[1] == value;
+ });
+}
+
+/**
+ * Given an nsIHangReport, returns true if there are one or more annotations
+ * with the TEST_CLOBBERED_USER_INTERACTION_ID name, and the passed value.
+ *
+ * This check should be used when we expect a pre-existing UserInteraction to
+ * have been clobbered by a new UserInteraction.
+ *
+ * @param {nsIHangReport} report
+ * The hang report to check the annotations of.
+ * @param {String} value
+ * The value that the annotation should have.
+ * @returns {boolean}
+ * True if the annotation was found.
+ */
+function hasClobberedHangAnnotation(report, value) {
+ return report.annotations.some(annotation => {
+ return (
+ annotation[0] == TEST_CLOBBERED_USER_INTERACTION_ID &&
+ annotation[1] == value
+ );
+ });
+}
+
+/**
+ * Tests that UserInteractions cause BHR annotations and profiler
+ * markers to be written.
+ */
+add_task(async function test_recording_annotations_and_markers() {
+ if (!Services.telemetry.canRecordExtended) {
+ Assert.ok("Hang reporting not enabled.");
+ return;
+ }
+
+ ensureProfilerInitialized();
+
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.OverridePreRelease,
+ true
+ );
+
+ // First, we'll check to see if we can get a single annotation and
+ // profiler marker to be set.
+ startProfiler();
+
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1);
+ let report = await hangAndWaitForReport(true);
+ UserInteraction.finish(TEST_USER_INTERACTION_ID);
+ let profile = Services.profiler.getProfileData();
+ stopProfiler();
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_1),
+ 1,
+ "Should have found the marker in the profile."
+ );
+
+ Assert.ok(
+ hasHangAnnotation(report, TEST_VALUE_1),
+ "Should have the BHR annotation set."
+ );
+
+ // Next, we'll make sure that when we're not running a UserInteraction,
+ // no marker or annotation is set.
+ startProfiler();
+
+ report = await hangAndWaitForReport(false);
+ profile = Services.profiler.getProfileData();
+
+ stopProfiler();
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_1),
+ 0,
+ "Should not find the marker in the profile."
+ );
+ Assert.ok(
+ !hasHangAnnotation(report),
+ "Should not have the BHR annotation set."
+ );
+
+ // Next, we'll ensure that we can set multiple markers and annotations
+ // by using the optional object argument to start() and finish().
+ startProfiler();
+
+ let obj1 = {};
+ let obj2 = {};
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1, obj1);
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_2, obj2);
+ report = await hangAndWaitForReport(true);
+ UserInteraction.finish(
+ TEST_USER_INTERACTION_ID,
+ obj1,
+ TEST_ADDITIONAL_TEXT_1
+ );
+ UserInteraction.finish(
+ TEST_USER_INTERACTION_ID,
+ obj2,
+ TEST_ADDITIONAL_TEXT_2
+ );
+ profile = Services.profiler.getProfileData();
+
+ stopProfiler();
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_1, TEST_ADDITIONAL_TEXT_1),
+ 1,
+ "Should have found first marker in the profile."
+ );
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_2, TEST_ADDITIONAL_TEXT_2),
+ 1,
+ "Should have found second marker in the profile."
+ );
+
+ Assert.ok(
+ hasHangAnnotation(report, TEST_VALUE_1),
+ "Should have the first BHR annotation set."
+ );
+
+ Assert.ok(
+ hasHangAnnotation(report, TEST_VALUE_2),
+ "Should have the second BHR annotation set."
+ );
+});
+
+/**
+ * Tests that UserInteractions can be updated, resulting in their BHR
+ * annotations and profiler markers to also be updated.
+ */
+add_task(async function test_updating_annotations_and_markers() {
+ if (!Services.telemetry.canRecordExtended) {
+ Assert.ok("Hang reporting not enabled.");
+ return;
+ }
+
+ ensureProfilerInitialized();
+
+ // First, we'll check to see if we can get a single annotation and
+ // profiler marker to be set.
+ startProfiler();
+
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1);
+ // Updating the UserInteraction means that a new value will overwrite
+ // the old.
+ UserInteraction.update(TEST_USER_INTERACTION_ID, TEST_VALUE_2);
+ let report = await hangAndWaitForReport(true);
+ UserInteraction.finish(TEST_USER_INTERACTION_ID);
+ let profile = Services.profiler.getProfileData();
+
+ stopProfiler();
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_1),
+ 0,
+ "Should not have found the original marker in the profile."
+ );
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_2),
+ 1,
+ "Should have found the updated marker in the profile."
+ );
+
+ Assert.ok(
+ !hasHangAnnotation(report, TEST_VALUE_1),
+ "Should not have the original BHR annotation set."
+ );
+
+ Assert.ok(
+ hasHangAnnotation(report, TEST_VALUE_2),
+ "Should have the updated BHR annotation set."
+ );
+
+ // Next, we'll ensure that we can update multiple markers and annotations
+ // by using the optional object argument to start() and finish().
+ startProfiler();
+
+ let obj1 = {};
+ let obj2 = {};
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1, obj1);
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_2, obj2);
+
+ // Now swap the values between the two UserInteractions
+ UserInteraction.update(TEST_USER_INTERACTION_ID, TEST_VALUE_2, obj1);
+ UserInteraction.update(TEST_USER_INTERACTION_ID, TEST_VALUE_1, obj2);
+
+ report = await hangAndWaitForReport(true);
+ UserInteraction.finish(
+ TEST_USER_INTERACTION_ID,
+ obj1,
+ TEST_ADDITIONAL_TEXT_1
+ );
+ UserInteraction.finish(
+ TEST_USER_INTERACTION_ID,
+ obj2,
+ TEST_ADDITIONAL_TEXT_2
+ );
+ profile = Services.profiler.getProfileData();
+
+ stopProfiler();
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_2, TEST_ADDITIONAL_TEXT_1),
+ 1,
+ "Should have found first marker in the profile."
+ );
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_1, TEST_ADDITIONAL_TEXT_2),
+ 1,
+ "Should have found second marker in the profile."
+ );
+
+ Assert.ok(
+ hasHangAnnotation(report, TEST_VALUE_1),
+ "Should have the first BHR annotation set."
+ );
+
+ Assert.ok(
+ hasHangAnnotation(report, TEST_VALUE_2),
+ "Should have the second BHR annotation set."
+ );
+});
+
+/**
+ * Tests that UserInteractions can be cancelled, resulting in no BHR
+ * annotations and profiler markers being recorded.
+ */
+add_task(async function test_cancelling_annotations_and_markers() {
+ if (!Services.telemetry.canRecordExtended) {
+ Assert.ok("Hang reporting not enabled.");
+ return;
+ }
+
+ ensureProfilerInitialized();
+
+ // First, we'll check to see if we can get a single annotation and
+ // profiler marker to be set.
+ startProfiler();
+
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1);
+ UserInteraction.cancel(TEST_USER_INTERACTION_ID);
+ let report = await hangAndWaitForReport(false);
+
+ let profile = Services.profiler.getProfileData();
+
+ stopProfiler();
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_1),
+ 0,
+ "Should not have found the marker in the profile."
+ );
+
+ Assert.ok(
+ !hasHangAnnotation(report, TEST_VALUE_1),
+ "Should not have the BHR annotation set."
+ );
+
+ // Next, we'll ensure that we can cancel multiple markers and annotations
+ // by using the optional object argument to start() and finish().
+ startProfiler();
+
+ let obj1 = {};
+ let obj2 = {};
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1, obj1);
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_2, obj2);
+
+ UserInteraction.cancel(TEST_USER_INTERACTION_ID, obj1);
+ UserInteraction.cancel(TEST_USER_INTERACTION_ID, obj2);
+
+ report = await hangAndWaitForReport(false);
+
+ Assert.ok(
+ !UserInteraction.finish(TEST_USER_INTERACTION_ID, obj1),
+ "Finishing a canceled UserInteraction should return false."
+ );
+
+ Assert.ok(
+ !UserInteraction.finish(TEST_USER_INTERACTION_ID, obj2),
+ "Finishing a canceled UserInteraction should return false."
+ );
+
+ profile = Services.profiler.getProfileData();
+
+ stopProfiler();
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_1),
+ 0,
+ "Should not have found the first marker in the profile."
+ );
+
+ Assert.equal(
+ markerCount(profile, TEST_VALUE_2),
+ 0,
+ "Should not have found the second marker in the profile."
+ );
+
+ Assert.ok(
+ !hasHangAnnotation(report, TEST_VALUE_1),
+ "Should not have the first BHR annotation set."
+ );
+
+ Assert.ok(
+ !hasHangAnnotation(report, TEST_VALUE_2),
+ "Should not have the second BHR annotation set."
+ );
+});
+
+/**
+ * Tests that starting UserInteractions with the same ID and object
+ * creates a clobber annotation.
+ */
+add_task(async function test_clobbered_annotations() {
+ if (!Services.telemetry.canRecordExtended) {
+ Assert.ok("Hang reporting not enabled.");
+ return;
+ }
+
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_1);
+ // Now clobber the original UserInteraction
+ UserInteraction.start(TEST_USER_INTERACTION_ID, TEST_VALUE_2);
+
+ let report = await hangAndWaitForReport(true);
+ Assert.ok(
+ UserInteraction.finish(TEST_USER_INTERACTION_ID),
+ "Should have been able to finish the UserInteraction."
+ );
+
+ Assert.ok(
+ !hasHangAnnotation(report, TEST_VALUE_1),
+ "Should not have the original BHR annotation set."
+ );
+
+ Assert.ok(
+ hasClobberedHangAnnotation(report, TEST_VALUE_2),
+ "Should have the clobber BHR annotation set."
+ );
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_UtilityScalars.js b/toolkit/components/telemetry/tests/unit/test_UtilityScalars.js
new file mode 100644
index 0000000000..b6c0842910
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_UtilityScalars.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+const { TelemetryController } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryController.sys.mjs"
+);
+const { ContentTaskUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/ContentTaskUtils.sys.mjs"
+);
+
+const UTILITY_ONLY_UINT_SCALAR = "telemetry.test.utility_only_uint";
+
+const utilityProcessTest = () => {
+ return Cc["@mozilla.org/utility-process-test;1"].createInstance(
+ Ci.nsIUtilityProcessTest
+ );
+};
+
+/**
+ * This function waits until utility scalars are reported into the
+ * scalar snapshot.
+ */
+async function waitForUtilityScalars() {
+ await ContentTaskUtils.waitForCondition(() => {
+ const scalars = Telemetry.getSnapshotForScalars("main", false);
+ return Object.keys(scalars).includes("utility");
+ }, "Waiting for utility scalars to have been set");
+}
+
+async function waitForUtilityValue() {
+ await ContentTaskUtils.waitForCondition(() => {
+ return (
+ UTILITY_ONLY_UINT_SCALAR in
+ Telemetry.getSnapshotForScalars("main", false).utility
+ );
+ }, "Waiting for utility uint value");
+}
+
+add_setup(async function setup_telemetry_utility() {
+ info("Start a UtilityProcess");
+ await utilityProcessTest().startProcess();
+
+ do_get_profile(true);
+ await TelemetryController.testSetup();
+});
+
+add_task(async function test_scalars_in_utility_process() {
+ Telemetry.clearScalars();
+ await utilityProcessTest().testTelemetryProbes();
+
+ // Once scalars are set by the utility process, they don't immediately get
+ // sent to the parent process. Wait for the Telemetry IPC Timer to trigger
+ // and batch send the data back to the parent process.
+ await waitForUtilityScalars();
+ await waitForUtilityValue();
+
+ Assert.equal(
+ Telemetry.getSnapshotForScalars("main", false).utility[
+ UTILITY_ONLY_UINT_SCALAR
+ ],
+ 42,
+ `${UTILITY_ONLY_UINT_SCALAR} must have the correct value (utility process).`
+ );
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_bug1555798.js b/toolkit/components/telemetry/tests/unit/test_bug1555798.js
new file mode 100644
index 0000000000..f5a84c200c
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_bug1555798.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+ChromeUtils.defineESModuleGetters(this, {
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs",
+});
+
+add_task(async function test_bug1555798() {
+ /*
+ The idea behind this bug is that the registration of dynamic scalars causes
+ the position of the scalarinfo for telemetry.dynamic_events_count to move
+ which causes things to asplode.
+
+ So to test this we'll be registering two dynamic events, recording to one of
+ the events (to ensure the Scalar for event1 is allocated from the unmoved
+ DynamicScalarInfo&), registering several dynamic scalars to cause the
+ nsTArray of DynamicScalarInfo to realloc, and then recording to the second
+ event to make the Event Summary Scalar for event2 try to allocate from where
+ the DynamicScalarInfo used to be.
+ */
+ Telemetry.clearEvents();
+
+ const DYNAMIC_CATEGORY = "telemetry.test.dynamic.event";
+ Telemetry.registerEvents(DYNAMIC_CATEGORY, {
+ an_event: {
+ methods: ["a_method"],
+ objects: ["an_object", "another_object"],
+ record_on_release: true,
+ expired: false,
+ },
+ });
+ Telemetry.recordEvent(DYNAMIC_CATEGORY, "a_method", "an_object");
+
+ for (let i = 0; i < 100; ++i) {
+ Telemetry.registerScalars("telemetry.test.dynamic" + i, {
+ scalar_name: {
+ kind: Ci.nsITelemetry.SCALAR_TYPE_COUNT,
+ record_on_release: true,
+ },
+ });
+ Telemetry.scalarAdd("telemetry.test.dynamic" + i + ".scalar_name", 1);
+ }
+
+ Telemetry.recordEvent(DYNAMIC_CATEGORY, "a_method", "another_object");
+
+ TelemetryTestUtils.assertNumberOfEvents(2, {}, { process: "dynamic" });
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_client_id.js b/toolkit/components/telemetry/tests/unit/test_client_id.js
new file mode 100644
index 0000000000..c20f70e2b2
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_client_id.js
@@ -0,0 +1,163 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const { ClientID } = ChromeUtils.importESModule(
+ "resource://gre/modules/ClientID.sys.mjs"
+);
+
+const PREF_CACHED_CLIENTID = "toolkit.telemetry.cachedClientID";
+
+var drsPath;
+
+const uuidRegex =
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
+
+function run_test() {
+ do_get_profile();
+ drsPath = PathUtils.join(PathUtils.profileDir, "datareporting", "state.json");
+
+ Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+ );
+ run_next_test();
+}
+
+add_task(function test_setup() {
+ // FOG needs a profile and to be init.
+ do_get_profile();
+ Services.fog.initializeFOG();
+});
+
+add_task(async function test_client_id() {
+ const invalidIDs = [
+ [-1, "setIntPref"],
+ [0.5, "setIntPref"],
+ ["INVALID-UUID", "setStringPref"],
+ [true, "setBoolPref"],
+ ["", "setStringPref"],
+ ["3d1e1560-682a-4043-8cf2-aaaaaaaaaaaZ", "setStringPref"],
+ ];
+
+ // If there is no DRS file, and no cached id, we should get a new client ID.
+ await ClientID._reset();
+ Services.prefs.clearUserPref(PREF_CACHED_CLIENTID);
+ await IOUtils.remove(drsPath, { ignoreAbsent: true });
+ let clientID = await ClientID.getClientID();
+ Assert.equal(typeof clientID, "string");
+ Assert.ok(uuidRegex.test(clientID));
+ if (AppConstants.platform != "android") {
+ Assert.equal(clientID, Glean.legacyTelemetry.clientId.testGetValue());
+ }
+
+ // We should be guarded against invalid DRS json.
+ await ClientID._reset();
+ Services.prefs.clearUserPref(PREF_CACHED_CLIENTID);
+ await IOUtils.writeUTF8(drsPath, "abcd", {
+ tmpPath: drsPath + ".tmp",
+ });
+ clientID = await ClientID.getClientID();
+ Assert.equal(typeof clientID, "string");
+ Assert.ok(uuidRegex.test(clientID));
+ if (AppConstants.platform != "android") {
+ Assert.equal(clientID, Glean.legacyTelemetry.clientId.testGetValue());
+ }
+
+ // If the DRS data is broken, we should end up with the cached ID.
+ let oldClientID = clientID;
+ for (let [invalidID] of invalidIDs) {
+ await ClientID._reset();
+ await IOUtils.writeJSON(drsPath, { clientID: invalidID });
+ clientID = await ClientID.getClientID();
+ Assert.equal(clientID, oldClientID);
+ if (AppConstants.platform != "android") {
+ Assert.equal(clientID, Glean.legacyTelemetry.clientId.testGetValue());
+ }
+ }
+
+ // Test that valid DRS actually works.
+ const validClientID = "5afebd62-a33c-416c-b519-5c60fb988e8e";
+ await ClientID._reset();
+ await IOUtils.writeJSON(drsPath, { clientID: validClientID });
+ clientID = await ClientID.getClientID();
+ Assert.equal(clientID, validClientID);
+ if (AppConstants.platform != "android") {
+ Assert.equal(clientID, Glean.legacyTelemetry.clientId.testGetValue());
+ }
+
+ // Test that reloading a valid DRS works.
+ await ClientID._reset();
+ Services.prefs.clearUserPref(PREF_CACHED_CLIENTID);
+ clientID = await ClientID.getClientID();
+ Assert.equal(clientID, validClientID);
+ if (AppConstants.platform != "android") {
+ Assert.equal(clientID, Glean.legacyTelemetry.clientId.testGetValue());
+ }
+
+ // Assure that cached IDs are being checked for validity.
+ for (let [invalidID, prefFunc] of invalidIDs) {
+ await ClientID._reset();
+ Services.prefs[prefFunc](PREF_CACHED_CLIENTID, invalidID);
+ let cachedID = ClientID.getCachedClientID();
+ Assert.strictEqual(
+ cachedID,
+ null,
+ "ClientID should ignore invalid cached IDs"
+ );
+ Assert.ok(
+ !Services.prefs.prefHasUserValue(PREF_CACHED_CLIENTID),
+ "ClientID should reset invalid cached IDs"
+ );
+ Assert.ok(
+ Services.prefs.getPrefType(PREF_CACHED_CLIENTID) ==
+ Ci.nsIPrefBranch.PREF_INVALID,
+ "ClientID should reset invalid cached IDs"
+ );
+ }
+});
+
+add_task(async function test_setCanaryClientID() {
+ const KNOWN_UUID = "c0ffeec0-ffee-c0ff-eec0-ffeec0ffeec0";
+
+ await ClientID._reset();
+
+ // We should be able to set a valid UUID
+ await ClientID.setCanaryClientID();
+ let clientID = await ClientID.getClientID();
+ Assert.equal(KNOWN_UUID, clientID);
+ if (AppConstants.platform != "android") {
+ Assert.equal(clientID, Glean.legacyTelemetry.clientId.testGetValue());
+ }
+});
+
+add_task(async function test_removeParallelGet() {
+ // We should get a valid UUID after reset
+ await ClientID.removeClientID();
+ let firstClientID = await ClientID.getClientID();
+ if (AppConstants.platform != "android") {
+ Assert.equal(firstClientID, Glean.legacyTelemetry.clientId.testGetValue());
+ }
+
+ // We should get the same ID twice when requesting it in parallel to a reset.
+ let promiseRemoveClientID = ClientID.removeClientID();
+ let p = ClientID.getClientID();
+ let newClientID = await ClientID.getClientID();
+ await promiseRemoveClientID;
+ let otherClientID = await p;
+
+ Assert.notEqual(
+ firstClientID,
+ newClientID,
+ "After reset client ID should be different."
+ );
+ Assert.equal(
+ newClientID,
+ otherClientID,
+ "Getting the client ID in parallel to a reset should give the same id."
+ );
+ if (AppConstants.platform != "android") {
+ Assert.equal(newClientID, Glean.legacyTelemetry.clientId.testGetValue());
+ }
+});
diff --git a/toolkit/components/telemetry/tests/unit/test_failover_retry.js b/toolkit/components/telemetry/tests/unit/test_failover_retry.js
new file mode 100644
index 0000000000..01305a10b1
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_failover_retry.js
@@ -0,0 +1,263 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+const { HttpServer } = ChromeUtils.importESModule(
+ "resource://testing-common/httpd.sys.mjs"
+);
+
+const { TelemetryUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/TelemetryUtils.sys.mjs"
+);
+
+// Enable the collection (during test) for all products so even products
+// that don't collect the data will be able to run the test without failure.
+Services.prefs.setBoolPref(
+ "toolkit.telemetry.testing.overrideProductsCheck",
+ true
+);
+
+Services.prefs.setBoolPref("network.dns.native-is-localhost", true);
+
+// Trigger a proper telemetry init.
+do_get_profile(true);
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+AddonTestUtils.createAppInfo(
+ "xpcshell@tests.mozilla.org",
+ "XPCShell",
+ "42",
+ "42"
+);
+
+// setup and configure a proxy server that will just deny connections.
+const proxy = AddonTestUtils.createHttpServer();
+proxy.registerPrefixHandler("/", (request, response) => {
+ response.setStatusLine(request.httpVersion, 504, "hello proxy user");
+ response.write("ok!");
+});
+
+// Register a proxy to be used by TCPSocket connections later.
+let proxy_info;
+
+function getBadProxyPort() {
+ let server = new HttpServer();
+ server.start(-1);
+ const badPort = server.identity.primaryPort;
+ server.stop();
+ return badPort;
+}
+
+function registerProxy() {
+ let pps = Cc["@mozilla.org/network/protocol-proxy-service;1"].getService(
+ Ci.nsIProtocolProxyService
+ );
+
+ const proxyFilter = {
+ applyFilter(uri, defaultProxyInfo, callback) {
+ if (
+ proxy_info &&
+ uri.host == PingServer.host &&
+ uri.port == PingServer.port
+ ) {
+ let proxyInfo = pps.newProxyInfo(
+ proxy_info.type,
+ proxy_info.host,
+ proxy_info.port,
+ "",
+ "",
+ 0,
+ 4096,
+ null
+ );
+ proxyInfo.sourceId = proxy_info.sourceId;
+ callback.onProxyFilterResult(proxyInfo);
+ } else {
+ callback.onProxyFilterResult(defaultProxyInfo);
+ }
+ },
+ };
+
+ pps.registerFilter(proxyFilter, 0);
+ registerCleanupFunction(() => {
+ pps.unregisterFilter(proxyFilter);
+ });
+}
+
+add_task(async function setup() {
+ fakeIntlReady();
+
+ // Make sure we don't generate unexpected pings due to pref changes.
+ await setEmptyPrefWatchlist();
+ Services.prefs.setBoolPref(
+ TelemetryUtils.Preferences.HealthPingEnabled,
+ false
+ );
+ TelemetryStopwatch.setTestModeEnabled(true);
+
+ registerProxy();
+
+ PingServer.start();
+
+ // accept proxy connections for PingServer
+ proxy.identity.add("http", PingServer.host, PingServer.port);
+
+ await TelemetrySend.setup(true);
+ TelemetrySend.setTestModeEnabled(true);
+ TelemetrySend.setServer(`http://localhost:${PingServer.port}`);
+});
+
+function checkEvent() {
+ // ServiceRequest should have recorded an event for this.
+ let expected = [
+ "service_request",
+ "bypass",
+ "proxy_info",
+ "telemetry.send",
+ {
+ source: proxy_info.sourceId,
+ type: "api",
+ },
+ ];
+ let snapshot = Telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_ALL_CHANNELS,
+ false
+ );
+
+ let received = snapshot.parent[0];
+ received.shift();
+ Assert.deepEqual(
+ expected,
+ received,
+ `retry telemetry data matched ${JSON.stringify(received)}`
+ );
+ Telemetry.clearEvents();
+}
+
+async function submitPingWithDate(date, expected) {
+ fakeNow(new Date(date));
+ let pingId = await TelemetryController.submitExternalPing(
+ "test-send-date-header",
+ {}
+ );
+ let req = await PingServer.promiseNextRequest();
+ let ping = decodeRequestPayload(req);
+ Assert.equal(
+ req.getHeader("Date"),
+ expected,
+ "Telemetry should send the correct Date header with requests."
+ );
+ Assert.equal(ping.id, pingId, "Should have received the correct ping id.");
+}
+
+// While there is no specific indiction, this test causes the
+// telemetrySend doPing onload handler to be invoked.
+add_task(async function test_failed_server() {
+ proxy_info = {
+ type: "http",
+ host: proxy.identity.primaryHost,
+ port: proxy.identity.primaryPort,
+ sourceId: "failed_server_test",
+ };
+
+ await TelemetrySend.reset();
+ await submitPingWithDate(
+ Date.UTC(2011, 1, 1, 11, 0, 0),
+ "Tue, 01 Feb 2011 11:00:00 GMT"
+ );
+ checkEvent();
+});
+
+// While there is no specific indiction, this test causes the
+// telemetrySend doPing error handler to be invoked.
+add_task(async function test_no_server() {
+ // Make sure the underlying proxy failover is disabled to easily force
+ // telemetry to retry the request.
+ Services.prefs.setBoolPref("network.proxy.failover_direct", false);
+
+ proxy_info = {
+ type: "http",
+ host: "localhost",
+ port: getBadProxyPort(),
+ sourceId: "no_server_test",
+ };
+
+ await TelemetrySend.reset();
+ await submitPingWithDate(
+ Date.UTC(2012, 1, 1, 11, 0, 0),
+ "Wed, 01 Feb 2012 11:00:00 GMT"
+ );
+ checkEvent();
+});
+
+// Mock out the send timer activity.
+function waitForTimer() {
+ return new Promise(resolve => {
+ fakePingSendTimer(
+ (callback, timeout) => {
+ resolve([callback, timeout]);
+ },
+ () => {}
+ );
+ });
+}
+
+add_task(async function test_no_bypass() {
+ // Make sure the underlying proxy failover is disabled to easily force
+ // telemetry to retry the request.
+ Services.prefs.setBoolPref("network.proxy.failover_direct", false);
+ // Disable the retry and submit again.
+ Services.prefs.setBoolPref("network.proxy.allow_bypass", false);
+
+ proxy_info = {
+ type: "http",
+ host: "localhost",
+ port: getBadProxyPort(),
+ sourceId: "no_server_test",
+ };
+
+ await TelemetrySend.reset();
+
+ fakeNow(new Date(Date.UTC(2013, 1, 1, 11, 0, 0)));
+
+ let timerPromise = waitForTimer();
+ let pingId = await TelemetryController.submitExternalPing(
+ "test-send-date-header",
+ {}
+ );
+ let [pingSendTimerCallback] = await timerPromise;
+ Assert.ok(!!pingSendTimerCallback, "Should have a timer callback");
+
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ 1,
+ "Should have correct pending ping count"
+ );
+
+ // Reset the proxy, trigger the next tick - we should receive the ping.
+ proxy_info = null;
+ pingSendTimerCallback();
+ let req = await PingServer.promiseNextRequest();
+ let ping = decodeRequestPayload(req);
+
+ // PingServer finished before telemetry, so make sure it's done.
+ await TelemetrySend.testWaitOnOutgoingPings();
+
+ Assert.equal(
+ req.getHeader("Date"),
+ "Fri, 01 Feb 2013 11:00:00 GMT",
+ "Telemetry should send the correct Date header with requests."
+ );
+ Assert.equal(ping.id, pingId, "Should have received the correct ping id.");
+
+ // reset to save any pending pings
+ Assert.equal(
+ TelemetrySend.pendingPingCount,
+ 0,
+ "Should not have any pending pings"
+ );
+
+ await TelemetrySend.reset();
+ PingServer.stop();
+});
diff --git a/toolkit/components/telemetry/tests/unit/xpcshell.toml b/toolkit/components/telemetry/tests/unit/xpcshell.toml
new file mode 100644
index 0000000000..2dbaaff1a3
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/xpcshell.toml
@@ -0,0 +1,183 @@
+[DEFAULT]
+head = "head.js"
+firefox-appdir = "browser"
+# The *.xpi files are only needed for test_TelemetryEnvironment.js, but
+# xpcshell fails to install tests if we move them under the test entry.
+support-files = [
+ "data/search-extensions/engines.json",
+ "data/search-extensions/telemetrySearchIdentifier/manifest.json",
+ "engine.xml",
+ "system.xpi",
+ "restartless.xpi",
+ "testUnicodePDB32.dll",
+ "testNoPDB32.dll",
+ "testUnicodePDB64.dll",
+ "testNoPDB64.dll",
+ "testUnicodePDBAArch64.dll",
+ "testNoPDBAArch64.dll",
+ "!/toolkit/mozapps/extensions/test/xpcshell/head_addons.js",
+]
+generated-files = [
+ "system.xpi",
+ "restartless.xpi",
+]
+
+["test_ChildEvents.js"]
+skip-if = ["os == 'android'"] # Disabled due to crashes (see bug 1331366)
+
+["test_ChildHistograms.js"]
+skip-if = ["os == 'android'"] # Disabled due to crashes (see bug 1331366)
+tags = "addons"
+
+["test_ChildScalars.js"]
+skip-if = ["os == 'android'"] # Disabled due to crashes (see bug 1331366)
+
+["test_CoveragePing.js"]
+
+["test_EventPing.js"]
+tags = "coverage"
+
+["test_HealthPing.js"]
+skip-if = [
+ "verify && os == 'win'",
+ "os == 'android' && processor == 'x86_64'",
+]
+tags = "addons"
+
+["test_MigratePendingPings.js"]
+
+["test_ModulesPing.js"]
+skip-if = [
+ "apple_silicon", # bug 1707747
+ "apple_catalina", # Bug 1713329
+]
+
+["test_PingAPI.js"]
+
+["test_PingSender.js"]
+skip-if = ["os == 'android'"]
+
+["test_RDDScalars.js"]
+skip-if = ["os == 'android'"] # RDD is not a thing on Android?
+
+["test_SocketScalars.js"]
+run-if = ["socketprocess_networking"] # Needs socket process (bug 1716307)
+
+["test_SubsessionChaining.js"]
+tags = "addons"
+
+["test_SyncPingIntegration.js"]
+skip-if = ["os == 'android'"]
+
+["test_TelemetryAndroidEnvironment.js"]
+
+["test_TelemetryChildEvents_buildFaster.js"]
+skip-if = ["os == 'android'"] # Disabled due to crashes (see bug 1331366)
+
+["test_TelemetryClientID_reset.js"]
+skip-if = ["os == 'android'"] # Disabled as Android/GeckoView doesn't run TelemetryController
+
+["test_TelemetryController.js"]
+
+["test_TelemetryControllerBuildID.js"]
+
+["test_TelemetryControllerShutdown.js"]
+skip-if = ["os == 'android' && processor == 'x86_64'"] # Disabled as Android/GeckoView doesn't run TelemetryController
+tags = "addons"
+
+["test_TelemetryController_idle.js"]
+
+["test_TelemetryEnvironment.js"]
+skip-if = [
+ "os == 'android'",
+ "os == 'win' && msix", # https://bugzilla.mozilla.org/show_bug.cgi?id=1807929
+]
+tags = "addons"
+run-sequentially = "very high failure rate in parallel"
+
+["test_TelemetryEnvironment_search.js"]
+skip-if = [
+ "os == 'android'",
+ "os == 'win' && msix", # https://bugzilla.mozilla.org/show_bug.cgi?id=1807929
+]
+
+["test_TelemetryEvents.js"]
+
+["test_TelemetryEvents_buildFaster.js"]
+skip-if = ["os == 'win' && msix"] # https://bugzilla.mozilla.org/show_bug.cgi?id=1807929
+
+["test_TelemetryFlagClear.js"]
+
+["test_TelemetryHistograms.js"]
+
+["test_TelemetryLateWrites.js"]
+
+["test_TelemetryLockCount.js"]
+
+["test_TelemetryReportingPolicy.js"]
+tags = "addons"
+
+["test_TelemetryScalars.js"]
+
+["test_TelemetryScalars_buildFaster.js"]
+skip-if = ["os == 'win' && msix"] # https://bugzilla.mozilla.org/show_bug.cgi?id=1807929
+
+["test_TelemetryScalars_impressionId.js"]
+
+["test_TelemetryScalars_multistore.js"]
+
+["test_TelemetrySend.js"]
+skip-if = ["os == 'linux' && ccov"] # Bug 1701874
+
+["test_TelemetrySendOldPings.js"]
+skip-if = ["os == 'android'"] # Disabled due to intermittent orange on Android
+tags = "addons"
+
+["test_TelemetrySession.js"]
+tags = "addons"
+skip-if = ["os == 'linux' && verify && debug"]
+
+["test_TelemetrySession_abortedSessionQueued.js"]
+skip-if = ["os == 'android'"]
+
+["test_TelemetrySession_activeTicks.js"]
+
+["test_TelemetryStopwatch.js"]
+
+["test_TelemetryTimestamps.js"]
+
+["test_TelemetryUtils.js"]
+
+["test_ThirdPartyModulesPing.js"]
+run-if = ["(os == 'win' && !msix)"] # Disabled for MSIX due to https://bugzilla.mozilla.org/show_bug.cgi?id=1807929
+
+["test_UninstallPing.js"]
+support-files = ["file_UninstallPing.worker.js"]
+run-if = ["os == 'win'"]
+
+["test_UserInteraction.js"]
+
+["test_UserInteraction_annotations.js"]
+# BHR is disabled on tsan, asan, android and outside of Nightly.
+skip-if = [
+ "debug",
+ "asan",
+ "tsan",
+ "os == 'android'",
+ "release_or_beta",
+ "apple_catalina", # Bug 1713329
+ "apple_silicon", # bug 1707747
+ "win11_2009 && bits == 32 && !debug", # Bug 1781452
+ "os == 'linux' && !debug", # Bug 1781452
+]
+run-sequentially = "very high failure rate in parallel"
+
+["test_UtilityScalars.js"]
+run-if = ["os == 'win'"]
+
+["test_bug1555798.js"]
+
+["test_client_id.js"]
+
+["test_failover_retry.js"]
+skip-if = ["os == 'android'"] # Android doesn't support telemetry though some tests manage to pass with xpcshell
diff --git a/toolkit/components/telemetry/tests/utils/TelemetryTestUtils.sys.mjs b/toolkit/components/telemetry/tests/utils/TelemetryTestUtils.sys.mjs
new file mode 100644
index 0000000000..9e608c1c5e
--- /dev/null
+++ b/toolkit/components/telemetry/tests/utils/TelemetryTestUtils.sys.mjs
@@ -0,0 +1,430 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+import { Assert } from "resource://testing-common/Assert.sys.mjs";
+
+export var TelemetryTestUtils = {
+ /* Scalars */
+
+ /**
+ * A helper that asserts the value of a scalar.
+ *
+ * @param {Object} scalars The snapshot of the scalars.
+ * @param {String} scalarName The name of the scalar to check.
+ * @param {Boolean|Number|String} value The expected value for the scalar.
+ * @param {String} msg The message to print when checking the value.
+ */
+ assertScalar(scalars, scalarName, value, msg) {
+ Assert.equal(scalars[scalarName], value, msg);
+ },
+
+ /**
+ * A helper that asserts a scalar is not set.
+ *
+ * @param {Object} scalars The snapshot of the scalars.
+ * @param {String} scalarName The name of the scalar to check.
+ */
+ assertScalarUnset(scalars, scalarName) {
+ Assert.ok(!(scalarName in scalars), scalarName + " must not be reported.");
+ },
+
+ /**
+ * Asserts if the snapshotted keyed scalars contain the expected
+ * data.
+ *
+ * @param {Object} scalars The snapshot of the keyed scalars.
+ * @param {String} scalarName The name of the keyed scalar to check.
+ * @param {String} key The key that must be within the keyed scalar.
+ * @param {String|Boolean|Number} expectedValue The expected value for the
+ * provided key in the scalar.
+ */
+ assertKeyedScalar(scalars, scalarName, key, expectedValue) {
+ Assert.ok(scalarName in scalars, scalarName + " must be recorded.");
+ Assert.ok(
+ key in scalars[scalarName],
+ scalarName + " must contain the '" + key + "' key."
+ );
+ Assert.equal(
+ scalars[scalarName][key],
+ expectedValue,
+ scalarName + "['" + key + "'] must contain the expected value"
+ );
+ },
+
+ /**
+ * Returns a snapshot of scalars from the specified process.
+ *
+ * @param {String} aProcessName Name of the process. Could be parent or
+ * something else.
+ * @param {boolean} [aKeyed] Set to true if keyed scalars rather than normal
+ * scalars should be snapshotted.
+ * @param {boolean} [aClear] Set to true to clear the scalars once the snapshot
+ * has been obtained.
+ * @param {Number} aChannel The channel dataset type from nsITelemetry.
+ * @returns {Object} The snapshotted scalars from the parent process.
+ */
+ getProcessScalars(
+ aProcessName,
+ aKeyed = false,
+ aClear = false,
+ aChannel = Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS
+ ) {
+ const extended = aChannel == Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS;
+ const currentExtended = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = extended;
+ const scalars = aKeyed
+ ? Services.telemetry.getSnapshotForKeyedScalars("main", aClear)[
+ aProcessName
+ ]
+ : Services.telemetry.getSnapshotForScalars("main", aClear)[aProcessName];
+ Services.telemetry.canRecordExtended = currentExtended;
+ return scalars || {};
+ },
+
+ /* Events */
+
+ /**
+ * Asserts that the number of events, after filtering, is equal to numEvents.
+ *
+ * @param {Number} numEvents The number of events to assert.
+ * @param {Object} filter As per assertEvents.
+ * @param {Object} options As per assertEvents.
+ */
+ assertNumberOfEvents(numEvents, filter, options) {
+ // Create an array of empty objects of length numEvents
+ TelemetryTestUtils.assertEvents(
+ Array.from({ length: numEvents }, () => ({})),
+ filter,
+ options
+ );
+ },
+
+ /**
+ * Returns the events in a snapshot, after optional filtering.
+ *
+ * @param {Object} filter An object of strings or RegExps for first filtering
+ * the event snapshot. Of the form {category, method, object}.
+ * Absent filters filter nothing.
+ * @param {Object} options An object containing any of
+ * - process {string} the process to examine. Default parent.
+ */
+ getEvents(filter = {}, { process = "parent" } = {}) {
+ // Step 0: Snapshot and clear.
+ let snapshots = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ false
+ );
+
+ if (!(process in snapshots)) {
+ return [];
+ }
+
+ let snapshot = snapshots[process];
+
+ // Step 1: Filter.
+ // Shared code with the below function
+ let {
+ category: filterCategory,
+ method: filterMethod,
+ object: filterObject,
+ } = filter;
+ let matches = (expected, actual) => {
+ if (expected === undefined) {
+ return true;
+ } else if (expected && expected.test) {
+ // Possibly a RegExp.
+ return expected.test(actual);
+ } else if (typeof expected === "function") {
+ return expected(actual);
+ }
+ return expected === actual;
+ };
+
+ return snapshot
+ .map(([, /* timestamp */ category, method, object, value, extra]) => {
+ // We don't care about the `timestamp` value.
+ // Tests that examine that value should use `snapshotEvents` directly.
+ return [category, method, object, value, extra];
+ })
+ .filter(([category, method, object]) => {
+ return (
+ matches(filterCategory, category) &&
+ matches(filterMethod, method) &&
+ matches(filterObject, object)
+ );
+ })
+ .map(([category, method, object, value, extra]) => {
+ return { category, method, object, value, extra };
+ });
+ },
+
+ /**
+ * Asserts that, after optional filtering, the current events snapshot
+ * matches expectedEvents.
+ *
+ * @param {Array} expectedEvents An array of event structures of the form
+ * [category, method, object, value, extra]
+ * or the same as an object with fields named as above.
+ * The array can be empty to assert that there are no events
+ * that match the filter.
+ * Each field can be absent/undefined (to match
+ * everything), a string or null (to match that value), a
+ * RegExp to match what it can match, or a function which
+ * matches by returning true when called with the field.
+ * `extra` is slightly different. If present it must be an
+ * object whose fields are treated the same way as the others.
+ * @param {Object} filter An object of strings or RegExps for first filtering
+ * the event snapshot. Of the form {category, method, object}.
+ * Absent filters filter nothing.
+ * @param {Object} options An object containing any of
+ * - clear {bool} clear events. Default true.
+ * - process {string} the process to examine. Default parent.
+ */
+ assertEvents(
+ expectedEvents,
+ filter = {},
+ { clear = true, process = "parent" } = {}
+ ) {
+ // Step 0: Snapshot and clear.
+ let snapshots = Services.telemetry.snapshotEvents(
+ Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS,
+ clear
+ );
+ if (expectedEvents.length === 0 && !(process in snapshots)) {
+ // Job's done!
+ return;
+ }
+ Assert.ok(
+ process in snapshots,
+ `${process} must be in snapshot. Has [${Object.keys(snapshots)}].`
+ );
+ let snapshot = snapshots[process];
+
+ // Step 1: Filter.
+ // Shared code with the above function
+ let {
+ category: filterCategory,
+ method: filterMethod,
+ object: filterObject,
+ } = filter;
+ let matches = (expected, actual) => {
+ if (expected === undefined) {
+ return true;
+ } else if (expected && expected.test) {
+ // Possibly a RegExp.
+ return expected.test(actual);
+ } else if (typeof expected === "function") {
+ return expected(actual);
+ }
+ return expected === actual;
+ };
+
+ let filtered = snapshot
+ .map(([, /* timestamp */ category, method, object, value, extra]) => {
+ // We don't care about the `timestamp` value.
+ // Tests that examine that value should use `snapshotEvents` directly.
+ return [category, method, object, value, extra];
+ })
+ .filter(([category, method, object]) => {
+ return (
+ matches(filterCategory, category) &&
+ matches(filterMethod, method) &&
+ matches(filterObject, object)
+ );
+ });
+
+ // Step 2: Match.
+ Assert.equal(
+ filtered.length,
+ expectedEvents.length,
+ `After filtering we must have the expected number of events. Filtered events: ${JSON.stringify(
+ filtered
+ )}`
+ );
+ if (expectedEvents.length === 0) {
+ // Job's done!
+ return;
+ }
+
+ // Transform object-type expected events to array-type to match snapshot.
+ if (!Array.isArray(expectedEvents[0])) {
+ expectedEvents = expectedEvents.map(
+ ({ category, method, object, value, extra }) => [
+ category,
+ method,
+ object,
+ value,
+ extra,
+ ]
+ );
+ }
+
+ const FIELD_NAMES = ["category", "method", "object", "value", "extra"];
+ const EXTRA_INDEX = 4;
+ for (let i = 0; i < expectedEvents.length; ++i) {
+ let expected = expectedEvents[i];
+ let actual = filtered[i];
+
+ // Match everything up to `extra`
+ for (let j = 0; j < EXTRA_INDEX; ++j) {
+ if (expected[j] === undefined) {
+ // Don't spam the assert log with unspecified fields.
+ continue;
+ }
+ Assert.report(
+ !matches(expected[j], actual[j]),
+ actual[j],
+ expected[j],
+ `${FIELD_NAMES[j]} in event ${actual[0]}#${actual[1]}#${actual[2]} must match.`,
+ "matches"
+ );
+ }
+
+ // Match extra
+ if (
+ expected.length > EXTRA_INDEX &&
+ expected[EXTRA_INDEX] !== undefined
+ ) {
+ Assert.ok(
+ actual.length > EXTRA_INDEX,
+ `Actual event ${actual[0]}#${actual[1]}#${actual[2]} expected to have extra.`
+ );
+ let expectedExtra = expected[EXTRA_INDEX];
+ let actualExtra = actual[EXTRA_INDEX];
+ for (let [key, value] of Object.entries(expectedExtra)) {
+ Assert.ok(
+ key in actualExtra,
+ `Expected key ${key} must be in actual extra. Actual keys: [${Object.keys(
+ actualExtra
+ )}].`
+ );
+ Assert.report(
+ !matches(value, actualExtra[key]),
+ actualExtra[key],
+ value,
+ `extra[${key}] must match in event ${actual[0]}#${actual[1]}#${actual[2]}.`,
+ "matches"
+ );
+ }
+ }
+ }
+ },
+
+ /* Histograms */
+
+ /**
+ * Clear and get the named histogram.
+ *
+ * @param {String} name The name of the histogram
+ * @returns {Object} The obtained histogram.
+ */
+ getAndClearHistogram(name) {
+ let histogram = Services.telemetry.getHistogramById(name);
+ histogram.clear();
+ return histogram;
+ },
+
+ /**
+ * Clear and get the named keyed histogram.
+ *
+ * @param {String} name The name of the keyed histogram
+ * @returns {Object} The obtained keyed histogram.
+ */
+ getAndClearKeyedHistogram(name) {
+ let histogram = Services.telemetry.getKeyedHistogramById(name);
+ histogram.clear();
+ return histogram;
+ },
+
+ /**
+ * Assert that the histogram index is the right value. It expects that
+ * other indexes are all zero.
+ *
+ * @param {Object} histogram The histogram to check.
+ * @param {Number} index The index to check against the expected value.
+ * @param {Number} expected The expected value of the index.
+ */
+ assertHistogram(histogram, index, expected) {
+ const snapshot = histogram.snapshot();
+ let found = false;
+ for (let [i, val] of Object.entries(snapshot.values)) {
+ if (i == index) {
+ found = true;
+ Assert.equal(
+ val,
+ expected,
+ `expected counts should match for ${histogram.name()} at index ${i}`
+ );
+ } else {
+ Assert.equal(
+ val,
+ 0,
+ `unexpected counts should be zero for ${histogram.name()} at index ${i}`
+ );
+ }
+ }
+ Assert.ok(
+ found,
+ `Should have found an entry for ${histogram.name()} at index ${index}`
+ );
+ },
+
+ /**
+ * Assert that a key within a keyed histogram contains the required sum.
+ *
+ * @param {Object} histogram The keyed histogram to check.
+ * @param {String} key The key to check.
+ * @param {Number} [expected] The expected sum for the key.
+ */
+ assertKeyedHistogramSum(histogram, key, expected) {
+ const snapshot = histogram.snapshot();
+ if (expected === undefined) {
+ Assert.ok(
+ !(key in snapshot),
+ `The histogram ${histogram.name()} must not contain ${key}.`
+ );
+ return;
+ }
+ Assert.ok(
+ key in snapshot,
+ `The histogram ${histogram.name()} must contain ${key}.`
+ );
+ Assert.equal(
+ snapshot[key].sum,
+ expected,
+ `The key ${key} must contain the expected sum in ${histogram.name()}.`
+ );
+ },
+
+ /**
+ * Assert that the value of a key within a keyed histogram is the right value.
+ * It expects that other values are all zero.
+ *
+ * @param {Object} histogram The keyed histogram to check.
+ * @param {String} key The key to check.
+ * @param {Number} index The index to check against the expected value.
+ * @param {Number} [expected] The expected values for the key.
+ */
+ assertKeyedHistogramValue(histogram, key, index, expected) {
+ const snapshot = histogram.snapshot();
+ if (!(key in snapshot)) {
+ Assert.ok(false, `The histogram ${histogram.name()} must contain ${key}`);
+ return;
+ }
+ for (let [i, val] of Object.entries(snapshot[key].values)) {
+ if (i == index) {
+ Assert.equal(
+ val,
+ expected,
+ `expected counts should match for ${histogram.name()} at index ${i}`
+ );
+ } else {
+ Assert.equal(
+ val,
+ 0,
+ `unexpected counts should be zero for ${histogram.name()} at index ${i}`
+ );
+ }
+ }
+ },
+};