Skip to content

Commit 6f2ccf7

Browse files
authored
Merge pull request #88 from theodo/support-wpt-private-instances
Add support for WebPageTest Private Instances
2 parents ed28ebf + cf472fd commit 6f2ccf7

23 files changed

Lines changed: 611 additions & 226 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

77
## [Unreleased]
8+
- Add support for WebPageTest Private Instances 🎉 (@phacks)
89

910
## [1.0.3] - 2019-12-10
1011

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
- 👥 Invite the whole team so that everyone (devs, ops, product, marketing…) is involved in performance
3030
- 🗺 Audit the performance of individual URLs or entire user journeys ([even on Single Page Apps!](https://css-tricks.com/recipes-for-performance-testing-single-page-applications-in-webpagetest/))
3131
- 📸 Easily access and compare WebPageTest results between audits
32+
- 🙈 Can be used with your own Private Instance of WebPageTest
3233

3334
You can try a demo version by logging in to https://falco.theo.do with the credentials `demo / demodemo`.
3435

@@ -40,6 +41,8 @@ You can deploy Falco on Heroku by clicking on the following button:
4041

4142
You will need to provide your credit card details to Heroku, but you will be under the free tier by default. You can find more details on why they are needed and Heroku’s pricing policy [in the docs](https://getfal.co).
4243

44+
After deployment, you can connect to Falco (and the admin interface at `/admin/`) with the credentials `admin` and `admin`: make sure to change your password after connecting!
45+
4346
<details>
4447
<summary>Heroku Teams user? Click here to deploy Falco.</summary>
4548
<br />

backend/audits/tasks.py

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,13 @@ def request_audit(audit_uuid):
3737
payload["url"] = audit.page.url
3838
payload["lighthouse"] = 1
3939
payload["k"] = audit.page.project.wpt_api_key
40+
wpt_instance_url = audit.page.project.wpt_instance_url
4041
elif audit.script is not None:
4142
payload["script"] = audit.script.script
4243
payload["k"] = audit.script.project.wpt_api_key
44+
wpt_instance_url = audit.script.project.wpt_instance_url
4345

44-
r = requests.post("https://www.webpagetest.org/runtest.php", params=payload)
46+
r = requests.post(f"{wpt_instance_url}/runtest.php", params=payload)
4547
response = r.json()
4648
if response["statusCode"] == 200:
4749
audit_status_queueing = AuditStatusHistory(
@@ -85,9 +87,14 @@ def poll_audit_results(audit_uuid, json_url):
8587
audit_status_requested.save()
8688
poll_audit_results.apply_async((audit_uuid, json_url), countdown=15)
8789
elif status_code == 200:
90+
if audit.page is not None:
91+
wpt_instance_url = audit.page.project.wpt_instance_url
92+
elif audit.script is not None:
93+
wpt_instance_url = audit.script.project.wpt_instance_url
94+
8895
parsed_url = urlparse(json_url)
8996
test_id = parse_qs(parsed_url.query)["test"][0]
90-
wpt_results_user_url = f"https://www.webpagetest.org/result/{test_id}"
97+
wpt_results_user_url = f"{wpt_instance_url}/result/{test_id}"
9198
try:
9299
if audit.page is not None:
93100
project = audit.page.project
@@ -259,37 +266,53 @@ def clean_old_audit_statuses():
259266

260267

261268
@shared_task
262-
def get_wpt_audit_configurations():
269+
def get_wpt_audit_configurations(wpt_instance_url="https://webpagetest.org"):
263270
"""gets all the available locations from WPT"""
264-
response = requests.get("https://www.webpagetest.org/getLocations.php?f=json&k=A")
271+
272+
# For some reason, the key mask to get API-available locations is different between
273+
# public and private WPT instances
274+
wpt_key_mask = ""
275+
if wpt_instance_url == "https://webpagetest.org":
276+
wpt_key_mask = "A"
277+
278+
response = requests.get(
279+
f"{wpt_instance_url}/getLocations.php?f=json&k={wpt_key_mask}"
280+
)
265281

266282
if response.status_code != 200:
267283
logging.error("Invalid response from WebPageTest API: non-200 response code")
268-
return
284+
raise Exception("Invalid response from WebPageTest API: non-200 response code")
269285

270286
try:
271287
data = response.json()["data"]
272288
except KeyError:
273289
logging.error(
274290
"Invalid response from WebPageTest API: 'data' key is not present"
275291
)
276-
return
292+
raise Exception(
293+
"Invalid response from WebPageTest API: 'data' key is not present"
294+
)
277295

278-
for available_audit_parameter in AvailableAuditParameters.objects.all():
296+
for available_audit_parameter in AvailableAuditParameters.objects.filter(
297+
wpt_instance_url=wpt_instance_url
298+
):
279299
available_audit_parameter.is_active = False
280300
available_audit_parameter.save()
281301

282302
for location, location_data in data.items():
283303
browsers = location_data["Browsers"].split(",")
284-
group = location_data["group"]
304+
group = location_data.get(
305+
"group", ""
306+
) # Private instances locations may not be grouped
285307
label = location_data["labelShort"]
286-
for brower in browsers:
308+
for browser in browsers:
287309
configuration, created = AvailableAuditParameters.objects.update_or_create(
288-
browser=brower,
310+
browser=browser,
289311
location=location,
290312
defaults={
291313
"location_label": label,
292314
"location_group": group,
293315
"is_active": True,
294316
},
317+
wpt_instance_url=wpt_instance_url,
295318
)
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
{
2+
"statusCode": 200,
3+
"statusText": "Ok",
4+
"data": {
5+
"Dulles_MotoG4": {
6+
"Label": "Moto G (gen 4)",
7+
"location": "Dulles_MotoG4",
8+
"Browsers": "Moto G4 - Chrome,Moto G4 - Firefox",
9+
"status": "OK",
10+
"relayServer": null,
11+
"relayLocation": null,
12+
"labelShort": "Dulles, VA",
13+
"group": "Android Devices - Dulles, VA",
14+
"PendingTests": {
15+
"p1": 0,
16+
"p2": 1,
17+
"p3": 0,
18+
"p4": 0,
19+
"p5": 84,
20+
"p6": 0,
21+
"p7": 0,
22+
"p8": 0,
23+
"p9": 0,
24+
"Total": 101,
25+
"HighPriority": 0,
26+
"LowPriority": 85,
27+
"Testing": 16,
28+
"Idle": 0
29+
}
30+
},
31+
"Dulles_MotoG": {
32+
"Label": "Moto G (gen 1)",
33+
"location": "Dulles_MotoG",
34+
"Browsers": "Moto G - Chrome",
35+
"status": "OK",
36+
"relayServer": null,
37+
"relayLocation": null,
38+
"labelShort": "Dulles, VA",
39+
"group": "Android Devices - Dulles, VA",
40+
"PendingTests": {
41+
"p1": 0,
42+
"p2": 0,
43+
"p3": 0,
44+
"p4": 0,
45+
"p5": 0,
46+
"p6": 0,
47+
"p7": 0,
48+
"p8": 0,
49+
"p9": 0,
50+
"Total": 0,
51+
"HighPriority": 0,
52+
"LowPriority": 0,
53+
"Testing": 0,
54+
"Idle": 13
55+
}
56+
}
57+
}
58+
}

backend/audits/tests/test_tasks.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import httpretty
22
from unittest.mock import MagicMock
3-
import json
4-
from django.test import TestCase, override_settings
5-
from audits.tasks import request_audit
3+
from django.test import TestCase
4+
from audits.tasks import request_audit, get_wpt_audit_configurations
65
from projects.models import (
76
Page,
87
Project,
@@ -15,22 +14,21 @@
1514

1615
class TasksTestCase(TestCase):
1716
@httpretty.activate
18-
@override_settings(CELERY_EAGER=True)
1917
@patch("audits.tasks.poll_audit_results")
2018
def test_request_audit(self, poll_audit_results_mock):
2119
poll_audit_results_mock.apply_async = MagicMock()
2220
POST_runtest_data = open("audits/tests/json_mocks/wpt_POST_runtest.json").read()
23-
GET_jsonResults_data = json.load(
24-
open("audits/tests/json_mocks/wpt_GET_jsonResult.json")
25-
)
21+
GET_jsonResults_data = open(
22+
"audits/tests/json_mocks/wpt_GET_jsonResult.json"
23+
).read()
2624
httpretty.register_uri(
2725
httpretty.POST,
28-
"https://www.webpagetest.org/runtest.php",
26+
"https://webpagetest.org/runtest.php",
2927
body=POST_runtest_data,
3028
)
3129
httpretty.register_uri(
3230
httpretty.GET,
33-
"https://www.webpagetest.org/jsonResult.php?test=191024_HA_976b046886025ec8693cbe4f1145929e",
31+
"https://webpagetest.org/jsonResult.php?test=191024_HA_976b046886025ec8693cbe4f1145929e",
3432
body=GET_jsonResults_data,
3533
)
3634
project = Project.objects.create()
@@ -71,3 +69,36 @@ def test_request_audit(self, poll_audit_results_mock):
7169
),
7270
countdown=15,
7371
)
72+
73+
@httpretty.activate
74+
def test_get_wpt_audit_configurations__create_configurations_for_default_instance(
75+
self
76+
):
77+
GET_getLocations_data = open(
78+
"audits/tests/json_mocks/wpt_GET_getLocations.json"
79+
).read()
80+
httpretty.register_uri(
81+
httpretty.GET,
82+
"https://webpagetest.org/getLocations.php?f=json&k=A",
83+
body=GET_getLocations_data,
84+
)
85+
get_wpt_audit_configurations()
86+
available_audit_parameters = AvailableAuditParameters.objects.all()
87+
self.assertEqual(len(available_audit_parameters), 3)
88+
89+
@httpretty.activate
90+
def test_get_wpt_audit_configurations__create_configurations_for_private_instance(
91+
self
92+
):
93+
GET_getLocations_data = open(
94+
"audits/tests/json_mocks/wpt_GET_getLocations.json"
95+
).read()
96+
private_instance_name = "http://myprivateinstance.com"
97+
httpretty.register_uri(
98+
httpretty.GET,
99+
f"{private_instance_name}/getLocations.php?f=json&k=",
100+
body=GET_getLocations_data,
101+
)
102+
get_wpt_audit_configurations(private_instance_name)
103+
available_audit_parameters = AvailableAuditParameters.objects.all()
104+
self.assertEqual(len(available_audit_parameters), 3)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Generated by Django 2.2.4 on 2019-11-22 10:54
2+
3+
from django.db import migrations, models
4+
5+
6+
class Migration(migrations.Migration):
7+
8+
dependencies = [("projects", "0030_auto_20190904_1152")]
9+
10+
operations = [
11+
migrations.AddField(
12+
model_name="availableauditparameters",
13+
name="wpt_instance_url",
14+
field=models.CharField(default="https://webpagetest.org", max_length=100),
15+
),
16+
migrations.AddField(
17+
model_name="project",
18+
name="wpt_instance_url",
19+
field=models.CharField(default="https://webpagetest.org", max_length=100),
20+
),
21+
]

backend/projects/models.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ class Project(BaseModel):
1414
User, blank=True, related_name="member_of", through="ProjectMemberRole"
1515
)
1616
is_active = models.BooleanField(default=True)
17+
wpt_instance_url = models.CharField(
18+
max_length=100, blank=False, null=False, default="https://webpagetest.org"
19+
)
1720

1821
@property
1922
def latest_audit_at(self):
@@ -108,6 +111,9 @@ class AvailableAuditParameters(BaseModel):
108111
location_label = models.CharField(max_length=100, blank=False, null=False)
109112
location_group = models.CharField(max_length=100, blank=False, null=False)
110113
is_active = models.BooleanField(default=True)
114+
wpt_instance_url = models.CharField(
115+
max_length=100, blank=False, null=False, default="https://webpagetest.org"
116+
)
111117

112118
class Meta:
113119
ordering = ("location", "browser")

backend/projects/serializers.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,13 @@ class Meta:
8686
class AvailableAuditParameterSerializer(serializers.ModelSerializer):
8787
class Meta:
8888
model = AvailableAuditParameters
89-
fields = ("uuid", "browser", "location_label", "location_group")
89+
fields = (
90+
"uuid",
91+
"browser",
92+
"location_label",
93+
"location_group",
94+
"wpt_instance_url",
95+
)
9096

9197

9298
class ProjectAuditParametersSerializer(serializers.ModelSerializer):
@@ -156,5 +162,6 @@ class Meta:
156162
"screenshot_url",
157163
"latest_audit_at",
158164
"wpt_api_key",
165+
"wpt_instance_url",
159166
"has_siblings",
160167
)

backend/projects/urls.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
views.project_audit_parameters_detail,
1818
),
1919
path("available_audit_parameters", views.available_audit_parameters),
20+
path(
21+
"available_audit_parameters/discover", views.discover_available_audit_parameters
22+
),
2023
path("<uuid:project_uuid>/scripts", views.project_scripts),
2124
path("<uuid:project_uuid>/scripts/<uuid:script_uuid>", views.project_script_detail),
2225
]

