Webhook log improvements (#16200)

* fix: include all headers in webhook log

* feat: return webhook log status from server

* feat: make webhook logs deep linkable

* feat: add webhook log pagination

* feat: improve webhook request/response body preview
This commit is contained in:
Rasmus John Pedersen
2024-05-06 13:14:11 +02:00
committed by GitHub
parent 23d0a6b9b2
commit cfcdc9c98d
10 changed files with 80 additions and 26 deletions

View File

@@ -27,4 +27,6 @@ public class WebhookLog
public string ResponseBody { get; set; } = string.Empty;
public bool ExceptionOccured { get; set; }
public bool IsSuccessStatusCode { get; set; }
}

View File

@@ -16,7 +16,7 @@ public class WebhookLogFactory : IWebhookLogFactory
Url = webhook.Url,
WebhookKey = webhook.Key,
RetryCount = retryCount,
RequestHeaders = requestMessage.Headers.ToString(),
RequestHeaders = $"{requestMessage.Content?.Headers}{requestMessage.Headers}",
RequestBody = await requestMessage.Content?.ReadAsStringAsync(cancellationToken)!,
ExceptionOccured = exception is not null,
};
@@ -24,7 +24,8 @@ public class WebhookLogFactory : IWebhookLogFactory
if (httpResponseMessage is not null)
{
log.StatusCode = MapStatusCodeToMessage(httpResponseMessage.StatusCode);
log.ResponseHeaders = httpResponseMessage.Headers.ToString();
log.IsSuccessStatusCode = httpResponseMessage.IsSuccessStatusCode;
log.ResponseHeaders = $"{httpResponseMessage.Content.Headers}{httpResponseMessage.Headers}";
log.ResponseBody = await httpResponseMessage.Content.ReadAsStringAsync(cancellationToken);
}
else if (exception is HttpRequestException httpRequestException)

View File

@@ -1,4 +1,5 @@
using Umbraco.Cms.Core.Models;
using System.Text.RegularExpressions;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Webhooks;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
@@ -33,6 +34,7 @@ internal static class WebhookLogFactory
ResponseBody = dto.ResponseBody,
RetryCount = dto.RetryCount,
StatusCode = dto.StatusCode,
IsSuccessStatusCode = Regex.IsMatch(dto.StatusCode, "^.*\\(2(\\d{2})\\)$"),
Key = dto.Key,
Id = dto.Id,
Url = dto.Url,

View File

