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(utils): Introduce Baggage API #5066

Merged
merged 5 commits into from May 17, 2022
Merged

Conversation

AbhiPrasad
Copy link
Member

@AbhiPrasad AbhiPrasad commented May 10, 2022

This patch introduced a basic data structure for baggage, as well as controls to create, manipulate and serialize the baggage data structure.

The baggage data structure represents key,value pairs based on the baggage spec: https://www.w3.org/TR/baggage

It is expected that users interact with baggage using the helpers methods: createBaggage, getBaggageValue, and setBaggageValue.

Internally, the baggage data structure is a tuple of length 2, separating baggage values based on if they are related to Sentry or not. If the baggage values are set/used by sentry, they will be stored in an object to be easily accessed. If they are not, they are kept as a string to be only accessed when serialized at baggage propagation time.

As a next step, let's add the baggage values to the envelope header so they can be processed and used by relay - this will allow us to do some early validations of our assumptions.

Resolves https://getsentry.atlassian.net/browse/WEB-910

@github-actions
Copy link
Contributor

github-actions bot commented May 10, 2022

size-limit report 📦

Path Size
@sentry/browser - ES5 CDN Bundle (gzipped + minified) 18.75 KB (-6.91% 🔽)
@sentry/browser - ES5 CDN Bundle (minified) 58.19 KB (-9.95% 🔽)
@sentry/browser - ES6 CDN Bundle (gzipped + minified) 17.5 KB (-7.24% 🔽)
@sentry/browser - ES6 CDN Bundle (minified) 52.43 KB (-9.56% 🔽)
@sentry/browser - Webpack (gzipped + minified) 19.33 KB (-16.84% 🔽)
@sentry/browser - Webpack (minified) 61.44 KB (-24.82% 🔽)
@sentry/react - Webpack (gzipped + minified) 19.35 KB (-16.88% 🔽)
@sentry/nextjs Client - Webpack (gzipped + minified) 42.81 KB (-10.92% 🔽)
@sentry/browser + @sentry/tracing - ES5 CDN Bundle (gzipped + minified) 24.41 KB (-6.39% 🔽)
@sentry/browser + @sentry/tracing - ES6 CDN Bundle (gzipped + minified) 22.96 KB (-6.24% 🔽)

Copy link
Member

@Lms24 Lms24 left a comment

Choose a reason for hiding this comment

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

Hope you don't mind me reviewing a draft but I wanted to dive in deeper into the Baggage API discussions. Overall, this looks good to me. I left some questions to better understand some aspects. Will review again when this is ready for review.

Oh, one more question: Why are we adding this to utils rather than to the tracing package? I thought that Baggage is only useful when people install tracing?
EDIT: nvm I think I figured it out: If we added it to tracing, we couldn't add the baggage header to outgoing requests without importing @sentry/tracing in other packages (e.g. browser/core?) which we don't want to do, right? To get rid of the dependency again, we'd need something similar to hubextensions to do that which adds more complexity (?)

Comment on lines 1 to 2
export type AllowedBaggageKeys = 'environment' | 'release'; // TODO: Add remaining allowed baggage keys | 'transaction' | 'userid' | 'usersegment';
export type Baggage = Partial<Record<AllowedBaggageKeys, string> & Record<string, string>>;
Copy link
Member

Choose a reason for hiding this comment

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

Probably a dumb question but what's the advantage of making this a record instead of creating e..g a Baggage interface with the baggage keys as properties?

Copy link
Member Author

Choose a reason for hiding this comment

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

A record type is just sugar over creating a interface: https://www.typescriptlang.org/docs/handbook/utility-types.html#recordkeys-type - this just makes is so that to add additional keys we just need to add to the union type, not to the interface.

Copy link
Member

Choose a reason for hiding this comment

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

Jumping on this thread, I would vote for just making this an interface if it's not too much of an effort to change. Currently it is quite hard to read.

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 that an interface is easier to understand. Kinda had to analyse what this type is but after reading the TS docs it makes more sense to me. Plus I wonder if we need the flexibility of the Record because I guess those properties won't change too often (might be wrong about it though).
But as long as we only need the types internally, I think we can live with it either way. If users are expected to use it, I'd vote for an interface for easier understandability.

Copy link
Member Author

Choose a reason for hiding this comment

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

type BaggageObj = Partial<Record<AllowedBaggageKeys, string> & Record<string, string>>;

desugars down to

type BaggageObj = {
    // comes from Record<string, string>
    [x: string]: string | undefined;
    // comes from Record<AllowedBaggageKeys, string>
    environment?: string | undefined;
    release?: string | undefined;
}

IMO the union type makes it clear how the two object types (created by the Record type) will come together to form the final object structure. In addition, users have to use the functions to interact with baggage - it should be sufficiently abstracted from them.

