Skip to content

Commit

Permalink
✨ feat: webhook logs (#562)
Browse files Browse the repository at this point in the history
* get webhook deliveries api

* bug fix

we should perform order-by operation before skip

* add webhook deliveries component

* i18n

* styling and filtering

* i18n

* define type for webhook delivery error

* do not include stacktrace when record error for security reason
  • Loading branch information
deleteLater committed Dec 26, 2023
1 parent 706a248 commit e07a393
Show file tree
Hide file tree
Showing 24 changed files with 674 additions and 249 deletions.
15 changes: 15 additions & 0 deletions modules/back-end/src/Api/Controllers/WebhookController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,19 @@ public async Task<ApiResponse<WebhookDelivery>> SendAsync(SendWebhook request)
var delivery = await Mediator.Send(request);
return Ok(delivery);
}

[HttpGet("{id:guid}/deliveries")]
public async Task<ApiResponse<PagedResult<WebhookDelivery>>> GetDeliveriesAsync(
Guid id,
[FromQuery] WebhookDeliveryFilter filter)
{
var request = new GetWebhookDeliveryList
{
WebhookId = id,
Filter = filter
};

var deliveries = await Mediator.Send(request);
return Ok(deliveries);
}
}
2 changes: 2 additions & 0 deletions modules/back-end/src/Application/Services/IWebhookService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,6 @@ public interface IWebhookService : IService<Webhook>
Task<bool> IsNameUsedAsync(Guid orgId, string name);

Task DeleteAsync(Guid id);

Task<PagedResult<WebhookDelivery>> GetDeliveriesAsync(Guid webhookId, WebhookDeliveryFilter filter);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using Application.Bases.Models;
using Domain.Webhooks;

namespace Application.Webhooks;

public class GetWebhookDeliveryList : IRequest<PagedResult<WebhookDelivery>>
{
public Guid WebhookId { get; set; }

public WebhookDeliveryFilter Filter { get; set; }
}

public class GetWebhookDeliveryListHandler : IRequestHandler<GetWebhookDeliveryList, PagedResult<WebhookDelivery>>
{
private readonly IWebhookService _service;

public GetWebhookDeliveryListHandler(IWebhookService service)
{
_service = service;
}

public async Task<PagedResult<WebhookDelivery>> Handle(GetWebhookDeliveryList request, CancellationToken cancellationToken)
{
var deliveries = await _service.GetDeliveriesAsync(request.WebhookId, request.Filter);
return deliveries;
}
}
12 changes: 12 additions & 0 deletions modules/back-end/src/Application/Webhooks/WebhookDeliveryFilter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
using Application.Bases.Models;

namespace Application.Webhooks;

