Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New analytics #4163

Merged
merged 34 commits into from
Feb 8, 2022
Merged

New analytics #4163

merged 34 commits into from
Feb 8, 2022

Conversation

kneth
Copy link
Contributor

@kneth kneth commented Dec 20, 2021

What, How & Why?

☑️ ToDos

  • 📝 Changelog entry
  • [ ] 📝 Compatibility label is updated or copied from previous entry
  • 🚦 Tests
  • [ ] 📱 Check the React Native/other sample apps work if necessary
  • [ ] 📝 Public documentation PR created or is not necessary
  • [ ] 💥 Breaking label has been applied or is not necessary

If this PR adds or changes public API's:

  • [ ] typescript definitions file is updated
  • [ ] jsdoc files updated
  • [ ] Chrome debug API is updated if API is available on React Native

@cla-bot cla-bot bot added the cla: yes label Dec 20, 2021
@kneth kneth changed the title Kneth/new analytics New analytics Dec 20, 2021
@kneth kneth self-assigned this Dec 20, 2021
@kneth kneth force-pushed the kneth/new-analytics branch 2 times, most recently from 295c009 to 5082a9d Compare January 13, 2022 16:02
@kneth
Copy link
Contributor Author

kneth commented Jan 13, 2022

Creating test package

To verify that the script is working as expected, I have modified package.json to:

"postinstall": "node scripts/submit-analytics.js --dryRun --log"

After creating a NPM package (as tgz), I have installed it in a plain node.js app and a React Native using the following command:

npm install --loglevel verbose -ddd --foreground-scripts ~/Projects/realm-js/realm-10.11.0.tgz

React Native

payload: {"webHook":{"event":"install","properties":{"token":"aab85907a13e1ff44a95be539d9942a9","JS Analytics Version":2,"distinct_id":"e6c2d8cd214bcb1925b36a120c4cfb7b069832fd78ba6b61dba48bde361d3e45","Anonymized Machine Identifier":"e6c2d8cd214bcb1925b36a120c4cfb7b069832fd78ba6b61dba48bde361d3e45","Anonymized Application ID":"e20852f3c853f54e4a8ce229ed53a93dfd12b0f7099cab55983db172de41c150","Binding":"javascript","Version":"0.0.1","Language":"javascript","Framework":"react-native","Framework Version":"0.66.4","JavaScript Engine":"hermes","Host OS Type":"darwin","Host OS Version":"20.6.0","Node.js version":"v16.13.0"}}}
Dry run; will not submit analytics

node.js

payload: {"webHook":{"event":"install","properties":{"token":"aab85907a13e1ff44a95be539d9942a9","JS Analytics Version":2,"distinct_id":"e6c2d8cd214bcb1925b36a120c4cfb7b069832fd78ba6b61dba48bde361d3e45","Anonymized Machine Identifier":"e6c2d8cd214bcb1925b36a120c4cfb7b069832fd78ba6b61dba48bde361d3e45","Anonymized Application ID":"c7f9a7ee0a2d96427a4e7d05d41d147b9eafa9d9a7f342de0976d605f00ccbc7","Binding":"javascript","Version":"1.0.0","Language":"javascript","Framework":"node.js","Framework Version":"v16.13.0","JavaScript Engine":"v8","Host OS Type":"darwin","Host OS Version":"20.6.0","Node.js version":"v16.13.0"}}}
Dry run; will not submit analytics

Electron

payload: {"webHook":{"event":"install","properties":{"token":"aab85907a13e1ff44a95be539d9942a9","JS Analytics Version":2,"distinct_id":"e6c2d8cd214bcb1925b36a120c4cfb7b069832fd78ba6b61dba48bde361d3e45","Anonymized Machine Identifier":"e6c2d8cd214bcb1925b36a120c4cfb7b069832fd78ba6b61dba48bde361d3e45","Anonymized Application ID":"c7f9a7ee0a2d96427a4e7d05d41d147b9eafa9d9a7f342de0976d605f00ccbc7","Binding":"javascript","Version":"1.0.0","Language":"javascript","Framework":"electron","Framework Version":"^16.0.7","JavaScript Engine":"v8","Host OS Type":"darwin","Host OS Version":"20.6.0","Node.js version":"v16.13.0"}}}
Dry run; will not submit analytics