packages/utils/src/baggage.ts Outdated Show resolved Hide resolved
@AbhiPrasad AbhiPrasad added this to the Dynamic Sampling milestone May 12, 2022
@AbhiPrasad AbhiPrasad marked this pull request as ready for review May 12, 2022 19:07
@AbhiPrasad
Copy link
Member Author

This is now ready to review!

Copy link
Member

@Lms24 Lms24 left a comment

Choose a reason for hiding this comment

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

LGTM! Just some small remarks left but then this is ready to go IMO

packages/utils/src/baggage.ts Outdated Show resolved Hide resolved
packages/utils/src/baggage.ts Outdated Show resolved Hide resolved
Copy link
Member

@lforst lforst left a comment

Choose a reason for hiding this comment

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

Just want to get one train of thought in: Isn't (because of immutability) the only operation we really need something like:

// Does not mutate already existing keys.
function addValueToBaggageString(
  baggageString: string,
  key: 'sentry-environment' | 'sentry-release',
  value: string
): string;
  
// or

function addValuesToBaggageString(
  baggageString: string,
  keyValuePairs: ['sentry-environment' | 'sentry-release', string][],
): string;

What use cases did you have in mind for the baggage object? Do we ever read singular values from the string?

EDIT: Ok just noticed the mutating section in the spec. Question still is: Do we ever want to mutate?

}

/** Parse a baggage header to a string */
export function parseBaggageString(baggageString: string): Baggage {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
export function parseBaggageString(baggageString: string): Baggage {
export function parseBaggageString(inputBaggageString: string): Baggage {

Nit: Can be anything but there bagageString is in the same scope twice.

/** Serialize a baggage object */
export function serializeBaggage(baggage: Baggage): string {
return Object.keys(baggage[0]).reduce((prev, key) => {
const val = baggage[0][key as keyof BaggageObj] as string;
Copy link
Member

Choose a reason for hiding this comment

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

Nit: as keyof BaggageObj is not needed here

* If they are not, they are kept as a string to be only accessed when serialized
* at baggage propagation time.
*/
export type Baggage = [BaggageObj, string];
Copy link
Member

Choose a reason for hiding this comment

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

In my opinion the readability of all of the functions in this file (and also their type definitions) suffer a lot from this being a tuple. I'd much rather have an interface with descriptive keys. But feel free to overrule me on this one!

Comment on lines +5 to +12
it.each([
['creates an empty baggage instance', {}, [{}, '']],
[
'creates a baggage instance with initial values',
{ environment: 'production', anyKey: 'anyValue' },
[{ environment: 'production', anyKey: 'anyValue' }, ''],
],
])('%s', (_: string, input, output) => {
Copy link
Member

Choose a reason for hiding this comment

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

This is very high up on the cleverness scale 😄 What do you think about splitting this up into separate tests for readability?

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'm a big fan of table driven tests, and I believe they lend themselves well to maintainable and extensible unit tests. Picked it up from the golang ecosystem: https://dave.cheney.net/2019/05/07/prefer-table-driven-tests.

It removes the boilerplate, and makes it easy to understand the that the setup + usage of the function is the exact same over the various scenarios. If the setup ever needs to change, just create a new test block!

@AbhiPrasad
Copy link
Member Author

The immutability/mutability concern in the API is very valid, but considering that we are just looking to test stuff right now (and there are other open questions about this) - I'm going to go ahead and table that conversation for future iterations of the API.

For now, let's get this in and start testing with Relay!

@lforst
Copy link
Member

lforst commented May 16, 2022

The immutability/mutability concern in the API is very valid, but considering that we are just looking to test stuff right now (and there are other open questions about this) - I'm going to go ahead and table that conversation for future iterations of the API.

Agree, sounds good to me!

@AbhiPrasad AbhiPrasad merged commit 0a6b4f2 into 7.x May 17, 2022
@AbhiPrasad AbhiPrasad deleted the v7-abhi-basic-baggage-api branch May 17, 2022 15:57
AbhiPrasad added a commit that referenced this pull request May 30, 2022
This patch introduced a basic data structure for baggage, as well as controls to create, manipulate and serialize the baggage data structure.

The baggage data structure represents key,value pairs based on the baggage spec: https://www.w3.org/TR/baggage

It is expected that users interact with baggage using the helpers methods: `createBaggage`, `getBaggageValue`, and `setBaggageValue`.

Internally, the baggage data structure is a tuple of length 2, separating baggage values based on if they are related to Sentry or not. If the baggage values are set/used by sentry, they will be stored in an object to be easily accessed. If they are not, they are kept as a string to be only accessed when serialized at baggage propagation time.

As a next step, let's add the baggage values to the envelope header so they can be processed and used by relay - this will allow us to do some early validations of our assumptions.
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.

None yet

3 participants