Очистка веб-перехватчиков графа контента в PaaS CMS: запланированное задание

Проблема

Это немного нишевая проблема, но мы создаем автономное решение, в котором уровень представления размещается на Netlify, когда в обычном цикле доставки и работы необходимо протестировать предварительный просмотр развертывания, созданный с уникальным URL-адресом, как часть развертывания внешнего интерфейса, новый веб-хук регистрируется в Optimizely Content Graph, поэтому при публикации контента кеш внешнего интерфейса может быть признан недействительным. Проблема здесь в том, что при таком большом количестве развертываний список веб-перехватчиков может стать довольно длинным. Хотя мы могли бы настроить предварительные просмотры развертывания так, чтобы пропускать регистрацию веб-перехватчиков, это означало бы кэширование контента в этих средах, что препятствовало бы правильному тестированию обновлений контента в реальном времени.

Решение

Вместо того, чтобы ставить под угрозу наш рабочий процесс тестирования, мы создали решение для автоматической очистки, используя функцию запланированных заданий Optimizely. Это задание периодически сканирует все зарегистрированные веб-перехватчики и удаляет все, что не принадлежит нашей производственной среде, сохраняя список чистым и управляемым.

Выполнение

using EPiServer.PlugIn;
using EPiServer.Scheduler;
using RestSharp;
using System.Text;
using System.Text.Json;

namespace Client.Cms.Infrastructure.Jobs
{
    [ScheduledPlugIn(DisplayName = "Clear Graph Webhooks", GUID = "D4F5A6B7-C8D9-4E0F-AB12-3456789ABCDE")]
    public class ClearGraphWebhooksJob : ScheduledJobBase
    {
        private readonly IConfiguration _configuration;
        private readonly IContentLoader _contentLoader;
        private bool _stopSignaled;

        public ClearGraphWebhooksJob(IConfiguration configuration, IContentLoader contentLoader)
        {
            _configuration = configuration;
            _contentLoader = contentLoader;
            IsStoppable = true;
        }

        public override string Execute()
        {
            try
            {
                // Fetch Content Graph Gateway Address, AppKey and Secret from appSettings
                var gatewayAddress = _configuration["Optimizely:ContentGraph:GatewayAddress"];
                var appKey = _configuration["Optimizely:ContentGraph:AppKey"];
                var appSecret = _configuration["Optimizely:ContentGraph:Secret"];

                if (string.IsNullOrEmpty(gatewayAddress) || string.IsNullOrEmpty(appKey) || string.IsNullOrEmpty(appSecret))
                {
                    return "Failed: Missing configuration values for ContentGraph";
                }

                var auth = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{appKey}:{appSecret}"));
                var baseUrl = $"{gatewayAddress}/api/webhooks";

                var client = new RestClient(new RestClientOptions(baseUrl));
                var request = new RestRequest();
                request.AddHeader("Accept", "application/json");
                request.AddHeader("Authorization", $"Basic {auth}");

                var response = client.GetAsync(request).GetAwaiter().GetResult();

                if (!response.IsSuccessful)
                {
                    OnStatusChanged($"Failed to fetch webhooks: {response.StatusCode} {response.ErrorMessage}");
                    return $"Failed: {response.StatusCode} - {response.ErrorMessage}";
                }

                var json = response.Content;
                var webhooks = JsonSerializer.Deserialize>(json, new JsonSerializerOptions
                {
                    PropertyNameCaseInsensitive = true
                });

                if (webhooks == null || webhooks.Count == 0)
                {
                    OnStatusChanged("No webhooks found or failed to deserialize response.");
                    return "Success: No webhooks to process";
                }

                int deletedCount = 0;
                int skippedCount = 0;

                foreach (var webhook in webhooks)
                {
                    if (_stopSignaled)
                    {
                        return $"Stopped: Processed {deletedCount} deletions, {skippedCount} skipped";
                    }

                    var urls = GetUrls().ToList();
                    if (urls.Any(url => webhook.Request.Url.Contains(url)))
                    {
                        OnStatusChanged($"Skipping webhook {webhook.Id} with URL {webhook.Request.Url}");
                        skippedCount++;
                        continue;
                    }

                    DeleteWebhook(webhook.Id.ToString(), auth, baseUrl);
                    deletedCount++;
                }

                return $"Success: Deleted {deletedCount} webhooks, skipped {skippedCount}";
            }
            catch (Exception ex)
            {
                OnStatusChanged($"Error: {ex.Message}");
                return $"Failed: {ex.Message}";
            }
        }