@kneth kneth marked this pull request as ready for review January 21, 2022 16:04
Copy link
Contributor

@tomduncalf tomduncalf left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good, just some minor comments

Comment on lines 24 to 42
class Webhook {
/**
* Path and credentials required to submit analytics through the webhook (production mode).
*/
constructor() {
this.urlPrefix =
"https://webhooks.mongodb-realm.com/api/client/v2.0/app/realmsdkmetrics-zmhtm/service/metric_webhook/incoming_webhook/metric?ip=1&data=";
}

/**
* Constructs the full URL that will submit analytics to the webhook.
* @param {Object} payload Information that will be submitted through the webhook.
* @returns {string} Complete analytics submission URL
*/
buildRequest(payload) {
const request = this.urlPrefix + Buffer.from(JSON.stringify(payload.webHook), "utf8").toString("base64");
return request;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it might make sense to simplify this to just a constant + function rather than a class, as it doesn't do anything stateful. I don't have very strong feelings either way though so I'm happy for it to stay as it is equally!

Suggested change
class Webhook {
/**
* Path and credentials required to submit analytics through the webhook (production mode).
*/
constructor() {
this.urlPrefix =
"https://webhooks.mongodb-realm.com/api/client/v2.0/app/realmsdkmetrics-zmhtm/service/metric_webhook/incoming_webhook/metric?ip=1&data=";
}
/**
* Constructs the full URL that will submit analytics to the webhook.
* @param {Object} payload Information that will be submitted through the webhook.
* @returns {string} Complete analytics submission URL
*/
buildRequest(payload) {
const request = this.urlPrefix + Buffer.from(JSON.stringify(payload.webHook), "utf8").toString("base64");
return request;
}
}
/**
* Path and credentials required to submit analytics through the webhook (production mode).
*/
const ANALYTICS_BASE_URL = "https://webhooks.mongodb-realm.com/api/client/v2.0/app/realmsdkmetrics-zmhtm/service/metric_webhook/incoming_webhook/metric?ip=1&data=";
/**
* Constructs the full URL that will submit analytics to the webhook.
* @param {Object} payload Information that will be submitted through the webhook.
* @returns {string} Complete analytics submission URL
*/
const getAnalyticsRequestUrl = (payload) => ANALYTICS_BASE_URL + Buffer.from(JSON.stringify(payload.webHook), "utf8").toString("base64");

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to suggest something to the art of:

const ANALYTICS_BASE_URL = "https://webhooks.mongodb-realm.com/api/client/v2.0/app/realmsdkmetrics-zmhtm/service/metric_webhook/incoming_webhook/metric";

const requestUrl = `${ANALYTICS_BASE_URL}?ip=1&data=${getAnalyticsData()}`;

This makes the request more maintainable in the future.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of splitting it.

return;
}

const context = require("../../../package.json");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder whether there are any strange case where this require could not work as expected (e.g. a user has changed their NODE_PATH to somewhere else, or some new npm replacement decides to store packages somewhere else...) and what the impact would be if it does?

Perhaps it is worth wrapping this block in try/catch just in case.

Maybe there's an alternative strategy (e.g. walk up the directory tree looking for package.json), but perhaps that is overkill and it's OK to fail silently

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we added this to @realm/react then this would be one directory deeper.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could use the npm_config_local_prefix environment to get the path of the package we're getting installed into. We also have fs-extra as a dependency already, we might as well use readJsonSync here.

Also isn't it a biiiit excessive to send off the contents of the entire package.json? Isn't the dependencies object enough for what we want here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I dob't think we are sending off the entire package.json unless I misread the payload?

const payloads = {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ahh - my bad. Read it too fast.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if it is save to assume that we are interested in package.json in the same directory as node_modules. If so, we can use process.cwd() and chop off from node_modules and down.

Copy link
Member

@kraenhansen kraenhansen Jan 26, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The working directory is set by NPM to our package root, so that won't work.
I really think we use something like the npm_config_local_prefix environment variable.

scripts/submit-analytics.js Show resolved Hide resolved
frameworkVersion = context.dependencies["react-native"];
try {
const podfile = fs.readFileSync("../../ios/Podfile", "utf8");
if (podfile.includes("hermes_enabled => true")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This feels a little brittle – maybe a regex match would be better to allow for formatting/syntax differences?

Suggested change
if (podfile.includes("hermes_enabled => true")) {
if (/hermes_enabled.*true/.test(podfile)) {

framework = "react-native";
frameworkVersion = context.dependencies["react-native"];
try {
const podfile = fs.readFileSync("../../ios/Podfile", "utf8");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think there's much we can do about this, but just pointing out that this path is not necessarily fixed, e.g. in my old project we kept the Podfile somewhere else for reasons

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to try to detect if they have Hermes enabled on Android too or is iOS enough?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to see the Hermes vs. Android/JSC statistics

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I picked iOS since a Podfile is easier to parse 😄

frameworkVersion = context.dependencies["electron"];
}

const payloads = {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
const payloads = {
const payload = {

const https = require("https");

return new Promise((resolve, reject) => {
const webhookRequest = new Webhook().buildRequest(payload);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If my suggestion at the top is useful, then

Suggested change
const webhookRequest = new Webhook().buildRequest(payload);
const webhookRequest = getAnalyticsRequestUrl(payload);

}

await Promise.all([
// send in analytics in the newer S3 format
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this comment still useful as there's no mention of the older format?

Suggested change
// send in analytics in the newer S3 format

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed, it is mostly a comment for old-timers.

Comment on lines 193 to 194
// eslint-disable-next-line no-unused-vars
doLog = (_) => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or could this just be

Suggested change
// eslint-disable-next-line no-unused-vars
doLog = (_) => {
doLog = () => {

@@ -66,7 +66,6 @@ if (global.enableSyncTests) {
TESTS.SetSyncTests = node_require("./set-sync-tests");
TESTS.DictionarySyncTests = node_require("./dictionary-sync-tests");
TESTS.MixedSyncTests = node_require("./mixed-sync-tests");
TESTS.AnalyticsTests = require("./analytics-tests");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it's worth having some kind of integration test for the new way of doing analytics? Maybe one for a follow up in any case

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See my comment above about the staging. If we don't have access to such a bucket, I don't think we can meaningfully do integration tests on this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it could still be interesting to have, for example, a set of skeleton "test projects" with the various different Podfile/package.json permutations that we know of (RN Hermes iOS, RN Hermes Android, RN JSC iOS, RN JSC Android, Electron etc...), then run the script on them with a mocked https library and assert that (parts of) the payload match what we expect for each project.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree - having tests for this would be great.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At the moment, we will skip it all if CI is set - which Github Actions does.

Copy link
Contributor

@takameyer takameyer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. Just a few suggestions and comments. I'm wondering if we can repurpose this to be used in any of our packages? Probably as a part of the re-packaging effort.

README.md Outdated Show resolved Hide resolved
CHANGELOG.md Outdated Show resolved Hide resolved
react-native/android/build.gradle Outdated Show resolved Hide resolved
Comment on lines 24 to 42
class Webhook {
/**
* Path and credentials required to submit analytics through the webhook (production mode).
*/
constructor() {
this.urlPrefix =
"https://webhooks.mongodb-realm.com/api/client/v2.0/app/realmsdkmetrics-zmhtm/service/metric_webhook/incoming_webhook/metric?ip=1&data=";
}

/**
* Constructs the full URL that will submit analytics to the webhook.
* @param {Object} payload Information that will be submitted through the webhook.
* @returns {string} Complete analytics submission URL
*/
buildRequest(payload) {
const request = this.urlPrefix + Buffer.from(JSON.stringify(payload.webHook), "utf8").toString("base64");
return request;
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to suggest something to the art of:

const ANALYTICS_BASE_URL = "https://webhooks.mongodb-realm.com/api/client/v2.0/app/realmsdkmetrics-zmhtm/service/metric_webhook/incoming_webhook/metric";

const requestUrl = `${ANALYTICS_BASE_URL}?ip=1&data=${getAnalyticsData()}`;

This makes the request more maintainable in the future.

return;
}

const context = require("../../../package.json");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we added this to @realm/react then this would be one directory deeper.

@@ -0,0 +1,201 @@
////////////////////////////////////////////////////////////////////////////
//
// Copyright 2021 Realm Inc.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Copyright 2021 Realm Inc.
// Copyright 2022 Realm Inc.


class Webhook {
/**
* Path and credentials required to submit analytics through the webhook (production mode).
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we no longer have the option to submit to a staging bucket for testing?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so but I can check.

Copy link

@fronck fronck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please review comments.

If my concerns are wrong: LGTM :)

@kneth kneth force-pushed the kneth/new-analytics branch 4 times, most recently from d41cd23 to 90464f3 Compare January 27, 2022 17:30
}

async function submitAnalytics(dryRun) {
const wd = process.cwd();
Copy link
Member

@kraenhansen kraenhansen Jan 31, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still feel it's less brittle if we relied on process.env.npm_config_local_prefix instead.

}

const optionDefinitions = [
{ name: "dryRun", type: Boolean, multiple: false, description: "If true, don't submit analytics" },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm pretty sure these objects take a default value, making your next couple of lines obsolete.

Comment on lines 206 to 208
(async function () {
await submitAnalytics(dryRun);
})();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This might result on an unhandled promise rejection.

Suggested change
(async function () {
await submitAnalytics(dryRun);
})();
submitAnalytics(dryRun).catch(console.error);

cd integration-tests/tests
tmpfile=$(mktemp)
node ../../scripts/submit-analytics.js --test > $tmpfile
cat $tmpfile | grep ^payload | cut -f2- -d' ' | jq '.webHook.properties' | jq 'keys' | jq 'join(",")' | grep -c 'Anonymized Application ID,Anonymized Machine Identifier,Binding,Framework,Framework Version,Host OS Type,Host OS Version,JS Analytics Version,JavaScript Engine,Language,Node.js version,Version,distinct_id,token'
Copy link
Member

@kraenhansen kraenhansen Jan 31, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like the analytics code to be imported from the JS (integration) tests and the fetchPlatformData called with different contexts and asserted accordingly. I feel this bash assertion is a bit hard to maintain and it doesn't answer anything about the actual values in the object.

Copy link
Contributor Author

@kneth kneth Feb 2, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see your point and we can expand the testing.

The approach here is valuable as it is testing the script as such (including the package.json discovery parts).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

including the package.json discovery parts.

But it doesn't test the values of the payload properties?

kraenhansen and others added 3 commits February 4, 2022 14:52
* Suggested changes to the submit-analytics script

* Scaffolding Node.js specific analytics tests
@takameyer
Copy link
Contributor

takameyer commented Feb 7, 2022

@kneth With this change, can we remove the analytics from react-native/ios/RealmReact ? Specifically RealmAnalytics.h and RealmAnalytics.mm

@kneth kneth merged commit e6295a9 into master Feb 8, 2022
@kneth kneth deleted the kneth/new-analytics branch February 8, 2022 15:57
@github-actions github-actions bot locked as resolved and limited conversation to collaborators Mar 15, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants