From ff29dd55fafd7c3d27bd30f40945847aa4278309 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Sat, 5 Jun 2021 13:15:58 -0700 Subject: [PATCH] ?_trace=1 now depends on trace_debug setting, closes #1359 --- .github/workflows/deploy-latest.yml | 2 +- datasette/app.py | 20 +++++++++++++------- docs/json_api.rst | 18 ++++++++++-------- docs/settings.rst | 16 ++++++++++++++++ tests/fixtures.py | 6 ++++++ tests/test_api.py | 20 ++++++++++++++++---- tests/test_csv.py | 13 +++++++------ 7 files changed, 69 insertions(+), 26 deletions(-) diff --git a/.github/workflows/deploy-latest.yml b/.github/workflows/deploy-latest.yml index 43e46fb4..d9f23f7d 100644 --- a/.github/workflows/deploy-latest.yml +++ b/.github/workflows/deploy-latest.yml @@ -53,7 +53,7 @@ jobs: --plugins-dir=plugins \ --branch=$GITHUB_SHA \ --version-note=$GITHUB_SHA \ - --extra-options="--setting template_debug 1 --crossdb" \ + --extra-options="--setting template_debug 1 --setting trace_debug 1 --crossdb" \ --install=pysqlite3-binary \ --service=datasette-latest # Deploy docs.db to a different service diff --git a/datasette/app.py b/datasette/app.py index c0e8ad01..d85517e6 100644 --- a/datasette/app.py +++ b/datasette/app.py @@ -162,6 +162,11 @@ SETTINGS = ( False, "Allow display of template debug information with ?_context=1", ), + Setting( + "trace_debug", + False, + "Allow display of SQL trace debug information with ?_trace=1", + ), Setting("base_url", "/", "Datasette URLs should use this base path"), ) @@ -1041,14 +1046,15 @@ class Datasette: if not database.is_mutable: await database.table_counts(limit=60 * 60 * 1000) + asgi = asgi_csrf.asgi_csrf( + DatasetteRouter(self, routes), + signing_secret=self._secret, + cookie_name="ds_csrftoken", + ) + if self.setting("trace_debug"): + asgi = AsgiTracer(asgi) asgi = AsgiLifespan( - AsgiTracer( - asgi_csrf.asgi_csrf( - DatasetteRouter(self, routes), - signing_secret=self._secret, - cookie_name="ds_csrftoken", - ) - ), + asgi, on_startup=setup_db, ) for wrapper in pm.hook.asgi_wrapper(datasette=self): diff --git a/docs/json_api.rst b/docs/json_api.rst index 660fbc1c..09cac1f9 100644 --- a/docs/json_api.rst +++ b/docs/json_api.rst @@ -206,6 +206,16 @@ query string arguments: For how many seconds should this response be cached by HTTP proxies? Use ``?_ttl=0`` to disable HTTP caching entirely for this request. +``?_trace=1`` + Turns on tracing for this page: SQL queries executed during the request will + be gathered and included in the response, either in a new ``"_traces"`` key + for JSON responses or at the bottom of the page if the response is in HTML. + + The structure of the data returned here should be considered highly unstable + and very likely to change. + + Only available if the :ref:`setting_trace_debug` setting is enabled. + .. _table_arguments: Table arguments @@ -389,14 +399,6 @@ Special table arguments ``?_nocount=1`` Disable the ``select count(*)`` query used on this page - a count of ``None`` will be returned instead. -``?_trace=1`` - Turns on tracing for this page: SQL queries executed during the request will - be gathered and included in the response, either in a new ``"_traces"`` key - for JSON responses or at the bottom of the page if the response is in HTML. - - The structure of the data returned here should be considered highly unstable - and very likely to change. - .. _expand_foreign_keys: Expanding foreign key references diff --git a/docs/settings.rst b/docs/settings.rst index db17a45e..c246d33a 100644 --- a/docs/settings.rst +++ b/docs/settings.rst @@ -286,6 +286,22 @@ Some examples: * https://latest.datasette.io/fixtures?_context=1 * https://latest.datasette.io/fixtures/roadside_attractions?_context=1 +.. _setting_trace_debug: + +trace_debug +~~~~~~~~~~~ + +This setting enables appending ``?_trace=1`` to any page in order to see the SQL queries and other trace information that was used to generate that page. + +Enable it like this:: + + datasette mydatabase.db --setting trace_debug 1 + +Some examples: + +* https://latest.datasette.io/?_trace=1 +* https://latest.datasette.io/fixtures/roadside_attractions?_trace=1 + .. _setting_base_url: base_url diff --git a/tests/fixtures.py b/tests/fixtures.py index 2690052a..cdd2e987 100644 --- a/tests/fixtures.py +++ b/tests/fixtures.py @@ -214,6 +214,12 @@ def app_client_with_hash(): yield client +@pytest.fixture(scope="session") +def app_client_with_trace(): + with make_app_client(config={"trace_debug": True}, is_immutable=True) as client: + yield client + + @pytest.fixture(scope="session") def app_client_shorter_time_limit(): with make_app_client(20) as client: diff --git a/tests/test_api.py b/tests/test_api.py index 3b789bb7..e5e609d6 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -15,6 +15,7 @@ from .fixtures import ( # noqa app_client_conflicting_database_names, app_client_with_cors, app_client_with_dot, + app_client_with_trace, app_client_immutable_and_inspect_file, generate_compound_rows, generate_sortable_rows, @@ -1422,6 +1423,7 @@ def test_settings_json(app_client): "force_https_urls": False, "hash_urls": False, "template_debug": False, + "trace_debug": False, "base_url": "/", } == response.json @@ -1692,8 +1694,10 @@ def test_nocount(app_client, nocount, expected_count): assert response.json["filtered_table_rows_count"] == expected_count -def test_nocount_nofacet_if_shape_is_object(app_client): - response = app_client.get("/fixtures/facetable.json?_trace=1&_shape=object") +def test_nocount_nofacet_if_shape_is_object(app_client_with_trace): + response = app_client_with_trace.get( + "/fixtures/facetable.json?_trace=1&_shape=object" + ) assert "count(*)" not in response.text @@ -1863,9 +1867,17 @@ def test_custom_query_with_unicode_characters(app_client): assert [{"id": 1, "name": "San Francisco"}] == response.json -def test_trace(app_client): - response = app_client.get("/fixtures/simple_primary_key.json?_trace=1") +@pytest.mark.parametrize("trace_debug", (True, False)) +def test_trace(trace_debug): + with make_app_client(config={"trace_debug": trace_debug}) as client: + response = client.get("/fixtures/simple_primary_key.json?_trace=1") + assert response.status == 200 + data = response.json + if not trace_debug: + assert "_trace" not in data + return + assert "_trace" in data trace_info = data["_trace"] assert isinstance(trace_info["request_duration_ms"], float) diff --git a/tests/test_csv.py b/tests/test_csv.py index 01f739e2..3debf320 100644 --- a/tests/test_csv.py +++ b/tests/test_csv.py @@ -3,6 +3,7 @@ from .fixtures import ( # noqa app_client, app_client_csv_max_mb_one, app_client_with_cors, + app_client_with_trace, ) EXPECTED_TABLE_CSV = """id,content @@ -160,8 +161,8 @@ def test_table_csv_stream(app_client): assert 1002 == len([b for b in response.body.split(b"\r\n") if b]) -def test_csv_trace(app_client): - response = app_client.get("/fixtures/simple_primary_key.csv?_trace=1") +def test_csv_trace(app_client_with_trace): + response = app_client_with_trace.get("/fixtures/simple_primary_key.csv?_trace=1") assert response.headers["content-type"] == "text/html; charset=utf-8" soup = Soup(response.text, "html.parser") assert ( @@ -171,13 +172,13 @@ def test_csv_trace(app_client): assert "select id, content from simple_primary_key" in soup.find("pre").text -def test_table_csv_stream_does_not_calculate_facets(app_client): - response = app_client.get("/fixtures/simple_primary_key.csv?_trace=1") +def test_table_csv_stream_does_not_calculate_facets(app_client_with_trace): + response = app_client_with_trace.get("/fixtures/simple_primary_key.csv?_trace=1") soup = Soup(response.text, "html.parser") assert "select content, count(*) as n" not in soup.find("pre").text -def test_table_csv_stream_does_not_calculate_counts(app_client): - response = app_client.get("/fixtures/simple_primary_key.csv?_trace=1") +def test_table_csv_stream_does_not_calculate_counts(app_client_with_trace): + response = app_client_with_trace.get("/fixtures/simple_primary_key.csv?_trace=1") soup = Soup(response.text, "html.parser") assert "select count(*)" not in soup.find("pre").text