Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
5cc888d
Fix wagtail transaction names
hmstepanek Apr 6, 2026
b1fea93
Fixup
hmstepanek Apr 24, 2026
7e15be2
Fixup wagtail
hmstepanek May 1, 2026
efd8cd2
Use migration in wagtail tests
hmstepanek Jun 8, 2026
024a45f
Fixup
hmstepanek Jul 6, 2026
29c659d
Fixup lint issues
hmstepanek Jul 7, 2026
d830f7c
Apply ruff suggestions
hmstepanek Jul 7, 2026
07a3a6c
Fixup: license headers
hmstepanek Jul 7, 2026
e9e5ece
[MegaLinter] Apply linters fixes
hmstepanek Jul 7, 2026
e80e1a7
Remove unnecessary fixture
hmstepanek Jul 8, 2026
e253a3e
Cache Page import in global and move to django hook
hmstepanek Jul 9, 2026
dee1339
Merge branch 'main' into fix-wagtail-transaction-names
mergify[bot] Jul 9, 2026
c6feead
Fixup: lint
hmstepanek Jul 10, 2026
59073ba
Merge branch 'fix-wagtail-transaction-names' of github.com:newrelic/n…
hmstepanek Jul 10, 2026
c97beab
Merge branch 'main' into fix-wagtail-transaction-names
mergify[bot] Jul 10, 2026
1bc7d04
Merge branch 'main' into fix-wagtail-transaction-names
mergify[bot] Jul 17, 2026
c04e64c
Merge branch 'main' into fix-wagtail-transaction-names
mergify[bot] Jul 17, 2026
e95b589
Merge branch 'main' into fix-wagtail-transaction-names
TimPansino Jul 20, 2026
2912ed5
Merge branch 'main' into fix-wagtail-transaction-names
mergify[bot] Jul 20, 2026
e69ca52
Merge branch 'main' into fix-wagtail-transaction-names
mergify[bot] Jul 22, 2026
6007a8f
Move fixtures back to conftest
hmstepanek Jul 22, 2026
2dda382
[MegaLinter] Apply linters fixes
hmstepanek Jul 22, 2026
fecf747
Merge branch 'main' into fix-wagtail-transaction-names
mergify[bot] Jul 22, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions newrelic/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -3002,6 +3002,10 @@ def _process_module_builtin_defaults():
_process_module_definition("flask_restplus.api", "newrelic.hooks.component_flask_rest", "instrument_flask_rest")
_process_module_definition("flask_restx.api", "newrelic.hooks.component_flask_rest", "instrument_flask_rest")

_process_module_definition(
"wagtail.models.pages", "newrelic.hooks.framework_django", "instrument_wagtail_models_pages"
)

_process_module_definition("graphql_server", "newrelic.hooks.component_graphqlserver", "instrument_graphqlserver")

_process_module_definition(
Expand Down
37 changes: 36 additions & 1 deletion newrelic/hooks/framework_django.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
"off": False,
}

WAGTAIL_PAGE = None


def _setting_boolean(value):
if value.lower() not in _boolean_states:
Expand Down Expand Up @@ -484,7 +486,17 @@ def wrapper(wrapped, instance, args, kwargs):
if transaction is None:
return wrapped(*args, **kwargs)

transaction.set_transaction_name(name, priority=priority)
# Wagtail is built on top of Django. It uses metaclasses where the route method
# is located on the base class which results in transaction having the same
# name. Use the child class instead of the base class name. Set the priority=6
# to override the priority set in other parts of this hook file so that the
# more explicit name takes precedence.
new_name = name
if WAGTAIL_PAGE and instance and isinstance(instance, WAGTAIL_PAGE):
new_name = f"{callable_name(instance)}.{wrapped.__name__}"
transaction.set_transaction_name(new_name, priority=6)
else:
transaction.set_transaction_name(new_name, priority=priority)
with FunctionTrace(name=name, source=wrapped):
try:
return wrapped(*args, **kwargs)
Expand Down Expand Up @@ -1213,6 +1225,21 @@ def _bind_params(original_middleware, *args, **kwargs):
return _nr_wrap_converted_middleware_(converted_middleware, name)


def _nr_wrapper_route_for_request(wrapped, instance, args, kwargs):
transaction = current_transaction()

if not transaction:
return wrapped(*args, **kwargs)

route_result = wrapped(*args, **kwargs)
if route_result:
page, args, kwargs = route_result
name = callable_name(page.route)
transaction.set_transaction_name(name, priority=6)

return route_result


def instrument_django_core_handlers_exception(module):
if hasattr(module, "convert_exception_to_response"):
wrap_function_wrapper(module, "convert_exception_to_response", _nr_wrapper_convert_exception_to_response_)
Expand All @@ -1230,3 +1257,11 @@ def instrument_django_core_handlers_asgi(module):
from newrelic.api.asgi_application import wrap_asgi_application

wrap_asgi_application(module, "ASGIHandler.__call__", framework=framework)


def instrument_wagtail_models_pages(module):
if hasattr(module, "Page"):
global WAGTAIL_PAGE
WAGTAIL_PAGE = module.Page
if hasattr(module.Page, "route_for_request"):
wrap_function_wrapper(module, "Page.route_for_request", _nr_wrapper_route_for_request)
14 changes: 14 additions & 0 deletions tests/framework_wagtail/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright 2010 New Relic, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

18 changes: 18 additions & 0 deletions tests/framework_wagtail/_target_application.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copyright 2010 New Relic, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import webtest
from wsgi import application

