unique-search-proxy 2026.32.0.dev2__tar.gz → 2026.32.0.dev3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/PKG-INFO +3 -3
  2. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/pyproject.toml +3 -3
  3. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/api/v1/crawl.py +38 -6
  4. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/api/v1/search.py +5 -0
  5. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/crawlers/basic/service.py +1 -0
  6. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/provider_response.py +2 -1
  7. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/error_handlers.py +0 -6
  8. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/monitoring/metrics.py +52 -0
  9. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/README.md +0 -0
  10. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/__init__.py +0 -0
  11. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/__init__.py +0 -0
  12. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/api/__init__.py +0 -0
  13. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/api/health.py +0 -0
  14. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/api/v1/__init__.py +0 -0
  15. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/api/v1/agent_search.py +0 -0
  16. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/api/v1/configuration.py +0 -0
  17. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/api/v1/context_headers.py +0 -0
  18. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/api/v1/openapi_examples.py +0 -0
  19. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/app.py +0 -0
  20. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/context.py +0 -0
  21. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/__init__.py +0 -0
  22. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/agent_engines/__init__.py +0 -0
  23. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/agent_engines/bing/client.py +0 -0
  24. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/agent_engines/bing/runner.py +0 -0
  25. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/agent_engines/bing/service.py +0 -0
  26. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/agent_engines/factory.py +0 -0
  27. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/agent_engines/serialization.py +0 -0
  28. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/agent_engines/service_base.py +0 -0
  29. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/agent_engines/structured_output.py +0 -0
  30. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/agent_engines/vertexai/client.py +0 -0
  31. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/agent_engines/vertexai/gemini.py +0 -0
  32. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/agent_engines/vertexai/service.py +0 -0
  33. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/client/__init__.py +0 -0
  34. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/client/service.py +0 -0
  35. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/crawlers/__init__.py +0 -0
  36. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/crawlers/basic/__init__.py +0 -0
  37. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/crawlers/basic/processing/__init__.py +0 -0
  38. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/crawlers/basic/processing/errors.py +0 -0
  39. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/crawlers/basic/processing/html_markdown.py +0 -0
  40. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/__init__.py +0 -0
  41. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/html.py +0 -0
  42. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/pdf.py +0 -0
  43. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/crawlers/basic/processing/processors/plain_text.py +0 -0
  44. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/crawlers/basic/processing/registry.py +0 -0
  45. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/crawlers/basic/settings.py +0 -0
  46. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/crawlers/basic/user_agent.py +0 -0
  47. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/crawlers/factory.py +0 -0
  48. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/crawlers/firecrawl/polling.py +0 -0
  49. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/crawlers/firecrawl/request_body.py +0 -0
  50. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/crawlers/firecrawl/service.py +0 -0
  51. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/crawlers/jina/request_body.py +0 -0
  52. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/crawlers/jina/service.py +0 -0
  53. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/crawlers/pinned_egress.py +0 -0
  54. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/crawlers/tavily/request_body.py +0 -0
  55. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/crawlers/tavily/service.py +0 -0
  56. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/providers.py +0 -0
  57. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/registry.py +0 -0
  58. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/search_engines/__init__.py +0 -0
  59. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/search_engines/brave/__init__.py +0 -0
  60. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/search_engines/brave/pagination.py +0 -0
  61. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/search_engines/brave/query_params.py +0 -0
  62. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/search_engines/brave/service.py +0 -0
  63. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/search_engines/descriptor.py +0 -0
  64. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/search_engines/factory.py +0 -0
  65. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/search_engines/google/__init__.py +0 -0
  66. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/search_engines/google/pagination.py +0 -0
  67. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/search_engines/google/query_params.py +0 -0
  68. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/search_engines/google/service.py +0 -0
  69. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/search_engines/pagination.py +0 -0
  70. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/search_engines/perplexity/__init__.py +0 -0
  71. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/search_engines/perplexity/request_body.py +0 -0
  72. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/search_engines/perplexity/service.py +0 -0
  73. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/search_engines/service_base.py +0 -0
  74. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/url_safety/__init__.py +0 -0
  75. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/core/url_safety/gate.py +0 -0
  76. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/helm/__init__.py +0 -0
  77. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/helm/generator/__init__.py +0 -0
  78. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/helm/generator/introspect.py +0 -0
  79. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/helm/generator/schema.py +0 -0
  80. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/helm/generator/template.py +0 -0
  81. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/helm/generator/templates/provider_values_yaml.j2 +0 -0
  82. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/helm/generator/values_yaml.py +0 -0
  83. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/helm/metadata.py +0 -0
  84. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/helm/registry.py +0 -0
  85. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/helm/url_safety_registration.py +0 -0
  86. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/logging_config.py +0 -0
  87. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/middleware/context.py +0 -0
  88. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/monitoring/__init__.py +0 -0
  89. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/monitoring/setup.py +0 -0
  90. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/presets/__init__.py +0 -0
  91. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/presets/common.py +0 -0
  92. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/presets/crawl.py +0 -0
  93. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/presets/search.py +0 -0
  94. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/presets/types.py +0 -0
  95. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/settings/__init__.py +0 -0
  96. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/settings/app.py +0 -0
  97. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/settings/base.py +0 -0
  98. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/settings/client.py +0 -0
  99. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/settings/monitoring.py +0 -0
  100. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/settings/providers/__init__.py +0 -0
  101. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/settings/providers/base.py +0 -0
  102. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/settings/providers/bing_agent.py +0 -0
  103. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/settings/providers/brave.py +0 -0
  104. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/settings/providers/firecrawl.py +0 -0
  105. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/settings/providers/google.py +0 -0
  106. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/settings/providers/jina.py +0 -0
  107. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/settings/providers/perplexity.py +0 -0
  108. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/settings/providers/tavily.py +0 -0
  109. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/settings/providers/vertexai_agent.py +0 -0
  110. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/settings/secret_str.py +0 -0
  111. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/startup_report.py +0 -0
  112. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/utils/__init__.py +0 -0
  113. {unique_search_proxy-2026.32.0.dev2 → unique_search_proxy-2026.32.0.dev3}/unique_search_proxy_client/web/utils/url.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: unique-search-proxy
