New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add feature flag and config setting to load client from an external URL #4355
Conversation
WIP because it needs more tests. |
Codecov Report
@@ Coverage Diff @@
## master #4355 +/- ##
==========================================
- Coverage 94.24% 94.23% -0.02%
==========================================
Files 319 319
Lines 18017 18071 +54
Branches 1062 1065 +3
==========================================
+ Hits 16980 17029 +49
+ Misses 938 936 -2
- Partials 99 106 +7
Continue to review full report at Codecov.
|
tests/h/views/client_test.py
Outdated
|
||
|
||
@pytest.fixture | ||
def pyramid_request_(pyramid_request): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This fixture needs to be named differently than the pyramid_request
fixture which it extends, as otherwise the registry gets reset to a new object before individual test cases actually run.
c5890a5
to
600a894
Compare
I think it would make more sense to have the feature flag operate at the view level and actually request from upstream (and then serve with appropriate cache headers), rather than having embed.js be a bootstrapper for a bootstrapper. |
This is currently blocked on #4363 which simplifies the client views a lot and makes them easier to test. |
600a894
to
763abad
Compare
4ef4081
to
b25880b
Compare
b25880b
to
2072f16
Compare
Slack discussion: https://hypothes-is.slack.com/archives/public/p1487328023004988 |
2072f16
to
47eec78
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally looks good, and does the right thing, but a couple of points (in more detail inline):
- splitting the
/embed.js
view into two (one for each value of the feature flag) will simplify the code and tests - I might be missing something, but I don't think we need to "resolve" the URL when using it in
app.html
- Setting defaults don't live in
.ini
files any more.
h/templates/embed.js.jinja2
Outdated
configEl.type = 'application/json'; | ||
configEl.textContent = JSON.stringify({ | ||
assetRoot: '{{ client_asset_root }}', | ||
sidebarAppUrl: '{{ app_html_url }}', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As I said before, putting this here still doesn't make a lot of sense to me, as it implies that the entrypoint isn't actually usable on its own, i.e. including https://unpkg.com/hypothesis
in the page won't actually work.
Is there a reason we can't hard code the defaults in the client itself? They can be overrideable, of course, but having sensible defaults for the https://hypothes.is
service in the client build would mean that we don't need to to this at all.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The defaults are now hardcoded to correct values in the client, with one caveat - the /app.html
URL can only be hardcoded to one value - so if we remove this config it won't be possible to load the QA version of /app.html
on the QA version of the service.
conf/app.ini
Outdated
@@ -19,6 +19,7 @@ use: call:h.app:create_app | |||
# If not provided, both default to a random URL-safe base64-encoded string. | |||
#h.client_id: | |||
#h.client_secret: | |||
h.client_url: https://unpkg.com/hypothesis |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're slowly but surely phasing out use of these configuration files, so perhaps we could put the default value in an appropriate module in the application?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I didn't know we were phasing these out. I can do that.
h/templates/app.html.jinja2
Outdated
@@ -9,7 +9,7 @@ | |||
only ever displays a single route. | |||
#} | |||
<base target="_top" href="/" /> | |||
{% for url in app_css_urls %} | |||
{% for url in app_css_urls %} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Doesn't seem like this diff is relevant to these changes, is it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, I think it stems from an indentation correction made during an earlier version of this PR.
h/views/client.py
Outdated
'inject_resource_urls': (absolute_asset_urls('inject_js') + | ||
absolute_asset_urls('inject_css')) | ||
} | ||
client_boot_url = _resolve_client_boot_url(request) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rather than adding complexity to this view function, it probably makes more sense to have a separate view function which uses the has_feature_flag
view predicate:
from pyramid.config import not_
...
@view_config(route_name='embed',
renderer='h:templates/embed.js.jinja2',
has_feature_flag=not_('use_client_boot_script'))
def embed(request):
# current view
@view_config(route_name='embed',
has_feature_flag='use_client_boot_script')
def embed_redirect(request):
# redirect
h/views/client.py
Outdated
|
||
if client_boot_url: | ||
rsp = HTTPFound(location=client_boot_url) | ||
rsp.cache_control.max_age = 60 * 5 # 5 minutes |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's use the http_cache
arg to view_config
instead, as that will ensure we set both Expires
and C-C
headers.
h/views/client.py
Outdated
@@ -85,6 +112,7 @@ def sidebar_app(request, extra=None): | |||
ga_client_tracking_id = settings.get('ga_client_tracking_id') | |||
|
|||
ctx = _app_html_context(api_url=request.route_url('api.index'), | |||
client_boot_url=_resolve_client_boot_url(request), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there a reason we need to actually resolve the URL here? Can we use the value of the setting without making any HTTP requests?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The client will have an /embed.js
=> https://unpkg.com/hypothesis@0.X.Y
redirect cached locally but won't have an https://unpkg.com/hypothesis
=> https://unpkg.com/hypothesis@0.X.Y
redirect cached locally because the service proxies the original redirect. So not resolving the URL would require the client to perform an extra request.
In an earlier version of this PR, I just had /app.html
include /embed.js
directly (which the client will have cached). Come to think of it, I think I could just revert back to doing that.
Add a feature flag which causes the service to use the client's boot script instead of using the boot logic and bundle list from the service. This is an initial step towards serving the client from an external CDN instead of from the Hypothesis service.
…t from Add a setting which specifies the URL for the client's boot script. Client assets are expected to be loaded from the same URL, but with a trailing slash added. The latest stable production client is loaded by default. It can be overridden in local development by setting the `CLIENT_URL` env var.
The client now includes the necessary config baked in, so the service does not need to provide it. For context, see Slack discussion https://hypothes-is.slack.com/archives/public/p1487328023004988
Split 'embed' view into two separate views with `has_feature_flag` view predicates. Since the view behaves completely differently depending on the value of the flag, this produces a simpler result.
As requested during CR, remove the `h.client_url` setting from the service's config INI files in favor of defining a default URL in `h.views.client` which is used unless the setting has been defined using the CLIENT_URL env var. This commit also removes the `assetRoot` configuration from the `/app.html` route. This config info is no longer needed or used by the client's boot script which now has the URL hardcoded in.
47eec78
to
9240adc
Compare
The `_client_url` helper already tests the feature flag and returns `None` if it is disabled.
Initialize it in the same way as `app_js_urls` for consistency.
app_css_urls = assets_env.urls('app_css') | ||
app_js_urls = assets_env.urls('app_js') | ||
if client_url: | ||
app_js_urls = [client_url] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought you were going to use request.route_path('embed')
here to avoid having to do an HTTP call every time we render app.html
-- which is notably not served with sensible cache headers at the moment.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, yes indeed. Done.
h/views/client.py
Outdated
@@ -132,6 +176,20 @@ def absolute_asset_urls(bundle_name): | |||
} | |||
|
|||
|
|||
@view_config(route_name='embed', | |||
has_feature_flag='use_client_boot_script', | |||
http_cache=60 * 5) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We want this to be cached by the CDN, so need to specify the public
C-C token: http_cache=(300, {'public': True})
as with robots.txt.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, although we should probably not do that while it's controlled by a feature flag. Leave this one for now and we'll follow up once we've deployed it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, no, it's fine. We send Vary: Cookie
so C-C: public
should be fine here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch. Done.
Two small changes needed:
|
This saves an extra request as the client will already have /embed.js cached.
These functions are never called when the feature flag is off.
Add a feature flag (
use_client_boot_script
) and a configuration setting (h.client_url
) which causes the service to use the client's boot script instead of using the boot code baked into the service itself. When the flag is enabled:/embed.js
view returns a redirect to the client's entry point athttps://unpkg.com/hypothesis
or the custom location set with theh.client_url
setting.app.html
view returns a minimal HTML page which just includes the client URL.Together, these changes will enable us to deploy and serve the client from a CDN rather than baking it into the Docker image for the service.
Manual testing:
H_SERVICE_URL
env var tohttp://localhost:5000
and rungulp watch
CLIENT_URL
env var tohttp://localhost:3001/hypothesis
and runmake dev
/docs/help
and/stream
and verify that the client loads. Also verify that the client's CSS and JS assets are loaded fromhttp://localhost:3001
.