fix(push): preserve devices on provider configuration errors
This commit is contained in:
parent
8e74f8601e
commit
28fc9559b3
|
|
@ -50,9 +50,13 @@ defmodule WhoNeedHelp.Push.DeviceDeliveryWorker do
|
||||||
_ = Notifications.disable_invalid_device(device)
|
_ = Notifications.disable_invalid_device(device)
|
||||||
:ok
|
:ok
|
||||||
|
|
||||||
{:error, {:rejected, status, _body}} when status in [400, 401, 403, 404, 410] ->
|
{:error, {:rejected, status, body}} ->
|
||||||
|
if invalid_device_rejection?(device.provider, status, body) do
|
||||||
_ = Notifications.disable_invalid_device(device)
|
_ = Notifications.disable_invalid_device(device)
|
||||||
{:cancel, :device_rejected}
|
{:cancel, :device_rejected}
|
||||||
|
else
|
||||||
|
{:cancel, {:provider_rejected, status}}
|
||||||
|
end
|
||||||
|
|
||||||
{:error, {:invalid_configuration, _field} = reason} ->
|
{:error, {:invalid_configuration, _field} = reason} ->
|
||||||
{:cancel, reason}
|
{:cancel, reason}
|
||||||
|
|
@ -61,4 +65,15 @@ defmodule WhoNeedHelp.Push.DeviceDeliveryWorker do
|
||||||
{:error, reason}
|
{:error, reason}
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
defp invalid_device_rejection?(:web_push, status, _body) when status in [404, 410],
|
||||||
|
do: true
|
||||||
|
|
||||||
|
defp invalid_device_rejection?(:fcm, _status, %{
|
||||||
|
"fcm_error_code" => error_code
|
||||||
|
})
|
||||||
|
when error_code in ["INVALID_ARGUMENT", "UNREGISTERED"],
|
||||||
|
do: true
|
||||||
|
|
||||||
|
defp invalid_device_rejection?(_provider, _status, _body), do: false
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -25,10 +25,10 @@ defmodule WhoNeedHelp.Push.FCMAdapter do
|
||||||
{:ok, %{id: name, duplicate: false}}
|
{:ok, %{id: name, duplicate: false}}
|
||||||
|
|
||||||
{:ok, %Req.Response{status: status, body: body}} when status in @retryable_statuses ->
|
{:ok, %Req.Response{status: status, body: body}} when status in @retryable_statuses ->
|
||||||
{:error, {:retryable, status, sanitize(body)}}
|
{:error, {:retryable, status, sanitize_error(body)}}
|
||||||
|
|
||||||
{:ok, %Req.Response{status: status, body: body}} ->
|
{:ok, %Req.Response{status: status, body: body}} ->
|
||||||
{:error, {:rejected, status, sanitize(body)}}
|
{:error, {:rejected, status, sanitize_error(body)}}
|
||||||
|
|
||||||
{:error, error} ->
|
{:error, error} ->
|
||||||
{:error, {:transport, transport_reason(error)}}
|
{:error, {:transport, transport_reason(error)}}
|
||||||
|
|
@ -101,10 +101,34 @@ defmodule WhoNeedHelp.Push.FCMAdapter do
|
||||||
|
|
||||||
defp priority(_kind), do: "normal"
|
defp priority(_kind), do: "normal"
|
||||||
|
|
||||||
defp sanitize(%{"error" => error}) when is_map(error),
|
@doc false
|
||||||
do: Map.take(error, ["code", "status"])
|
def sanitize_error(%{"error" => error}) when is_map(error) do
|
||||||
|
fcm_error_code =
|
||||||
|
error
|
||||||
|
|> Map.get("details", [])
|
||||||
|
|> Enum.find_value(fn
|
||||||
|
%{
|
||||||
|
"@type" => "type.googleapis.com/google.firebase.fcm.v1.FcmError",
|
||||||
|
"errorCode" => error_code
|
||||||
|
}
|
||||||
|
when is_binary(error_code) ->
|
||||||
|
error_code
|
||||||
|
|
||||||
defp sanitize(_body), do: nil
|
_detail ->
|
||||||
|
nil
|
||||||
|
end)
|
||||||
|
|
||||||
|
error
|
||||||
|
|> Map.take(["code", "status"])
|
||||||
|
|> maybe_put_fcm_error_code(fcm_error_code)
|
||||||
|
end
|
||||||
|
|
||||||
|
def sanitize_error(_body), do: nil
|
||||||
|
|
||||||
|
defp maybe_put_fcm_error_code(error, nil), do: error
|
||||||
|
|
||||||
|
defp maybe_put_fcm_error_code(error, fcm_error_code),
|
||||||
|
do: Map.put(error, "fcm_error_code", fcm_error_code)
|
||||||
|
|
||||||
defp transport_reason(%Req.TransportError{reason: reason}), do: reason
|
defp transport_reason(%Req.TransportError{reason: reason}), do: reason
|
||||||
defp transport_reason(%{__struct__: module}), do: module
|
defp transport_reason(%{__struct__: module}), do: module
|
||||||
|
|
|
||||||
|
|
@ -244,7 +244,50 @@ defmodule WhoNeedHelp.PushProductTest do
|
||||||
refute inspect(payload) =~ "exact"
|
refute inspect(payload) =~ "exact"
|
||||||
end
|
end
|
||||||
|
|
||||||
test "device delivery retries transient failures and disables rejected registrations",
|
test "FCM retains only the provider error code needed to classify a rejected FID" do
|
||||||
|
invalid_fid_response = %{
|
||||||
|
"error" => %{
|
||||||
|
"code" => 400,
|
||||||
|
"message" => "The Firebase Installation ID is invalid",
|
||||||
|
"status" => "INVALID_ARGUMENT",
|
||||||
|
"details" => [
|
||||||
|
%{
|
||||||
|
"@type" => "type.googleapis.com/google.firebase.fcm.v1.FcmError",
|
||||||
|
"errorCode" => "INVALID_ARGUMENT"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert FCMAdapter.sanitize_error(invalid_fid_response) == %{
|
||||||
|
"code" => 400,
|
||||||
|
"status" => "INVALID_ARGUMENT",
|
||||||
|
"fcm_error_code" => "INVALID_ARGUMENT"
|
||||||
|
}
|
||||||
|
|
||||||
|
invalid_payload_response = %{
|
||||||
|
"error" => %{
|
||||||
|
"code" => 400,
|
||||||
|
"message" => "The data payload is invalid",
|
||||||
|
"status" => "INVALID_ARGUMENT",
|
||||||
|
"details" => [
|
||||||
|
%{
|
||||||
|
"@type" => "type.googleapis.com/google.rpc.BadRequest",
|
||||||
|
"fieldViolations" => [%{"field" => "message.data"}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert FCMAdapter.sanitize_error(invalid_payload_response) == %{
|
||||||
|
"code" => 400,
|
||||||
|
"status" => "INVALID_ARGUMENT"
|
||||||
|
}
|
||||||
|
|
||||||
|
refute inspect(FCMAdapter.sanitize_error(invalid_fid_response)) =~ "Firebase Installation"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "device delivery retries transient failures and only disables a rejected FID",
|
||||||
context do
|
context do
|
||||||
Application.put_env(:who_need_help, :fcm_adapter, RecordingDeviceAdapter)
|
Application.put_env(:who_need_help, :fcm_adapter, RecordingDeviceAdapter)
|
||||||
|
|
||||||
|
|
@ -282,7 +325,45 @@ defmodule WhoNeedHelp.PushProductTest do
|
||||||
assert is_nil(Repo.reload!(device).disabled_at)
|
assert is_nil(Repo.reload!(device).disabled_at)
|
||||||
|
|
||||||
Application.put_env(:who_need_help, :device_delivery_options, %{
|
Application.put_env(:who_need_help, :device_delivery_options, %{
|
||||||
fcm: [test_pid: self(), result: {:error, {:rejected, 404, nil}}]
|
fcm: [
|
||||||
|
test_pid: self(),
|
||||||
|
result:
|
||||||
|
{:error,
|
||||||
|
{:rejected, 403,
|
||||||
|
%{"status" => "PERMISSION_DENIED", "fcm_error_code" => "SENDER_ID_MISMATCH"}}}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:cancel, {:provider_rejected, 403}} =
|
||||||
|
perform_job(DeviceDeliveryWorker, %{
|
||||||
|
"notification_id" => notification.id,
|
||||||
|
"device_id" => device.id
|
||||||
|
})
|
||||||
|
|
||||||
|
assert is_nil(Repo.reload!(device).disabled_at)
|
||||||
|
|
||||||
|
Application.put_env(:who_need_help, :device_delivery_options, %{
|
||||||
|
fcm: [
|
||||||
|
test_pid: self(),
|
||||||
|
result: {:error, {:rejected, 400, %{"status" => "INVALID_ARGUMENT"}}}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
assert {:cancel, {:provider_rejected, 400}} =
|
||||||
|
perform_job(DeviceDeliveryWorker, %{
|
||||||
|
"notification_id" => notification.id,
|
||||||
|
"device_id" => device.id
|
||||||
|
})
|
||||||
|
|
||||||
|
assert is_nil(Repo.reload!(device).disabled_at)
|
||||||
|
|
||||||
|
Application.put_env(:who_need_help, :device_delivery_options, %{
|
||||||
|
fcm: [
|
||||||
|
test_pid: self(),
|
||||||
|
result:
|
||||||
|
{:error,
|
||||||
|
{:rejected, 404, %{"status" => "NOT_FOUND", "fcm_error_code" => "UNREGISTERED"}}}
|
||||||
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
assert {:cancel, :device_rejected} =
|
assert {:cancel, :device_rejected} =
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user