3
- Version: 2026.32.0.dev2
3
+ Version: 2026.32.0.dev3
4
4
  Summary: Web Search Proxy implementation
5
5
  Author: ThePhilAz
6
6
  Author-email: ThePhilAz <rami.azouz@philico.com>
@@ -19,8 +19,8 @@ Requires-Dist: certifi>=2025.11.12,<2027
19
19
  Requires-Dist: google-genai>=1.73.0,<2
20
20
  Requires-Dist: google-auth>=2.43.0,<3
21
21
  Requires-Dist: jinja2>=3.1.6,<4
22
- Requires-Dist: unique-toolkit[monitoring]>=2026.32.0.dev2,<2026.32.0rc0
23
- Requires-Dist: unique-search-proxy-core>=2026.32.0.dev0,<2026.32.0rc0
22
+ Requires-Dist: unique-toolkit[monitoring]>=2026.32.0.dev3,<2026.32.0rc0
23
+ Requires-Dist: unique-search-proxy-core>=2026.32.0.dev1,<2026.32.0rc0
24
24
  Requires-Python: >=3.12
25
25
  Description-Content-Type: text/markdown
26
26
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "unique-search-proxy"
3
- version = "2026.32.0.dev2"
3
+ version = "2026.32.0.dev3"
4
4
  description = "Web Search Proxy implementation"
5
5
  authors = [{ name = "ThePhilAz", email = "rami.azouz@philico.com" }]
6
6
  readme = "README.md"