_target_application = webtest.TestApp(application)
61 changes: 61 additions & 0 deletions tests/framework_wagtail/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Copyright 2010 New Relic, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os

import pytest
from testing_support.fixtures import (
collector_agent_registration_fixture,
collector_available_fixture, # autouse fixture, must be importable in this module
)

_default_settings = {
"package_reporting.enabled": False, # Turn off package reporting for testing as it causes slow downs.
"transaction_tracer.explain_threshold": 0.0,
"transaction_tracer.transaction_threshold": 0.0,
"transaction_tracer.stack_trace_threshold": 0.0,
"debug.log_data_collector_payloads": True,
"debug.record_transaction_failure": True,
"debug.log_autorum_middleware": True,
}

collector_agent_registration = collector_agent_registration_fixture(
app_name="Python Agent Test (framework_wagtail)", default_settings=_default_settings, scope="module"
)


@pytest.fixture(autouse=True)
def database():
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
import django

django.setup()
from django.core.management import call_command

call_command("migrate", verbosity=0, interactive=False, run_syncdb=True)

# Wagtail's own migrations seed a default "Welcome" home page (a plain
# ``Page``) and a default ``Site``. Replace that root with a ``HomePage``
# and hang a ``RoutablePage`` beneath it so that "/" and "/routable/"
# resolve to real, renderable pages served by the dummy_app page types.
from dummy_app.models import HomePage, RoutablePage
from wagtail.models import Page, Site

if not HomePage.objects.exists():
site = Site.objects.get(is_default_site=True)
default_home = site.root_page
home = Page.objects.get(depth=1).add_child(instance=HomePage(title="Home", slug="home-page"))
site.root_page = home
site.save()
default_home.delete()
home.add_child(instance=RoutablePage(title="Routable", slug="routable"))
14 changes: 14 additions & 0 deletions tests/framework_wagtail/dummy_app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright 2010 New Relic, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

20 changes: 20 additions & 0 deletions tests/framework_wagtail/dummy_app/apps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Copyright 2010 New Relic, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from django.apps import AppConfig


class LibraryConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "dummy_app"
41 changes: 41 additions & 0 deletions tests/framework_wagtail/dummy_app/models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Copyright 2010 New Relic, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from django.db import models
from wagtail.contrib.routable_page.models import RoutablePage, re_path
from wagtail.fields import RichTextField
from wagtail.models import Page


class HomePage(Page):
body = RichTextField(blank=True)

content_panels = [*Page.content_panels, "body"]


class RoutablePage(RoutablePage):
body = RichTextField(blank=True)

content_panels = [*Page.content_panels, "body"]

@re_path(r"^routable")
def index(self, request):
# Handle URLs of the form /<id>
return super().serve(request)

class Meta:
verbose_name = "Routable page"

def __str__(self):
return "Page from routable"
63 changes: 63 additions & 0 deletions tests/framework_wagtail/migrations/0001_initial.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# Copyright 2010 New Relic, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import django.db.models.deletion
import wagtail.contrib.routable_page.models
import wagtail.fields
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [("wagtailcore", "0070_rename_pagerevision_revision")]

operations = [
migrations.CreateModel(
name="HomePage",
fields=[
(
"page_ptr",
models.OneToOneField(
on_delete=models.CASCADE,
parent_link=True,
auto_created=True,
primary_key=True,
serialize=False,
to="wagtailcore.Page",
),
)
],
options={"abstract": False},
bases=("wagtailcore.page",),
),
migrations.AddField(model_name="homepage", name="body", field=wagtail.fields.RichTextField(blank=True)),
migrations.CreateModel(
name="RoutablePage",
fields=[
(
"page_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="wagtailcore.page",
),
)
],
options={"verbose_name": "Routable page"},
bases=(wagtail.contrib.routable_page.models.RoutablePage,),
),
migrations.AddField(model_name="routablepage", name="body", field=wagtail.fields.RichTextField(blank=True)),
]
14 changes: 14 additions & 0 deletions tests/framework_wagtail/migrations/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Copyright 2010 New Relic, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

67 changes: 67 additions & 0 deletions tests/framework_wagtail/settings.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Copyright 2010 New Relic, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from pathlib import Path

BASE_DIR = Path(__file__).parent
DEBUG = True

# Make this unique, and don't share it with anybody.
SECRET_KEY = "cookies"

# List of callables that know how to import templates from various sources.
TEMPLATE_LOADERS = ("django.template.loaders.filesystem.Loader", "django.template.loaders.app_directories.Loader")

MIDDLEWARE = (
"django.middleware.common.CommonMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.gzip.GZipMiddleware",
)

ROOT_URLCONF = "urls"

TEMPLATE_DIRS = [BASE_DIR / "templates"]

# For Django 1.10 compatibility because TEMPLATE_DIRS is deprecated
TEMPLATES = [{"BACKEND": "django.template.backends.django.DjangoTemplates", "DIRS": TEMPLATE_DIRS}]

INSTALLED_APPS = [
"dummy_app",
"wagtail.contrib.forms",
"wagtail.contrib.redirects",
"wagtail.embeds",
"wagtail.sites",
"wagtail.users",
"wagtail.snippets",
"wagtail.documents",
"wagtail.images",
"wagtail.search",
"wagtail.admin",
"wagtail",
"taggit",
"django_filters",
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
]

DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": Path(BASE_DIR) / "db.sqlite3"}}

MIGRATION_MODULES = {"dummy_app": "migrations"}
Loading
Loading