Bug 2063316: Add additional properties to downgrade ping. r=nika,niklas,janerik

Differential Revision: https://phabricator.services.mozilla.com/D320429
This commit is contained in:
Dave Townsend
2026-09-11 18:21:21 +00:00
committed by dtownsend@mozilla.com
parent 732bf65881
commit 490a0964f7
9 changed files with 562 additions and 44 deletions
@@ -19,11 +19,17 @@ Structure:
profileGroupId: <UUID>,
payload: {
lastVersion: "", // The last version of the application that ran this profile
lastBuildId: "", // The last build ID 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
isMSIX: <bool>, // Whether this install is an MSIX package
profileSelectionReason: "", // How the profile was selected during startup (see the startup.profile_selection_reason metric)
daysSinceLock: <int>, // (optional) Days since the profile lock was last held
isNewInstall: <bool>, // (Windows only, optional) Whether the current install happened after the profile was last locked
isNewUpdate: <bool>, // (optional) Whether the last update was applied after the profile was last locked
isDifferentInstall: <bool>, // Whether the profile was last used by a different install of the application
}
}
```
+2 -1
View File
@@ -30,7 +30,8 @@ interface nsIProfileLock : nsISupports
readonly attribute nsIFile localDirectory;
/**
* The timestamp of an existing profile lock at lock time.
* The timestamp of an existing profile lock at lock time in milliseconds
* from midnight (00:00:00), January 1, 1970 Greenwich Mean Time (GMT).
*/
readonly attribute PRTime replacedLockTime;
@@ -756,6 +756,10 @@ void nsToolkitProfileService::CompleteStartup() {
}
}
const nsACString& nsToolkitProfileService::ProfileSelectionReason() {
return mStartupReason;
}
// Tests whether the passed profile was last used by this install.
bool nsToolkitProfileService::IsProfileForCurrentInstall(
nsToolkitProfile* aProfile) {
@@ -95,6 +95,7 @@ class nsToolkitProfileService final : public nsIToolkitProfileService {
bool HasShowProfileSelector();
void UpdateCurrentProfile();
void CompleteStartup();
const nsACString& ProfileSelectionReason();
using AsyncFlushPromise =
mozilla::MozPromise<bool /* ignored */, nsresult, false>;
+1
View File
@@ -256,6 +256,7 @@ LOCAL_INCLUDES += [
"/js/xpconnect/loader",
"/testing/gtest/mozilla",
"/third_party/sqlite3/ext/",
"/toolkit/components/jsoncpp/include",
"/toolkit/crashreporter",
"/xpcom/build",
]
+176 -35
View File
@@ -26,6 +26,7 @@
#include "mozilla/ProcessType.h"
#include "mozilla/ResultExtensions.h"
#include "mozilla/RuntimeExceptionModule.h"
#include "mozilla/FileUtils.h"
#include "mozilla/ScopeExit.h"
#include "mozilla/StaticPrefs_browser.h"
#include "mozilla/StaticPrefs_fission.h"
@@ -155,6 +156,7 @@
# endif
#endif
#include "json/json.h"
#include "nsCRT.h"
#include "nsCOMPtr.h"
#include "nsDirectoryServiceDefs.h"
@@ -179,6 +181,7 @@
#include "mozilla/LateWriteChecks.h"
#include <stdlib.h>
#include <string_view>
#ifdef XP_UNIX
# include <errno.h>
@@ -3322,10 +3325,66 @@ struct FileWriteFunc final : public JSONWriteFunc {
}
};
Maybe<PathString> GenerateDowngradeTelemetry(const nsACString& aPingId,
const nsCString& aLastVersion,
bool aHasSync, int32_t aButton,
const nsACString& aChannel) {
// Reads aJsonFile and returns the raw install_timestamp value, or Nothing() if
// the file is absent, unreadable, or lacks the property.
static mozilla::Maybe<uint64_t> ReadInstallTimestamp(nsIFile* aJsonFile,
bool aIsUTF16LE) {
FILE* raw = nullptr;
if (NS_FAILED(aJsonFile->OpenANSIFileDesc("rb", &raw)) || !raw) {
return mozilla::Nothing();
}
ScopedCloseFile f(raw);
fseek(f.get(), 0, SEEK_END);
auto len = ftell(f.get());
if (len <= 0) {
return mozilla::Nothing();
}
rewind(f.get());
auto buf = MakeUnique<uint8_t[]>(len);
if (fread(buf.get(), 1, len, f.get()) != (size_t)len) {
return mozilla::Nothing();
}
nsAutoCString converted;
std::string_view utf8View;
if (aIsUTF16LE) {
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
const char16_t* chars = reinterpret_cast<const char16_t*>(buf.get());
uint32_t charLen = len / 2;
CopyUTF16toUTF8(Span(chars, charLen), converted);
utf8View = std::string_view(converted.get(), converted.Length());
#else
MOZ_ASSERT_UNREACHABLE(
"UTF-16LE reading not supported on big-endian architectures");
return mozilla::Nothing();
#endif
} else {
utf8View = std::string_view(reinterpret_cast<const char*>(buf.get()), len);
}
Json::Value root;
Json::Reader reader;
if (!reader.parse(utf8View.data(), utf8View.data() + utf8View.size(), root) ||
!root.isMember("install_timestamp")) {
return mozilla::Nothing();
}
std::string tsStr = root["install_timestamp"].asString();
char* end = nullptr;
uint64_t val = strtoull(tsStr.c_str(), &end, 10);
if (*end != '\0') {
return mozilla::Nothing();
}
return mozilla::Some(val);
}
Maybe<mozilla::PathString> GenerateDowngradeTelemetry(
const nsACString& aPingId, const nsCString& aLastVersion, bool aHasSync,
int32_t aButton, const nsACString& aChannel,
const nsACString& aProfileSelectionReason,
mozilla::Maybe<PRTime> aReplacedLockTime, bool aIsDifferentInstall) {
nsCOMPtr<nsIPrefService> prefSvc =
do_GetService("@mozilla.org/preferences-service;1");
NS_ENSURE_TRUE(prefSvc, Nothing());
@@ -3358,10 +3417,47 @@ Maybe<PathString> GenerateDowngradeTelemetry(const nsACString& aPingId,
}
# endif
time_t now;
time(&now);
mozilla::Maybe<PRTime> maybeInstallTime;
mozilla::Maybe<PRTime> maybeUpdateTime;
nsCOMPtr<nsIFile> greDir;
if (NS_SUCCEEDED(
NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(greDir)))) {
# ifdef XP_WIN
// installation_telemetry.json uses a Windows FILETIME (100ns intervals
// since Jan 1, 1601 UTC).
nsCOMPtr<nsIFile> installTelemetry;
if (NS_SUCCEEDED(greDir->Clone(getter_AddRefs(installTelemetry))) &&
NS_SUCCEEDED(
installTelemetry->Append(u"installation_telemetry.json"_ns))) {
if (auto filetime =
ReadInstallTimestamp(installTelemetry, /* aIsUTF16LE */ true)) {
constexpr uint64_t kEpochOffset = 116444736000000000ULL;
if (*filetime > kEpochOffset) {
// The offset converts to an epoch of 1970, divide by 10 to convert
// from 100ns to usec
maybeInstallTime =
mozilla::Some(PRTime((*filetime - kEpochOffset) / 10));
}
}
}
# endif
// update_telemetry.json uses a Unix timestamp in milliseconds.
nsCOMPtr<nsIFile> updateTelemetry;
if (NS_SUCCEEDED(greDir->Clone(getter_AddRefs(updateTelemetry))) &&
NS_SUCCEEDED(updateTelemetry->Append(u"update_telemetry.json"_ns))) {
if (auto msTime =
ReadInstallTimestamp(updateTelemetry, /* aIsUTF16LE */ false)) {
maybeUpdateTime =
mozilla::Some(PRTime(int64_t(*msTime) * PR_USEC_PER_MSEC));
}
}
}
PRTime nowUsec = PR_Now();
time_t nowTime = time_t(nowUsec / PR_USEC_PER_SEC);
char date[sizeof "YYYY-MM-DDThh:mm:ss.000Z"];
strftime(date, sizeof date, "%FT%T.000Z", gmtime(&now));
strftime(date, sizeof date, "%FT%T.000Z", gmtime(&nowTime));
constexpr auto pingType = "downgrade"_ns;
@@ -3431,6 +3527,27 @@ Maybe<PathString> GenerateDowngradeTelemetry(const nsACString& aPingId,
w.BoolProperty("hasSync", aHasSync);
w.IntProperty("button", aButton);
w.BoolProperty("isMSIX", isMSIX);
w.StringProperty("profileSelectionReason",
PromiseFlatCString(aProfileSelectionReason));
w.BoolProperty("isDifferentInstall", aIsDifferentInstall);
if (aReplacedLockTime) {
// aReplacedLockTime is in milliseconds.
PRTime lockTimeUsec = *aReplacedLockTime * PR_USEC_PER_MSEC;
constexpr int64_t kUsecsPerDay =
int64_t(PR_USEC_PER_SEC) * 60 * 60 * 24;
int64_t elapsedUsec = nowUsec - lockTimeUsec;
if (elapsedUsec >= 0) {
w.IntProperty("daysSinceLock", elapsedUsec / kUsecsPerDay);
}
if (maybeInstallTime) {
w.BoolProperty("isNewInstall", *maybeInstallTime > lockTimeUsec);
}
if (maybeUpdateTime) {
w.BoolProperty("isNewUpdate", *maybeUpdateTime > lockTimeUsec);
}
}
}
w.EndObject();
}
@@ -3462,8 +3579,11 @@ bool BuildDowngradePingUrl(const nsACString& aPingId,
return true;
}
static void SubmitDowngradeTelemetry(const nsCString& aLastVersion,
bool aHasSync, int32_t aButton) {
static void SubmitDowngradeTelemetry(const nsACString& aProfileSelectionReason,
mozilla::Maybe<PRTime> aReplacedLockTime,
const nsCString& aLastVersion,
bool aHasSync, int32_t aButton,
bool aIsDifferentInstall) {
nsCOMPtr<nsIPrefService> prefSvc =
do_GetService("@mozilla.org/preferences-service;1");
NS_ENSURE_TRUE_VOID(prefSvc);
@@ -3513,7 +3633,8 @@ static void SubmitDowngradeTelemetry(const nsCString& aLastVersion,
}
Maybe<PathString> filePath = GenerateDowngradeTelemetry(
pingId, aLastVersion, aHasSync, aButton, channel);
pingId, aLastVersion, aHasSync, aButton, channel, aProfileSelectionReason,
aReplacedLockTime, aIsDifferentInstall);
if (!filePath) {
return;
}
@@ -3546,7 +3667,8 @@ static const char kProfileDowngradeURL[] =
static ReturnAbortOnError HandleDetectedDowngrade(
nsIFile* aProfileDir, nsINativeAppSupport* aNative,
nsIToolkitProfileService* aProfileSvc, const nsCString& aLastVersion) {
nsToolkitProfileService* aProfileSvc, nsIProfileLock* aProfileLock,
const nsCString& aLastVersion, bool aIsDifferentInstall) {
int32_t result = 0;
nsresult rv;
@@ -3625,7 +3747,15 @@ static ReturnAbortOnError HandleDetectedDowngrade(
paramBlock->GetInt(1, &result);
SubmitDowngradeTelemetry(aLastVersion, hasSync, result);
PRTime replacedLockTime = 0;
mozilla::Maybe<PRTime> maybeReplacedLockTime;
if (NS_SUCCEEDED(aProfileLock->GetReplacedLockTime(&replacedLockTime)) &&
replacedLockTime != 0) {
maybeReplacedLockTime = mozilla::Some(replacedLockTime);
}
SubmitDowngradeTelemetry(aProfileSvc->ProfileSelectionReason(),
maybeReplacedLockTime, aLastVersion, hasSync,
result, aIsDifferentInstall);
}
}
@@ -3757,6 +3887,36 @@ CompatCheckResult CheckCompatibility(nsIFile* aProfileDir,
return result;
}
// Check whether this is the same install by comparing the platform and app
// directories. Done before the version comparison so it is set even for
// downgrades.
result.isDifferentInstall = ![&]() {
nsAutoCString dirBuf;
nsCOMPtr<nsIFile> lf;
bool eq = false;
if (NS_FAILED(
parser.GetString("Compatibility", "LastPlatformDir", dirBuf)) ||
NS_FAILED(NS_NewLocalFileWithPersistentDescriptor(
dirBuf, getter_AddRefs(lf))) ||
NS_FAILED(lf->Equals(aXULRunnerDir, &eq)) || !eq) {
return false;
}
if (!aAppDir) {
return true;
}
if (NS_FAILED(parser.GetString("Compatibility", "LastAppDir", dirBuf)) ||
NS_FAILED(NS_NewLocalFileWithPersistentDescriptor(
dirBuf, getter_AddRefs(lf))) ||
NS_FAILED(lf->Equals(aAppDir, &eq)) || !eq) {
return false;
}
return true;
}();
if (!result.lastVersion.Equals(aVersion)) {
// The version is not the same. Whether it's a downgrade depends on an
// actual comparison:
@@ -3777,27 +3937,7 @@ CompatCheckResult CheckCompatibility(nsIFile* aProfileDir,
rv = parser.GetString("Compatibility", "LastOSABI", buf);
if (NS_FAILED(rv) || !aOSABI.Equals(buf)) return result;
rv = parser.GetString("Compatibility", "LastPlatformDir", buf);
if (NS_FAILED(rv)) return result;
nsCOMPtr<nsIFile> lf;
rv = NS_NewLocalFileWithPersistentDescriptor(buf, getter_AddRefs(lf));
if (NS_FAILED(rv)) return result;
bool eq;
rv = lf->Equals(aXULRunnerDir, &eq);
if (NS_FAILED(rv) || !eq) return result;
if (aAppDir) {
rv = parser.GetString("Compatibility", "LastAppDir", buf);
if (NS_FAILED(rv)) return result;
rv = NS_NewLocalFileWithPersistentDescriptor(buf, getter_AddRefs(lf));
if (NS_FAILED(rv)) return result;
rv = lf->Equals(aAppDir, &eq);
if (NS_FAILED(rv) || !eq) return result;
}
if (result.isDifferentInstall) return result;
// If we see this flag, caches are invalid.
rv = parser.GetString("Compatibility", "InvalidateCaches", buf);
@@ -5707,8 +5847,9 @@ int XREMain::XRE_mainStartup(bool* aExitFlag) {
# ifdef XP_MACOSX
InitializeMacApp();
# endif
rv = HandleDetectedDowngrade(mProfD, mNativeApp, mProfileSvc,
compatResult.lastVersion);
rv = HandleDetectedDowngrade(mProfD, mNativeApp, mProfileSvc, mProfileLock,
compatResult.lastVersion,
compatResult.isDifferentInstall);
if (rv == NS_ERROR_LAUNCHED_CHILD_PROCESS || rv == NS_ERROR_ABORT) {
*aExitFlag = true;
return 0;
+4 -1
View File
@@ -93,6 +93,7 @@ struct CompatCheckResult {
bool isCompatible = false;
bool cachesOK = false;
bool isDowngrade = false;
bool isDifferentInstall = false;
bool hasEncryptedDatabases = false;
nsCString lastAppVersion{VoidCString()};
nsCString lastAppBuildID{VoidCString()};
@@ -108,7 +109,9 @@ CompatCheckResult CheckCompatibility(nsIFile* aProfileDir,
#ifdef MOZ_BLOCK_PROFILE_DOWNGRADE
mozilla::Maybe<mozilla::PathString> GenerateDowngradeTelemetry(
const nsACString& aPingId, const nsCString& aLastVersion, bool aHasSync,
int32_t aButton, const nsACString& aChannel);
int32_t aButton, const nsACString& aChannel,
const nsACString& aProfileSelectionReason,
mozilla::Maybe<PRTime> aReplacedLockTime, bool aIsDifferentInstall);
bool BuildDowngradePingUrl(const nsACString& aPingId,
const nsACString& aChannel, nsACString& aUrlOut);
@@ -137,6 +137,7 @@ TEST_F(CheckCompatibilityTest, VersionMatchesEverythingIdentical) {
EXPECT_TRUE(result.isCompatible);
EXPECT_TRUE(result.cachesOK);
EXPECT_FALSE(result.isDowngrade);
EXPECT_FALSE(result.isDifferentInstall);
EXPECT_FALSE(result.hasEncryptedDatabases);
EXPECT_STREQ(result.lastAppVersion.get(), "130.0");
EXPECT_STREQ(result.lastAppBuildID.get(), "20250101000000");
@@ -153,6 +154,7 @@ TEST_F(CheckCompatibilityTest, VersionMatchesOSABIDiffers) {
EXPECT_FALSE(result.isCompatible);
EXPECT_FALSE(result.cachesOK);
EXPECT_FALSE(result.isDowngrade);
EXPECT_FALSE(result.isDifferentInstall);
EXPECT_FALSE(result.hasEncryptedDatabases);
EXPECT_STREQ(result.lastAppVersion.get(), "130.0");
EXPECT_STREQ(result.lastAppBuildID.get(), "20250101000000");
@@ -170,6 +172,7 @@ TEST_F(CheckCompatibilityTest, VersionMatchesInvalidateCaches) {
EXPECT_TRUE(result.isCompatible);
EXPECT_FALSE(result.cachesOK);
EXPECT_FALSE(result.isDowngrade);
EXPECT_FALSE(result.isDifferentInstall);
EXPECT_FALSE(result.hasEncryptedDatabases);
EXPECT_STREQ(result.lastAppVersion.get(), "130.0");
EXPECT_STREQ(result.lastAppBuildID.get(), "20250101000000");
@@ -189,6 +192,78 @@ TEST_F(CheckCompatibilityTest, VersionMatchesPurgeCachesFileExists) {
EXPECT_TRUE(result.isCompatible);
EXPECT_FALSE(result.cachesOK);
EXPECT_FALSE(result.isDowngrade);
EXPECT_FALSE(result.isDifferentInstall);
EXPECT_FALSE(result.hasEncryptedDatabases);
EXPECT_STREQ(result.lastAppVersion.get(), "130.0");
EXPECT_STREQ(result.lastAppBuildID.get(), "20250101000000");
}
TEST_F(CheckCompatibilityTest, VersionMatchesDifferentPlatformDir) {
nsCString version("130.0_20250101000000/20250101000000");
nsCOMPtr<nsIFile> tmpDir;
NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(tmpDir));
nsCOMPtr<nsIFile> otherDir;
tmpDir->Clone(getter_AddRefs(otherDir));
otherDir->Append(u"test_compat_other_platform"_ns);
otherDir->Remove(true);
(void)otherDir->Create(nsIFile::DIRECTORY_TYPE, 0755);
nsAutoCString otherDesc;
(void)otherDir->GetPersistentDescriptor(otherDesc);
nsAutoCString appDesc;
(void)mAppDir->GetPersistentDescriptor(appDesc);
nsAutoCString content;
content.AppendPrintf(
"[Compatibility]\n"
"LastVersion=%s\n"
"LastOSABI=Darwin_aarch64-gcc3\n"
"LastPlatformDir=%s\n"
"LastAppDir=%s\n",
version.get(), otherDesc.get(), appDesc.get());
WriteCompatIni(content);
CompatCheckResult result =
CheckCompatibility(mProfileDir, version, "Darwin_aarch64-gcc3"_ns,
mPlatformDir, mAppDir, nullptr);
EXPECT_FALSE(result.isCompatible);
EXPECT_FALSE(result.cachesOK);
EXPECT_FALSE(result.isDowngrade);
EXPECT_TRUE(result.isDifferentInstall);
EXPECT_FALSE(result.hasEncryptedDatabases);
EXPECT_STREQ(result.lastAppVersion.get(), "130.0");
EXPECT_STREQ(result.lastAppBuildID.get(), "20250101000000");
otherDir->Remove(true);
}
TEST_F(CheckCompatibilityTest, VersionMatchesDifferentAppDir) {
nsCString version("130.0_20250101000000/20250101000000");
nsAutoCString platformDesc;
(void)mPlatformDir->GetPersistentDescriptor(platformDesc);
nsAutoCString content;
content.AppendPrintf(
"[Compatibility]\n"
"LastVersion=%s\n"
"LastOSABI=Darwin_aarch64-gcc3\n"
"LastPlatformDir=%s\n"
"LastAppDir=/nonexistent/path\n",
version.get(), platformDesc.get());
WriteCompatIni(content);
CompatCheckResult result =
CheckCompatibility(mProfileDir, version, "Darwin_aarch64-gcc3"_ns,
mPlatformDir, mAppDir, nullptr);
EXPECT_FALSE(result.isCompatible);
EXPECT_FALSE(result.cachesOK);
EXPECT_FALSE(result.isDowngrade);
EXPECT_TRUE(result.isDifferentInstall);
EXPECT_FALSE(result.hasEncryptedDatabases);
EXPECT_STREQ(result.lastAppVersion.get(), "130.0");
EXPECT_STREQ(result.lastAppBuildID.get(), "20250101000000");
@@ -206,6 +281,7 @@ TEST_F(CheckCompatibilityTest, VersionUpgrade) {
EXPECT_FALSE(result.isCompatible);
EXPECT_FALSE(result.cachesOK);
EXPECT_FALSE(result.isDowngrade);
EXPECT_FALSE(result.isDifferentInstall);
EXPECT_FALSE(result.hasEncryptedDatabases);
EXPECT_STREQ(result.lastAppVersion.get(), "129.0");
EXPECT_STREQ(result.lastAppBuildID.get(), "20240901000000");
@@ -223,6 +299,7 @@ TEST_F(CheckCompatibilityTest, VersionDowngrade) {
EXPECT_FALSE(result.isCompatible);
EXPECT_FALSE(result.cachesOK);
EXPECT_TRUE(result.isDowngrade);
EXPECT_FALSE(result.isDifferentInstall);
EXPECT_FALSE(result.hasEncryptedDatabases);
EXPECT_STREQ(result.lastAppVersion.get(), "131.0");
EXPECT_STREQ(result.lastAppBuildID.get(), "20250201000000");
@@ -244,6 +321,7 @@ TEST_F(CheckCompatibilityTest, MinorVersionChangeNotDowngrade) {
EXPECT_FALSE(result.isCompatible);
EXPECT_FALSE(result.cachesOK);
EXPECT_FALSE(result.isDowngrade);
EXPECT_FALSE(result.isDifferentInstall);
EXPECT_FALSE(result.hasEncryptedDatabases);
EXPECT_STREQ(result.lastAppVersion.get(), "130.1");
EXPECT_STREQ(result.lastAppBuildID.get(), "20250201000000");
@@ -261,6 +339,7 @@ TEST_F(CheckCompatibilityTest, PatchVersionChangeNotDowngrade) {
EXPECT_FALSE(result.isCompatible);
EXPECT_FALSE(result.cachesOK);
EXPECT_FALSE(result.isDowngrade);
EXPECT_FALSE(result.isDifferentInstall);
EXPECT_FALSE(result.hasEncryptedDatabases);
EXPECT_STREQ(result.lastAppVersion.get(), "130.0.1");
EXPECT_STREQ(result.lastAppBuildID.get(), "20250201000000");
@@ -278,6 +357,7 @@ TEST_F(CheckCompatibilityTest, EncryptedDatabases) {
EXPECT_TRUE(result.isCompatible);
EXPECT_TRUE(result.cachesOK);
EXPECT_FALSE(result.isDowngrade);
EXPECT_FALSE(result.isDifferentInstall);
EXPECT_TRUE(result.hasEncryptedDatabases);
EXPECT_STREQ(result.lastAppVersion.get(), "130.0");
EXPECT_STREQ(result.lastAppBuildID.get(), "20250101000000");
@@ -294,6 +374,7 @@ TEST_F(CheckCompatibilityTest, EncryptedDatabasesAbsent) {
EXPECT_TRUE(result.isCompatible);
EXPECT_TRUE(result.cachesOK);
EXPECT_FALSE(result.isDowngrade);
EXPECT_FALSE(result.isDifferentInstall);
EXPECT_FALSE(result.hasEncryptedDatabases);
EXPECT_STREQ(result.lastAppVersion.get(), "130.0");
EXPECT_STREQ(result.lastAppBuildID.get(), "20250101000000");
@@ -309,6 +390,7 @@ TEST_F(CheckCompatibilityTest, NoCompatIni) {
EXPECT_FALSE(result.isCompatible);
EXPECT_FALSE(result.cachesOK);
EXPECT_FALSE(result.isDowngrade);
EXPECT_FALSE(result.isDifferentInstall);
EXPECT_FALSE(result.hasEncryptedDatabases);
EXPECT_TRUE(result.lastAppVersion.IsVoid());
EXPECT_TRUE(result.lastAppBuildID.IsVoid());
@@ -325,6 +407,7 @@ TEST_F(CheckCompatibilityTest, SafeMode) {
EXPECT_FALSE(result.isCompatible);
EXPECT_FALSE(result.cachesOK);
EXPECT_FALSE(result.isDowngrade);
EXPECT_FALSE(result.isDifferentInstall);
EXPECT_FALSE(result.hasEncryptedDatabases);
EXPECT_STREQ(result.lastAppVersion.get(), "Safe Mode");
EXPECT_TRUE(result.lastAppBuildID.IsEmpty());
@@ -351,6 +434,7 @@ TEST_F(CheckCompatibilityTest, VersionMatchesNoAppDir) {
EXPECT_TRUE(result.isCompatible);
EXPECT_TRUE(result.cachesOK);
EXPECT_FALSE(result.isDowngrade);
EXPECT_FALSE(result.isDifferentInstall);
EXPECT_FALSE(result.hasEncryptedDatabases);
EXPECT_STREQ(result.lastAppVersion.get(), "130.0");
EXPECT_STREQ(result.lastAppBuildID.get(), "20250101000000");
+283 -6
View File
@@ -2,9 +2,12 @@
* http://creativecommons.org/publicdomain/zero/1.0/
*/
#include <cinttypes>
#include "gtest/gtest.h"
#include "json/json.h"
#include "mozilla/Preferences.h"
#include "mozilla/Printf.h"
#include "mozilla/XREAppData.h"
#include "nsAppRunner.h"
#include "nsDirectoryServiceDefs.h"
@@ -70,6 +73,10 @@ class DowngradePingTest : public ::testing::Test {
Preferences::ClearUser("toolkit.telemetry.server");
Preferences::ClearUser("toolkit.telemetry.cachedClientID");
Preferences::ClearUser("toolkit.telemetry.cachedProfileGroupID");
RemoveUpdateTelemetryJson();
#ifdef XP_WIN
RemoveInstallTelemetryJson();
#endif
if (mTmpAppDataDir) {
nsCOMPtr<nsIProperties> dirSvc =
do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID);
@@ -99,6 +106,65 @@ class DowngradePingTest : public ::testing::Test {
fclose(f);
return bytesRead == static_cast<size_t>(fileSize);
}
bool WriteUpdateTelemetryJson(uint64_t aTimestampMs) {
nsCOMPtr<nsIFile> greDir;
if (NS_FAILED(NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(greDir))))
return false;
nsCOMPtr<nsIFile> file;
if (NS_FAILED(greDir->Clone(getter_AddRefs(file)))) return false;
if (NS_FAILED(file->Append(u"update_telemetry.json"_ns))) return false;
FILE* f = nullptr;
if (NS_FAILED(file->OpenANSIFileDesc("w", &f)) || !f) return false;
fprintf(f, "{\"install_timestamp\":\"%" PRIu64 "\"}", aTimestampMs);
fclose(f);
return true;
}
void RemoveUpdateTelemetryJson() {
nsCOMPtr<nsIFile> greDir;
if (NS_FAILED(NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(greDir))))
return;
nsCOMPtr<nsIFile> file;
if (NS_FAILED(greDir->Clone(getter_AddRefs(file)))) return;
if (NS_FAILED(file->Append(u"update_telemetry.json"_ns))) return;
file->Remove(false);
}
#ifdef XP_WIN
bool WriteInstallTelemetryJson(uint64_t aFileTime) {
nsCOMPtr<nsIFile> greDir;
if (NS_FAILED(NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(greDir))))
return false;
nsCOMPtr<nsIFile> file;
if (NS_FAILED(greDir->Clone(getter_AddRefs(file)))) return false;
if (NS_FAILED(file->Append(u"installation_telemetry.json"_ns)))
return false;
char json[128];
SprintfLiteral(json, "{\"install_timestamp\":\"%" PRIu64 "\"}", aFileTime);
nsAutoString utf16;
utf16.AppendASCII(json);
FILE* f = nullptr;
if (NS_FAILED(file->OpenANSIFileDesc("wb", &f)) || !f) return false;
fwrite(utf16.get(), sizeof(char16_t), utf16.Length(), f);
fclose(f);
return true;
}
void RemoveInstallTelemetryJson() {
nsCOMPtr<nsIFile> greDir;
if (NS_FAILED(NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(greDir))))
return;
nsCOMPtr<nsIFile> file;
if (NS_FAILED(greDir->Clone(getter_AddRefs(file)))) return;
if (NS_FAILED(file->Append(u"installation_telemetry.json"_ns))) return;
file->Remove(false);
}
#endif
};
TEST_F(DowngradePingTest, MissingClientId) {
@@ -106,19 +172,19 @@ TEST_F(DowngradePingTest, MissingClientId) {
EXPECT_FALSE(GenerateDowngradeTelemetry(
"test-ping-id"_ns, "131.0_20250201000000/20250201000000"_ns, false, 0,
"test-channel"_ns));
"test-channel"_ns, "default"_ns, mozilla::Nothing(), false));
}
TEST_F(DowngradePingTest, InvalidVersionFormat) {
EXPECT_FALSE(GenerateDowngradeTelemetry("test-ping-id"_ns,
"131.0-no-underscore"_ns, false, 0,
"test-channel"_ns));
EXPECT_FALSE(GenerateDowngradeTelemetry(
"test-ping-id"_ns, "131.0-no-underscore"_ns, false, 0, "test-channel"_ns,
"default"_ns, mozilla::Nothing(), false));
}
TEST_F(DowngradePingTest, FullPingStructure) {
auto result = GenerateDowngradeTelemetry(
"test-ping-id"_ns, "131.0_20250201000000/20250201000000"_ns, true, 1,
"test-channel"_ns);
"test-channel"_ns, "default"_ns, mozilla::Nothing(), true);
ASSERT_TRUE(result);
mCreatedPingPath = *result;
@@ -183,12 +249,14 @@ TEST_F(DowngradePingTest, FullPingStructure) {
EXPECT_STREQ(payload["lastBuildId"].asCString(), "20250201000000");
EXPECT_TRUE(payload["hasSync"].asBool());
EXPECT_EQ(payload["button"].asInt(), 1);
EXPECT_TRUE(payload["isDifferentInstall"].asBool());
EXPECT_STREQ(payload["profileSelectionReason"].asCString(), "default");
}
TEST_F(DowngradePingTest, PayloadBooleansFalse) {
auto result = GenerateDowngradeTelemetry(
"test-ping-id"_ns, "131.0_20250201000000/20250201000000"_ns, false, 0,
"test-channel"_ns);
"test-channel"_ns, "restart"_ns, mozilla::Nothing(), false);
ASSERT_TRUE(result);
mCreatedPingPath = *result;
@@ -203,4 +271,213 @@ TEST_F(DowngradePingTest, PayloadBooleansFalse) {
Json::Value payload = root["payload"];
EXPECT_FALSE(payload["hasSync"].asBool());
EXPECT_EQ(payload["button"].asInt(), 0);
EXPECT_FALSE(payload["isDifferentInstall"].asBool());
EXPECT_STREQ(payload["profileSelectionReason"].asCString(), "restart");
}
TEST_F(DowngradePingTest, IsNewUpdateTrue) {
// Update happened after the lock time.
PRTime lockTime = PRTime(1700000000) * PR_MSEC_PER_SEC;
uint64_t updateTimestampMs = 1701388800000ULL;
if (!WriteUpdateTelemetryJson(updateTimestampMs)) return;
auto result = GenerateDowngradeTelemetry(
"test-ping-id"_ns, "131.0_20250201000000/20250201000000"_ns, false, 0,
"test-channel"_ns, "default"_ns, mozilla::Some(lockTime), false);
ASSERT_TRUE(result);
mCreatedPingPath = *result;
nsCString contents;
ASSERT_TRUE(ReadPingFile(*result, contents));
Json::Value root;
Json::Reader reader;
ASSERT_TRUE(
reader.parse(contents.BeginReading(), contents.EndReading(), root));
Json::Value payload = root["payload"];
EXPECT_TRUE(payload.isMember("isNewUpdate"));
EXPECT_TRUE(payload["isNewUpdate"].asBool());
}
TEST_F(DowngradePingTest, IsNewUpdateFalse) {
// Update happened before the lock time.
uint64_t updateTimestampMs = 1700000000000ULL;
PRTime lockTime = PRTime(1701388800) * PR_MSEC_PER_SEC;
if (!WriteUpdateTelemetryJson(updateTimestampMs)) return;
auto result = GenerateDowngradeTelemetry(
"test-ping-id"_ns, "131.0_20250201000000/20250201000000"_ns, false, 0,
"test-channel"_ns, "default"_ns, mozilla::Some(lockTime), false);
ASSERT_TRUE(result);
mCreatedPingPath = *result;
nsCString contents;
ASSERT_TRUE(ReadPingFile(*result, contents));
Json::Value root;
Json::Reader reader;
ASSERT_TRUE(
reader.parse(contents.BeginReading(), contents.EndReading(), root));
Json::Value payload = root["payload"];
EXPECT_TRUE(payload.isMember("isNewUpdate"));
EXPECT_FALSE(payload["isNewUpdate"].asBool());
}
TEST_F(DowngradePingTest, IsNewUpdateAbsentWithoutLockTime) {
uint64_t updateTimestampMs = 1701388800000ULL;
if (!WriteUpdateTelemetryJson(updateTimestampMs)) return;
auto result = GenerateDowngradeTelemetry(
"test-ping-id"_ns, "131.0_20250201000000/20250201000000"_ns, false, 0,
"test-channel"_ns, "default"_ns, mozilla::Nothing(), false);
ASSERT_TRUE(result);
mCreatedPingPath = *result;
nsCString contents;
ASSERT_TRUE(ReadPingFile(*result, contents));
Json::Value root;
Json::Reader reader;
ASSERT_TRUE(
reader.parse(contents.BeginReading(), contents.EndReading(), root));
Json::Value payload = root["payload"];
EXPECT_FALSE(payload.isMember("isNewUpdate"));
}
TEST_F(DowngradePingTest, DaysSinceLock) {
PRTime lockTime = PRTime(1700000000) * PR_MSEC_PER_SEC;
auto result = GenerateDowngradeTelemetry(
"test-ping-id"_ns, "131.0_20250201000000/20250201000000"_ns, false, 0,
"test-channel"_ns, "default"_ns, mozilla::Some(lockTime), false);
ASSERT_TRUE(result);
mCreatedPingPath = *result;
nsCString contents;
ASSERT_TRUE(ReadPingFile(*result, contents));
Json::Value root;
Json::Reader reader;
ASSERT_TRUE(
reader.parse(contents.BeginReading(), contents.EndReading(), root));
Json::Value payload = root["payload"];
EXPECT_TRUE(payload.isMember("daysSinceLock"));
int64_t expectedDays = (int64_t(time(nullptr)) - 1700000000) / 86400;
int64_t actual = payload["daysSinceLock"].asInt64();
EXPECT_GE(actual, expectedDays - 1);
EXPECT_LE(actual, expectedDays + 1);
}
#ifdef XP_WIN
TEST_F(DowngradePingTest, IsNewInstallTrue) {
// Install happened after the lock time.
PRTime lockTime = PRTime(1700000000) * PR_MSEC_PER_SEC;
constexpr uint64_t kEpochOffset = 116444736000000000ULL;
int64_t installUnixSec = 1701388800;
uint64_t installFileTime =
uint64_t(installUnixSec) * PR_USEC_PER_SEC * 10 + kEpochOffset;
if (!WriteInstallTelemetryJson(installFileTime)) return;
auto result = GenerateDowngradeTelemetry(
"test-ping-id"_ns, "131.0_20250201000000/20250201000000"_ns, false, 0,
"test-channel"_ns, "default"_ns, mozilla::Some(lockTime), false);
ASSERT_TRUE(result);
mCreatedPingPath = *result;
nsCString contents;
ASSERT_TRUE(ReadPingFile(*result, contents));
Json::Value root;
Json::Reader reader;
ASSERT_TRUE(
reader.parse(contents.BeginReading(), contents.EndReading(), root));
Json::Value payload = root["payload"];
EXPECT_TRUE(payload.isMember("isNewInstall"));
EXPECT_TRUE(payload["isNewInstall"].asBool());
}
TEST_F(DowngradePingTest, IsNewInstallFalse) {
// Install happened before the lock time.
constexpr uint64_t kEpochOffset = 116444736000000000ULL;
int64_t installUnixSec = 1700000000;
uint64_t installFileTime =
uint64_t(installUnixSec) * PR_USEC_PER_SEC * 10 + kEpochOffset;
PRTime lockTime = PRTime(1701388800) * PR_MSEC_PER_SEC;
if (!WriteInstallTelemetryJson(installFileTime)) return;
auto result = GenerateDowngradeTelemetry(
"test-ping-id"_ns, "131.0_20250201000000/20250201000000"_ns, false, 0,
"test-channel"_ns, "default"_ns, mozilla::Some(lockTime), false);
ASSERT_TRUE(result);
mCreatedPingPath = *result;
nsCString contents;
ASSERT_TRUE(ReadPingFile(*result, contents));
Json::Value root;
Json::Reader reader;
ASSERT_TRUE(
reader.parse(contents.BeginReading(), contents.EndReading(), root));
Json::Value payload = root["payload"];
EXPECT_TRUE(payload.isMember("isNewInstall"));
EXPECT_FALSE(payload["isNewInstall"].asBool());
}
TEST_F(DowngradePingTest, IsNewInstallAbsentWithoutLockTime) {
constexpr uint64_t kEpochOffset = 116444736000000000ULL;
int64_t installUnixSec = 1701388800;
uint64_t installFileTime =
uint64_t(installUnixSec) * PR_USEC_PER_SEC * 10 + kEpochOffset;
if (!WriteInstallTelemetryJson(installFileTime)) return;
auto result = GenerateDowngradeTelemetry(
"test-ping-id"_ns, "131.0_20250201000000/20250201000000"_ns, false, 0,
"test-channel"_ns, "default"_ns, mozilla::Nothing(), false);
ASSERT_TRUE(result);
mCreatedPingPath = *result;
nsCString contents;
ASSERT_TRUE(ReadPingFile(*result, contents));
Json::Value root;
Json::Reader reader;
ASSERT_TRUE(
reader.parse(contents.BeginReading(), contents.EndReading(), root));
Json::Value payload = root["payload"];
EXPECT_FALSE(payload.isMember("isNewInstall"));
}
#else
TEST_F(DowngradePingTest, IsNewInstallNotPresentOnNonWindows) {
PRTime lockTime = PRTime(1700000000) * PR_MSEC_PER_SEC;
auto result = GenerateDowngradeTelemetry(
"test-ping-id"_ns, "131.0_20250201000000/20250201000000"_ns, false, 0,
"test-channel"_ns, "default"_ns, mozilla::Some(lockTime), false);
ASSERT_TRUE(result);
mCreatedPingPath = *result;
nsCString contents;
ASSERT_TRUE(ReadPingFile(*result, contents));
Json::Value root;
Json::Reader reader;
ASSERT_TRUE(
reader.parse(contents.BeginReading(), contents.EndReading(), root));
Json::Value payload = root["payload"];
EXPECT_FALSE(payload.isMember("isNewInstall"));
}
#endif