Skip to content

Conversation

pablohashescobar
Copy link
Member

@pablohashescobar pablohashescobar commented Oct 3, 2025

Description

  • Added a new function save_webhook_log to log webhook requests and responses to MongoDB, with a fallback to the database if the MongoDB save fails.
  • Updated the webhook_send_task to utilize the new logging function.
  • Modified the get_webhook_logs_queryset to order logs by creation date and adjusted the chunk size for iteration.

Type of Change

  • Improvement (change that would cause existing functionality to not work as expected)

Test Scenarios

  • validate the webhook logs are getting saved in the mongo database

References

WEB-5071

Summary by CodeRabbit

  • New Features
    • Enhanced webhook logging with resilient storage and consistent event details, improving reliability and traceability of webhook deliveries.
  • Bug Fixes
    • Implemented deterministic ordering when processing webhook logs to prevent inconsistent processing sequences.
  • Chores
    • Optimized webhook log batching to smaller, predictable chunks for more stable background processing.

…7887)

- Added a new function `save_webhook_log` to log webhook requests and responses to MongoDB, with a fallback to the database if the MongoDB save fails.
- Updated the `webhook_send_task` to utilize the new logging function.
- Modified the `get_webhook_logs_queryset` to order logs by creation date and adjusted the chunk size for iteration.
@Copilot Copilot AI review requested due to automatic review settings October 3, 2025 06:22
Copy link
Contributor

coderabbitai bot commented Oct 3, 2025

Walkthrough

Introduces save_webhook_log to persist webhook logs via MongoDB with DB fallback and updates webhook_send_task to use it. Adjusts webhook log cleanup iteration to explicit created_at ordering and reduces batch size from 500 to 100.

Changes

Cohort / File(s) Summary
Webhook logging refactor
apps/api/plane/bgtasks/webhook_task.py
Added save_webhook_log using MongoConnection with DB fallback; replaced direct WebhookLog creation in webhook_send_task; centralized request/response log assembly; imported MongoConnection.
Cleanup batching/order tweak
apps/api/plane/bgtasks/cleanup_task.py
get_webhook_logs_queryset now orders by created_at and uses fixed batch size 100 instead of 500; switches to two-step iterator chain.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant WS as webhook_send_task
  participant SL as save_webhook_log
  participant MG as MongoConnection.webhook_logs
  participant DB as WebhookLog (DB)

  WS->>SL: save_webhook_log(webhook, req/resp, retry, event_type)
  Note over SL: Build log payload

  alt Mongo collection available
    SL->>MG: insert_one(payload)
    alt Insert OK
      MG-->>SL: ack
      SL-->>WS: return
    else Insert fails
      MG--x SL: error
      SL->>DB: create(payload)
      DB-->>SL: ack/fail
      SL-->>WS: return
    end
  else No collection/connection
    SL->>DB: create(payload)
    DB-->>SL: ack/fail
    SL-->>WS: return
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

ready to merge

Suggested reviewers

  • dheeru0198
  • sriramveeraghanta

Poem

Hop hop, logs now hop to Mongo first,
If that burrow’s blocked, SQL’s not the worst.
Small bites of batches, ordered by date,
A tidy warren—clean, concise, straight.
Thump of success, retries in tow—
Carrots for code paths wherever they go. 🥕🐇

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 60.00% which is insufficient. The required threshold is 80.00%. You can run `@coderabbitai generate docstrings` to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title Check ✅ Passed The title succinctly captures the primary change by stating the implementation of webhook logging to MongoDB with a fallback to the database, directly reflecting the core functionality added in this PR without extraneous detail.
Description Check ✅ Passed The description follows the repository template by providing a detailed summary of changes, specifying the type of change, outlining test scenarios, and including references; the only omitted section is screenshots, which are not applicable for this backend change.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix-webhook-log-saves

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

makeplane bot commented Oct 3, 2025

Linked to Plane Work Item(s)

This comment was auto-generated by Plane

Copy link
Contributor

@Copilot Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull Request Overview

This PR implements webhook logging to MongoDB with database fallback functionality. The changes introduce a new save_webhook_log function that attempts to save webhook logs to MongoDB first, and falls back to the existing database if MongoDB operations fail.

  • Extracted webhook logging logic into a reusable save_webhook_log function with MongoDB primary storage and database fallback
  • Updated webhook task to use the new logging function instead of direct database operations
  • Modified webhook logs queryset to include ordering and adjusted chunk size

Reviewed Changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 3 comments.

File Description
apps/api/plane/bgtasks/webhook_task.py Added save_webhook_log function with MongoDB/database fallback and updated webhook task to use new logging
apps/api/plane/bgtasks/cleanup_task.py Updated webhook logs queryset with ordering and modified chunk size

Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.