public class WebhookDeliveryFilter : PagedRequest
{
public string Event { get; set; }

public bool? Success { get; set; }

public DateTime? NotBefore { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ public async Task<PagedResult<AccessToken>> GetListAsync(Guid organizationId, Ac

var totalCount = await query.CountAsync();
var items = await query
.Skip(filter.PageIndex * filter.PageSize)
.OrderByDescending(x => x.CreatedAt)
.Skip(filter.PageIndex * filter.PageSize)
.Take(filter.PageSize)
.ToListAsync();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@ public async Task<PagedResult<Policy>> GetListAsync(Guid organizationId, PolicyF

var totalCount = await query.CountAsync();
var items = await query
.Skip(filter.PageIndex * filter.PageSize)
.OrderByDescending(x => x.CreatedAt)
.Skip(filter.PageIndex * filter.PageSize)
.Take(filter.PageSize)
.ToListAsync();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ public async Task<PagedResult<RelayProxy>> GetListAsync(Guid organizationId, Rel

var totalCount = await query.CountAsync();
var items = await query
.Skip(filter.PageIndex * filter.PageSize)
.OrderByDescending(x => x.CreatedAt)
.Skip(filter.PageIndex * filter.PageSize)
.Take(filter.PageSize)
.ToListAsync();

Expand Down
5 changes: 2 additions & 3 deletions modules/back-end/src/Infrastructure/Webhooks/WebhookSender.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ public async Task<WebhookDelivery> SendAsync(Webhook webhook, Dictionary<string,
message = "Cannot construct a valid JSON payload by using the template and the data object",
dataObject,
payloadTemplate = webhook.PayloadTemplate,
exceptionMessage = ex.Message,
exceptionMessage = ex.Message
};
delivery.SetError(error);

Expand Down Expand Up @@ -97,8 +97,7 @@ public async Task<WebhookDelivery> SendAsync(WebhookRequest request)

var error = new
{
message = ex.Message,
stackTrace = ex.StackTrace
message = ex.Message
};
delivery.SetError(error);
}
Expand Down
33 changes: 32 additions & 1 deletion modules/back-end/src/Infrastructure/Webhooks/WebhookService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ public async Task<PagedResult<Webhook>> GetListAsync(Guid orgId, WebhookFilter f

var totalCount = await query.CountAsync();
var webhooks = await query
.Skip(filter.PageIndex * filter.PageSize)
.OrderByDescending(x => x.CreatedAt)
.Skip(filter.PageIndex * filter.PageSize)
.Take(filter.PageSize)
.ToListAsync();

Expand Down Expand Up @@ -78,4 +78,35 @@ public async Task DeleteAsync(Guid id)
{
await Collection.DeleteOneAsync(x => x.Id == id);
}

public async Task<PagedResult<WebhookDelivery>> GetDeliveriesAsync(Guid webhookId, WebhookDeliveryFilter filter)
{
var query = MongoDb.QueryableOf<WebhookDelivery>().Where(x => x.WebhookId == webhookId);

// not before filter, default to 15 days ago
var notBefore = filter.NotBefore ?? DateTime.UtcNow.AddDays(-15);
query = query.Where(x => x.StartedAt >= notBefore);

// event filter
if (!string.IsNullOrWhiteSpace(filter.Event))
{
query = query.Where(x => x.Events.Contains(filter.Event));
}

// success filter
var success = filter.Success;
if (success.HasValue)
{
query = query.Where(x => x.Success == success.Value);
}

var totalCount = await query.CountAsync();
var deliveries = await query
.OrderByDescending(x => x.StartedAt)
.Skip(filter.PageIndex * filter.PageSize)
.Take(filter.PageSize)
.ToListAsync();

return new PagedResult<WebhookDelivery>(totalCount, deliveries);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<nz-drawer
nzClosable="false"
[nzExtra]="extra"
[nzVisible]="visible"
nzTitle="Webhook deliveries"
i18n-nzTitle="@@integrations.webhooks.webhook-deliveries"
[nzWidth]="1000"
(nzOnClose)="onClose()">
<ng-container *nzDrawerContent>
<div class="searches">
<span i18n="@@integrations.webhooks.webhook-deliveries-tip" class="tip">Webhook deliveries sent to your endpoint in the past 15 days.</span>
<nz-select nzShowSearch nzAllowClear nzPlaceHolder="Filter by event" [(ngModel)]="filter.event" (ngModelChange)="doSearch()">
<nz-option *ngFor="let event of events" [nzValue]="event" [nzLabel]="event"></nz-option>
</nz-select>
<nz-segmented [nzOptions]="statuses" (nzValueChange)="onStatusChange($event)"></nz-segmented>
</div>
<nz-table
#table nzSize="small"
[nzShowTotal]="totalTemplate"
[nzData]="deliveries.items"
[nzFrontPagination]="false"
[nzLoading]="isLoading"
[nzTotal]="deliveries.totalCount"
[nzPageSize]="filter.pageSize"
[(nzPageIndex)]="filter.pageIndex"
(nzPageIndexChange)="loadDeliveries()"
>
<thead>
<tr>
<th i18n="@@common.status">Status</th>
<th i18n="@@integrations.webhooks.events">Events</th>
<th i18n="@@common.happened-at">Happened At</th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let item of table.data">
<tr>
<td (click)="expandRow(item.id)">
<i class="animated" nz-icon nzType="right" [nzRotate]="isRowExpanded(item.id) ? 90 : 0"></i>
<nz-tag *ngIf="item.success" nzColor="success">
<span nz-icon nzType="check-circle"></span>
{{ item.response?.statusCode ?? 200 }}
</nz-tag>
<nz-tag *ngIf="!item.success" nzColor="error">
<span nz-icon nzType="close-circle"></span>
{{ item.response?.statusCode ?? 'ERROR' }}
</nz-tag>
</td>
<td>
<span>{{item.events}}</span>
</td>
<td>{{item.startedAt | date: 'YYYY/MM/dd HH:mm'}}</td>
</tr>
<tr [nzExpand]="isRowExpanded(item.id)">
<webhook-delivery [delivery]="item"></webhook-delivery>
</tr>
</ng-container>
</tbody>
<ng-template #totalTemplate let-total>
<span class="total"><strong>{{ total }}</strong> results</span>
</ng-template>
</nz-table>
</ng-container>
<ng-template #extra>
<i (click)="onClose()" nz-icon nzType="icons:icon-close"></i>
</ng-template>
</nz-drawer>
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
@import "variables";

.searches {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 12px;
margin-bottom: 12px;

.tip {
margin-right: auto;
font-size: 14px;
}

nz-select {
width: 250px;
}
}

nz-tag {
padding: 0 8px;
}

.total {
font-size: 14px;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
import {
PagedWebhookDelivery,
Webhook,
WebhookDeliveryFilter,
WebhookEvents
} from "@features/safe/integrations/webhooks/webhooks";
import { Subject } from "rxjs";
import { WebhookService } from "@services/webhook.service";
import { NzMessageService } from "ng-zorro-antd/message";
import { debounceTime, finalize } from "rxjs/operators";

@Component({
selector: 'webhook-deliveries',
templateUrl: './webhook-deliveries.component.html',
styleUrls: ['./webhook-deliveries.component.less']
})
export class WebhookDeliveriesComponent {
@Input()
visible: boolean;
private _webhook: Webhook;
@Input()
set webhook(value: Webhook) {
this._webhook = value;
if (value) {
this.loadDeliveries();
}
}
@Output()
close: EventEmitter<void> = new EventEmitter();

isLoading: boolean = true;
deliveries: PagedWebhookDelivery = {
totalCount: 0,
items: []
};
filter: WebhookDeliveryFilter = new WebhookDeliveryFilter();
search$ = new Subject<void>();

constructor(private webhookService: WebhookService, private message: NzMessageService) {
this.search$.pipe(debounceTime(250)).subscribe(() => this.loadDeliveries());
}

events: string[] = WebhookEvents.map(e => e.value);
statuses: string[] = ['All', 'Succeeded', 'Failed'];

loadDeliveries() {
this.isLoading = true;
this.webhookService.getDeliveries(this._webhook.id, this.filter)
.pipe(finalize(() => this.isLoading = false))
.subscribe({
next: deliveries => {
this.deliveries = deliveries;
this.expandedRowId = deliveries.items.length > 0 ? deliveries.items[0].id : '';
},
error: () => this.message.error($localize`:@@common.loading-failed-try-again:Loading failed, please try again`),
});
}


doSearch() {
this.filter.pageIndex = 1;
this.search$.next();
}

onStatusChange(index: number) {
const status = this.statuses[index];
this.filter.success = status === 'All' ? null : status === 'Succeeded';
this.doSearch();
}

expandedRowId: string = '';
expandRow(id: string): void {
this.expandedRowId = this.expandedRowId === id ? '' : id;
}
isRowExpanded(id: string): boolean {
return this.expandedRowId === id;
}

onClose() {
// reset status
this.filter = new WebhookDeliveryFilter();
this.deliveries = {
totalCount: 0,
items: []
};

this.visible = false;
this.close.emit();
}
}
11 changes: 8 additions & 3 deletions modules/front-end/src/app/core/core.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,8 @@ import { HandlebarsService } from "@services/handlebars.service";
import { TestWebhookModalComponent } from './components/test-webhook-modal/test-webhook-modal.component';
import { WebhookDeliveryComponent } from './components/webhook-delivery/webhook-delivery.component';
import { NzSkeletonModule } from "ng-zorro-antd/skeleton";
import { WebhookDeliveriesComponent } from './components/webhook-deliveries/webhook-deliveries.component';
import { NzSegmentedModule } from "ng-zorro-antd/segmented";

@NgModule({
declarations: [
Expand Down Expand Up @@ -134,7 +136,8 @@ import { NzSkeletonModule } from "ng-zorro-antd/skeleton";
LicenseComponent,
WebhookDrawerComponent,
TestWebhookModalComponent,
WebhookDeliveryComponent
WebhookDeliveryComponent,
WebhookDeliveriesComponent
],
imports: [
CommonModule,
Expand Down Expand Up @@ -184,7 +187,8 @@ import { NzSkeletonModule } from "ng-zorro-antd/skeleton";
NzCollapseModule,
NzSwitchModule,
ChangeListModule,
NzSkeletonModule
NzSkeletonModule,
NzSegmentedModule
],
exports: [
SlugifyPipe,
Expand Down Expand Up @@ -232,7 +236,8 @@ import { NzSkeletonModule } from "ng-zorro-antd/skeleton";
LicenseComponent,
WebhookDrawerComponent,
TestWebhookModalComponent,
WebhookDeliveryComponent
WebhookDeliveryComponent,
WebhookDeliveriesComponent
]
})
export class CoreModule {
Expand Down
Loading

0 comments on commit e07a393

Please sign in to comment.