diff --git a/toolkit/components/telemetry/docs/data/downgrade-ping.md b/toolkit/components/telemetry/docs/data/downgrade-ping.md index 7afac091b0b6..93ef719309e2 100644 --- a/toolkit/components/telemetry/docs/data/downgrade-ping.md +++ b/toolkit/components/telemetry/docs/data/downgrade-ping.md @@ -19,11 +19,17 @@ Structure: profileGroupId: , 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: , // Whether the profile is signed in to sync - hasBinary: , // Whether the last version of the application is available to run button: // The button the user chose to click from the UI: // 0 - Quit // 1 - Create new profile + isMSIX: , // Whether this install is an MSIX package + profileSelectionReason: "", // How the profile was selected during startup (see the startup.profile_selection_reason metric) + daysSinceLock: , // (optional) Days since the profile lock was last held + isNewInstall: , // (Windows only, optional) Whether the current install happened after the profile was last locked + isNewUpdate: , // (optional) Whether the last update was applied after the profile was last locked + isDifferentInstall: , // Whether the profile was last used by a different install of the application } } ``` diff --git a/toolkit/profile/nsIToolkitProfile.idl b/toolkit/profile/nsIToolkitProfile.idl index 10be1a624b8d..6b6d94d251a1 100644 --- a/toolkit/profile/nsIToolkitProfile.idl +++ b/toolkit/profile/nsIToolkitProfile.idl @@ -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; diff --git a/toolkit/profile/nsToolkitProfileService.cpp b/toolkit/profile/nsToolkitProfileService.cpp index f9768c7d4b3f..bf884a1ce673 100644 --- a/toolkit/profile/nsToolkitProfileService.cpp +++ b/toolkit/profile/nsToolkitProfileService.cpp @@ -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) { diff --git a/toolkit/profile/nsToolkitProfileService.h b/toolkit/profile/nsToolkitProfileService.h index 539ef9074aec..0952230f4852 100644 --- a/toolkit/profile/nsToolkitProfileService.h +++ b/toolkit/profile/nsToolkitProfileService.h @@ -95,6 +95,7 @@ class nsToolkitProfileService final : public nsIToolkitProfileService { bool HasShowProfileSelector(); void UpdateCurrentProfile(); void CompleteStartup(); + const nsACString& ProfileSelectionReason(); using AsyncFlushPromise = mozilla::MozPromise; diff --git a/toolkit/xre/moz.build b/toolkit/xre/moz.build index 1450382bbb81..13445c86026b 100644 --- a/toolkit/xre/moz.build +++ b/toolkit/xre/moz.build @@ -256,6 +256,7 @@ LOCAL_INCLUDES += [ "/js/xpconnect/loader", "/testing/gtest/mozilla", "/third_party/sqlite3/ext/", + "/toolkit/components/jsoncpp/include", "/toolkit/crashreporter", "/xpcom/build", ] diff --git a/toolkit/xre/nsAppRunner.cpp b/toolkit/xre/nsAppRunner.cpp index 1fc4c231450d..2512c029ec8a 100644 --- a/toolkit/xre/nsAppRunner.cpp +++ b/toolkit/xre/nsAppRunner.cpp @@ -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 +#include #ifdef XP_UNIX # include @@ -3322,10 +3325,66 @@ struct FileWriteFunc final : public JSONWriteFunc { } }; -Maybe 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 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(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(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(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 GenerateDowngradeTelemetry( + const nsACString& aPingId, const nsCString& aLastVersion, bool aHasSync, + int32_t aButton, const nsACString& aChannel, + const nsACString& aProfileSelectionReason, + mozilla::Maybe aReplacedLockTime, bool aIsDifferentInstall) { nsCOMPtr prefSvc = do_GetService("@mozilla.org/preferences-service;1"); NS_ENSURE_TRUE(prefSvc, Nothing()); @@ -3358,10 +3417,47 @@ Maybe GenerateDowngradeTelemetry(const nsACString& aPingId, } # endif - time_t now; - time(&now); + mozilla::Maybe maybeInstallTime; + mozilla::Maybe maybeUpdateTime; + nsCOMPtr 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 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 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 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 aReplacedLockTime, + const nsCString& aLastVersion, + bool aHasSync, int32_t aButton, + bool aIsDifferentInstall) { nsCOMPtr prefSvc = do_GetService("@mozilla.org/preferences-service;1"); NS_ENSURE_TRUE_VOID(prefSvc); @@ -3513,7 +3633,8 @@ static void SubmitDowngradeTelemetry(const nsCString& aLastVersion, } Maybe 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 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 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 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; diff --git a/toolkit/xre/nsAppRunner.h b/toolkit/xre/nsAppRunner.h index 8c21e10f1e61..c86d0a2b0e17 100644 --- a/toolkit/xre/nsAppRunner.h +++ b/toolkit/xre/nsAppRunner.h @@ -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 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 aReplacedLockTime, bool aIsDifferentInstall); bool BuildDowngradePingUrl(const nsACString& aPingId, const nsACString& aChannel, nsACString& aUrlOut); diff --git a/toolkit/xre/test/gtest/TestCheckCompatibility.cpp b/toolkit/xre/test/gtest/TestCheckCompatibility.cpp index 9549147726f9..2bc811cbb888 100644 --- a/toolkit/xre/test/gtest/TestCheckCompatibility.cpp +++ b/toolkit/xre/test/gtest/TestCheckCompatibility.cpp @@ -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 tmpDir; + NS_GetSpecialDirectory(NS_OS_TEMP_DIR, getter_AddRefs(tmpDir)); + nsCOMPtr 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"); diff --git a/toolkit/xre/test/gtest/TestDowngradePing.cpp b/toolkit/xre/test/gtest/TestDowngradePing.cpp index 247ede214342..81e952ef5d0b 100644 --- a/toolkit/xre/test/gtest/TestDowngradePing.cpp +++ b/toolkit/xre/test/gtest/TestDowngradePing.cpp @@ -2,9 +2,12 @@ * http://creativecommons.org/publicdomain/zero/1.0/ */ +#include + #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 dirSvc = do_GetService(NS_DIRECTORY_SERVICE_CONTRACTID); @@ -99,6 +106,65 @@ class DowngradePingTest : public ::testing::Test { fclose(f); return bytesRead == static_cast(fileSize); } + + bool WriteUpdateTelemetryJson(uint64_t aTimestampMs) { + nsCOMPtr greDir; + if (NS_FAILED(NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(greDir)))) + return false; + nsCOMPtr 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 greDir; + if (NS_FAILED(NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(greDir)))) + return; + nsCOMPtr 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 greDir; + if (NS_FAILED(NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(greDir)))) + return false; + nsCOMPtr 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 greDir; + if (NS_FAILED(NS_GetSpecialDirectory(NS_GRE_DIR, getter_AddRefs(greDir)))) + return; + nsCOMPtr 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