Comment on lines +106 to +115
"workspace_id":str(webhook.workspace_id),
"webhook":str(webhook.id),
"event_type":str(event_type),
"request_method":str(request_method),
"request_headers":str(request_headers),
"request_body":str(request_body),
"response_status":str(response_status),
"response_headers":str(response_headers),
"response_body":str(response_body),
"retry_count":str(retry_count),
Copy link
Preview

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

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

[nitpick] Inconsistent spacing after colons in dictionary keys. Some keys have spaces after colons while others don't. Standardize to either no spaces or consistent spacing throughout the dictionary.

Suggested change
"workspace_id":str(webhook.workspace_id),
"webhook":str(webhook.id),
"event_type":str(event_type),
"request_method":str(request_method),
"request_headers":str(request_headers),
"request_body":str(request_body),
"response_status":str(response_status),
"response_headers":str(response_headers),
"response_body":str(response_body),
"retry_count":str(retry_count),
"workspace_id": str(webhook.workspace_id),
"webhook": str(webhook.id),
"event_type": str(event_type),
"request_method": str(request_method),
"request_headers": str(request_headers),
"request_body": str(request_body),
"response_status": str(response_status),
"response_headers": str(response_headers),
"response_body": str(response_body),
"retry_count": str(retry_count),

Copilot uses AI. Check for mistakes.

Comment on lines +106 to +115
"workspace_id":str(webhook.workspace_id),
"webhook":str(webhook.id),
"event_type":str(event_type),
"request_method":str(request_method),
"request_headers":str(request_headers),
"request_body":str(request_body),
"response_status":str(response_status),
"response_headers":str(response_headers),
"response_body":str(response_body),
"retry_count":str(retry_count),
Copy link
Preview

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

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

Converting all values to strings may be unnecessary and could impact performance. Consider preserving native data types (int, dict, etc.) for MongoDB storage, as MongoDB can handle these types natively and it would allow for better querying and indexing.

Copilot uses AI. Check for mistakes.

if not mongo_save_success:
try:
# insert the log data into the database
WebhookLog.objects.create(**log_data)
Copy link
Preview

Copilot AI Oct 3, 2025

Choose a reason for hiding this comment

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

Using the same log_data dictionary for both MongoDB and database operations may cause issues if the field names or data types differ between the two storage systems. The database model might expect different field names or data types than what's prepared for MongoDB.

Copilot uses AI. Check for mistakes.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
apps/api/plane/bgtasks/cleanup_task.py (1)

413-414: Consider using a constant for the chunk size.

The explicit ordering by created_at is beneficial for deterministic processing and aligns well with the new centralized logging approach.

However, the hardcoded chunk size of 100 deviates from the pattern used in other queryset functions (which use BATCH_SIZE). If this smaller chunk size is intentional for webhook logs (e.g., due to larger payloads), consider introducing a dedicated constant like WEBHOOK_BATCH_SIZE for maintainability.

Apply this diff if you'd like to introduce a dedicated constant:

+WEBHOOK_BATCH_SIZE = 100
+
 def get_webhook_logs_queryset():
     """Get email logs older than cutoff days."""
     cutoff_days = int(os.environ.get("HARD_DELETE_AFTER_DAYS", 30))
     cutoff_time = timezone.now() - timedelta(days=cutoff_days)
     logger.info(f"Webhook logs cutoff time: {cutoff_time}")
 
     return (
         WebhookLog.all_objects.filter(created_at__lte=cutoff_time)
         .values(
             "id",
             "created_at",
             "workspace_id",
             "webhook",
             "event_type",
             # Request
             "request_method",
             "request_headers",
             "request_body",
             # Response
             "response_status",
             "response_body",
             "response_headers",
             "retry_count",
         )
         .order_by("created_at")
-        .iterator(chunk_size=100)
+        .iterator(chunk_size=WEBHOOK_BATCH_SIZE)
     )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 794271a and dcb02ea.

