UNPKG

realm

Version:

Realm is a mobile database: an alternative to SQLite and key-value stores

344 lines (306 loc) 12.1 kB
//////////////////////////////////////////////////////////////////////////// // // Copyright 2022 Realm Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // //////////////////////////////////////////////////////////////////////////// // Submits install information to Realm. // // Why are we doing this? In short, because it helps us build a better product // for you. None of the data personally identifies you, your employer or your // app, but it *will* help us understand what language you use, what Node.js // versions you target, etc. Having this info will help prioritizing our time, // adding new features and deprecating old features. Collecting an anonymized // application path & anonymized machine identifier is the only way for us to // count actual usage of the other metrics accurately. If we don’t have a way to // deduplicate the info reported, it will be useless, as a single developer // `npm install`-ing the same app 10 times would report 10 times more than another // developer that only installs once, making the data all but useless. // No one likes sharing data unless it’s necessary, we get it, and we’ve // debated adding this for a long long time. If you truly, absolutely // feel compelled to not send this data back to Realm, then you can set an // environment variable named REALM_DISABLE_ANALYTICS. // // Currently the following information is reported: // - What version of Realm is being installed. // - The OS platform and version which is being used. // - Node.js version numbers. // - JavaScript framework (React Native and Electron) version numbers. // - An anonymous machine identifier and hashed application path to aggregate // the other information on. import fs from "node:fs"; import { fileURLToPath } from "node:url"; import path from "node:path"; import process from "node:process"; import https from "node:https"; import os from "node:os"; import console from "node:console"; import { createHmac } from "node:crypto"; import { Buffer } from "node:buffer"; import machineId from "node-machine-id"; import createDebug from "debug"; export const debug = createDebug("realm:submit-analytics"); export { collectPlatformData }; // emulate old __dirname: https://flaviocopes.com/fix-dirname-not-defined-es-module-scope/ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); /** * Path and credentials required to submit analytics through the webhook. */ const ANALYTICS_BASE_URL = "https://data.mongodb-api.com/app/realmsdkmetrics-zmhtm/endpoint/metric_webhook/metric"; /** * Constructs the full URL that will submit analytics to the webhook. * @param payload Information that will be submitted through the webhook. * @returns Complete analytics submission URL */ const getAnalyticsRequestUrl = (payload) => ANALYTICS_BASE_URL + "?data=" + Buffer.from(JSON.stringify(payload.webHook), "utf8").toString("base64"); /** * Generate a hash value of data using salt. * @returns base64 encoded SHA256 of data */ function sha256(data) { const salt = "Realm is great"; return createHmac("sha256", Buffer.from(salt)).update(data).digest().toString("base64"); } /** * Finds the root directory of the project. * @returns the root of the project */ function getProjectRoot() { let wd = process.env.npm_config_local_prefix; if (!wd) { wd = process.cwd(); const index = wd.indexOf("node_modules"); wd = index === -1 ? wd : wd.slice(0, index); } return wd; } /** * Finds and read package.json * @returns package.json as a JavaScript object */ function getPackageJson(packagePath) { const packageJsonPath = path.resolve(packagePath, "package.json"); const packageJson = fs.readFileSync(packageJsonPath, "utf-8"); return JSON.parse(packageJson); } /** * Heuristics to decide if analytics should be disabled. * @returns true if analytics is disabled */ function isAnalyticsDisabled() { let isDisabled = false; // NODE_ENV is commonly used by JavaScript framework if ("NODE_ENV" in process.env) { isDisabled |= process.env["NODE_ENV"] === "production" || process.env["NODE_ENV"] === "test"; } // If the user has specifically opted-out or if we're running in a CI environment isDisabled |= "REALM_DISABLE_ANALYTICS" in process.env || "CI" in process.env; return isDisabled; } function getRealmVersion() { const packageJsonPath = path.resolve(__dirname, "..", "package.json"); const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf-8")); return packageJson["version"]; } /** * Reads and parses `dependencies.list`. * Each line has be form "KEY=VALUE", and we to find "REALM_CORE_VERSION" * @returns the Realm Core version as a string */ function getRealmCoreVersion() { const dependenciesListPath = path.resolve(__dirname, "../dependencies.list"); const dependenciesList = fs .readFileSync(dependenciesListPath) .toString() .split("\n") .map((s) => s.split("=")); return dependenciesList.find((e) => e[0] === "REALM_CORE_VERSION")[1]; } /** * Save the anonymized bundle ID for later usage at runtime. */ function saveBundleId(anonymizedBundleId) { const localPath = path.resolve(path.join(getProjectRoot(), "node_modules", "realm")); fs.mkdirSync(localPath, { recursive: true }); const realmConstantsFile = path.resolve(path.join(localPath, "realm-constants.json")); const realmConstants = { REALM_ANONYMIZED_BUNDLE_ID: anonymizedBundleId }; fs.writeFileSync(realmConstantsFile, JSON.stringify(realmConstants)); } /** * Determines if `npm` or `yarn` is used. * @returns An array with two elements: method and version */ function getInstallationMethod() { const userAgent = process.env["npm_config_user_agent"]; return userAgent.split(" ")[0].split("/"); } /** * Collect analytics data from the runtime system * @returns Analytics payload */ async function collectPlatformData(packagePath = getProjectRoot()) { // node-machine-id returns the ID SHA-256 hashed, if we cannot get the ID we send hostname instead let identifier; try { identifier = await machineId.machineId(); } catch (err) { debug(`Cannot get machine id: ${err}`); identifier = os.hostname(); } const realmVersion = getRealmVersion(); const realmCoreVersion = getRealmCoreVersion(); let framework = "node.js"; let frameworkVersion = process.version.slice(1); // skip the leading 'v' let jsEngine = "v8"; let bundleId = "unknown"; const packageJson = getPackageJson(packagePath); if (packageJson.name) { bundleId = packageJson["name"]; } const anonymizedBundleId = sha256(bundleId); saveBundleId(anonymizedBundleId); if (packageJson.dependencies && packageJson.dependencies["react-native"]) { framework = "react-native"; frameworkVersion = packageJson.dependencies["react-native"]; } if (packageJson.devDependencies && packageJson.devDependencies["react-native"]) { framework = "react-native"; frameworkVersion = packageJson.devDependencies["react-native"]; } if (framework === "react-native") { try { const podfilePath = path.resolve(packagePath, "ios", "Podfile"); const podfile = fs.readFileSync(podfilePath, "utf8"); if (/hermes_enabled.*true/.test(podfile)) { jsEngine = "hermes"; } else { jsEngine = "jsc"; } } catch (err) { debug(`Cannot read ios/Podfile: ${err}`); jsEngine = "unknown"; } try { const rnPath = path.resolve(packagePath, "node_modules", "react-native", "package.json"); const rnPackageJson = JSON.parse(fs.readFileSync(rnPath, "utf-8")); frameworkVersion = rnPackageJson["version"]; } catch (err) { debug(`Cannot read react-native package.json: ${err}`); } } if (packageJson.dependencies && packageJson.dependencies["electron"]) { framework = "electron"; frameworkVersion = packageJson.dependencies["electron"]; } if (packageJson.devDependencies && packageJson.devDependencies["electron"]) { framework = "electron"; frameworkVersion = packageJson.devDependencies["electron"]; } if (framework === "electron") { try { const electronPath = path.resolve(packagePath, "node_modules", "electron", "package.json"); const electronPackageJson = JSON.parse(fs.readFileSync(electronPath, "utf-8")); frameworkVersion = electronPackageJson["version"]; } catch (err) { debug(`Cannot read electron package.json: ${err}`); } } // JavaScript or TypeScript - we don't consider Flow as a programming language let language = "javascript"; let languageVersion = "unknown"; if (packageJson.dependencies && packageJson.dependencies["typescript"]) { language = "typescript"; languageVersion = packageJson.dependencies["typescript"]; } if (packageJson.devDependencies && packageJson.devDependencies["typescript"]) { language = "typescript"; languageVersion = packageJson.devDependencies["typescript"]; } if (language === "typescript") { try { const typescriptPath = path.resolve(packagePath, "node_modules", "typescript", "package.json"); const typescriptPackageJson = JSON.parse(fs.readFileSync(typescriptPath, "utf-8")); languageVersion = typescriptPackageJson["version"]; } catch (err) { debug(`Cannot read typescript package.json: ${err}`); } } const installationMethod = getInstallationMethod(); return { token: "ce0fac19508f6c8f20066d345d360fd0", "JS Analytics Version": 3, distinct_id: identifier, "Anonymized Builder Id": sha256(identifier), "Anonymized Bundle Id": anonymizedBundleId, "Realm Version": realmVersion, Binding: "Javascript", Version: packageJson.version, Language: language, "Language Version": languageVersion, Framework: framework, "Framework Version": frameworkVersion, "Host OS Type": os.platform(), "Host OS Version": os.release(), "Host CPU Arch": os.arch(), "Node.js version": process.version.slice(1), "Core Version": realmCoreVersion, "Sync Enabled": true, "Installation Method": installationMethod[0], "Installation Method Version": installationMethod[1], "Runtime Engine": jsEngine, }; } /** * Collect and send analytics data to MongoDB over HTTPS * If `REALM_DISABLE_ANALYTICS` is set, no data is submitted to MongoDB */ async function submitAnalytics() { const data = await collectPlatformData(); const payload = { webHook: { event: "install", properties: data, }, }; debug(`payload: ${JSON.stringify(payload)}`); if ("REALM_PRINT_ANALYTICS" in process.env) { console.log("REALM ANALYTICS", JSON.stringify(data, null, 2)); } if (isAnalyticsDisabled()) { debug("Analytics is disabled"); return; } return new Promise((resolve, reject) => { // node 19 turns on keep-alive by default and it will make the https.get() to hang // https://github.com/realm/realm-js/issues/5136 const requestUrl = getAnalyticsRequestUrl(payload); https .get(requestUrl, { agent: new https.Agent({ keepAlive: false }) }, (res) => { resolve({ statusCode: res.statusCode, statusMessage: res.statusMessage, }); }) .on("error", (error) => { const message = error && error.message ? error.message : error; const err = new Error(`Failed to dispatch analytics: ${message}`); reject(err); }); }); } if (process.argv[1] === fileURLToPath(import.meta.url)) { submitAnalytics().catch(console.error); }