        public override void Stop()
        {
            _stopSignaled = true;
            base.Stop();
        }

        private void DeleteWebhook(string id, string auth, string baseUrl)
        {
            var client = new RestClient(new RestClientOptions($"{baseUrl}/{id}"));
            var request = new RestRequest();
            request.AddHeader("Authorization", $"Basic {auth}");

            var response = client.DeleteAsync(request).GetAwaiter().GetResult();

            if (!response.IsSuccessful)
            {
                OnStatusChanged($"Failed to delete webhook {id}: {response.StatusCode} {response.ErrorMessage}");
            }
            else
            {
                OnStatusChanged($"Deleted webhook {id}");
            }
        }

        // Fetch URLs from Site Config Block
        private IEnumerable GetUrls()
        {
            var globalSettingsBlock = _contentLoader
                .GetChildren(ContentReference.GlobalBlockFolder)
                .FirstOrDefault();

            return globalSettingsBlock?.FrontendUrls ?? Enumerable.Empty;
        }
    }

    public class WebhookRequest
    {
        public string Url { get; set; }
        public string Method { get; set; }
        public Dictionary Headers { get; set; }
    }

    public class WebhookItem
    {
        public Guid Id { get; set; }
        public WebhookRequest Request { get; set; }
        public string Preset { get; set; }
    }
}

Как это работает

  1. Аутентификация: задание проверяется с помощью Content Graph с использованием ключа приложения и секретного кода из конфигурации.
  2. Извлечение веб-перехватчиков: извлекает все зарегистрированные веб-перехватчики через Content Graph API.
  3. Фильтрация по URL-адресу: задание проверяет каждый URL-адрес веб-перехватчика по настраиваемому списку рабочих URL-адресов, хранящемуся в блоке глобальных настроек.
  4. Интеллектуальное удаление: веб-перехватчики, соответствующие рабочим URL-адресам, сохраняются; все остальные удалены
  5. Отслеживание хода выполнения: задание сообщает о ходе выполнения и предоставляет сводную информацию об удалениях и пропусках.
Read more:  Во Всемирный день борьбы с диабетом ВОЗ выпускает глобальные рекомендации по диабету во время беременности

Ключевые особенности

  • Конфигурация на основе контента: рабочие URL-адреса хранятся в блоке Optimizely, что позволяет редакторам контента редактировать их без изменения кода.
  • Stoppable: задание реализует IsStoppable = true и правильно обрабатывает сигналы остановки для корректной отмены.
  • Комплексное ведение журнала: использует OnStatusChanged() для предоставления подробных обновлений хода выполнения, видимых в интерфейсе запланированных заданий.
  • Обработка ошибок: надежные блоки try-catch и проверка гарантируют корректное завершение задания с содержательными сообщениями об ошибках.

Преимущества

  • Предварительные версии развертывания могут регистрировать веб-перехватчики и тестировать обновления контента в режиме реального времени.
  • Список веб-перехватчиков остается чистым благодаря автоматическому обслуживанию.
  • Не требуется ручное вмешательство
  • Редакторы контента контролируют, какие URL-адреса сохранять через интерфейс CMS.

Заключение

Используя платформу запланированных заданий Optimizely, мы создали автоматизированное решение, которое поддерживает чистоту реестра веб-перехватчиков, сохраняя при этом полную функциональность во всех средах. Этот подход гораздо более удобен в обслуживании, чем ручное управление веб-перехватчиками или компрометация возможностей тестирования в предварительных версиях развертывания.

Запланированное задание может выполняться ежедневно, еженедельно или с любым интервалом, подходящим для вашей частоты развертывания, гарантируя, что ваш список веб-перехватчиков Content Graph останется компактным и управляемым.

17 декабря 2025 г.

По теме

Leave a Comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.