📒 Files selected for processing (2)
  • apps/api/plane/bgtasks/cleanup_task.py (1 hunks)
  • apps/api/plane/bgtasks/webhook_task.py (3 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
apps/api/plane/bgtasks/webhook_task.py (2)
apps/api/plane/settings/mongo.py (2)
  • MongoConnection (18-122)
  • get_collection (91-109)
apps/api/plane/db/models/webhook.py (2)
  • Webhook (30-57)
  • WebhookLog (60-86)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Lint API
  • GitHub Check: Cursor Bugbot
  • GitHub Check: Analyze (javascript)
🔇 Additional comments (1)
apps/api/plane/bgtasks/webhook_task.py (1)

51-51: LGTM!

The import is necessary for MongoDB integration in the new logging function.

Comment on lines +90 to +139
def save_webhook_log(
webhook: Webhook,
request_method: str,
request_headers: str,
request_body: str,
response_status: str,
response_headers: str,
response_body: str,
retry_count: int,
event_type: str,
) -> None:

# webhook_logs
mongo_collection = MongoConnection.get_collection("webhook_logs")

log_data = {
"workspace_id":str(webhook.workspace_id),
"webhook":str(webhook.id),
"event_type":str(event_type),
"request_method":str(request_method),
"request_headers":str(request_headers),
"request_body":str(request_body),
"response_status":str(response_status),
"response_headers":str(response_headers),
"response_body":str(response_body),
"retry_count":str(retry_count),
}

mongo_save_success = False
if mongo_collection is not None:
try:
# insert the log data into the mongo collection
mongo_collection.insert_one(log_data)
logger.info("Webhook log saved successfully to mongo")
mongo_save_success = True
except Exception as e:
log_exception(e)
logger.error(f"Failed to save webhook log: {e}")
mongo_save_success = False

# if the mongo save is not successful, save the log data into the database
if not mongo_save_success:
try:
# insert the log data into the database
WebhookLog.objects.create(**log_data)
logger.info("Webhook log saved successfully to database")
except Exception as e:
log_exception(e)
logger.error(f"Failed to save webhook log: {e}")

Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Type mismatch will cause database fallback to fail.

The log_data dictionary converts all values to strings (lines 106-115), including retry_count on line 115. However, the WebhookLog model (see apps/api/plane/db/models/webhook.py line 76) defines retry_count as a PositiveSmallIntegerField, which expects an integer, not a string.

When MongoDB persistence fails and the fallback attempts WebhookLog.objects.create(**log_data) on line 134, Django will raise a validation error due to the type mismatch, causing webhook logs to be lost entirely.

Apply this diff to fix the type issue:

     log_data = {
-            "workspace_id":str(webhook.workspace_id),
-            "webhook":str(webhook.id),
-            "event_type":str(event_type),
-            "request_method":str(request_method),
-            "request_headers":str(request_headers),
-            "request_body":str(request_body),
-            "response_status":str(response_status),
-            "response_headers":str(response_headers),
-            "response_body":str(response_body),
-            "retry_count":str(retry_count),
+            "workspace_id": str(webhook.workspace_id),
+            "webhook": str(webhook.id),
+            "event_type": str(event_type),
+            "request_method": str(request_method),
+            "request_headers": str(request_headers),
+            "request_body": str(request_body),
+            "response_status": str(response_status),
+            "response_headers": str(response_headers),
+            "response_body": str(response_body),
+            "retry_count": retry_count,
     }

Additionally, for MongoDB storage, you may want to preserve native types. Consider creating two separate dictionaries:

# For MongoDB - preserve types and structure
mongo_log_data = {
    "workspace_id": str(webhook.workspace_id),
    "webhook": str(webhook.id),
    "event_type": event_type,
    "request_method": request_method,
    "request_headers": request_headers,
    "request_body": request_body,
    "response_status": response_status,
    "response_headers": response_headers,
    "response_body": response_body,
    "retry_count": retry_count,
}

# For database - ensure field compatibility
db_log_data = {
    "workspace_id": webhook.workspace_id,
    "webhook": webhook.id,
    "event_type": str(event_type),
    "request_method": str(request_method),
    "request_headers": str(request_headers),
    "request_body": str(request_body),
    "response_status": str(response_status),
    "response_headers": str(response_headers),
    "response_body": str(response_body),
    "retry_count": retry_count,
}

Comment on lines +330 to 340
save_webhook_log(
webhook=webhook,
request_method=action,
request_headers=headers,
request_body=payload,
response_status=response.status_code,
response_headers=response.headers,
response_body=response.text,
retry_count=self.request.retries,
event_type=event,
)
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Ensure proper serialization of complex data types.

The calls to save_webhook_log pass dictionaries and dict-like objects (e.g., headers at line 333 and 347, payload at line 334 and 348) which will be stringified using str() in the logging function. This may produce non-JSON-serializable representations like "{'key': 'value'}" instead of proper JSON.

Consider serializing these to JSON before passing to save_webhook_log:

         # Log the webhook request
         save_webhook_log(
             webhook=webhook,
             request_method=action,
-            request_headers=headers,
-            request_body=payload,
+            request_headers=json.dumps(headers),
+            request_body=json.dumps(payload),
             response_status=response.status_code,
-            response_headers=response.headers,
+            response_headers=json.dumps(dict(response.headers)),
             response_body=response.text,
             retry_count=self.request.retries,
             event_type=event,
         )

And similarly for the error path:

         # Log the failed webhook request
         save_webhook_log(
             webhook=webhook,
             request_method=action,
-            request_headers=headers,
-            request_body=payload,
+            request_headers=json.dumps(headers),
+            request_body=json.dumps(payload),
             response_status=500,
             response_headers="",
             response_body=str(e),
             retry_count=self.request.retries,
             event_type=event,
         )

Also applies to: 344-354

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.

1 participant