From 4dc650e2f07974ed51ea63d37c7a1d50ad84f7c9 Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Tue, 2 Jun 2026 12:03:03 +0100 Subject: [PATCH 1/4] fix: preserve crashlytics options --- src/firebase_functions/options.py | 43 ++++++++---- tests/test_options.py | 106 +++++++++++++++++++++++++++++- 2 files changed, 137 insertions(+), 12 deletions(-) diff --git a/src/firebase_functions/options.py b/src/firebase_functions/options.py index ee084cbe..f0016c64 100644 --- a/src/firebase_functions/options.py +++ b/src/firebase_functions/options.py @@ -652,6 +652,23 @@ def _endpoint( ) +def _alert_options_to_firebase_alert_options( + options: EventHandlerOptions, + alert_type: str | AlertType, + app_id: str | None = None, +) -> FirebaseAlertOptions: + option_values = { + field.name: getattr(options, field.name) + for field in _dataclasses.fields(options) + if field.name not in {"alert_type", "app_id"} + } + return FirebaseAlertOptions( + **option_values, + alert_type=alert_type, + app_id=app_id, + ) + + @_dataclasses.dataclass(frozen=True, kw_only=True) class AppDistributionOptions(EventHandlerOptions): """ @@ -669,9 +686,10 @@ def _endpoint( **kwargs, ) -> _manifest.ManifestEndpoint: assert kwargs["alert_type"] is not None - return FirebaseAlertOptions( - alert_type=kwargs["alert_type"], - app_id=self.app_id, + return _alert_options_to_firebase_alert_options( + self, + kwargs["alert_type"], + self.app_id, )._endpoint(**kwargs) @@ -692,9 +710,10 @@ def _endpoint( **kwargs, ) -> _manifest.ManifestEndpoint: assert kwargs["alert_type"] is not None - return FirebaseAlertOptions( - alert_type=kwargs["alert_type"], - app_id=self.app_id, + return _alert_options_to_firebase_alert_options( + self, + kwargs["alert_type"], + self.app_id, )._endpoint(**kwargs) @@ -715,9 +734,10 @@ def _endpoint( **kwargs, ) -> _manifest.ManifestEndpoint: assert kwargs["alert_type"] is not None - return FirebaseAlertOptions( - alert_type=kwargs["alert_type"], - app_id=self.app_id, + return _alert_options_to_firebase_alert_options( + self, + kwargs["alert_type"], + self.app_id, )._endpoint(**kwargs) @@ -733,8 +753,9 @@ def _endpoint( **kwargs, ) -> _manifest.ManifestEndpoint: assert kwargs["alert_type"] is not None - return FirebaseAlertOptions( - alert_type=kwargs["alert_type"], + return _alert_options_to_firebase_alert_options( + self, + kwargs["alert_type"], )._endpoint(**kwargs) diff --git a/tests/test_options.py b/tests/test_options.py index 2f4ceb87..266fdb72 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -17,11 +17,14 @@ from pytest import raises -from firebase_functions import https_fn, options, params +from firebase_functions import alerts_fn, https_fn, options, params +from firebase_functions.alerts import app_distribution_fn, billing_fn, crashlytics_fn, performance_fn from firebase_functions.private.serving import functions_as_yaml, merge_required_apis # pylint: disable=protected-access +ALERT_SECRET = params.SecretParam("GITLAB_PERSONAL_ACCESS_TOKEN") + @https_fn.on_call() def asamplefunction(_): @@ -196,3 +199,104 @@ def test_invoker_with_no_element_throws(): AssertionError, match="HttpsOptions: Invalid option for invoker - must be a non-empty list." ): options.HttpsOptions(invoker=[])._endpoint(func_name="test") + + +def _assert_alert_endpoint_options(endpoint, expected_alert_type, expect_app_id: str | None = None): + assert endpoint.region == ["europe-west1"] + assert endpoint.maxInstances == 1 + assert endpoint.secretEnvironmentVariables == [{"key": "GITLAB_PERSONAL_ACCESS_TOKEN"}] + assert endpoint.eventTrigger["retry"] is True + assert endpoint.eventTrigger["eventFilters"]["alerttype"] == expected_alert_type + if expect_app_id is None: + assert "appid" not in endpoint.eventTrigger["eventFilters"] + else: + assert endpoint.eventTrigger["eventFilters"]["appid"] == expect_app_id + + +def test_crashlytics_options_preserved_in_alert_endpoint(): + @crashlytics_fn.on_new_fatal_issue_published( + secrets=[ALERT_SECRET], + region="europe-west1", + max_instances=1, + retry=True, + app_id="app-123", + ) + def sample(_event): + return None + + _assert_alert_endpoint_options( + sample.__firebase_endpoint__, + "crashlytics.newFatalIssue", + expect_app_id="app-123", + ) + + +def test_app_distribution_options_preserved_in_alert_endpoint(): + @app_distribution_fn.on_new_tester_ios_device_published( + secrets=[ALERT_SECRET], + region="europe-west1", + max_instances=1, + retry=True, + app_id="app-123", + ) + def sample(_event): + return None + + _assert_alert_endpoint_options( + sample.__firebase_endpoint__, + "appDistribution.newTesterIosDevice", + expect_app_id="app-123", + ) + + +def test_performance_options_preserved_in_alert_endpoint(): + @performance_fn.on_threshold_alert_published( + secrets=[ALERT_SECRET], + region="europe-west1", + max_instances=1, + retry=True, + app_id="app-123", + ) + def sample(_event): + return None + + _assert_alert_endpoint_options( + sample.__firebase_endpoint__, + "performance.threshold", + expect_app_id="app-123", + ) + + +def test_billing_options_preserved_in_alert_endpoint(): + @billing_fn.on_plan_update_published( + secrets=[ALERT_SECRET], + region="europe-west1", + max_instances=1, + retry=True, + ) + def sample(_event): + return None + + _assert_alert_endpoint_options( + sample.__firebase_endpoint__, + "billing.planUpdate", + ) + + +def test_firebase_alert_options_preserved_in_alert_endpoint(): + @alerts_fn.on_alert_published( + alert_type=alerts_fn.AlertType.CRASHLYTICS_NEW_FATAL_ISSUE, + secrets=[ALERT_SECRET], + region="europe-west1", + max_instances=1, + retry=True, + app_id="app-123", + ) + def sample(_event): + return None + + _assert_alert_endpoint_options( + sample.__firebase_endpoint__, + "crashlytics.newFatalIssue", + expect_app_id="app-123", + ) From 8663261c3d50de52c46e68a6f76cf46fc0f06601 Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Tue, 2 Jun 2026 12:13:57 +0100 Subject: [PATCH 2/4] chore: fix linting/formatting --- tests/test_options.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/test_options.py b/tests/test_options.py index 266fdb72..3e9cb523 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -18,7 +18,12 @@ from pytest import raises from firebase_functions import alerts_fn, https_fn, options, params -from firebase_functions.alerts import app_distribution_fn, billing_fn, crashlytics_fn, performance_fn +from firebase_functions.alerts import ( + app_distribution_fn, + billing_fn, + crashlytics_fn, + performance_fn, +) from firebase_functions.private.serving import functions_as_yaml, merge_required_apis # pylint: disable=protected-access From fbd9359b6abf7c5ad89dd5e800c18ab748d2c33e Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Mon, 8 Jun 2026 14:31:47 +0100 Subject: [PATCH 3/4] refactor: extract app_id from options --- src/firebase_functions/options.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/firebase_functions/options.py b/src/firebase_functions/options.py index f0016c64..5defb929 100644 --- a/src/firebase_functions/options.py +++ b/src/firebase_functions/options.py @@ -655,8 +655,8 @@ def _endpoint( def _alert_options_to_firebase_alert_options( options: EventHandlerOptions, alert_type: str | AlertType, - app_id: str | None = None, ) -> FirebaseAlertOptions: + app_id = getattr(options, "app_id", None) option_values = { field.name: getattr(options, field.name) for field in _dataclasses.fields(options) @@ -689,7 +689,6 @@ def _endpoint( return _alert_options_to_firebase_alert_options( self, kwargs["alert_type"], - self.app_id, )._endpoint(**kwargs) @@ -713,7 +712,6 @@ def _endpoint( return _alert_options_to_firebase_alert_options( self, kwargs["alert_type"], - self.app_id, )._endpoint(**kwargs) @@ -737,7 +735,6 @@ def _endpoint( return _alert_options_to_firebase_alert_options( self, kwargs["alert_type"], - self.app_id, )._endpoint(**kwargs) From 0359eb415ba53f6107e464c31581e792969cc010 Mon Sep 17 00:00:00 2001 From: Izaak Gough Date: Wed, 17 Jun 2026 10:50:41 +0100 Subject: [PATCH 4/4] fix: filter option_values to only include valid firebaseAlertOptions fields --- src/firebase_functions/options.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/firebase_functions/options.py b/src/firebase_functions/options.py index 5defb929..3d432739 100644 --- a/src/firebase_functions/options.py +++ b/src/firebase_functions/options.py @@ -657,10 +657,14 @@ def _alert_options_to_firebase_alert_options( alert_type: str | AlertType, ) -> FirebaseAlertOptions: app_id = getattr(options, "app_id", None) + + # Restrict to fields supported by FirebaseAlertOptions + allowed_fields = {f.name for f in _dataclasses.fields(FirebaseAlertOptions)} + option_values = { field.name: getattr(options, field.name) for field in _dataclasses.fields(options) - if field.name not in {"alert_type", "app_id"} + if field.name in allowed_fields and field.name not in {"alert_type", "app_id"} } return FirebaseAlertOptions( **option_values,