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 webhook triggered on Order Cycle Open #9687

Merged

Conversation

dacook
Copy link
Member

@dacook dacook commented Sep 20, 2022

What? Why?

A webhook can be sent to a service like Zapier, which can generate an email notification or perform some other action.

How?

Currently, we have no way of sending a webhook. There's no pre-built way of doing this for Rails surprisingly, although this tutorial looks like a good formula. I followed this, but found some parts unnecessary, and ended up with a different solution.

My focus was on making this work firstly for this single event type. Of course we'd like to extend it to for other events in the future, but let's get one working first before making further decisions.

There's bit going on here. A quick summary:

  1. WebhookDeliveryJob: a job that will be enqueued to deliver a payload for each triggered webhook
  2. WebhookEndpoint: a model to store each webhook endpoint url (in the future it would also store the event type it is listening for).
  3. OrderCycleWebhookService: create webhook payloads for an order cycle event.
  4. OrderCycleOpenedJob: Uses OrderCycleWebhookService to schedule WebhookDeliveryJobs. The order cycle remembers if it's been opened, and can only open once.
  5. (removed)
  6. User account interface to manage webhooks. I was surprised how hard it was to fiddle around and keep the form simple, but got there in the end.
    • Rather than enable/disable as originally suggested, you can just create or delete which has the same effect.
    • a test mode hasn't been implemented yet, in order to keep this PR from dragging out.
Screen Shot 2023-03-08 at 10 14 45 am Screen Shot 2023-03-08 at 10 15 58 amScreen Shot 2023-03-08 at 10 16 27 am

What should we test?

