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

feat(node): new unified code snippet for both the webhook and metrics #879

Merged
merged 66 commits into from
Feb 6, 2024

Conversation

mjcuva
Copy link
Member

@mjcuva mjcuva commented Jul 17, 2023

Redo the node code snippet for the webhook/metrics! We now have one code snippet that handles both cases super easily.

import { ReadMe } from 'readmeio'

const readme = ReadMe('API_KEY')

router.use(
  readme(async (req, getUser) => {
    // We pass in getUser which will call the correct function to fetch a 
    // user depending on what we have in the request
    const user = getUser({
      byEmail: getUserByEmail, // Function defined by them that returns a user given an email address
      byAPIKey: getUserByAPIKey, // Function defined by them that returns a user given an API key
      manualAPIKey: getAPIKey(req.headers.authorization), // Optional way to explicitly tell us the api key if we can't guess it
    });

    return {
      name: user.name,
      email: user.email,
      keys: user.apiKeys,
    };
  }, {
    development: process.env.NODE_ENV === 'development', // require development: true to show local testing page
  })
);

Other things:

  • Added a /readme-setup page that walks users through making sure everything is configured properly. This page is only enabled if development: true to make sure that it won't be enabled in prod on accident.
  • Handles all of the metrics config in additional options parameter (which is optional)
  • Exported everything that was already there, so this is purely additive
  • User provided function should return all of the information about the user, even if it's not relevant for the specific API call that is happening. This means we can have one set of config for the webhook and metrics and the sdk will just figure out what is relevant for the call to metrics.
  • Makes a call to the ReadMe API to get the JWT Secret & stable version (for linking to the dash)

Update 1/31

  • I renamed some of the exports to clean it up a bit. Now we just have a ReadMe export that you call to pass in the API Key. Before we had a separate auth export that was required to be called before the middelware, which was confusing. This also cleans up how you call it (no more readmeio.readme stuff). This matches Sentry's sdks closer which makes me feel better about it.

Testing

Things to look out for:

  • Do the docs make sense?
  • Issues with copy in the local setup page?
  • Testing various APIs (with various auth types)

packages/node/src/index.ts Outdated Show resolved Hide resolved
@kanadgupta kanadgupta mentioned this pull request Aug 23, 2023
4 tasks
kanadgupta and others added 6 commits September 6, 2023 11:05
* chore(node): tidy up types

* test: add better types to testbed

* chore: refresh lockfile

i had to downgrade to npm@7 for this... absolutely cursed

* chore: strict config fixes

* chore: more TS fixes

not sure why VS code didn't flag these the first time 🤔

also i know this is nuts but open to alternatives lol

* chore: use promise.resolve generics

Co-Authored-By: Jon Ursenbach <[email protected]>

* test: add test for undefined webhook

Co-Authored-By: Jon Ursenbach <[email protected]>

* refactor: consolidate type

this is the shit i live for

* chore: slightly reorganize tsconfigs

* revert: var change

* refactor: add'l stronger types

* refactor: another strict type

Co-Authored-By: Bill Mill <[email protected]>

* refactor: consolidate type

Co-Authored-By: Bill Mill <[email protected]>

* refactor: more type passes

Co-Authored-By: Bill Mill <[email protected]>

* chore: better comment

* chore: another stricter type

Co-Authored-By: Aaron Hedges <[email protected]>

* refactor: clientIPAddress is required

* revert: oops didn't mean to commit this

* refactor: stricter typing for headers and method

* chore: ts cleanup in test-verify-webhook file

* refactor(setup readme view): use crypto, TS cleanup

* chore: lint

* refactor: some low hanging fruit in index.ts

* chore: initial commit of example repo

* chore: replace snippet with marc's snippet

source: https://gist.github.com/mjcuva/c23a7c08d9528eeb8687ab83132622c1

* fix: some fixes to get the snippet working

* chore: swap out api usage

* chore: more type fixes

i dislike a lot of these

* fix: add JSON body parser to get onboarding working

* fix: use CJS api SDK instead

fixes this error:

```
/Users/kanadg/Code/readmeio/metrics-sdks/packages/node/.api/apis/developers/index.ts:1
import type * as types from './types';
^^^^^^

SyntaxError: Cannot use import statement outside a module
    at Object.compileFunction (node:vm:360:18)
    at wrapSafe (node:internal/modules/cjs/loader:1124:15)
    at Module._compile (node:internal/modules/cjs/loader:1160:27)
    at Object.Module._extensions..js (node:internal/modules/cjs/loader:1250:10)
    at Module.load (node:internal/modules/cjs/loader:1074:32)
    at Function.Module._load (node:internal/modules/cjs/loader:909:12)
    at Module.require (node:internal/modules/cjs/loader:1098:19)
    at require (node:internal/modules/cjs/helpers:108:18)
    at Object.<anonymous> (metrics-sdks/packages/node/dist/src/index.js:55:36)
    at Module._compile (node:internal/modules/cjs/loader:1196:14)
```

might be an api bug?

* fix: more typing fixes as a result of writing unit tests

* test: a couple of unit tests

(more to come)

* revert: don't commit example server for now

* fix(prettier): ignore .api directory

* chore: attempt to rebuild lockfiles?

* refactor: don't load in api as nested subpackage thing

* Revert "refactor: don't load in api as nested subpackage thing"

This reverts commit 650a8e3.

* Revert "chore: attempt to rebuild lockfiles?"

This reverts commit e260de6.

* chore: try doing this again

* chore: try this instead

* test: migrate nocks over to MSW

* revert: no need to export this

* revert: don't fork tests

* ci: run all tests

* fix: move `.api` directory so TS picks it up

* chore: lint

* chore: lint, migrate new tests over to vitest

* chore: regenerate `api` SDK, this time as TS

* chore: one more lockfile cleanup

have i mentioned i hate this? because i hate this

* chore: one more lockfile refresh my god

* chore: try this lockfile refresh

* chore(lint): my dev environment is gaslighting me

* test: wait there's no way that this is the fix lmao

---------

Co-authored-by: Jon Ursenbach <[email protected]>
Co-authored-by: Bill Mill <[email protected]>
Co-authored-by: Aaron Hedges <[email protected]>
* feat: new getUser function

* fix: types
@mjcuva mjcuva changed the title (wip): Node/unified snippet feat(node): new unified code snippet for both the webhook and metrics Sep 6, 2023
@mjcuva mjcuva marked this pull request as ready for review January 12, 2024 20:44
@mjcuva mjcuva requested review from kanadgupta, erunion, jamestclark and a team January 12, 2024 20:44
Copy link
Member

@erunion erunion left a comment

Choose a reason for hiding this comment

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

does the sdk-snippets library need to get updated too?