@@ -21,8 +21,8 @@ dependencies = [
21
21
  "google-genai>=1.73.0,<2",
22
22
  "google-auth>=2.43.0,<3",
23
23
  "jinja2>=3.1.6,<4",
24
- "unique-toolkit[monitoring]>=2026.32.0.dev2,<2026.32.0rc0",
25
- "unique-search-proxy-core>=2026.32.0.dev0,<2026.32.0rc0",
24
+ "unique-toolkit[monitoring]>=2026.32.0.dev3,<2026.32.0rc0",
25
+ "unique-search-proxy-core>=2026.32.0.dev1,<2026.32.0rc0",
26
26
  ]
27
27
 
28
28
  [dependency-groups]
@@ -11,7 +11,11 @@ from unique_search_proxy_core.errors import (
11
11
  UpstreamTimeoutError,
12
12
  attach_request_context,
13
13
  )
14
- from unique_search_proxy_core.schema import CrawlResponse, ProxyErrorCode
14
+ from unique_search_proxy_core.schema import (
15
+ CrawlResponse,
16
+ CrawlUrlResult,
17
+ ProxyErrorCode,
18
+ )
15
19
 
16
20
  from unique_search_proxy_client.web.api.v1.openapi_examples import (
17
21
  CRAWL_OPENAPI_EXAMPLES,
@@ -28,6 +32,7 @@ from unique_search_proxy_client.web.core.url_safety.gate import (
28
32
  from unique_search_proxy_client.web.monitoring.metrics import (
29
33
  record_crawl_error,
30
34
  record_crawl_success,
35
+ record_crawl_url_outcomes,
31
36
  )
32
37
 
33
38
  router = APIRouter(tags=["crawl"])
@@ -42,6 +47,22 @@ def _crawl_request_context(exc: ProxyError, *, crawler_id: str) -> ProxyError:
42
47
  )
43
48
 
44
49
 
50
+ def _url_outcomes(results: list[CrawlUrlResult]) -> list[tuple[str, str, str]]:
51
+ """Derive ``(outcome, error_code, http_status)`` triples from merged results."""
52
+ outcomes: list[tuple[str, str, str]] = []
53
+ for result in results:
54
+ if result.error is None:
55
+ outcomes.append(("success", "", ""))
56
+ continue
57
+ http_status = (
58
+ str(result.error.status_code)
59
+ if result.error.status_code is not None
60
+ else ""
61
+ )
62
+ outcomes.append(("error", result.error.code, http_status))
63
+ return outcomes
64
+
65
+
45
66
  @router.post(
46
67
  "/crawl",
47
68
  response_model=CrawlResponse,
@@ -71,6 +92,15 @@ async def crawl(
71
92
  len(body.urls),
72
93
  duration,
73
94
  )
95
+ merged_results = merge_crawl_results(
96
+ body.urls,
97
+ blocked_by_index=gate.blocked_by_index,
98
+ crawler_results=[],
99
+ )
100
+ record_crawl_url_outcomes(
101
+ crawler_id,
102
+ _url_outcomes(merged_results),
103
+ )
74
104
  _LOGGER.info(
75
105
  "crawl success crawler=%s urls=%d blocked=%d results=0 duration=%.0fms",
76
106
  crawler_id,
@@ -80,11 +110,7 @@ async def crawl(
80
110
  )
81
111
  return CrawlResponse(
82
112
  crawler=crawler_id,
83
- results=merge_crawl_results(
84
- body.urls,
85
- blocked_by_index=gate.blocked_by_index,
86
- crawler_results=[],
87
- ),
113
+ results=merged_results,
88
114
  )
89
115
 
90
116
  crawl_body = body.model_copy(
@@ -121,6 +147,11 @@ async def crawl(
121
147
  crawler_id=crawler_id,
122
148
  ) from exc
123
149
  except ProxyError as exc:
150
+ record_crawl_error(
151
+ crawler_id,
152
+ exc.code.value if hasattr(exc.code, "value") else str(exc.code),
153
+ time.perf_counter() - started,
154
+ )
124
155
  _LOGGER.warning(
125
156
  "crawl failed crawler=%s code=%s duration=%.0fms",
126
157
  crawler_id,
@@ -148,6 +179,7 @@ async def crawl(
148
179
  blocked_by_index=gate.blocked_by_index,
149
180
  crawler_results=crawler_results,
150
181
  )
182
+ record_crawl_url_outcomes(crawler_id, _url_outcomes(merged_results))
151
183
  _LOGGER.info(
152
184
  "crawl success crawler=%s urls=%d blocked=%d results=%d duration=%.0fms",
153
185
  crawler_id,
@@ -93,6 +93,11 @@ async def search(
93
93
  engine_id=engine_id,
94
94
  ) from exc
95
95
  except ProxyError as exc:
96
+ record_search_error(
97
+ engine_id,
98
+ exc.code.value if hasattr(exc.code, "value") else str(exc.code),
99
+ time.perf_counter() - started,
100
+ )
96
101
  _LOGGER.warning(
97
102
  "search failed engine=%s code=%s duration=%.0fms",
98
103
  engine_id,
@@ -144,6 +144,7 @@ class BasicCrawlerService(BaseCrawler[BasicCrawlRequest]):
144
144
  f"HTTP {response.status_code} while fetching URL",
145
145
  content_type=content_type,
146
146
  raw=raw_body,
147
+ status_code=response.status_code,
147
148
  )
148
149
 
149
150
  content = await self._maybe_process_content(
@@ -69,13 +69,14 @@ def crawl_upstream_error(
69
69
  raw: Any | None = None,
70
70
  content_type: str | None = "text/markdown",
71
71
  code: str = ProxyErrorCode.UPSTREAM_ERROR.value,
72
+ status_code: int | None = None,
72
73
  ) -> CrawlUrlResult:
73
74
  """Build a per-URL crawl failure with optional upstream response attached."""
74
75
  return CrawlUrlResult(
75
76
  url=url,
76
77
  content=None,
77
78
  content_type=content_type,
78
- error=PerUrlError(code=code, message=message),
79
+ error=PerUrlError(code=code, message=message, status_code=status_code),
79
80
  raw=raw,
80
81
  )
81
82
 
@@ -37,16 +37,10 @@ def register_exception_handlers(app: FastAPI) -> None:
37
37
  @app.exception_handler(ProxyError)
38
38
  async def proxy_error_handler(_request: Request, exc: ProxyError) -> JSONResponse:
39
39
  from unique_search_proxy_client.web.monitoring.metrics import (
40
- record_crawl_error,
41
40
  record_proxy_error,
42
- record_search_error,
43
41
  )
44
42
 
45
43
  record_proxy_error(exc.code.value)
46
- if exc.request == "search" and exc.provider is not None:
47
- record_search_error(exc.provider, exc.code.value, 0.0)
48
- if exc.request == "crawl" and exc.provider is not None:
49
- record_crawl_error(exc.provider, exc.code.value, 0.0)
50
44
  _LOGGER.warning(
51
45
  "Proxy error [%s] request=%s provider=%s: %s",
52
46
  exc.code.value,
@@ -1,13 +1,35 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from collections.abc import Iterable
4
+
3
5
  from unique_toolkit.monitoring import MetricNamespace
4
6
 
5
7
  m = MetricNamespace("unique_search_proxy")
6
8
 
9
+
10
+ def _linear_buckets(ceiling: float, steps: int) -> tuple[float, ...]:
11
+ """Constant-step latency buckets (seconds) up to ``ceiling`` in ``steps``.
12
+
13
+ The range ``(0, ceiling]`` is split into ``steps`` equal buckets, yielding
14
+ ``ceiling/steps, 2*ceiling/steps, ..., ceiling`` inclusive. Observations above
15
+ the ceiling land in the implicit +Inf bucket prometheus_client appends;
16
+ histogram_quantile cannot resolve past the largest finite bucket, so each
17
+ ceiling exceeds the workload's typical timeout. Using the same ``steps`` count
18
+ keeps resolution comparable across workloads while the ceiling adapts to range.
19
+ """
20
+ step = ceiling / steps
21
+ return tuple(round(step * i, 3) for i in range(1, steps + 1))
22
+
23
+
24
+ _SEARCH_LATENCY_BUCKETS = _linear_buckets(ceiling=5.0, steps=20)
25
+ _CRAWL_LATENCY_BUCKETS = _linear_buckets(ceiling=60.0, steps=20)
26
+ _AGENT_SEARCH_LATENCY_BUCKETS = _linear_buckets(ceiling=120.0, steps=20)
27
+
7
28
  search_duration_seconds = m.histogram(
8
29
  "search_duration_seconds",
9
30
  "Search request latency",
10
31
  ["engine"],
32
+ buckets=_SEARCH_LATENCY_BUCKETS,
11
33
  )
12
34
  search_total = m.counter(
13
35
  "search_total",
@@ -24,6 +46,7 @@ crawl_duration_seconds = m.histogram(
24
46
  "crawl_duration_seconds",
25
47
  "Crawl request latency",
26
48
  ["crawler"],
49
+ buckets=_CRAWL_LATENCY_BUCKETS,
27
50
  )
28
51
  crawl_total = m.counter(
29
52
  "crawl_total",
@@ -40,6 +63,11 @@ crawl_urls_total = m.counter(
40
63
  "URLs submitted to crawl",
41
64
  ["crawler"],
42
65
  )
66
+ crawl_url_outcomes_total = m.counter(
67
+ "crawl_url_outcomes_total",
68
+ "Per-URL crawl outcomes",
69
+ ["crawler", "outcome", "error_code", "http_status"],
70
+ )
43
71
 
44
72
  crawl_blocked_total = m.counter(
45
73
  "crawl_blocked_total",
@@ -51,6 +79,7 @@ agent_search_duration_seconds = m.histogram(
51
79
  "agent_search_duration_seconds",
52
80
  "Agent search request latency",
53
81
  ["engine"],
82
+ buckets=_AGENT_SEARCH_LATENCY_BUCKETS,
54
83
  )
55
84
  agent_search_total = m.counter(
56
85
  "agent_search_total",
@@ -105,6 +134,29 @@ def record_crawl_error(crawler: str, error_code: str, duration_seconds: float) -
105
134
  crawl_duration_seconds.labels(crawler=crawler).observe(duration_seconds)
106
135
 
107
136
 
137
+ def record_crawl_url_outcomes(
138
+ crawler: str,
139
+ outcomes: Iterable[tuple[str, str, str]],
140
+ ) -> None:
141
+ """Record per-URL crawl outcomes.
142
+
143
+ Each item is an ``(outcome, error_code, http_status)`` triple where
144
+ ``outcome`` is ``"success"`` or ``"error"``, ``error_code`` is ``""`` on
145
+ success or the per-URL :class:`PerUrlError` code otherwise, and
146
+ ``http_status`` is the upstream HTTP status (e.g. ``"403"``) when the failure
147
+ was an HTTP error or ``""`` when no HTTP response was received.
148
+ """
149
+ if not _metrics_enabled():
150
+ return
151
+ for outcome, error_code, http_status in outcomes:
152
+ crawl_url_outcomes_total.labels(
153
+ crawler=crawler,
154
+ outcome=outcome,
155
+ error_code=error_code,
156
+ http_status=http_status,
157
+ ).inc()
158
+
159
+
108
160
  def record_crawl_blocked(reason_category: str, count: int = 1) -> None:
109
161
  if not _metrics_enabled():
110
162
  return