@@ -67,6 +67,7 @@ public class WebhookMapDefinition : IMapDefinition
target.Url = source.Url;
target.RequestHeaders = source.RequestHeaders;
target.WebhookKey = source.WebhookKey;
target.IsSuccessStatusCode = source.IsSuccessStatusCode;
if (_hostingEnvironment.IsDebugMode)
{

View File

@@ -14,6 +14,9 @@ public class WebhookLogViewModel
[DataMember(Name = "statusCode")]
public string StatusCode { get; set; } = string.Empty;
[DataMember(Name = "isSuccessStatusCode")]
public bool IsSuccessStatusCode { get; set; }
[DataMember(Name = "date")]
public DateTime Date { get; set; }

View File

@@ -5,9 +5,13 @@
const vm = this;
vm.pagination = {
pageNumber: 1,
pageSize: 25
};
vm.logs = [];
vm.openLogOverlay = openLogOverlay;
vm.isChecked = isChecked;
function init() {
vm.loading = true;
@@ -22,9 +26,14 @@
}
function loadLogs() {
return webhooksResource.getLogs()
const take = vm.pagination.pageSize;
const skip = (vm.pagination.pageNumber - 1) * take;
return webhooksResource.getLogs(skip, take)
.then(data => {
vm.logs = data.items;
vm.pagination.totalPages = Math.ceil(data.totalItems/vm.pagination.pageSize);
vm.logs.forEach(log => {
formatDatesToLocal(log);
});
@@ -54,9 +63,16 @@
editorService.open(dialog);
}
function isChecked(log) {
return log.statusCode === "OK (200)";
}
vm.previousPage = () => vm.goToPage(vm.pagination.pageNumber - 1);
vm.nextPage = () => vm.goToPage(vm.pagination.pageNumber + 1);
vm.goToPage = (pageNumber) => {
vm.pagination.pageNumber = pageNumber;
vm.loading = true;
loadLogs().then(() => {
vm.loading = false;
});
};
init();
}

View File

@@ -3,7 +3,6 @@
<thead>
<tr>
<th></th>
<th><localize key="webhooks_webhookKey">Webhook key</localize></th>
<th><localize key="general_date">Date</localize></th>
<th><localize key="webhooks_url">Url</localize></th>
<th><localize key="webhooks_event">Event</localize></th>
@@ -14,13 +13,13 @@
<tr ng-repeat="log in vm.logs track by log.key" ng-click="vm.openLogOverlay(log)" style="cursor: pointer;">
<td style="width: 20px;">
<umb-checkmark
ng-if="vm.isChecked(log)"
checked="vm.isChecked(log)"
size="m">
ng-if="log.isSuccessStatusCode"
checked="true"
size="m"
title="{{ log.statusCode }}">
</umb-checkmark>
<umb-icon icon="icon-wrong" class="umb-checkmark umb-checkmark--m" style="cursor: default;" ng-if="!vm.isChecked(log)"></umb-icon>
<umb-icon icon="icon-wrong" class="umb-checkmark umb-checkmark--m" style="cursor: default;" ng-if="!log.isSuccessStatusCode" title="{{ log.statusCode }}"></umb-icon>
</td>
<td>{{ log.webhookKey }}</td>
<td>{{ log.formattedLogDate }}</td>
<td>{{ log.url }}</td>
<td>{{ log.eventAlias }}</td>
@@ -28,4 +27,16 @@
</tr>
</tbody>
</table>
<umb-empty-state ng-hide="vm.logs.length" position="center">
<localize key="content_listViewNoItems">There are no items show in the list.</localize>
</umb-empty-state>
<umb-pagination
page-number="vm.pagination.pageNumber"
total-pages="vm.pagination.totalPages"
on-next="vm.nextPage"
on-prev="vm.previousPage"
on-go-to-page="vm.goToPage">
</umb-pagination>
</div>

View File

@@ -5,6 +5,7 @@
vm.close = close;
vm.formatData = formatData;
vm.detectLanguage = detectLanguage;
function formatData(data) {
@@ -12,7 +13,7 @@
if (data.detectIsJson()) {
try {
obj = Utilities.fromJson(data)
obj = JSON.stringify(Utilities.fromJson(data), null, 2);
} catch (err) {
obj = data;
}
@@ -21,6 +22,21 @@
return obj;
}
function detectLanguage(headers, defaultLanguage) {
const matches = headers.match(/^Content-Type:\s*(?<type>[a-z\/+.-]+)(\;?.*?)$/mi)
if (matches) {
const contentType = matches.groups["type"];
if (contentType === "application/json")
return "JSON";
if (contentType === "text/html")
return "HTML";
if (contentType === "application/xml" || contentType === "text/xml")
return "XML";
}
return defaultLanguage || "TEXT";
}
function close() {
if ($scope.model && $scope.model.close) {
$scope.model.close();

View File

@@ -21,9 +21,9 @@
<div class="flex items-center">
<div class="flx-g0 flx-s0" style="flex-basis: 40px;">
<umb-checkmark checked="true" size="m" style="cursor: default"
ng-if="model.log.statusCode === 'OK (200)'"></umb-checkmark>
ng-if="model.log.isSuccessStatusCode"></umb-checkmark>
<umb-icon icon="icon-wrong" class="umb-checkmark umb-checkmark--m" style="cursor: default;"
ng-if="model.log.statusCode !== 'OK (200)'"></umb-icon>
ng-if="!model.log.isSuccessStatusCode"></umb-icon>
</div>
<div class="flx-g1 flx-s1 flx-b2">{{model.log.statusCode}}</div>
</div>
@@ -45,6 +45,10 @@
<div>{{model.log.retryCount}}</div>
</umb-control-group>
<umb-control-group label="Webhook key">
<div>{{model.log.webhookKey}}</div>
</umb-control-group>
</umb-box-content>
</umb-box>
@@ -53,16 +57,14 @@
<umb-box-header title="Request"></umb-box-header>
<umb-box-content class="block-form">
<pre class="code">{{model.log.requestHeaders}}</pre>
<umb-code-snippet language="'JSON'" wrap="true">{{vm.formatData(model.log.requestBody) | json}}
</umb-code-snippet>
<umb-code-snippet language="vm.detectLanguage(model.log.requestHeaders, 'JSON')" wrap="true">{{vm.formatData(model.log.requestBody)}}</umb-code-snippet>
</umb-box-content>
</umb-box>
<umb-box>
<umb-box-header title="Response"></umb-box-header>
<umb-box-content class="block-form">
<pre class="code">{{model.log.responseHeaders}}</pre>
<umb-code-snippet language="'TEXT'" wrap="true">{{vm.formatData(model.log.responseBody) | json}}
</umb-code-snippet>
<umb-code-snippet language="vm.detectLanguage(model.log.responseHeaders)" wrap="true">{{vm.formatData(model.log.responseBody)}}</umb-code-snippet>
</umb-box-content>
</umb-box>
<div ng-if="model.log.exceptionOccured">

View File

@@ -10,7 +10,7 @@
vm.page.name = "";
vm.page.navigation = [];
let webhookUri = $routeParams.method;
let webhookUri = $routeParams.id;
onInit();
@@ -33,8 +33,8 @@
{
"name": vm.page.labels.webhooks,
"icon": "icon-webhook",
"view": "views/webhooks/webhooks.html",
"active": webhookUri === 'overview',
"view": !webhookUri ? "views/webhooks/webhooks.html" : null,
"active": !webhookUri,
"alias": "umbWebhooks",
"action": function () {
$location.path("/settings/webhooks/overview");
@@ -43,11 +43,11 @@
{
"name": vm.page.labels.logs,
"icon": "icon-box-alt",
"view": "views/webhooks/logs.html",
"view": webhookUri === 'logs' ? "views/webhooks/logs.html" : null,
"active": webhookUri === 'logs',
"alias": "umbWebhookLogs",
"action": function () {
$location.path("/settings/webhooks/overview");
$location.path("/settings/webhooks/overview/logs");
}
}
];