backend/projects/views.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from rest_framework import permissions, status
77
from rest_framework.decorators import api_view, permission_classes
88
from rest_framework.parsers import JSONParser
9+
from requests.exceptions import ConnectionError
910
from projects.models import (
1011
Page,
1112
Project,
@@ -28,6 +29,8 @@
2829
is_admin_of_project,
2930
)
3031

32+
from audits.tasks import get_wpt_audit_configurations
33+
3134

3235
def get_user_projects(user_id):
3336
return Project.objects.filter(members__id=user_id, is_active=True)
@@ -404,6 +407,49 @@ def project_members(request, project_uuid):
404407
)
405408

406409

410+
@swagger_auto_schema(
411+
methods=["post"],
412+
request_body=openapi.Schema(
413+
type="object", properties={"wpt_instance_url": openapi.Schema(type="string")}
414+
),
415+
responses={
416+
201: openapi.Response(
417+
"Returns discovered available audit parameters for the WPT instance URL passed in parameter",
418+
AvailableAuditParameterSerializer,
419+
)
420+
},
421+
tags=["Project Audit Parameters"],
422+
)
423+
@api_view(["POST"])
424+
def discover_available_audit_parameters(request):
425+
data = JSONParser().parse(request)
426+
if "wpt_instance_url" in data:
427+
try:
428+
get_wpt_audit_configurations(data["wpt_instance_url"])
429+
except ConnectionError:
430+
return JsonResponse(
431+
{
432+
"error": "UNREACHABLE",
433+
"details": "The WPT instance is not reachable, please check the URL",
434+
},
435+
status=status.HTTP_400_BAD_REQUEST,
436+
)
437+
available_audit_parameters = AvailableAuditParameters.objects.filter(
438+
is_active=True
439+
)
440+
serializer = AvailableAuditParameterSerializer(
441+
available_audit_parameters, many=True
442+
)
443+
return JsonResponse(serializer.data, safe=False)
444+
return JsonResponse(
445+
{
446+
"error": "MISSING_PARAMETER",
447+
"details": "You must provide a wpt_instance_url in the request body",
448+
},
449+
status=status.HTTP_400_BAD_REQUEST,
450+
)
451+
452+
407453
@swagger_auto_schema(
408454
methods=["get"],
409455
responses={

0 commit comments

Comments
 (0)