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

Add more validation to Rite Aid API #547

Merged

Conversation

Mr0grog
Copy link
Collaborator

@Mr0grog Mr0grog commented Feb 2, 2022

I thought this wouldn’t be too complex, but alas, I was wrong, as usual.

This is a follow-on to the Rite Aid API issues discovered while counting slots last week in usdigitalresponse/appointment-data-insights#3. The main idea was to check the number of slots the API is reporting to make sure it’s reasonable (since it was reporting thousands per store for a couple months). Of course, as I got into it, it expanded:

  • Check the number of slots to make sure the total is reasonable (this only triggers a warning, since it’s possible our definition of “reasonable” could be bad).

  • Check the slots to make sure it’s not reporting mismatching values, i.e. where the available slots are more than the total slots. This causes an actual error, since this would seem to indicate things are wrong enough we don’t want to try and read this data.

  • Clean up the workflow slightly so an error formatting a single store doesn’t break all the other stores, more like how some of the more recent sources work.

  • Check the schema of the response to make sure it hasn’t changed (spoiler: it has changed).

  • Clean up phone numbers. I foolishly thought I should check the phone number format in the schema, and discovered that Rite Aid’s API removes leading zeroes from the local part of the phone number!!! For example, this is an actual phone number the API is currently surfacing: “(203) 382-9”

  • Clean up the error parsing issue in Improve handling of Rite Aid API errors #537 since we’re mucking about in Rite Aid code anyway.

Fixes #537.

- If the total slots is unreasonably high, log a warning, but don't fail.
- If the total slots is less than the available slots, throw an error (something is way broken).

Along the way, this amends the overall flow to match more recently written sources, where an exception formatting a single location doesn't stop other locations from being formatted and sent.
Also adds parsing for phone numbers, because I was foolish enough to try checking the phone number pattern in the schema, and discovered that leading zeroes on the local part of the phone numbers are missing in Rite Aid's data (!!!).
@Mr0grog Mr0grog requested a review from astonm February 2, 2022 08:25
Comment on lines +140 to +144
assertSchema(
riteAidWrapperSchema,
response.body,
"Response did not match schema"
);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I made separate schemas for the API response wrapper and the actual objects because:

  1. It’s possible only some locations could get out of spec, so this lets us avoid breaking everything in that case. (OTOH, maybe we should break everything in that case? Also, not sure this scenario is at all likely to occur.)

  2. From a practical standpoint, the error message lists every error, and if there is a systemic change, the error message would be incredibly long because it repeats the same things hundreds or thousands of times (one for every location object) if there was just one schema instead of a separate one for the locations that we check when formatting on each location.

    Of course, that could be mitigated by limiting how many errors we include in the actual error message here:

    constructor(errors, data, message = null) {
    message = message || "Data did not match schema";
    const details = errors.map(SchemaError.formatAjvError);
    const detailMessage = details.map((detail) => ` ${detail}`).join("\n");
    super(`${message} - ${errors.length} errors:\n${detailMessage}`);

Comment on lines +304 to +323
// Sentry's withScope() doesn't work for async code, so we have to manually
// track the context data we want to add. :(
const errorContext = { state, source: "Rite Aid API" };

const stores = [];
try {
stores = await queryState(state, rateLimit);
const rawData = await queryState(state, rateLimit);
for (const rawLocation of rawData) {
Sentry.withScope((scope) => {
scope.setContext("context", errorContext);
scope.setContext("location", { id: rawLocation.id });
try {
stores.push(formatStore(rawLocation));
} catch (error) {
warn(error);
}
});
}
} catch (error) {
warn(error, { state, source: "Rite Aid API" }, true);
warn(error, errorContext, true);
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This is kind of ugly, but withScope() can’t handle async code. Definitely open to changes here if you have good thoughts on better patterns, or nice sugar.

Sentry has built-in support for managing async stuff with domains (for example, the express plugin uses them), but domains are a deprecated feature that are going away at some point in Node.js (I think in favor of Async Hooks). So I’m hesitant to build a fancy abstraction on them.

There are also some Sentry issues tracking better async support:

Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, I'm not sure it's worthwhile trying to build a generalized solve here, or at least not yet. The code as written reads fine to me and doesn't seem overly messy.

booking_phone: provider.contact.booking_phone,
booking_phone:
provider.contact.booking_phone &&
parseUsPhoneNumber(provider.contact.booking_phone),
Copy link
Collaborator

Choose a reason for hiding this comment

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

nit: Any reason not to have parseUsPhoneNumber handle the falsy check as well?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ah, I was trying to match the behavior of parseUsAddress, which also does not support non-string input. It also seems a little easier to reason about if you can guarantee the return type will be a string.

But for this use case, it would definitely be simpler. Think I should update parseUsAddress, too? (I think the non-support for null/undefined in parseUsAddress is currently supporting some edge cases in NJVSS and Albertsons, so it may require some more changes.)

Copy link
Collaborator

Choose a reason for hiding this comment

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

One approach that can solve for that is to allow the caller to provide a default fallback value, or else the function will throw an error given bad input (including wrong data type).

That said, parseUsAddress is complicated enough and used elsewhere enough that I don't know if it's worth messing with. I do appreciate that the two parseUsXYZ functions are consistent in whether or not you need to look before you leap.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

In that case, I think I'll keep it as-is for now, and maybe find ways to handle null for both of them later.

Comment on lines +304 to +323
// Sentry's withScope() doesn't work for async code, so we have to manually
// track the context data we want to add. :(
const errorContext = { state, source: "Rite Aid API" };

const stores = [];
try {
stores = await queryState(state, rateLimit);
const rawData = await queryState(state, rateLimit);
for (const rawLocation of rawData) {
Sentry.withScope((scope) => {
scope.setContext("context", errorContext);
scope.setContext("location", { id: rawLocation.id });
try {
stores.push(formatStore(rawLocation));
} catch (error) {
warn(error);
}
});
}
} catch (error) {
warn(error, { state, source: "Rite Aid API" }, true);
warn(error, errorContext, true);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Yeah, I'm not sure it's worthwhile trying to build a generalized solve here, or at least not yet. The code as written reads fine to me and doesn't seem overly messy.

@Mr0grog Mr0grog merged commit 681ad77 into main Feb 2, 2022
@Mr0grog Mr0grog deleted the sometimes-rite-aid-gives-somewhat-unrealistic-numbers-of-slots branch February 2, 2022 21:36
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.

Improve handling of Rite Aid API errors
2 participants