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: Use get-metadata-subscription to get max_api_calls #2279

Merged

Conversation

novakzaballa
Copy link
Contributor

@novakzaballa novakzaballa commented Jun 8, 2023

Thanks for submitting a PR! Please check the boxes below:

  • I have run pre-commit to check linting
  • I have filled in the "Changes" section below?
  • I have filled in the "How did you test this code" section below?

Changes

Get value of max_api_calls from get-metadata-subscription endpoint.

How did you test this code?

In order to test locally

  • enable max_api_calls_alert for your identity
  • change FLAGSMITH_API_URL to production
  • Locally, you can hardcode values of max_api_calls or total_api_calls returned by the cooresponding endpoint. You can also change the hardcoded condition in common/utils/utiils.tx L58, that currently triggers the alert on 70% of usage and above in order to force the display of the alert message.

@vercel
Copy link

vercel bot commented Jun 8, 2023

The latest updates on your projects. Learn more about Vercel for Git ↗︎

Name Status Preview Comments Updated (UTC)
docs ✅ Ready (Inspect) Visit Preview 💬 Add feedback Aug 21, 2023 3:43pm
flagsmith-frontend-preview ✅ Ready (Inspect) Visit Preview 💬 Add feedback Aug 21, 2023 3:43pm
flagsmith-frontend-staging ✅ Ready (Inspect) Visit Preview 💬 Add feedback Aug 21, 2023 3:43pm

@github-actions
Copy link
Contributor

github-actions bot commented Jun 8, 2023

Uffizzi Preview deployment-27969 was deleted.

@@ -254,6 +258,17 @@ const App = class extends Component {
this.setState({ totalApiCalls: res[0]?.data?.totals.total })
})
}
const subscriptionModel = OrganisationStore.getSubscriptionMeta()
Copy link
Contributor

Choose a reason for hiding this comment

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

One thing that I'm concerned about here is that for each request the client makes to get-subscription-metadata, we are making a call to Chargebee's API. What that means is that:

a. This endpoint is not the most performant endpoint we have (as we're reliant on the performance of an external API). We should therefore ensure that we're not blocking anything waiting on the response from this endpoint. Maybe that's already happening here, but I'd like to confirm that.
b. For each request to this endpoint on our side, we trigger a request to Chargebee's API which has rate limits, etc. so we should be careful about abusing it.

Here are the CB API limits. We are on the 'Rise' plan, as per the highlighted section.

Screenshot 2023-06-19 at 15 07 08

Based on the above, is there anything we can do to optimise the requests from the frontend? Or, perhaps we should optimise this on the BE? Store some kind of additional cache of a customer's subscription information after a successful request from CB?

I'm guessing that by adding this here, we're triggering the request on every successful load of the Flagsmith dashboard. I don't know where our current traffic is right now, but I don't imagine this number is beyond the realms of possibility.

cc @kyle-ssg @dabeeeenster

Copy link
Contributor

Choose a reason for hiding this comment

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

Can we just store this with a 24 hour TTL in our DB? I thought we were already storing this data in our database? Isn't that what organisations_organisationsubscriptioninformationcache is for?

Copy link
Contributor

@matthewelwell matthewelwell Jun 19, 2023

Choose a reason for hiding this comment

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

Can we just store this with a 24 hour TTL in our DB?

Yes, I think that's what I would suggest.

I thought we were already storing this data in our database? Isn't that what organisations_organisationsubscriptioninformationcache is for?

Currently this table is only used for the sales dashboard. We could, however, expose this to the frontend for sure. We should make sure we implement the recurring task before we do that though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@matthewelwell do we have in place a job to execute the update of this data? That is what you are talking about when you say "recurring task" right?

Copy link
Contributor

Choose a reason for hiding this comment

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

Yep, you can see the functionality here.

Copy link
Contributor

Choose a reason for hiding this comment

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

TODO:

  • Use CB webhook to keep OrganisationSubscriptionInformationCache up to date with max_seats and max_api_calls
  • Update the get-subscription-metadata endpoint to use the OrganisationSubscriptionInformationCache object
  • Update the current task which updates the OrganisationSubscriptionInformationCache models to ONLY update influx data

To clarify, we should continue using live influx data in the frontend dashboard.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

Copy link
Contributor

@matthewelwell matthewelwell left a comment

Choose a reason for hiding this comment

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

Some general feedback is that we should try and extract logic to well named functions / methods here too. We're cramming a lot into the chargebee webhook view for example. I know the code for that view is already a bit messy, but we shouldn't add to the mess, we should try to improve it!

@@ -278,11 +283,37 @@ def chargebee_webhook(request):
)
logger.error(error_message)
return Response(status=status.HTTP_200_OK)
existing_organisation_subscription_information_cache = (
OrganisationSubscriptionInformationCache.objects.filter(
organisation_id=existing_subscription.id
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
organisation_id=existing_subscription.id
organisation_id=existing_subscription.organisation_id

Copy link
Contributor

Choose a reason for hiding this comment

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

This could have been really bad! We'd have ended up updating the data for the wrong organisations because we were using the id of the subscription.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yes, horrible. Corrected


subscription_status = subscription_data.get("status")
if subscription_status == "active":
if subscription_data.get("plan_id") != existing_subscription.plan:
existing_subscription.update_plan(subscription_data.get("plan_id"))
if existing_organisation_subscription_information_cache:
Copy link
Contributor

Choose a reason for hiding this comment

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

This (including the above) could all be simplified by using OrganisationSubscriptionInformationCache.objects.update_or_create() I believe?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right. Done

self.payment_source.title(),
self.payment_source.title()
if self.payment_source is not None
else "no title",
Copy link
Contributor

Choose a reason for hiding this comment

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

It's not a title don't forget, the .title() is converting the string to title case, not getting a title attribute!

We should use something like "unknown payment source"

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done

@@ -13,7 +13,7 @@
]


def update_caches():
def update_caches(update_influx=False):
Copy link
Contributor

Choose a reason for hiding this comment

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

typehinting please!

Also, this is a bit messy as we're implicitly updating chargebee if update influx is false but only updating influx if it's true. We should either just create 2 completely separate functions, or have one function that takes an attribute of update_from_chargebee: bool = False and then always update influx, but only update chargebee if the attribute is set.

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 my preference is for having 2 separate functions and just get rid of this one entirely, unless there's a reason to keep it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The code for those 2 functions would be almost exactly the same. I kept one function but clarified the parameter name and type.


subscription_status = subscription_data.get("status")
if subscription_status == "active":
if subscription_data.get("plan_id") != existing_subscription.plan:
existing_subscription.update_plan(subscription_data.get("plan_id"))
if existing_organisation_subscription_information_cache:
metadata = get_subscription_metadata(
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 need to do this? Isn't the data already available in the chargebee webhook payload? Maybe we still need to check the plan / addons against the chargebee cache, but I think that, by calling this function, we're making another outbound request to chargebee unnecessarily, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Improved as per our conversation in this regard.

@@ -1,4 +1,4 @@
import typing
from typing import Tuple
Copy link
Contributor

Choose a reason for hiding this comment

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

Not really an issue, but I'm not sure why these changes have snuck into this PR.

max_seats: int,
max_api_calls: int,
max_projects: int,
chargebee_updated_at: datetime = datetime.now(),
Copy link
Contributor

Choose a reason for hiding this comment

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

While this is probably ok in tests, it's bad practice to use function returns as default arguments as they are evaluated when the module is loaded. As such, the chargebee_updated_at will always take the value that python evaluates when the module is loaded.

}
}

