-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
Conversation
size-limit report 📦
|
There was a problem hiding this 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 (?)
packages/utils/src/baggage.ts
Outdated
export type AllowedBaggageKeys = 'environment' | 'release'; // TODO: Add remaining allowed baggage keys | 'transaction' | 'userid' | 'usersegment'; | ||
export type Baggage = Partial<Record<AllowedBaggageKeys, string> & Record<string, string>>; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
42d0a6f
to
ce42be8
Compare
ce42be8
to
0a2811d
Compare
This is now ready to review! |
There was a problem hiding this 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
There was a problem hiding this 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?
packages/utils/src/baggage.ts
Outdated
} | ||
|
||
/** Parse a baggage header to a string */ | ||
export function parseBaggageString(baggageString: string): Baggage { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
packages/utils/src/baggage.ts
Outdated
/** 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; |
There was a problem hiding this comment.
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]; |
There was a problem hiding this comment.
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!
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) => { |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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!
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! |
Agree, sounds good to me! |
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.
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
, andsetBaggageValue
.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