This is a new feature which doesn't touch any existing functionality.
It adds a new form to the Developer tab on the user Accounts page.
Suggested tests:

  1. Prepare a webhook URL ready to receive and capture a webhook payload. The easiest way is:
  2. Create/edit a user with API access:
    • Admin > Users > Edit
    • Tick "SHOW API KEY VIEW FOR USER", save.
  3. Login as this user
  4. Go to My account > Developer settings > Webhook Endpoints
  5. Enter your unique Webhook URL and click create
    • Verify that the URL has been saved.
  6. Set up an Order Cycle to open at the current time (anytime in the last hour):
    • Admin > Order Cycles > + New Order Cycle
    • Choose co-ordinator and click continue
    • Enter a name, and choose a time that is about to pass
    • Save (no need to complete further steps).
  7. Wait up to 5 minutes for the webhook event to be captured ☕️
    (refer back to the open tab for https://webhook.site/)
  8. De-activate the webhook:
    • Go to My account > Developer settings > Webhook Endpoints
    • Delete the webhook endpoint URL.
  9. Set up another Order Cycle to open at the current time
  10. Wait minimum 5 mins
    • Verify that no event was received.

Release notes

Changelog Category: User facing changes

If documentation has been written, share a link in the release notes

Documentation updates

I think it should be documented wherever the API is documented. But I can't find any public mention of the API??!
It seems worth mentioning in the user guide: https://guide.openfoodnetwork.org/

Ideally, a test mode will display the webhook payload format, and act as in-place documentation. But this will come in a future PR.

In the future, I suggest we integrate with our OpenAPI documentation too.

@dacook dacook force-pushed the 9616-order-cycle-open-webhook branch 4 times, most recently from 591ae53 to ccf3d5f Compare September 30, 2022 02:02
@dacook

This comment was marked as outdated.

mkllnk

This comment was marked as resolved.

@dacook dacook force-pushed the 9616-order-cycle-open-webhook branch from ccf3d5f to ae1ecc3 Compare October 7, 2022 05:31
@dacook dacook force-pushed the 9616-order-cycle-open-webhook branch 2 times, most recently from 5444c1a to 33dd37c Compare October 28, 2022 06:44
@dacook dacook force-pushed the 9616-order-cycle-open-webhook branch 2 times, most recently from 65292a4 to b1e4c2c Compare November 4, 2022 06:42
@dacook

This comment was marked as outdated.

@mkllnk
Copy link
Member

mkllnk commented Nov 9, 2022

Yes, that's fine for now. It's a pre-existing problem. And I hope that it can be improved when removing AngularJS.

Copy link
Contributor

@luisramos0 luisramos0 left a comment

Choose a reason for hiding this comment

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

Hi David!
This is a good start! I left a few comments as there are a couple of code/DB design problems imo.

Webhooks usually are named, to have webhooks for multiple events in the system we need named webhooks. For this first PR I'd recommend we have on the webhooks table a "webhook_type" column that we make "order_cycle_opened". On the user page this is a select box with only one option for now: so the user can add a webhook endpoint for a particular event, in this case "Order Cycle Opened".
As mentioned below on not overloading the OC table, I'd create webhook_log table that the Job would fill when the webhook is called, including user_id, oc_id (this can be a generic column like "event_target_object_id" so that other events can put other data in there), etc. This is better than having webhook log data scattered around the data model.

Good luck with this. This is a good start to a great feature!

app/jobs/webhook_delivery_job.rb Show resolved Hide resolved
def perform
ActiveRecord::Base.transaction do
recently_opened_order_cycles.find_each do |order_cycle|
SubscriptionPlacementJob.perform_later(order_cycle.id)
Copy link
Contributor

Choose a reason for hiding this comment

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

I dont understand the need to mix subscription placement logic with the web hook logic. I think this can be problematic. Can we separate concerns and just keep webhook notification on this job?

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 thought that it would be good to have one place where we define the logic selecting and processing order cycles as "opened".
It does seem strange to be creating so many jobs, yes, but it helps "separate concerns" in other ways I guess. I thought it would be more robust this way.

Copy link
Contributor

Choose a reason for hiding this comment

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

I understand but I think this field name is very misleading. What is a notification? I'd use the same terminology everywhere so that code readers will know what this means. It's a webhook. It could be called opened_webhook_sent_at.

I still think it would be much better and very easy to implement this outside the OC table (there's already lots of complexity in OCs).
A table where webhooks calls are registered is important imho. This would be one of them.

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 I agree, but this would be better considered as part of #10457. So for now, I plan to rename this column to match specifically what it refers to.

app/jobs/application_job.rb Outdated Show resolved Hide resolved

class AddOpenedNotificationAtToOrderCycle < ActiveRecord::Migration[6.1]
def change
add_column :order_cycles, :opened_notification_at, :timestamp
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this field on the OC meant to mark that a webhook was called? I'd recommend the name of the column to mention that.
I think this should not be on this OC table but rather on a specific webhook calls table/log. This way we are adding complexity to the critical/core OC table without a real need.

This comment was marked as outdated.

Copy link
Member Author

@dacook dacook Feb 7, 2023

Choose a reason for hiding this comment

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

Hmm coming back to this I have a different answer now. We're not intending to log when/if a webhook was called.
This is a flag to ensure the order cycle's "open" state is triggered once, and only once. The application code then schedules any webhooks needing to be delivered. I think I'll leave this as-is for now.

@dacook
Copy link
Member Author

dacook commented Dec 15, 2022

Thank you Luis, this is really helpful feedback! I've responded to some comments and will take this feedback on when I pick the task back up next year.

@dacook

This comment was marked as outdated.

@lin-d-hop
Copy link
Contributor

So exciting 😁

Copy link
Contributor

@luisramos0 luisramos0 left a comment

Choose a reason for hiding this comment

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

Hi David, I leave a few more suggestions.

app/jobs/webhook_delivery_job.rb Show resolved Hide resolved
app/jobs/webhook_delivery_job.rb Show resolved Hide resolved
def perform
ActiveRecord::Base.transaction do
recently_opened_order_cycles.find_each do |order_cycle|
SubscriptionPlacementJob.perform_later(order_cycle.id)
Copy link
Contributor

Choose a reason for hiding this comment

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

I understand but I think this field name is very misleading. What is a notification? I'd use the same terminology everywhere so that code readers will know what this means. It's a webhook. It could be called opened_webhook_sent_at.

I still think it would be much better and very easy to implement this outside the OC table (there's already lots of complexity in OCs).
A table where webhooks calls are registered is important imho. This would be one of them.

app/jobs/order_cycle_webhook_job.rb Outdated Show resolved Hide resolved
ActiveRecord::Base.transaction do
recently_opened_order_cycles.find_each do |order_cycle|
SubscriptionPlacementJob.perform_later(order_cycle.id)
OrderCycleWebhookJob.perform_later(order_cycle.id, 'order_cycle.opened')
Copy link
Contributor

Choose a reason for hiding this comment

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

This type of event 'order_cycle.opened' should be extracted to constant somewhere.

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 not sure where/how exactly yet, but will try to address this in my next update next week.

@dacook dacook force-pushed the 9616-order-cycle-open-webhook branch 2 times, most recently from 7a9993c to 7c91867 Compare February 16, 2023 01:22
@dacook
Copy link
Member Author

dacook commented Feb 16, 2023

Thanks @luisramos0 , this is valuable feedback. It seems this system design is not quite right, but I'm not clear on the best path and would like to communicate with the team. I will try to process this and perhaps create a forum post to discuss.

dacook and others added 11 commits March 7, 2023 15:38
This job is responsible for delivering a payload for one webhook event only. It allows the action to run asynchronously (and not slow down the calling process).
And thus retry later.
I tried to test that it actually retries, or ensuring the job remained in the queue to be retried, but couldn't get it to work.
Best reviewed with whitespace hidden.

Unfortunately the spec isn't allowed in CI. But it worked on my environment, I promise.
I chose `xit` so that it doesn't run unnecessarily. Perhaps we could use `pending` instead, which would execute, and notify us if it suddenly started working one day. But I doubt it.
This will store the URL for each user that wants a notification.

We probably don't need URL validation (it's not done on Enterprise for example). It could be validated by browser input, and anyway will be validated if the webhook actually works or not.

Inspired by Keygen: https://keygen.sh/blog/how-to-build-a-webhook-system-in-rails-using-sidekiq/
Although we won't be allowing multiple in the this PR, we certainly plan to in the future.

The migration helper add_reference couldn't handle the custom column name, so I had to put it together manually.
Using the clever concurrency testing borrowed from SubscriptionPlacementJob, but I thought a shorter pause time (just 100ms) would be sufficient.

I considered doing this with a new 'state' field (upcoming/open/close), but decided to keep it simple.
Allowing creation and deleting via the user association.
It probably won't be much effort to allow editing and multiple records, but I cut it down to the minimum needed to avoid any further delays.

I couldn't find a way to test a failure in the destroy method, but decided to keep the condition because I thought it was worth having.
@dacook dacook force-pushed the 9616-order-cycle-open-webhook branch from 9657974 to 9d5ca22 Compare March 7, 2023 04:43
@dacook
Copy link
Member Author

dacook commented Mar 7, 2023

Oops, I didn't get to review before the rebase but I'll do my best in retrospect.

Sorry, that was a mistake in what I said. I just meant that I rebased from master to resolve conflicts.
Anyway, thanks for the review, I've now also rebased to incorporate the changes (including the position of the FailedRequestError class in "Raise error on server error").

@dacook
Copy link
Member Author

dacook commented Mar 7, 2023

Now ready for a second review. @luisramos0 I understand if you don't have time, but if you do, would you like to re-review?

@mkllnk mkllnk dismissed luisramos0’s stale review March 7, 2023 05:13

Not sure if Luis will have time to re-review.

Copy link
Collaborator

@rioug rioug left a comment

Choose a reason for hiding this comment

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

Looks good 👏 I left a few comments but nothing that should block the PR.
I like your use of pending specs for work to be done, which I should start using myself

app/services/order_cycle_webhook_service.rb Show resolved Hide resolved
spec/jobs/order_cycle_opened_job_spec.rb Show resolved Hide resolved
spec/models/order_cycle_spec.rb Outdated Show resolved Hide resolved
spec/models/order_cycle_spec.rb Outdated Show resolved Hide resolved
spec/models/order_cycle_spec.rb Outdated Show resolved Hide resolved
spec/controllers/webhook_endpoints_controller_spec.rb Outdated Show resolved Hide resolved
@dacook dacook changed the title 9616 order cycle open webhook Add webhook triggered on Order Cycle Open Mar 15, 2023
The best way to check if something changed or not, is with 'change' of course.
@dacook dacook force-pushed the 9616-order-cycle-open-webhook branch from 0c6bf20 to d59074d Compare March 15, 2023 01:18
@dacook dacook requested a review from rioug March 15, 2023 01:45
@dacook
Copy link
Member Author

dacook commented Mar 16, 2023

Ready to test!

Tested deployment on au-staging: https://semaphoreci.com/openfoodfoundation/openfoodnetwork-2/servers/au-staging

@filipefurtad0 filipefurtad0 self-assigned this Mar 16, 2023
@filipefurtad0 filipefurtad0 added the pr-staged-uk staging.openfoodnetwork.org.uk label Mar 16, 2023
@filipefurtad0
Copy link
Contributor

filipefurtad0 commented Mar 16, 2023

Hey @dacook,

Thanks, really, for all the detail in the description of the PR. It comes really handy.

  1. Receiving payload, after OC is opened ✔️

After enabling the API key for a user which is coordinator and distributor of an OC, we can check the UI:

image

Creating the webhook displays:

image

Proceeded to create an order cycle to open in a future time, taking note of the order cycle id ("id": 3671) I've found it easier to use the procedure to open the OC as per this wiki -> thanks again to @luisramos0 for introducing this nice testing code, back in the day!! ❤️ This way, we don't need to consider timezone differences when creating the OC, and there is no wait-time till it opens 🎉

Some moments afterwards, the webhook site was updated 🚀 we can check the payload below:

image

And compare it to the info we get from the rails console, on the server:


[#<OrderCycle:0x00000000090d9540
  id: 3671,
  name: "webhook_OC",
  orders_open_at: Thu, 16 Mar 2023 12:32:55.095320000 EDT -04:00,
  orders_close_at: Thu, 16 Mar 2023 13:02:55.095421000 EDT -04:00,
  coordinator_id: 1822,
  created_at: Thu, 16 Mar 2023 12:38:36.100790000 EDT -04:00,
  updated_at: Thu, 16 Mar 2023 12:49:23.926191000 EDT -04:00,
  processed_at: nil,
  automatic_notifications: false,
  mails_sent: false,
  opened_at: Thu, 16 Mar 2023 12:49:23.926191000 EDT -04:00>]

All good here!

  1. Not receiving any payload, after deleting the webhook endpoint ✔️

After deleting the endpoint, we receive no payload, when the OC is re-opened.

  1. Re-entering the webhook endpoint on /account#/developer_settings ✔️
    ...works as before, and delivers the payload as expected:

image

  1. Opening an order cycle in which the user is supplier and distributor but not the coordinator ✔️

Also works, payload is delivered with the respective data ("id": 3661):

image

Notice the different coordinator_name, when comparing to the examples in 1) and 3).

The UI is according to specification, functionality as well, data is consistent.
I'd say this looks great - awesome! 👏 👏 👏
Merging.

@filipefurtad0 filipefurtad0 removed the pr-staged-uk staging.openfoodnetwork.org.uk label Mar 16, 2023
@filipefurtad0 filipefurtad0 merged commit 9ea6fa5 into openfoodfoundation:master Mar 16, 2023
@dacook
Copy link
Member Author

dacook commented Mar 20, 2023

📚 Draft documentation

See here: openfoodfoundation/ofn-UserGuide#41 (comment)

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.

Webhook triggered on Order Cycle Open
7 participants