packages/node/src/index.ts Outdated Show resolved Hide resolved
packages/node/src/index.ts Outdated Show resolved Hide resolved
packages/node/src/index.ts Outdated Show resolved Hide resolved
packages/node/src/index.ts Outdated Show resolved Hide resolved
packages/node/src/lib/test-verify-webhook.ts Outdated Show resolved Hide resolved
packages/node/src/lib/test-verify-webhook.ts Outdated Show resolved Hide resolved
metricsServerResponseCode = 202;
server.use(
...[
rest.post(`${config.host}/v1/request`, async (req, res, ctx) => {
Copy link
Member

Choose a reason for hiding this comment

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

@kanadgupta probably worth upgrading this to the latest msw

@@ -13,6 +13,7 @@
},
"include": [
"src/**/*",
"src/.api/**/*",
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: is the not redundant when we already have src/**/*?

Copy link
Member Author

Choose a reason for hiding this comment

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

@kanadgupta added this so I'll let him chime in with more context!

Copy link
Member

Choose a reason for hiding this comment

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

i think this is because tsconfig ignores directories that start with a period by default, but let me double check that. this may have been an accidental holdover from when i was getting the SDK up and running

Copy link
Member

Choose a reason for hiding this comment

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

yeah looks like those directories aren't included by default, see microsoft/TypeScript#49555


import findAPIKey from '../../src/lib/find-api-key';

// TODO: These tests were written by GPT-4 so probabbly aren't the best
Copy link
Contributor

Choose a reason for hiding this comment

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

lol

}

it('should send requests to the metrics server', async function () {
expect.assertions(10);
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: the expect.assertions is a little confusing. Could we sub it with toHaveBeenCalledTimes?

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

mock functions aren't applicable here, expect.assertions ensures that all the expect statements over here are being hit properly:

expect(body[0]._version).toBe(3);
expect(body[0].group).toStrictEqual(outgoingGroup);
expect(typeof body[0].request.log.entries[0].startedDateTime).toBe('string');

this pattern isn't great and i'm curious if others that have been in the weeds with MSW have better ideas for asserting request header content (or if that's even necessary) // @erunion

Copy link
Member

Choose a reason for hiding this comment

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

doing expect() in here or having the MSW mock return a 500 status code with a specific error is probably the best way to handle these

}

const securityRequirements = operation.getSecurity();
return securityRequirements.reduce((acc: string[], curr) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

just want to make sure I'm reading this right, but securityRequirements is an array of objects and then we reduce that to an array of all the keys on that array of objects? Does it matter that we may have duplicate entries of the same key?

Copy link
Member Author

Choose a reason for hiding this comment

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

@jamestclark added this but I'm struggling to find how it is used. It's possible this existed in the main codebase and we reused parts of it in the sdk? However I don't think we have the OAS file at any point so I'm not sure

Copy link
Contributor

Choose a reason for hiding this comment

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

Doesn't matter that we may have duplicates as we just loop through the array and find the first matching from the users apiKey.

And yeah it's not used directly by the metrics SDK. As the code is very similar to what we already had in the main codebase he idea here is to co-locate all the securityRequirements code here and have the main codebase use this once it's published.

@brendanluna
Copy link
Contributor

Adding @azinder1 as a reviewer here so he has context on unified snippet work (this came up just now in Metrics Pod sync!)

@mjcuva
Copy link
Member Author

mjcuva commented Jan 16, 2024

@erunion We likely will need to update sdk-snippets, but we're updating the UI in the dashboard separate from this work so I think we can just do that in another pr!

packages/node/src/index.ts Outdated Show resolved Hide resolved
packages/node/src/index.ts Outdated Show resolved Hide resolved
packages/node/src/index.ts Outdated Show resolved Hide resolved
packages/node/src/index.ts Outdated Show resolved Hide resolved
packages/node/src/lib/ReadMe.ts Outdated Show resolved Hide resolved
* your dashboard. You can read more about the API key in
* [our docs](https://docs.readme.com/reference/authentication).
*/
function ReadMe(key: string) {
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 curious about others' opinions on this but my understanding of JS conventions is that variable names should only be capitalized in the case of classes/constructors (e.g., new Class(), etc.) — thoughts on lowercasing it? what about calling this metrics or something like that?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sentry was my inspo here and they don't do that! https://docs.sentry.io/platforms/node/#configure

Copy link
Member

Choose a reason for hiding this comment

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

interesting! sentry's SDK always gave class-like energy with the whole Sentry.init constructor pattern and it's apparently using AsyncLocalStorage to do that (which i don't totally understand...)

another idea here — what if you made this a class with a constructor? your example would look mostly the same (arguably a little cleaner + more intuitive to someone that expects JS conventions, in my opinion) and that way you can store these values as properties rather than globally which feels a little safer:

import { ReadMe } from 'readmeio';
- const readme = ReadMe(key);
+ const readme = new ReadMe(key);
app.use(readme.express(() => {}));

Copy link
Member Author

Choose a reason for hiding this comment

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

I think it's fine the way it is! It mimics other popular sdks and I don't really want to refactor this anymore tbh. I think it's important we get something out and stop going back and forth on nuances of the design!

Copy link
Member

Choose a reason for hiding this comment

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

agreed that it's a little weird to have a function be cased this way when that's generally how classes are defined + used.

what other popular sdks have exports like this?

Copy link
Member

Choose a reason for hiding this comment

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

@kanadgupta I like the idea! It's definitely best practice from an initialization perspective, and would properly set expectations for consuming developers.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think if it's fine for Sentry it's understandable enough for our use case. I'm gonna move forward with it as it is! This PR is already way too long lived and we need to call off the rewriting somewhere. If someone else wants to take over this work feel free!

kanadgupta and others added 2 commits February 1, 2024 13:52
## 🧰 Changes

This updates the SDK to be a class. As part of this, the signature is
slightly updated:

```diff
import { ReadMe } from 'readmeio';
- const readme = ReadMe(key);
+ const readme = new ReadMe(key);
app.use(readme.express(() => {}));
```

## 🧬 QA & Testing

Strongly suggest hiding whitespace before reviewing! Does this signature
look good? Do tests still pass?
@mjcuva mjcuva merged commit 326e953 into main Feb 6, 2024
41 checks passed
@mjcuva mjcuva deleted the node/unified-snippet branch February 6, 2024 16:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants