0
0
Fork 0
mirror of https://github.com/healthchecks/healthchecks.git synced 2025-04-07 14:15:34 +00:00

Webhooks support PUT method.

.Webhooks can have different request bodies and headers for "up" and "events".
This commit is contained in:
Pēteris Caune 2019-05-28 14:25:29 +03:00
parent 8f6726d1ee
commit d054970b02
No known key found for this signature in database
GPG key ID: E28D7679E9A9EDE2
11 changed files with 486 additions and 276 deletions

View file

@ -6,6 +6,8 @@ All notable changes to this project will be documented in this file.
### Improvements ### Improvements
- Add the `prunetokenbucket` management command - Add the `prunetokenbucket` management command
- Show check counts in JSON "badges" (#251) - Show check counts in JSON "badges" (#251)
- Webhooks support PUT method (#249)
- Webhooks can have different request bodies and headers for "up" and "events" (#249)
### Bug Fixes ### Bug Fixes
- Fix badges for tags containing special characters (#240, #237) - Fix badges for tags containing special characters (#240, #237)

View file

@ -360,44 +360,62 @@ class Channel(models.Model):
prio = int(parts[1]) prio = int(parts[1])
return PO_PRIORITIES[prio] return PO_PRIORITIES[prio]
@property def webhook_spec(self, status):
def url_down(self):
assert self.kind == "webhook" assert self.kind == "webhook"
if not self.value.startswith("{"): if not self.value.startswith("{"):
parts = self.value.split("\n") parts = self.value.split("\n")
return parts[0] url_down = parts[0]
url_up = parts[1] if len(parts) > 1 else ""
post_data = parts[2] if len(parts) > 2 else ""
return {
"method": "POST" if post_data else "GET",
"url": url_down if status == "down" else url_up,
"body": post_data,
"headers": {},
}
doc = json.loads(self.value) doc = json.loads(self.value)
return doc.get("url_down") if "post_data" in doc:
# Legacy "post_data" in doc -- use the legacy fields
return {
"method": "POST" if doc["post_data"] else "GET",
"url": doc["url_down"] if status == "down" else doc["url_up"],
"body": doc["post_data"],
"headers": doc["headers"],
}
if status == "down" and "method_down" in doc:
return {
"method": doc["method_down"],
"url": doc["url_down"],
"body": doc["body_down"],
"headers": doc["headers_down"],
}
elif status == "up" and "method_up" in doc:
return {
"method": doc["method_up"],
"url": doc["url_up"],
"body": doc["body_up"],
"headers": doc["headers_up"],
}
@property
def down_webhook_spec(self):
return self.webhook_spec("down")
@property
def up_webhook_spec(self):
return self.webhook_spec("up")
@property
def url_down(self):
return self.down_webhook_spec["url"]
@property @property
def url_up(self): def url_up(self):
assert self.kind == "webhook" return self.up_webhook_spec["url"]
if not self.value.startswith("{"):
parts = self.value.split("\n")
return parts[1] if len(parts) > 1 else ""
doc = json.loads(self.value)
return doc.get("url_up")
@property
def post_data(self):
assert self.kind == "webhook"
if not self.value.startswith("{"):
parts = self.value.split("\n")
return parts[2] if len(parts) > 2 else ""
doc = json.loads(self.value)
return doc.get("post_data")
@property
def headers(self):
assert self.kind == "webhook"
if not self.value.startswith("{"):
return {}
doc = json.loads(self.value)
return doc.get("headers", {})
@property @property
def slack_team(self): def slack_team(self):

View file

@ -146,8 +146,8 @@ class NotifyTestCase(BaseTestCase):
self.assertIsInstance(kwargs["data"], bytes) self.assertIsInstance(kwargs["data"], bytes)
@patch("hc.api.transports.requests.request") @patch("hc.api.transports.requests.request")
def test_webhooks_handle_json_value(self, mock_request): def test_legacy_webhooks_handle_json_value(self, mock_request):
definition = {"url_down": "http://foo.com"} definition = {"url_down": "http://foo.com", "post_data": "", "headers": {}}
self._setup_data("webhook", json.dumps(definition)) self._setup_data("webhook", json.dumps(definition))
self.channel.notify(self.check) self.channel.notify(self.check)
@ -157,8 +157,8 @@ class NotifyTestCase(BaseTestCase):
) )
@patch("hc.api.transports.requests.request") @patch("hc.api.transports.requests.request")
def test_webhooks_handle_json_up_event(self, mock_request): def test_legacy_webhooks_handle_json_up_event(self, mock_request):
definition = {"url_up": "http://bar"} definition = {"url_up": "http://bar", "post_data": "", "headers": {}}
self._setup_data("webhook", json.dumps(definition), status="up") self._setup_data("webhook", json.dumps(definition), status="up")
self.channel.notify(self.check) self.channel.notify(self.check)
@ -167,7 +167,22 @@ class NotifyTestCase(BaseTestCase):
mock_request.assert_called_with("get", "http://bar", headers=headers, timeout=5) mock_request.assert_called_with("get", "http://bar", headers=headers, timeout=5)
@patch("hc.api.transports.requests.request") @patch("hc.api.transports.requests.request")
def test_webhooks_handle_post_headers(self, mock_request): def test_webhooks_handle_json_up_event(self, mock_request):
definition = {
"method_up": "GET",
"url_up": "http://bar",
"body_up": "",
"headers_up": {}
}
self._setup_data("webhook", json.dumps(definition), status="up")
self.channel.notify(self.check)
headers = {"User-Agent": "healthchecks.io"}
mock_request.assert_called_with("get", "http://bar", headers=headers, timeout=5)
@patch("hc.api.transports.requests.request")
def test_legacy_webhooks_handle_post_headers(self, mock_request):
definition = { definition = {
"url_down": "http://foo.com", "url_down": "http://foo.com",
"post_data": "data", "post_data": "data",
@ -183,9 +198,27 @@ class NotifyTestCase(BaseTestCase):
) )
@patch("hc.api.transports.requests.request") @patch("hc.api.transports.requests.request")
def test_webhooks_handle_get_headers(self, mock_request): def test_webhooks_handle_post_headers(self, mock_request):
definition = {
"method_down": "POST",
"url_down": "http://foo.com",
"body_down": "data",
"headers_down": {"Content-Type": "application/json"},
}
self._setup_data("webhook", json.dumps(definition))
self.channel.notify(self.check)
headers = {"User-Agent": "healthchecks.io", "Content-Type": "application/json"}
mock_request.assert_called_with(
"post", "http://foo.com", data=b"data", headers=headers, timeout=5
)
@patch("hc.api.transports.requests.request")
def test_legacy_webhooks_handle_get_headers(self, mock_request):
definition = { definition = {
"url_down": "http://foo.com", "url_down": "http://foo.com",
"post_data": "",
"headers": {"Content-Type": "application/json"}, "headers": {"Content-Type": "application/json"},
} }
@ -198,9 +231,27 @@ class NotifyTestCase(BaseTestCase):
) )
@patch("hc.api.transports.requests.request") @patch("hc.api.transports.requests.request")
def test_webhooks_allow_user_agent_override(self, mock_request): def test_webhooks_handle_get_headers(self, mock_request):
definition = {
"method_down": "GET",
"url_down": "http://foo.com",
"body_down": "",
"headers_down": {"Content-Type": "application/json"},
}
self._setup_data("webhook", json.dumps(definition))
self.channel.notify(self.check)
headers = {"User-Agent": "healthchecks.io", "Content-Type": "application/json"}
mock_request.assert_called_with(
"get", "http://foo.com", headers=headers, timeout=5
)
@patch("hc.api.transports.requests.request")
def test_legacy_webhooks_allow_user_agent_override(self, mock_request):
definition = { definition = {
"url_down": "http://foo.com", "url_down": "http://foo.com",
"post_data": "",
"headers": {"User-Agent": "My-Agent"}, "headers": {"User-Agent": "My-Agent"},
} }
@ -212,11 +263,30 @@ class NotifyTestCase(BaseTestCase):
"get", "http://foo.com", headers=headers, timeout=5 "get", "http://foo.com", headers=headers, timeout=5
) )
@patch("hc.api.transports.requests.request")
def test_webhooks_allow_user_agent_override(self, mock_request):
definition = {
"method_down": "GET",
"url_down": "http://foo.com",
"body_down": "",
"headers_down": {"User-Agent": "My-Agent"},
}
self._setup_data("webhook", json.dumps(definition))
self.channel.notify(self.check)
headers = {"User-Agent": "My-Agent"}
mock_request.assert_called_with(
"get", "http://foo.com", headers=headers, timeout=5
)
@patch("hc.api.transports.requests.request") @patch("hc.api.transports.requests.request")
def test_webhooks_support_variables_in_headers(self, mock_request): def test_webhooks_support_variables_in_headers(self, mock_request):
definition = { definition = {
"method_down": "GET",
"url_down": "http://foo.com", "url_down": "http://foo.com",
"headers": {"X-Message": "$NAME is DOWN"}, "body_down": "",
"headers_down": {"X-Message": "$NAME is DOWN"},
} }
self._setup_data("webhook", json.dumps(definition)) self._setup_data("webhook", json.dumps(definition))

