perchfall-notify 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 794750f0863a55a3da86f6127a9cee66b340bb551ab16a7d69181a7b793e09a6
4
+ data.tar.gz: 6b7591466107ed3c03581e817e079c6eb08b307eee7db7fde2f9f175dfe39e69
5
+ SHA512:
6
+ metadata.gz: cee07f7fcb3ab999a3dde58e0dd9f679c2cc4447711b437ff8872baa4b166fc08561e7f20ff54451bd807761f2f5343626af284e50c8d53a327e1fa7b24d0d67
7
+ data.tar.gz: ecf544f28317029bd970a63571a8107200e024cb9827aecf5fe7a6888ae5ce6809b7924082c044b7103cb83a62772cdb84d25b3c4861e2c9d362fbf5b2120e8e
data/CHANGELOG.md ADDED
@@ -0,0 +1,14 @@
1
+ # Changelog
2
+
3
+ ## [0.1.0] - 2026-03-20
4
+
5
+ ### Added
6
+
7
+ - `Perchfall::Notify.deliver` convenience method for one-line delivery
8
+ - `Perchfall::Notify::Notifier` — delivers reports to multiple channels, attempting all before raising
9
+ - `Perchfall::Notify::Channel` — base class for custom channel implementations
10
+ - `Perchfall::Notify::Channels::Slack` — Slack incoming webhook channel with Block Kit formatting
11
+ - `Perchfall::Notify::SlackFormatter` — default formatter; injectable for custom message shapes
12
+ - `Perchfall::Notify::DeliveryError` and `Perchfall::Notify::ConfigurationError`
13
+
14
+ [0.1.0]: https://github.com/beflagrant/perchfall-notify/releases/tag/v0.1.0
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jim Remsik
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,179 @@
1
+ # perchfall-notify
2
+
3
+ [![CI](https://github.com/beflagrant/perchfall-notify/actions/workflows/ci.yml/badge.svg)](https://github.com/beflagrant/perchfall-notify/actions/workflows/ci.yml)
4
+ [![Gem Version](https://badge.fury.io/rb/perchfall-notify.svg)](https://badge.fury.io/rb/perchfall-notify)
5
+
6
+ **Notification channels for [Perchfall](https://github.com/beflagrant/perchfall) synthetic monitoring.** Get alerted in Slack — or anywhere you like — the moment a check fails.
7
+
8
+ ```ruby
9
+ slack = Perchfall::Notify::Channels::Slack.new(
10
+ webhook_url: ENV["SLACK_WEBHOOK_URL"]
11
+ )
12
+
13
+ report = Perchfall.run(url: "https://example.com")
14
+ Perchfall::Notify.deliver(report, channels: [slack])
15
+ ```
16
+
17
+ ---
18
+
19
+ ## Why perchfall-notify
20
+
21
+ Perchfall tells you what a real browser saw. perchfall-notify tells your team about it.
22
+
23
+ - **Fail-open delivery** — if one channel errors, the others still fire. A Slack outage won't suppress your SMS alert.
24
+ - **Ships with Slack** — Block Kit messages with status, URL, HTTP code, duration, and a full list of network and console errors.
25
+ - **Bring your own channel** — implement one method and plug anything in: webhooks, PagerDuty, SMS, email.
26
+ - **No framework required** — works in Sidekiq jobs, Rake tasks, plain Ruby scripts, or anywhere Perchfall runs.
27
+
28
+ ---
29
+
30
+ ## Requirements
31
+
32
+ | Dependency | Version |
33
+ | --- | --- |
34
+ | Ruby | ≥ 3.2 |
35
+ | perchfall | ≥ 0.3.0 |
36
+
37
+ ---
38
+
39
+ ## Installation
40
+
41
+ ```sh
42
+ bundle add perchfall-notify
43
+ ```
44
+
45
+ ---
46
+
47
+ ## Quickstart
48
+
49
+ ```ruby
50
+ require "perchfall/notify"
51
+
52
+ slack = Perchfall::Notify::Channels::Slack.new(
53
+ webhook_url: ENV["SLACK_WEBHOOK_URL"]
54
+ )
55
+
56
+ report = Perchfall.run(url: "https://example.com")
57
+
58
+ Perchfall::Notify.deliver(report, channels: [slack])
59
+ ```
60
+
61
+ That's it. If the report has network errors or console errors, they'll appear in the Slack message.
62
+
63
+ ---
64
+
65
+ ## Slack channel
66
+
67
+ Create a [Slack incoming webhook](https://api.slack.com/messaging/webhooks) and pass the URL:
68
+
69
+ ```ruby
70
+ slack = Perchfall::Notify::Channels::Slack.new(
71
+ webhook_url: "https://hooks.slack.com/services/..."
72
+ )
73
+ ```
74
+
75
+ The default message format uses Block Kit and includes:
76
+
77
+ - Pass/fail header with optional scenario name
78
+ - URL, HTTP status, and duration
79
+ - Network errors with method, URL, and failure reason
80
+ - Console errors with type and message
81
+
82
+ ### Custom message format
83
+
84
+ Inject a formatter to control message shape:
85
+
86
+ ```ruby
87
+ class MyFormatter
88
+ def format(report)
89
+ {
90
+ text: report.ok? ? "✅ #{report.url} OK" : "🚨 #{report.url} FAILED"
91
+ }
92
+ end
93
+ end
94
+
95
+ slack = Perchfall::Notify::Channels::Slack.new(
96
+ webhook_url: ENV["SLACK_WEBHOOK_URL"],
97
+ formatter: MyFormatter.new
98
+ )
99
+ ```
100
+
101
+ ---
102
+
103
+ ## Delivering to multiple channels
104
+
105
+ Pass any number of channels. All are attempted — a failure in one does not skip the others.
106
+
107
+ ```ruby
108
+ Perchfall::Notify.deliver(report, channels: [slack, pagerduty, sms])
109
+ ```
110
+
111
+ If any channels fail, a single `Perchfall::Notify::DeliveryError` is raised after all have been attempted, with a summary of every failure.
112
+
113
+ ---
114
+
115
+ ## Building a custom channel
116
+
117
+ Subclass `Perchfall::Notify::Channel` and implement `#deliver`:
118
+
119
+ ```ruby
120
+ class WebhookChannel < Perchfall::Notify::Channel
121
+ def initialize(url)
122
+ @url = url
123
+ end
124
+
125
+ def deliver(report)
126
+ # post report.to_json to @url
127
+ rescue => e
128
+ raise Perchfall::Notify::DeliveryError, "Webhook failed: #{e.message}"
129
+ end
130
+ end
131
+ ```
132
+
133
+ Raise `DeliveryError` on failure so `Notifier` can accumulate it alongside other channel failures.
134
+
135
+ ---
136
+
137
+ ## Use in a background job
138
+
139
+ ```ruby
140
+ class SyntheticCheckJob
141
+ include Sidekiq::Job
142
+
143
+ def perform(url)
144
+ channels = [
145
+ Perchfall::Notify::Channels::Slack.new(webhook_url: ENV.fetch("SLACK_WEBHOOK_URL"))
146
+ ]
147
+ report = Perchfall.run(url: url)
148
+ Perchfall::Notify.deliver(report, channels: channels)
149
+ rescue Perchfall::Errors::PageLoadError => e
150
+ Perchfall::Notify.deliver(e.report, channels: channels)
151
+ end
152
+ end
153
+ ```
154
+
155
+ ---
156
+
157
+ ## Errors
158
+
159
+ | Exception | When |
160
+ | --- | --- |
161
+ | `Perchfall::Notify::ConfigurationError` | A channel was constructed with invalid configuration (e.g. missing webhook URL) |
162
+ | `Perchfall::Notify::DeliveryError` | One or more channels failed to deliver |
163
+ | `Perchfall::Notify::Error` | Base class — catches any perchfall-notify error |
164
+
165
+ ---
166
+
167
+ ## Development
168
+
169
+ ```sh
170
+ bundle install
171
+ bundle exec rspec
172
+ bin/console
173
+ ```
174
+
175
+ ---
176
+
177
+ ## License
178
+
179
+ MIT
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perchfall
4
+ module Notify
5
+ # Base interface for notification channels.
6
+ #
7
+ # A channel is responsible for delivering a formatted message derived from
8
+ # a Perchfall::Report to an external destination (Slack, SMS, webhook, etc.).
9
+ #
10
+ # Implement #deliver(report) in subclasses. Raise DeliveryError on failure.
11
+ #
12
+ # Example:
13
+ #
14
+ # class MyChannel < Perchfall::Notify::Channel
15
+ # def deliver(report)
16
+ # # send the report somewhere
17
+ # end
18
+ # end
19
+ class Channel
20
+ # Deliver a notification for the given report.
21
+ #
22
+ # @param report [Perchfall::Report]
23
+ # @raise [NotImplementedError] if subclass does not implement this method
24
+ # @raise [DeliveryError] if delivery fails
25
+ def deliver(report)
26
+ raise NotImplementedError, "#{self.class}#deliver is not implemented"
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perchfall
4
+ module Notify
5
+ module Channels
6
+ # Delivers Perchfall::Report notifications to a Slack incoming webhook.
7
+ #
8
+ # Requires a webhook URL. Accepts an optional formatter to control
9
+ # message shape, and an optional http_client for testability.
10
+ #
11
+ # Example:
12
+ #
13
+ # slack = Perchfall::Notify::Channels::Slack.new(
14
+ # webhook_url: "https://hooks.slack.com/services/..."
15
+ # )
16
+ # slack.deliver(report)
17
+ #
18
+ class Slack < Channel
19
+ # @param webhook_url [String] Slack incoming webhook URL
20
+ # @param formatter [#format] converts a Report to a Slack payload Hash
21
+ # @param http_client [#post] sends the payload; defaults to NetHttpClient
22
+ def initialize(webhook_url:, formatter: SlackFormatter.new, http_client: NetHttpClient.new)
23
+ super()
24
+ raise ConfigurationError, "webhook_url is required" if webhook_url.nil? || webhook_url.strip.empty?
25
+
26
+ uri = begin
27
+ URI.parse(webhook_url)
28
+ rescue URI::InvalidURIError
29
+ raise ConfigurationError, "webhook_url is not a valid URL"
30
+ end
31
+
32
+ raise ConfigurationError, "webhook_url must use https" unless uri.is_a?(URI::HTTPS)
33
+
34
+ @webhook_url = webhook_url
35
+ @formatter = formatter
36
+ @http_client = http_client
37
+ end
38
+
39
+ # @param report [Perchfall::Report]
40
+ # @raise [DeliveryError] if the HTTP request fails or Slack rejects the payload
41
+ def deliver(report)
42
+ payload = @formatter.format(report)
43
+ @http_client.post(@webhook_url, payload)
44
+ rescue DeliveryError
45
+ raise
46
+ rescue StandardError => e
47
+ raise DeliveryError, "Slack delivery failed: #{e.message}"
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perchfall
4
+ module Notify
5
+ # Base error for all perchfall-notify failures.
6
+ Error = Class.new(StandardError)
7
+
8
+ # Raised when a channel fails to deliver a notification.
9
+ DeliveryError = Class.new(Error)
10
+
11
+ # Raised when a channel is constructed with invalid configuration.
12
+ ConfigurationError = Class.new(Error)
13
+ end
14
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "json"
5
+ require "uri"
6
+
7
+ module Perchfall
8
+ module Notify
9
+ # Sends a JSON payload to a URL via HTTP POST using Ruby's net/http.
10
+ #
11
+ # This is the default http_client for Channels::Slack. Inject a fake or
12
+ # stub in tests — do not mock this class directly.
13
+ class NetHttpClient
14
+ OPEN_TIMEOUT = 5 # seconds
15
+ READ_TIMEOUT = 10 # seconds
16
+
17
+ # @param url [String] destination URL
18
+ # @param payload [Hash] will be serialized to JSON
19
+ # @raise [DeliveryError] on network failure or non-2xx response
20
+ def post(url, payload)
21
+ uri = URI.parse(url)
22
+ response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |http|
23
+ http.open_timeout = OPEN_TIMEOUT
24
+ http.read_timeout = READ_TIMEOUT
25
+ request = Net::HTTP::Post.new(uri.request_uri)
26
+ request["Content-Type"] = "application/json"
27
+ request.body = JSON.generate(payload)
28
+ http.request(request)
29
+ end
30
+
31
+ return if response.is_a?(Net::HTTPSuccess)
32
+
33
+ raise DeliveryError, "HTTP #{response.code}: #{response.body}"
34
+ rescue DeliveryError
35
+ raise
36
+ rescue StandardError => e
37
+ raise DeliveryError, "HTTP request failed: #{e.message}"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perchfall
4
+ module Notify
5
+ # Delivers a Perchfall::Report to one or more notification channels.
6
+ #
7
+ # Notifier is the primary entry point. It accepts an array of Channel
8
+ # objects and calls #deliver on each one in order. All channels are
9
+ # attempted even if one fails; DeliveryError is raised at the end if
10
+ # any channel failed, with all failures captured.
11
+ #
12
+ # Example:
13
+ #
14
+ # slack = Perchfall::Notify::Channels::Slack.new(webhook_url: "https://...")
15
+ # notifier = Perchfall::Notify::Notifier.new(channels: [slack])
16
+ # notifier.notify(report)
17
+ #
18
+ class Notifier
19
+ # @param channels [Array<Channel>] one or more delivery channels
20
+ def initialize(channels:)
21
+ raise ConfigurationError, "at least one channel is required" if channels.empty?
22
+
23
+ @channels = channels
24
+ end
25
+
26
+ # Deliver the report to all configured channels.
27
+ #
28
+ # @param report [Perchfall::Report]
29
+ # @raise [DeliveryError] if one or more channels fail
30
+ def notify(report)
31
+ failures = []
32
+
33
+ @channels.each do |channel|
34
+ channel.deliver(report)
35
+ rescue DeliveryError => e
36
+ failures << e
37
+ end
38
+
39
+ raise_if_failed(failures)
40
+ end
41
+
42
+ private
43
+
44
+ def raise_if_failed(failures)
45
+ return if failures.empty?
46
+
47
+ messages = failures.map(&:message).join("; ")
48
+ raise DeliveryError, "#{failures.size} channel(s) failed: #{messages}"
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perchfall
4
+ module Notify
5
+ # Converts a Perchfall::Report into a Slack Block Kit payload Hash.
6
+ #
7
+ # The default format posts a concise status message with:
8
+ # - pass/fail indicator
9
+ # - URL and scenario name (if present)
10
+ # - HTTP status and duration
11
+ # - counts of network and console errors (if any)
12
+ #
13
+ # Inject a custom formatter into Channels::Slack to change message shape.
14
+ class SlackFormatter
15
+ # @param report [Perchfall::Report]
16
+ # @return [Hash] Slack Block Kit payload
17
+ def format(report)
18
+ {
19
+ blocks: [
20
+ header_block(report),
21
+ details_block(report),
22
+ *error_blocks(report)
23
+ ]
24
+ }
25
+ end
26
+
27
+ private
28
+
29
+ def header_block(report)
30
+ icon = report.ok? ? ":white_check_mark:" : ":rotating_light:"
31
+ label = report.scenario_name ? " — #{report.scenario_name}" : ""
32
+ {
33
+ type: "header",
34
+ text: { type: "plain_text", text: "#{icon} Perchfall#{label}" }
35
+ }
36
+ end
37
+
38
+ def details_block(report)
39
+ status_text = report.ok? ? "OK" : "FAILED"
40
+ lines = [
41
+ "*Status:* #{status_text}",
42
+ "*URL:* #{report.url}",
43
+ "*HTTP:* #{report.http_status}",
44
+ "*Duration:* #{report.duration_ms}ms"
45
+ ]
46
+ lines << "*Error:* #{escape(report.error)}" if report.error
47
+
48
+ {
49
+ type: "section",
50
+ text: { type: "mrkdwn", text: lines.join("\n") }
51
+ }
52
+ end
53
+
54
+ def error_blocks(report)
55
+ blocks = []
56
+
57
+ if report.network_errors.any?
58
+ lines = report.network_errors.map { |e| "• `#{e.http_method} #{e.url}` — #{escape(e.failure)}" }
59
+ blocks << section("*Network errors (#{report.network_errors.size}):*\n#{lines.join("\n")}")
60
+ end
61
+
62
+ if report.console_errors.any?
63
+ lines = report.console_errors.map { |e| "• `#{e.type}` #{escape(e.text)}" }
64
+ blocks << section("*Console errors (#{report.console_errors.size}):*\n#{lines.join("\n")}")
65
+ end
66
+
67
+ blocks
68
+ end
69
+
70
+ def section(text)
71
+ { type: "section", text: { type: "mrkdwn", text: text } }
72
+ end
73
+
74
+ def escape(str)
75
+ str.to_s.gsub("&", "&amp;").gsub("<", "&lt;").gsub(">", "&gt;")
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Perchfall
4
+ module Notify
5
+ VERSION = "0.1.0"
6
+ end
7
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "perchfall"
4
+
5
+ require_relative "notify/version"
6
+ require_relative "notify/errors"
7
+ require_relative "notify/channel"
8
+ require_relative "notify/slack_formatter"
9
+ require_relative "notify/net_http_client"
10
+ require_relative "notify/notifier"
11
+ require_relative "notify/channels/slack"
12
+
13
+ module Perchfall
14
+ module Notify
15
+ # Convenience method. Delivers a report to one or more channels.
16
+ #
17
+ # Perchfall::Notify.deliver(report, channels: [slack_channel])
18
+ #
19
+ # @param report [Perchfall::Report]
20
+ # @param channels [Array<Channel>]
21
+ def self.deliver(report, channels:)
22
+ Notifier.new(channels: channels).notify(report)
23
+ end
24
+ end
25
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: perchfall-notify
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jim Remsik
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: perchfall
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: 0.3.0
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 0.3.0
26
+ - !ruby/object:Gem::Dependency
27
+ name: rspec
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '3.13'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '3.13'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rubocop
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: '1.70'
47
+ type: :development
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: '1.70'
54
+ - !ruby/object:Gem::Dependency
55
+ name: simplecov
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: '0.22'
61
+ type: :development
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '0.22'
68
+ description: |
69
+ Delivers Perchfall check reports via configurable notification channels.
70
+ Ships with Slack support; extensible to SMS, webhooks, and beyond.
71
+ email:
72
+ - jim@beflagrant.com
73
+ executables: []
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - CHANGELOG.md
78
+ - LICENSE.txt
79
+ - README.md
80
+ - lib/perchfall/notify.rb
81
+ - lib/perchfall/notify/channel.rb
82
+ - lib/perchfall/notify/channels/slack.rb
83
+ - lib/perchfall/notify/errors.rb
84
+ - lib/perchfall/notify/net_http_client.rb
85
+ - lib/perchfall/notify/notifier.rb
86
+ - lib/perchfall/notify/slack_formatter.rb
87
+ - lib/perchfall/notify/version.rb
88
+ homepage: https://github.com/beflagrant/perchfall-notify
89
+ licenses:
90
+ - MIT
91
+ metadata:
92
+ source_code_uri: https://github.com/beflagrant/perchfall-notify
93
+ changelog_uri: https://github.com/beflagrant/perchfall-notify/blob/main/CHANGELOG.md
94
+ rubygems_mfa_required: 'true'
95
+ rdoc_options: []
96
+ require_paths:
97
+ - lib
98
+ required_ruby_version: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: 3.2.0
103
+ required_rubygems_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ requirements: []
109
+ rubygems_version: 4.0.6
110
+ specification_version: 4
111
+ summary: Notification channels for Perchfall synthetic monitoring
112
+ test_files: []