# When
if plan_id != "updated-plan-id":
Copy link
Contributor

Choose a reason for hiding this comment

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

Hmm... I don't like that we're basing some logic here on a hard coded string that might get passed in. Can't we just pass a boolean as an argument to this method?

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 had moved these tests to be pytest based function tests, then we could have just used parameterisation and this could all have been much neater.

max_projects = 3 # The maximum number of projects allowed in the current plan

# When
res, subscription_information_cache = self._get_mocked_subscription_plan_data(
Copy link
Contributor

Choose a reason for hiding this comment

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

My concern here (and why I think we should have used parameterisation instead) is that, when you look at the test in isolation, it looks like we're testing some mocked data which isn't very useful.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@matthewelwell Tests changed as suggested. Can you please take a look at them?

@mock.patch("organisations.models.get_plan_meta_data")
@mock.patch("organisations.views.extract_subscription_metadata")
def test_when_plan_is_changed_max_seats_and_max_api_calls_are_updated(
mock_get_subscription_metadata,
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
mock_get_subscription_metadata,
mock_extract_subscription_metadata,

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done.

api/organisations/tests/test_views.py Show resolved Hide resolved
api/organisations/tests/test_views.py Show resolved Hide resolved
assert subscription_information_cache.chargebee_updated_at
assert subscription_information_cache.influx_updated_at is None
if is_updated:
assert subscription_information_cache.chargebee_updated_at != updated_at
Copy link
Contributor

Choose a reason for hiding this comment

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

This should be >, no?

Copy link
Contributor

Choose a reason for hiding this comment

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

We could also use pytest-freezegun if we want to be accurate with these timestamps.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This should be >, no?

Corrected.

We could also use pytest-freezegun if we want to be accurate with these timestamps.

Added and used

)

subscription.refresh_from_db()
subscription_information_cache.refresh_from_db()
Copy link
Contributor

Choose a reason for hiding this comment

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

This is unnecessary since we're getting it from the database directly above.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

subscription_information_cache.refresh_from_db(), deleted.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
api Issue related to the REST API front-end Issue related to the React Front End Dashboard
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Ensure that the data about max API calls and actual API calls is on-sync and up to date
4 participants