View file

@ -178,22 +178,24 @@ class Webhook(HttpTransport):
return False return False
def notify(self, check): def notify(self, check):
url = self.channel.url_down spec = self.channel.webhook_spec(check.status)
if check.status == "up": assert spec["url"]
url = self.channel.url_up
assert url url = self.prepare(spec["url"], check, urlencode=True)
url = self.prepare(url, check, urlencode=True)
headers = {} headers = {}
for key, value in self.channel.headers.items(): for key, value in spec["headers"].items():
headers[key] = self.prepare(value, check) headers[key] = self.prepare(value, check)
if self.channel.post_data: body = spec["body"]
payload = self.prepare(self.channel.post_data, check) if body:
return self.post(url, data=payload.encode(), headers=headers) body = self.prepare(body, check)
else:
if spec["method"] == "GET":
return self.get(url, headers=headers) return self.get(url, headers=headers)
elif spec["method"] == "POST":
return self.post(url, data=body.encode(), headers=headers)
elif spec["method"] == "PUT":
return self.put(url, data=body.encode(), headers=headers)
class Slack(HttpTransport): class Slack(HttpTransport):

View file

@ -1,10 +1,11 @@
from datetime import timedelta as td from datetime import timedelta as td
import json import json
import re
from urllib.parse import quote, urlencode from urllib.parse import quote, urlencode
from django import forms from django import forms
from django.forms import URLField
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError
from django.core.validators import RegexValidator from django.core.validators import RegexValidator
from hc.front.validators import ( from hc.front.validators import (
CronExpressionValidator, CronExpressionValidator,
@ -14,6 +15,37 @@ from hc.front.validators import (
import requests import requests
class HeadersField(forms.Field):
message = """Use "Header-Name: value" pairs, one per line."""
def to_python(self, value):
if not value:
return {}
headers = {}
for line in value.split("\n"):
if not line.strip():
continue
if ":" not in value:
raise ValidationError(self.message)
n, v = line.split(":", maxsplit=1)
n, v = n.strip(), v.strip()
if not n or not v:
raise ValidationError(message=self.message)
headers[n] = v
return headers
def validate(self, value):
super().validate(value)
for k, v in value.items():
if len(k) > 1000 or len(v) > 1000:
raise ValidationError("Value too long")
class NameTagsForm(forms.Form): class NameTagsForm(forms.Form):
name = forms.CharField(max_length=100, required=False) name = forms.CharField(max_length=100, required=False)
tags = forms.CharField(max_length=500, required=False) tags = forms.CharField(max_length=500, required=False)
@ -68,49 +100,28 @@ class AddUrlForm(forms.Form):
value = forms.URLField(max_length=1000, validators=[WebhookValidator()]) value = forms.URLField(max_length=1000, validators=[WebhookValidator()])
_valid_header_name = re.compile(r"\A[^:\s][^:\r\n]*\Z").match METHODS = ("GET", "POST", "PUT")
class AddWebhookForm(forms.Form): class AddWebhookForm(forms.Form):
error_css_class = "has-error" error_css_class = "has-error"
url_down = forms.URLField( method_down = forms.ChoiceField(initial="GET", choices=zip(METHODS, METHODS))
body_down = forms.CharField(max_length=1000, required=False)
headers_down = HeadersField(required=False)
url_down = URLField(
max_length=1000, required=False, validators=[WebhookValidator()] max_length=1000, required=False, validators=[WebhookValidator()]
) )
method_up = forms.ChoiceField(initial="GET", choices=zip(METHODS, METHODS))
body_up = forms.CharField(max_length=1000, required=False)
headers_up = HeadersField(required=False)
url_up = forms.URLField( url_up = forms.URLField(
max_length=1000, required=False, validators=[WebhookValidator()] max_length=1000, required=False, validators=[WebhookValidator()]
) )
post_data = forms.CharField(max_length=1000, required=False)
def __init__(self, *args, **kwargs):
super(AddWebhookForm, self).__init__(*args, **kwargs)
self.invalid_header_names = set()
self.headers = {}
if "header_key[]" in self.data and "header_value[]" in self.data:
keys = self.data.getlist("header_key[]")
values = self.data.getlist("header_value[]")
for key, value in zip(keys, values):
if not key:
continue
if not _valid_header_name(key):
self.invalid_header_names.add(key)
self.headers[key] = value
def clean(self):
if self.invalid_header_names:
raise forms.ValidationError("Invalid header names")
return self.cleaned_data
def get_value(self): def get_value(self):
val = dict(self.cleaned_data) return json.dumps(dict(self.cleaned_data), sort_keys=True)
val["headers"] = self.headers
return json.dumps(val, sort_keys=True)
phone_validator = RegexValidator( phone_validator = RegexValidator(

View file

@ -123,3 +123,8 @@ def fix_asterisks(s):
""" Prepend asterisks with "Combining Grapheme Joiner" characters. """ """ Prepend asterisks with "Combining Grapheme Joiner" characters. """
return s.replace("*", "\u034f*") return s.replace("*", "\u034f*")
@register.filter
def format_headers(headers):
return "\n".join("%s: %s" % (k, v) for k, v in headers.items())

View file

@ -8,24 +8,32 @@ class AddWebhookTestCase(BaseTestCase):
def test_instructions_work(self): def test_instructions_work(self):
self.client.login(username="alice@example.org", password="password") self.client.login(username="alice@example.org", password="password")
r = self.client.get(self.url) r = self.client.get(self.url)
self.assertContains(r, "Runs a HTTP GET or HTTP POST") self.assertContains(r, "Executes an HTTP request")
def test_it_adds_two_webhook_urls_and_redirects(self): def test_it_adds_two_webhook_urls_and_redirects(self):
form = {"url_down": "http://foo.com", "url_up": "https://bar.com"} form = {
"method_down": "GET",
"url_down": "http://foo.com",
"method_up": "GET",
"url_up": "https://bar.com",
}
self.client.login(username="alice@example.org", password="password") self.client.login(username="alice@example.org", password="password")
r = self.client.post(self.url, form) r = self.client.post(self.url, form)
self.assertRedirects(r, "/integrations/") self.assertRedirects(r, "/integrations/")
c = Channel.objects.get() c = Channel.objects.get()
self.assertEqual(
c.value,
'{"headers": {}, "post_data": "", "url_down": "http://foo.com", "url_up": "https://bar.com"}',
)
self.assertEqual(c.project, self.project) self.assertEqual(c.project, self.project)
self.assertEqual(c.down_webhook_spec["url"], "http://foo.com")
self.assertEqual(c.up_webhook_spec["url"], "https://bar.com")
def test_it_adds_webhook_using_team_access(self): def test_it_adds_webhook_using_team_access(self):
form = {"url_down": "http://foo.com", "url_up": "https://bar.com"} form = {
"method_down": "GET",
"url_down": "http://foo.com",
"method_up": "GET",
"url_up": "https://bar.com",
}
# Logging in as bob, not alice. Bob has team access so this # Logging in as bob, not alice. Bob has team access so this
# should work. # should work.
@ -34,10 +42,8 @@ class AddWebhookTestCase(BaseTestCase):
c = Channel.objects.get() c = Channel.objects.get()
self.assertEqual(c.project, self.project) self.assertEqual(c.project, self.project)
self.assertEqual( self.assertEqual(c.down_webhook_spec["url"], "http://foo.com")
c.value, self.assertEqual(c.up_webhook_spec["url"], "https://bar.com")
'{"headers": {}, "post_data": "", "url_down": "http://foo.com", "url_up": "https://bar.com"}',
)
def test_it_rejects_bad_urls(self): def test_it_rejects_bad_urls(self):
urls = [ urls = [
@ -52,7 +58,12 @@ class AddWebhookTestCase(BaseTestCase):
self.client.login(username="alice@example.org", password="password") self.client.login(username="alice@example.org", password="password")
for url in urls: for url in urls:
form = {"url_down": url, "url_up": ""} form = {
"method_down": "GET",
"url_down": url,
"method_up": "GET",
"url_up": "",
}
r = self.client.post(self.url, form) r = self.client.post(self.url, form)
self.assertContains(r, "Enter a valid URL.", msg_prefix=url) self.assertContains(r, "Enter a valid URL.", msg_prefix=url)
@ -60,35 +71,41 @@ class AddWebhookTestCase(BaseTestCase):
self.assertEqual(Channel.objects.count(), 0) self.assertEqual(Channel.objects.count(), 0)
def test_it_handles_empty_down_url(self): def test_it_handles_empty_down_url(self):
form = {"url_down": "", "url_up": "http://foo.com"} form = {
"method_down": "GET",
"url_down": "",
"method_up": "GET",
"url_up": "http://foo.com",
}
self.client.login(username="alice@example.org", password="password") self.client.login(username="alice@example.org", password="password")
self.client.post(self.url, form) self.client.post(self.url, form)
c = Channel.objects.get() c = Channel.objects.get()
self.assertEqual( self.assertEqual(c.down_webhook_spec["url"], "")
c.value, self.assertEqual(c.up_webhook_spec["url"], "http://foo.com")
'{"headers": {}, "post_data": "", "url_down": "", "url_up": "http://foo.com"}',
)
def test_it_adds_post_data(self): def test_it_adds_request_body(self):
form = {"url_down": "http://foo.com", "post_data": "hello"} form = {
"method_down": "POST",
"url_down": "http://foo.com",
"body_down": "hello",
"method_up": "GET",
}
self.client.login(username="alice@example.org", password="password") self.client.login(username="alice@example.org", password="password")
r = self.client.post(self.url, form) r = self.client.post(self.url, form)
self.assertRedirects(r, "/integrations/") self.assertRedirects(r, "/integrations/")
c = Channel.objects.get() c = Channel.objects.get()
self.assertEqual( self.assertEqual(c.down_webhook_spec["body"], "hello")
c.value,
'{"headers": {}, "post_data": "hello", "url_down": "http://foo.com", "url_up": ""}',
)
def test_it_adds_headers(self): def test_it_adds_headers(self):
form = { form = {
"method_down": "GET",
"url_down": "http://foo.com", "url_down": "http://foo.com",
"header_key[]": ["test", "test2"], "headers_down": "test:123\ntest2:abc",
"header_value[]": ["123", "abc"], "method_up": "GET",
} }
self.client.login(username="alice@example.org", password="password") self.client.login(username="alice@example.org", password="password")
@ -96,16 +113,34 @@ class AddWebhookTestCase(BaseTestCase):
self.assertRedirects(r, "/integrations/") self.assertRedirects(r, "/integrations/")
c = Channel.objects.get() c = Channel.objects.get()
self.assertEqual(c.headers, {"test": "123", "test2": "abc"}) self.assertEqual(
c.down_webhook_spec["headers"], {"test": "123", "test2": "abc"}
)
def test_it_rejects_bad_header_names(self): def test_it_rejects_bad_headers(self):
self.client.login(username="alice@example.org", password="password") self.client.login(username="alice@example.org", password="password")
form = { form = {
"method_down": "GET",
"url_down": "http://example.org", "url_down": "http://example.org",
"header_key[]": ["ill:egal"], "headers_down": "invalid-headers",
"header_value[]": ["123"], "method_up": "GET",
} }
r = self.client.post(self.url, form) r = self.client.post(self.url, form)
self.assertContains(r, "Please use valid HTTP header names.") self.assertContains(r, """invalid-headers""")
self.assertEqual(Channel.objects.count(), 0) self.assertEqual(Channel.objects.count(), 0)
def test_it_strips_headers(self):
form = {
"method_down": "GET",
"url_down": "http://foo.com",
"headers_down": " test : 123 ",
"method_up": "GET",
}
self.client.login(username="alice@example.org", password="password")
r = self.client.post(self.url, form)
self.assertRedirects(r, "/integrations/")
c = Channel.objects.get()
self.assertEqual(c.down_webhook_spec["headers"], {"test": "123"})

View file

@ -2,3 +2,39 @@
border-color: #a94442; border-color: #a94442;
} }
#add-webhook-form div.bootstrap-select {
width: 100px;
}
.method-url-group {
display: flex;
}
.method-url-group > div.dropdown button {
border-right: 0;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
.method-url-group input {
z-index: 1;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
#webhook-variables tr:first-child th, #webhook-variables tr:first-child td {
border-top: 0;
}
#webhook-variables th {
white-space: nowrap;
}
.label-down {
color: #d9534f;
}
.label-up {
color: #5cb85c
}

View file

@ -1,25 +1,15 @@
$(function() { $(function() {
function haveBlankHeaderForm() { $("#method-down").change(function() {
return $("#webhook-headers .webhook-header").filter(function() { var method = this.value;
var key = $(".key", this).val(); $("#body-down-group").toggle(method != "GET");
var value = $(".value", this).val(); });
return !key && !value;
}).length;
}
function ensureBlankHeaderForm() { $("#method-up").change(function() {
if (!haveBlankHeaderForm()) { var method = this.value;
var tmpl = $("#header-template").html(); $("#body-up-group").toggle(method != "GET");
$("#webhook-headers").append(tmpl); });
}
}
$("#webhook-headers").on("click", "button", function(e) { // On page load, check if we need to show "request body" fields
e.preventDefault(); $("#method-down").trigger("change");
$(this).closest(".webhook-header").remove(); $("#method-up").trigger("change");
ensureBlankHeaderForm();
})
$("#webhook-headers").on("keyup", "input", ensureBlankHeaderForm);
ensureBlankHeaderForm();
}); });

View file

@ -398,30 +398,38 @@
</div> </div>
{% if ch.kind == "webhook" %} {% if ch.kind == "webhook" %}
{% with ch.down_webhook_spec as spec %}
{% if spec.url %}
<p><strong>Execute on "down" events:</strong></p>
<pre>{{ spec.method }} {{ spec.url }}</pre>
{% if spec.body %}
<p>Request Body</p>
<pre>{{ spec.body }}</pre>
{% endif %}
{% if ch.url_down %} {% if spec.headers %}
<p><strong>URL for "down" events</strong></p> <p>Request Headers</p>
<pre>{{ ch.url_down }}</pre> <pre>{{ spec.headers|format_headers }}</pre>
{% endif %}
{% endif %}
{% endwith %}
{% with ch.up_webhook_spec as spec %}
{% if spec.url %}
<p><strong>Execute on "up" events:</strong></p>
<pre>{{ spec.method }} {{ spec.url }}</pre>
{% if spec.body %}
<p>Request Body</p>
<pre>{{ spec.body }}</pre>
{% endif %}
{% if spec.headers %}
<p>Request Headers</p>
<pre>{{ spec.headers|format_headers }}</pre>
{% endif %}
{% endif %}
{% endwith %}
{% endif %} {% endif %}
{% if ch.url_up %}
<p><strong>URL for "up" events</strong></p>
<pre>{{ ch.url_up }}</pre>
{% endif %}
{% if ch.post_data %}
<p><strong>POST data</strong></p>
<pre>{{ ch.post_data }}</pre>
{% endif %}
{% for k, v in ch.headers.items %}
<p><strong>Header <code>{{ k }}</code></strong></p>
<pre>{{ v }}</pre>
{% endfor %}
{% endif %}
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>

View file

@ -6,168 +6,201 @@
{% block content %} {% block content %}
<div class="row"> <div class="row">
<div class="col-sm-12"> <div class="col-sm-12">
<h1>Webhook</h1> <h1>Webhook</h1>
<p>Runs a HTTP GET or HTTP POST to your specified URL when a check <p>Executes an HTTP request to your specified URL when a check
goes up or down. Uses GET by default, and uses POST if you specify goes up or down.</p>
any POST data.</p>
<p>You can use the following variables in webhook URLs:</p> <p>
<table class="table webhook-variables"> You can use placeholders <strong>$NAME</strong>, <strong>$STATUS</strong> and others in webhook URLs,
<tr> request body and header values
<th class="variable-column">Variable</th> <a href="#" data-toggle="modal" data-target="#reference-modal">(quick reference)</a>.
<td>Will be replaced with…</td> </p>
</tr>
<tr>
<th><code>$CODE</code></th>
<td>The UUID code of the check</td>
</tr>
<tr>
<th><code>$NAME</code></th>
<td>Name of the check</td>
</tr>
<tr>
<th><code>$NOW</code></th>
<td>
Current UTC time in ISO8601 format.
Example: "{{ now }}"
</td>
</tr>
<tr>
<th><code>$STATUS</code></th>
<td>Check's current status ("up" or "down")</td>
</tr>
<tr>
<th><code>$TAG1, $TAG2, …</code></th>
<td>Value of the first tag, the second tag, …</td>
</tr>
</table>
<p>For example, a callback URL using variables might look like so: <form id="add-webhook-form" method="post">
<pre>http://requestb.in/1hhct291?message=<strong>$NAME</strong>:<strong>$STATUS</strong></pre>
<p>
After encoding and replacing the variables, {% site_name %} would then call:
</p>
<pre>http://requestb.in/1hhct291?message=<strong>My%20Check</strong>:<strong>down</strong></pre>
<h2>Integration Settings</h2>
<form method="post" class="form-horizontal">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="kind" value="webhook" />
<div class="form-group {{ form.url_down.css_classes }}"> <div class="row">
<label class="col-sm-2 control-label">URL for "down" events</label> <div class="col-sm-6">
<div class="col-sm-10"> <h2>Execute when a check goes <strong class="label-down">down</strong></h2>
<input <br />
type="text"
class="form-control" <div class="form-group {{ form.url_down.css_classes }}">
name="url_down" <label>URL</label>
placeholder="http://..." <div class="method-url-group">
value="{{ form.url_down.value|default:"" }}"> <select id="method-down" name="method_down" class="selectpicker">
<option{% if form.method_down.value == "GET" %} selected{% endif %}>GET</option>
<option{% if form.method_down.value == "POST" %} selected{% endif %}>POST</option>
<option{% if form.method_down.value == "PUT" %} selected{% endif %}>PUT</option>
</select>
<input
name="url_down"
value="{{ form.url_down.value|default:"" }}"
type="text"
class="form-control"
placeholder="https://..." />
</div>
{% if form.url_down.errors %} {% if form.url_down.errors %}
<div class="help-block"> <div class="help-block">
{{ form.url_down.errors|join:"" }} {{ form.url_down.errors|join:"" }}
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div>
<div class="form-group {{ form.url_up.css_classes }}"> <div id="body-down-group" class="form-group {{ form.body_down.css_classes }}" style="display: none">
<label class="col-sm-2 control-label">URL for "up" events</label> <label class="control-label">Request Body</label>
<div class="col-sm-10"> <textarea
<input
type="text"
class="form-control" class="form-control"
name="url_up" rows="3"
placeholder="http://..." name="body_down"
value="{{ form.url_up.value|default:"" }}"> placeholder='{"status": "$STATUS"}'>{{ form.body_down.value|default:"" }}</textarea>
{% if form.body_down.errors %}
<div class="help-block">
{{ form.body_down.errors|join:"" }}
</div>
{% endif %}
</div>
<div class="form-group {{ form.headers_down.css_classes }}">
<label class="control-label">Request Headers</label>
<textarea
class="form-control"
rows="3"
name="headers_down"
placeholder="X-Sample-Header: $NAME has gone down">{{ form.headers_down.value|default:"" }}</textarea>
<div class="help-block">
{% if form.headers_down.errors %}
{{ form.headers_down.errors|join:"" }}
{% else %}
Optional "Header-Name: value" pairs, one pair per line.
{% endif %}
</div>
</div>
</div>
<div class="col-sm-6">
<h2>Execute when a check goes <strong class="label-up">up</strong></h2>
<br />
<div class="form-group {{ form.url_up.css_classes }}">
<label>URL</label>
<div class="method-url-group">
<select id="method-up" name="method_up" class="selectpicker">
<option{% if form.method_up.value == "GET" %} selected{% endif %}>GET</option>
<option{% if form.method_up.value == "POST" %} selected{% endif %}>POST</option>
<option{% if form.method_up.value == "PUT" %} selected{% endif %}>PUT</option>
</select>
<input
name="url_up"
value="{{ form.url_up.value|default:"" }}"
type="text"
class="form-control"
placeholder="https://..." />
</div>
{% if form.url_up.errors %} {% if form.url_up.errors %}
<div class="help-block"> <div class="help-block">
{{ form.url_up.errors|join:"" }} {{ form.url_up.errors|join:"" }}
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div> <div id="body-up-group" class="form-group {{ form.body_up.css_classes }}" style="display: none">
<div class="form-group {{ form.post_data.css_classes }}"> <label class="control-label">Request Body</label>
<label class="col-sm-2 control-label">POST data</label> <textarea
<div class="col-sm-10">
<input
type="text"
class="form-control" class="form-control"
name="post_data" rows="3"
placeholder='{"status": "$STATUS"}' name="body_up"
value="{{ form.post_data.value|default:"" }}"> placeholder='{"status": "$STATUS"}'>{{ form.body_up.value|default:"" }}</textarea>
{% if form.post_data.errors %} {% if form.post_data.errors %}
<div class="help-block"> <div class="help-block">
{{ form.post_data.errors|join:"" }} {{ form.post_data.errors|join:"" }}
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div>
<div class="form-group"> <div class="form-group {{ form.headers_up.css_classes }}">
<label class="col-sm-2 control-label">Request Headers</label> <label class="control-label">Request Headers</label>
<div class="col-xs-12 col-sm-10"> <textarea
<div id="webhook-headers"> class="form-control"
{% for k, v in form.headers.items %} rows="3"
<div class="form-inline webhook-header"> name="headers_up"
<input placeholder="X-Sample-Header: $NAME is back up">{{ form.headers_up.value|default:"" }}</textarea>
type="text" <div class="help-block">
class="form-control key {% if k in form.invalid_header_names %}error{% endif %}" {% if form.headers_up.errors %}
name="header_key[]" {{ form.headers_up.errors|join:"" }}
placeholder="Content-Type" {% else %}
value="{{ k }}" /> Optional "Header-Name: value" pairs, one pair per line.
<input {% endif %}
type="text"
class="form-control value"
name="header_value[]"
placeholder="application/json"
value="{{ v }}" />
<button class="btn btn-default" type="button">
<span class="icon-delete"></span>
</button>
</div>
{% endfor %}
</div> </div>
{% if form.invalid_header_names %}
<div class="text-danger">
Please use valid HTTP header names.
</div>
{% endif %}
</div> </div>
</div> </div>
<div class="form-group"> </div>
<div class="col-sm-offset-2 col-sm-10">
<div class="form-group" class="clearfix">
<br>
<br>
<div class="text-right">
<button type="submit" class="btn btn-primary">Save Integration</button> <button type="submit" class="btn btn-primary">Save Integration</button>
</div> </div>
</div> </div>
</form> </form>
</div>
</div>
<div id="header-template" class="hide">
<div class="form-inline webhook-header">
<input
type="text"
class="form-control key"
name="header_key[]"
placeholder="Content-Type" />
<input
type="text"
class="form-control value"
name="header_value[]"
placeholder="application/json" />
<button class="btn btn-default" type="button">
<span class="icon-delete"></span>
</button>
</div> </div>
</div> </div>
<div id="reference-modal" class="modal">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h4>Supported Placeholders</h4>
</div>
<div class="modal-body">
<p>
You can use the below placeholders in webhook URL, request body
and header values. {% site_name %} will replace the placeholders
with the correct values.
</p>
<table id="webhook-variables" class="table modal-body">
<tr>
<th><code>$CODE</code></th>
<td>The UUID code of the check</td>
</tr>
<tr>
<th><code>$NAME</code></th>
<td>Name of the check</td>
</tr>
<tr>
<th><code>$NOW</code></th>
<td>
Current UTC time in ISO8601 format.
Example: "{{ now }}"
</td>
</tr>
<tr>
<th><code>$STATUS</code></th>
<td>Check's current status ("up" or "down")</td>
</tr>
<tr>
<th><code>$TAG1, $TAG2, …</code></th>
<td>Value of the first tag, the second tag, …</td>
</tr>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Got It!</button>
</div>
</div>
</div>
</div>
{% endblock %} {% endblock %}
{% block scripts %} {% block scripts %}
{% compress js %} {% compress js %}
<script src="{% static 'js/jquery-2.1.4.min.js' %}"></script> <script src="{% static 'js/jquery-2.1.4.min.js' %}"></script>
<script src="{% static 'js/bootstrap.min.js' %}"></script> <script src="{% static 'js/bootstrap.min.js' %}"></script>
<script src="{% static 'js/bootstrap-select.min.js' %}"></script>
<script src="{% static 'js/webhook.js' %}"></script> <script src="{% static 'js/webhook.js' %}"></script>
{% endcompress %} {% endcompress %}
{% endblock %} {% endblock %}