reqkey 0.2.0__tar.gz → 0.2.1__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 (26) hide show
  1. {reqkey-0.2.0 → reqkey-0.2.1}/PKG-INFO +24 -8
  2. {reqkey-0.2.0 → reqkey-0.2.1}/README.md +23 -7
  3. {reqkey-0.2.0 → reqkey-0.2.1}/pyproject.toml +1 -1
  4. {reqkey-0.2.0 → reqkey-0.2.1}/src/reqkey/__init__.py +1 -1
  5. {reqkey-0.2.0 → reqkey-0.2.1}/src/reqkey/asgi.py +14 -1
  6. {reqkey-0.2.0 → reqkey-0.2.1}/src/reqkey/client.py +13 -1
  7. {reqkey-0.2.0 → reqkey-0.2.1}/src/reqkey/django.py +20 -2
  8. {reqkey-0.2.0 → reqkey-0.2.1}/src/reqkey/wsgi.py +12 -1
  9. {reqkey-0.2.0 → reqkey-0.2.1}/tests/test_client.py +4 -0
  10. {reqkey-0.2.0 → reqkey-0.2.1}/tests/test_django.py +9 -2
  11. {reqkey-0.2.0 → reqkey-0.2.1}/tests/test_fastapi.py +8 -2
  12. {reqkey-0.2.0 → reqkey-0.2.1}/tests/test_wsgi.py +6 -2
  13. {reqkey-0.2.0 → reqkey-0.2.1}/.github/workflows/ci.yml +0 -0
  14. {reqkey-0.2.0 → reqkey-0.2.1}/.github/workflows/release.yml +0 -0
  15. {reqkey-0.2.0 → reqkey-0.2.1}/.gitignore +0 -0
  16. {reqkey-0.2.0 → reqkey-0.2.1}/LICENSE +0 -0
  17. {reqkey-0.2.0 → reqkey-0.2.1}/examples/django_settings.py +0 -0
  18. {reqkey-0.2.0 → reqkey-0.2.1}/examples/fastapi_app.py +0 -0
  19. {reqkey-0.2.0 → reqkey-0.2.1}/examples/flask_app.py +0 -0
  20. {reqkey-0.2.0 → reqkey-0.2.1}/src/reqkey/_middleware.py +0 -0
  21. {reqkey-0.2.0 → reqkey-0.2.1}/src/reqkey/exceptions.py +0 -0
  22. {reqkey-0.2.0 → reqkey-0.2.1}/src/reqkey/fastapi.py +0 -0
  23. {reqkey-0.2.0 → reqkey-0.2.1}/src/reqkey/flask.py +0 -0
  24. {reqkey-0.2.0 → reqkey-0.2.1}/src/reqkey/models.py +0 -0
  25. {reqkey-0.2.0 → reqkey-0.2.1}/src/reqkey/py.typed +0 -0
  26. {reqkey-0.2.0 → reqkey-0.2.1}/tests/test_middleware.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: reqkey
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: The official Python SDK for ReqKey API key validation and usage analytics
5
5
  Project-URL: Homepage, https://reqkey.com
6
6
  Project-URL: Documentation, https://reqkey.com/docs
@@ -551,6 +551,8 @@ Metadata sent by default:
551
551
  - response status
552
552
  - endpoint processing latency
553
553
  - user agent
554
+ - the consumer API key in the dedicated `apiKey` field, so ReqKey can resolve
555
+ the internal consumer name
554
556
  - `consumerName` when a resolver supplies one
555
557
 
556
558
  Additional data is opt-in:
@@ -574,9 +576,11 @@ app.add_middleware(
574
576
  ```
575
577
 
576
578
  Authorization, cookies, `Set-Cookie`, common API-key headers, and the configured
577
- consumer-key header are always removed from captured headers. Response-body
578
- capture is limited to textual content and a hard maximum of 1,000 characters.
579
- The limit is enforced again by the HTTP client before `/ingest` is called.
579
+ consumer-key header are always removed from captured headers and paths. The
580
+ middleware sends the extracted consumer key only through `/ingest`'s dedicated
581
+ `apiKey` field for consumer resolution. Response-body capture is limited to
582
+ textual content and a hard maximum of 1,000 characters. The limit is enforced
583
+ again by the HTTP client before `/ingest` is called.
580
584
 
581
585
  ### Consumer names
582
586
 
@@ -600,8 +604,18 @@ app.add_middleware(
600
604
  )
601
605
  ```
602
606
 
603
- The resolved value is sent as analytics metadata. It is not used as the
604
- consumer API key and does not change authorization.
607
+ The resolved value is optional analytics metadata. It is not used as the
608
+ consumer API key and does not change authorization. ReqKey resolves the
609
+ consumer display name in this order:
610
+
611
+ 1. A non-empty `consumerName` supplied by the resolver.
612
+ 2. The middleware's extracted consumer key, sent as `apiKey` for an internal
613
+ consumer lookup.
614
+ 3. A `consumerId` supplied to the direct client.
615
+
616
+ No resolver is required for normal ReqKey-managed consumers. The middleware
617
+ still sends the validation `requestId` for log correlation, but `/ingest` does
618
+ not use it to resolve consumer identity.
605
619
 
606
620
  For Django, set `CONSUMER_NAME_RESOLVER` in `REQKEY`. For WSGI, the resolver
607
621
  receives the WSGI environment, so a header can be read as
@@ -767,7 +781,7 @@ with ReqKey(project_key=os.environ["REQKEY_PROJECT_KEY"]) as reqkey:
767
781
  decision = reqkey.verify("consumer_key_...", api_id="api_payments")
768
782
  ```
769
783
 
770
- Direct ingestion also accepts an optional consumer label:
784
+ Direct ingestion accepts the same consumer identity inputs as `/ingest`:
771
785
 
772
786
  ```python
773
787
  reqkey.ingest(
@@ -776,7 +790,9 @@ reqkey.ingest(
776
790
  method="POST",
777
791
  path="/payments",
778
792
  status_code=201,
779
- consumer_name="rapidapi-user",
793
+ api_key="consumer_key_...", # resolves the internal consumer name
794
+ # consumer_name="rapidapi-user", # optional explicit override
795
+ # consumer_id="consumer_...", # fallback when no API key is available
780
796
  )
781
797
  ```
782
798
 
@@ -478,6 +478,8 @@ Metadata sent by default:
478
478
  - response status
479
479
  - endpoint processing latency
480
480
  - user agent
481
+ - the consumer API key in the dedicated `apiKey` field, so ReqKey can resolve
482
+ the internal consumer name
481
483
  - `consumerName` when a resolver supplies one
482
484
 
483
485
  Additional data is opt-in:
@@ -501,9 +503,11 @@ app.add_middleware(
501
503
  ```
502
504
 
503
505
  Authorization, cookies, `Set-Cookie`, common API-key headers, and the configured
504
- consumer-key header are always removed from captured headers. Response-body
505
- capture is limited to textual content and a hard maximum of 1,000 characters.
506
- The limit is enforced again by the HTTP client before `/ingest` is called.
506
+ consumer-key header are always removed from captured headers and paths. The
507
+ middleware sends the extracted consumer key only through `/ingest`'s dedicated
508
+ `apiKey` field for consumer resolution. Response-body capture is limited to
509
+ textual content and a hard maximum of 1,000 characters. The limit is enforced
510
+ again by the HTTP client before `/ingest` is called.
507
511
 
508
512
  ### Consumer names
509
513
 
@@ -527,8 +531,18 @@ app.add_middleware(
527
531
  )
528
532
  ```
529
533
 
530
- The resolved value is sent as analytics metadata. It is not used as the
531
- consumer API key and does not change authorization.
534
+ The resolved value is optional analytics metadata. It is not used as the
535
+ consumer API key and does not change authorization. ReqKey resolves the
536
+ consumer display name in this order:
537
+
538
+ 1. A non-empty `consumerName` supplied by the resolver.
539
+ 2. The middleware's extracted consumer key, sent as `apiKey` for an internal
540
+ consumer lookup.
541
+ 3. A `consumerId` supplied to the direct client.
542
+
543
+ No resolver is required for normal ReqKey-managed consumers. The middleware
544
+ still sends the validation `requestId` for log correlation, but `/ingest` does
545
+ not use it to resolve consumer identity.
532
546
 
533
547
  For Django, set `CONSUMER_NAME_RESOLVER` in `REQKEY`. For WSGI, the resolver
534
548
  receives the WSGI environment, so a header can be read as
@@ -694,7 +708,7 @@ with ReqKey(project_key=os.environ["REQKEY_PROJECT_KEY"]) as reqkey:
694
708
  decision = reqkey.verify("consumer_key_...", api_id="api_payments")
695
709
  ```
696
710
 
697
- Direct ingestion also accepts an optional consumer label:
711
+ Direct ingestion accepts the same consumer identity inputs as `/ingest`:
698
712
 
699
713
  ```python
700
714
  reqkey.ingest(
@@ -703,7 +717,9 @@ reqkey.ingest(
703
717
  method="POST",
704
718
  path="/payments",
705
719
  status_code=201,
706
- consumer_name="rapidapi-user",
720
+ api_key="consumer_key_...", # resolves the internal consumer name
721
+ # consumer_name="rapidapi-user", # optional explicit override
722
+ # consumer_id="consumer_...", # fallback when no API key is available
707
723
  )
708
724
  ```
709
725
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "reqkey"
7
- version = "0.2.0"
7
+ version = "0.2.1"
8
8
  description = "The official Python SDK for ReqKey API key validation and usage analytics"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -11,7 +11,7 @@ from .exceptions import (
11
11
  )
12
12
  from .models import MiddlewareErrorEvent, VerificationReason, VerificationResult
13
13
 
14
- __version__ = "0.2.0"
14
+ __version__ = "0.2.1"
15
15
 
16
16
  __all__ = [
17
17
  "AsyncReqKey",
@@ -135,6 +135,8 @@ class AsyncReqKeyLike(Protocol):
135
135
  user_agent: str | None = None,
136
136
  user_id: str | None = None,
137
137
  consumer_name: str | None = None,
138
+ api_key: str | None = None,
139
+ consumer_id: str | None = None,
138
140
  query_params: Mapping[str, Any] | None = None,
139
141
  request_headers: Mapping[str, str] | None = None,
140
142
  response_headers: Mapping[str, str] | None = None,
@@ -276,13 +278,14 @@ class ReqKeyMiddleware:
276
278
  request_started = time.perf_counter()
277
279
  decision: VerificationResult | None = None
278
280
  validation_time_ms: float | None = None
281
+ consumer_key = await self._consumer_key(request)
279
282
 
280
283
  if self.mode in {"validate", "both"}:
281
- consumer_key = await self._consumer_key(request)
282
284
  if consumer_key is None:
283
285
  await self._ingest_denied_request(
284
286
  request,
285
287
  decision=None,
288
+ consumer_key=None,
286
289
  status_code=401,
287
290
  error="missing_api_key",
288
291
  headers={},
@@ -336,6 +339,7 @@ class ReqKeyMiddleware:
336
339
  await self._ingest_denied_request(
337
340
  request,
338
341
  decision=decision,
342
+ consumer_key=consumer_key,
339
343
  status_code=status_code,
340
344
  error=error,
341
345
  headers=headers,
@@ -372,6 +376,7 @@ class ReqKeyMiddleware:
372
376
  receive,
373
377
  send,
374
378
  decision=decision,
379
+ consumer_key=consumer_key,
375
380
  validation_time_ms=validation_time_ms,
376
381
  )
377
382
 
@@ -398,6 +403,7 @@ class ReqKeyMiddleware:
398
403
  send: Send,
399
404
  *,
400
405
  decision: VerificationResult | None,
406
+ consumer_key: str | None,
401
407
  validation_time_ms: float | None,
402
408
  ) -> None:
403
409
  response_status = 500
@@ -431,6 +437,7 @@ class ReqKeyMiddleware:
431
437
  await self._ingest_safely(
432
438
  request=request,
433
439
  decision=decision,
440
+ consumer_key=consumer_key,
434
441
  response_status=response_status,
435
442
  latency_ms=round((time.perf_counter() - started) * 1000),
436
443
  response_headers=response_headers,
@@ -446,6 +453,7 @@ class ReqKeyMiddleware:
446
453
  await self._ingest_safely(
447
454
  request=request,
448
455
  decision=decision,
456
+ consumer_key=consumer_key,
449
457
  response_status=500,
450
458
  latency_ms=round((time.perf_counter() - started) * 1000),
451
459
  response_headers=response_headers,
@@ -457,6 +465,7 @@ class ReqKeyMiddleware:
457
465
  await self._ingest_safely(
458
466
  request=request,
459
467
  decision=decision,
468
+ consumer_key=consumer_key,
460
469
  response_status=response_status,
461
470
  latency_ms=round((time.perf_counter() - started) * 1000),
462
471
  response_headers=response_headers,
@@ -537,6 +546,7 @@ class ReqKeyMiddleware:
537
546
  *,
538
547
  request: Request,
539
548
  decision: VerificationResult | None,
549
+ consumer_key: str | None,
540
550
  response_status: int,
541
551
  latency_ms: int,
542
552
  response_headers: Headers,
@@ -572,6 +582,7 @@ class ReqKeyMiddleware:
572
582
  client_ip=client_ip,
573
583
  user_agent=user_agent,
574
584
  consumer_name=consumer_name,
585
+ api_key=consumer_key,
575
586
  query_params=query_params,
576
587
  request_headers=request_headers,
577
588
  response_headers=captured_response_headers,
@@ -592,6 +603,7 @@ class ReqKeyMiddleware:
592
603
  request: Request,
593
604
  *,
594
605
  decision: VerificationResult | None,
606
+ consumer_key: str | None,
595
607
  status_code: int,
596
608
  error: str,
597
609
  headers: Mapping[str, str],
@@ -617,6 +629,7 @@ class ReqKeyMiddleware:
617
629
  await self._ingest_safely(
618
630
  request=request,
619
631
  decision=decision,
632
+ consumer_key=consumer_key,
620
633
  response_status=status_code,
621
634
  latency_ms=round((time.perf_counter() - started) * 1000),
622
635
  response_headers=response_headers,
@@ -20,7 +20,7 @@ from .models import VerificationReason, VerificationResult
20
20
 
21
21
  DEFAULT_BASE_URL = "https://api.reqkey.com"
22
22
  DEFAULT_TIMEOUT_SECONDS = 2.0
23
- USER_AGENT = "reqkey-python/0.2.0"
23
+ USER_AGENT = "reqkey-python/0.2.1"
24
24
  MAX_BODY_CHARACTERS = 1000
25
25
  _DECISION_STATUS_CODES = {200, 402, 403, 429}
26
26
 
@@ -168,6 +168,8 @@ def _ingest_payload(
168
168
  user_agent: str | None = None,
169
169
  user_id: str | None = None,
170
170
  consumer_name: str | None = None,
171
+ api_key: str | None = None,
172
+ consumer_id: str | None = None,
171
173
  query_params: Mapping[str, Any] | None = None,
172
174
  request_headers: Mapping[str, str] | None = None,
173
175
  response_headers: Mapping[str, str] | None = None,
@@ -196,6 +198,8 @@ def _ingest_payload(
196
198
  "userAgent": user_agent,
197
199
  "userId": user_id,
198
200
  "consumerName": consumer_name,
201
+ "apiKey": api_key,
202
+ "consumerId": consumer_id,
199
203
  "queryParams": dict(query_params) if query_params is not None else None,
200
204
  "requestHeaders": dict(request_headers) if request_headers is not None else None,
201
205
  "responseHeaders": dict(response_headers) if response_headers is not None else None,
@@ -297,6 +301,8 @@ class ReqKey:
297
301
  user_agent: str | None = None,
298
302
  user_id: str | None = None,
299
303
  consumer_name: str | None = None,
304
+ api_key: str | None = None,
305
+ consumer_id: str | None = None,
300
306
  query_params: Mapping[str, Any] | None = None,
301
307
  request_headers: Mapping[str, str] | None = None,
302
308
  response_headers: Mapping[str, str] | None = None,
@@ -318,6 +324,8 @@ class ReqKey:
318
324
  user_agent=user_agent,
319
325
  user_id=user_id,
320
326
  consumer_name=consumer_name,
327
+ api_key=api_key,
328
+ consumer_id=consumer_id,
321
329
  query_params=query_params,
322
330
  request_headers=request_headers,
323
331
  response_headers=response_headers,
@@ -418,6 +426,8 @@ class AsyncReqKey:
418
426
  user_agent: str | None = None,
419
427
  user_id: str | None = None,
420
428
  consumer_name: str | None = None,
429
+ api_key: str | None = None,
430
+ consumer_id: str | None = None,
421
431
  query_params: Mapping[str, Any] | None = None,
422
432
  request_headers: Mapping[str, str] | None = None,
423
433
  response_headers: Mapping[str, str] | None = None,
@@ -439,6 +449,8 @@ class AsyncReqKey:
439
449
  user_agent=user_agent,
440
450
  user_id=user_id,
441
451
  consumer_name=consumer_name,
452
+ api_key=api_key,
453
+ consumer_id=consumer_id,
442
454
  query_params=query_params,
443
455
  request_headers=request_headers,
444
456
  response_headers=response_headers,
@@ -67,6 +67,8 @@ class SyncReqKeyLike(Protocol):
67
67
  user_agent: str | None = None,
68
68
  user_id: str | None = None,
69
69
  consumer_name: str | None = None,
70
+ api_key: str | None = None,
71
+ consumer_id: str | None = None,
70
72
  query_params: Mapping[str, Any] | None = None,
71
73
  request_headers: Mapping[str, str] | None = None,
72
74
  response_headers: Mapping[str, str] | None = None,
@@ -100,6 +102,8 @@ class AsyncReqKeyLike(Protocol):
100
102
  user_agent: str | None = None,
101
103
  user_id: str | None = None,
102
104
  consumer_name: str | None = None,
105
+ api_key: str | None = None,
106
+ consumer_id: str | None = None,
103
107
  query_params: Mapping[str, Any] | None = None,
104
108
  request_headers: Mapping[str, str] | None = None,
105
109
  response_headers: Mapping[str, str] | None = None,
@@ -245,9 +249,9 @@ class ReqKeyMiddleware:
245
249
  decision: VerificationResult | None = None
246
250
  validation_time_ms: float | None = None
247
251
  client = cast(SyncReqKeyLike, self.client)
252
+ consumer_key = self._consumer_key_sync(request)
248
253
 
249
254
  if self.mode in {"validate", "both"}:
250
- consumer_key = self._consumer_key_sync(request)
251
255
  if consumer_key is None:
252
256
  missing_response = self._respond(401, "missing_api_key")
253
257
  if self.mode == "both" and self.ingest_denied_requests:
@@ -255,6 +259,7 @@ class ReqKeyMiddleware:
255
259
  client,
256
260
  request=request,
257
261
  decision=None,
262
+ consumer_key=None,
258
263
  response=missing_response,
259
264
  status_code=401,
260
265
  latency_ms=round(
@@ -294,6 +299,7 @@ class ReqKeyMiddleware:
294
299
  client,
295
300
  request=request,
296
301
  decision=decision,
302
+ consumer_key=consumer_key,
297
303
  response=denied,
298
304
  status_code=denied.status_code,
299
305
  latency_ms=round(
@@ -312,6 +318,7 @@ class ReqKeyMiddleware:
312
318
  client,
313
319
  request=request,
314
320
  decision=decision,
321
+ consumer_key=consumer_key,
315
322
  response=None,
316
323
  status_code=500,
317
324
  latency_ms=round((time.perf_counter() - started) * 1000),
@@ -323,6 +330,7 @@ class ReqKeyMiddleware:
323
330
  client,
324
331
  request=request,
325
332
  decision=decision,
333
+ consumer_key=consumer_key,
326
334
  response=response,
327
335
  status_code=response.status_code,
328
336
  latency_ms=round((time.perf_counter() - started) * 1000),
@@ -338,9 +346,9 @@ class ReqKeyMiddleware:
338
346
  decision: VerificationResult | None = None
339
347
  validation_time_ms: float | None = None
340
348
  client = cast(AsyncReqKeyLike, self.client)
349
+ consumer_key = await self._consumer_key_async(request)
341
350
 
342
351
  if self.mode in {"validate", "both"}:
343
- consumer_key = await self._consumer_key_async(request)
344
352
  if consumer_key is None:
345
353
  missing_response = self._respond(401, "missing_api_key")
346
354
  if self.mode == "both" and self.ingest_denied_requests:
@@ -348,6 +356,7 @@ class ReqKeyMiddleware:
348
356
  client,
349
357
  request=request,
350
358
  decision=None,
359
+ consumer_key=None,
351
360
  response=missing_response,
352
361
  status_code=401,
353
362
  latency_ms=round(
@@ -387,6 +396,7 @@ class ReqKeyMiddleware:
387
396
  client,
388
397
  request=request,
389
398
  decision=decision,
399
+ consumer_key=consumer_key,
390
400
  response=denied,
391
401
  status_code=denied.status_code,
392
402
  latency_ms=round(
@@ -405,6 +415,7 @@ class ReqKeyMiddleware:
405
415
  client,
406
416
  request=request,
407
417
  decision=decision,
418
+ consumer_key=consumer_key,
408
419
  response=None,
409
420
  status_code=500,
410
421
  latency_ms=round((time.perf_counter() - started) * 1000),
@@ -416,6 +427,7 @@ class ReqKeyMiddleware:
416
427
  client,
417
428
  request=request,
418
429
  decision=decision,
430
+ consumer_key=consumer_key,
419
431
  response=response,
420
432
  status_code=response.status_code,
421
433
  latency_ms=round((time.perf_counter() - started) * 1000),
@@ -508,6 +520,7 @@ class ReqKeyMiddleware:
508
520
  *,
509
521
  request: HttpRequest,
510
522
  decision: VerificationResult | None,
523
+ consumer_key: str | None,
511
524
  response: HttpResponseBase | None,
512
525
  status_code: int,
513
526
  latency_ms: int,
@@ -524,6 +537,7 @@ class ReqKeyMiddleware:
524
537
  self._resource_path_sync(request),
525
538
  self._client_ip_sync(request),
526
539
  self._consumer_name_sync(request),
540
+ consumer_key,
527
541
  ),
528
542
  )
529
543
  except ReqKeyError as exc:
@@ -542,6 +556,7 @@ class ReqKeyMiddleware:
542
556
  *,
543
557
  request: HttpRequest,
544
558
  decision: VerificationResult | None,
559
+ consumer_key: str | None,
545
560
  response: HttpResponseBase | None,
546
561
  status_code: int,
547
562
  latency_ms: int,
@@ -558,6 +573,7 @@ class ReqKeyMiddleware:
558
573
  await self._resource_path_async(request),
559
574
  await self._client_ip_async(request),
560
575
  await self._consumer_name_async(request),
576
+ consumer_key,
561
577
  ),
562
578
  )
563
579
  except ReqKeyError as exc:
@@ -579,6 +595,7 @@ class ReqKeyMiddleware:
579
595
  resource_path: str,
580
596
  client_ip: str | None,
581
597
  consumer_name: str | None,
598
+ consumer_key: str | None,
582
599
  ) -> dict[str, Any]:
583
600
  query_params, query = self._captured_query(request)
584
601
  request_headers = dict(request.headers.items())
@@ -596,6 +613,7 @@ class ReqKeyMiddleware:
596
613
  ),
597
614
  "query_params": query_params,
598
615
  "consumer_name": consumer_name,
616
+ "api_key": consumer_key,
599
617
  "request_headers": (
600
618
  filtered_headers(request_headers, self.excluded_headers)
601
619
  if self.capture_request_headers
@@ -84,6 +84,8 @@ class ReqKeyLike(Protocol):
84
84
  user_agent: str | None = None,
85
85
  user_id: str | None = None,
86
86
  consumer_name: str | None = None,
87
+ api_key: str | None = None,
88
+ consumer_id: str | None = None,
87
89
  query_params: Mapping[str, Any] | None = None,
88
90
  request_headers: Mapping[str, str] | None = None,
89
91
  response_headers: Mapping[str, str] | None = None,
@@ -255,13 +257,14 @@ class ReqKeyMiddleware:
255
257
  request_started = time.perf_counter()
256
258
  decision: VerificationResult | None = None
257
259
  validation_time_ms: float | None = None
260
+ consumer_key = self._consumer_key(environ)
258
261
 
259
262
  if self.mode in {"validate", "both"}:
260
- consumer_key = self._consumer_key(environ)
261
263
  if consumer_key is None:
262
264
  self._ingest_denied_safely(
263
265
  environ,
264
266
  decision=None,
267
+ consumer_key=None,
265
268
  status_code=401,
266
269
  error="missing_api_key",
267
270
  headers={},
@@ -301,6 +304,7 @@ class ReqKeyMiddleware:
301
304
  self._ingest_denied_safely(
302
305
  environ,
303
306
  decision=decision,
307
+ consumer_key=consumer_key,
304
308
  status_code=status_code,
305
309
  error=error,
306
310
  headers=headers,
@@ -321,6 +325,7 @@ class ReqKeyMiddleware:
321
325
  environ,
322
326
  wrapped_start,
323
327
  decision=decision,
328
+ consumer_key=consumer_key,
324
329
  )
325
330
 
326
331
  def _run_with_ingest(
@@ -329,6 +334,7 @@ class ReqKeyMiddleware:
329
334
  start_response: StartResponse,
330
335
  *,
331
336
  decision: VerificationResult | None,
337
+ consumer_key: str | None,
332
338
  ) -> Iterator[bytes]:
333
339
  response = _ResponseState()
334
340
  started = time.perf_counter()
@@ -372,6 +378,7 @@ class ReqKeyMiddleware:
372
378
  self._ingest_safely(
373
379
  environ=environ,
374
380
  decision=decision,
381
+ consumer_key=consumer_key,
375
382
  response_status=500 if failed else response.status_code,
376
383
  latency_ms=round((time.perf_counter() - started) * 1000),
377
384
  response_headers=response.headers,
@@ -431,6 +438,7 @@ class ReqKeyMiddleware:
431
438
  *,
432
439
  environ: WSGIEnvironment,
433
440
  decision: VerificationResult | None,
441
+ consumer_key: str | None,
434
442
  response_status: int,
435
443
  latency_ms: int,
436
444
  response_headers: Mapping[str, str],
@@ -456,6 +464,7 @@ class ReqKeyMiddleware:
456
464
  request_headers.get("user-agent") if self.capture_user_agent else None
457
465
  ),
458
466
  consumer_name=self._consumer_name(environ),
467
+ api_key=consumer_key,
459
468
  query_params=query_params,
460
469
  request_headers=(
461
470
  filtered_headers(request_headers, self.excluded_headers)
@@ -484,6 +493,7 @@ class ReqKeyMiddleware:
484
493
  environ: WSGIEnvironment,
485
494
  *,
486
495
  decision: VerificationResult | None,
496
+ consumer_key: str | None,
487
497
  status_code: int,
488
498
  error: str,
489
499
  headers: Mapping[str, str],
@@ -498,6 +508,7 @@ class ReqKeyMiddleware:
498
508
  self._ingest_safely(
499
509
  environ=environ,
500
510
  decision=decision,
511
+ consumer_key=consumer_key,
501
512
  response_status=status_code,
502
513
  latency_ms=round((time.perf_counter() - started) * 1000),
503
514
  response_headers=response_headers,
@@ -128,6 +128,8 @@ async def test_async_ingest_uses_request_id_and_truncates_bodies() -> None:
128
128
  method="POST",
129
129
  endpoint="/payments",
130
130
  consumer_name="rapidapi-user",
131
+ api_key="prod_consumer",
132
+ consumer_id="consumer_123",
131
133
  response_body="x" * 1200,
132
134
  )
133
135
  await http_client.aclose()
@@ -135,6 +137,8 @@ async def test_async_ingest_uses_request_id_and_truncates_bodies() -> None:
135
137
  assert captured["requestId"] == "request_123"
136
138
  assert captured["method"] == "POST"
137
139
  assert captured["consumerName"] == "rapidapi-user"
140
+ assert captured["apiKey"] == "prod_consumer"
141
+ assert captured["consumerId"] == "consumer_123"
138
142
  assert len(str(captured["responseBody"])) == 1000
139
143
 
140
144
 
@@ -170,6 +170,7 @@ def test_django_sync_middleware_validates_attaches_state_and_ingests() -> None:
170
170
  }
171
171
  event = fake.ingest_calls[0]
172
172
  assert event["request_id"] == "request_123"
173
+ assert event["api_key"] == "prod_consumer"
173
174
  assert event["path"] == "/protected?page=2"
174
175
  assert event["query_params"] == {"page": "2"}
175
176
  assert event["request_headers"]["X-Safe"] == "visible"
@@ -196,6 +197,7 @@ def test_django_missing_key_short_circuits_before_view() -> None:
196
197
  assert called is False
197
198
  assert fake.verify_calls == []
198
199
  assert fake.ingest_calls[0]["request_id"] is None
200
+ assert fake.ingest_calls[0]["api_key"] is None
199
201
  assert fake.ingest_calls[0]["status_code"] == 401
200
202
  assert fake.ingest_calls[0]["path"] == "/protected"
201
203
 
@@ -217,6 +219,7 @@ def test_django_rate_limit_preserves_retry_after_and_skips_view() -> None:
217
219
  assert json.loads(response.content)["error"] == "rate_limited"
218
220
  assert fake.ingest_calls[0]["request_id"] == "request_limited"
219
221
  assert fake.ingest_calls[0]["status_code"] == 429
222
+ assert fake.ingest_calls[0]["api_key"] == "prod_consumer"
220
223
 
221
224
 
222
225
  def test_django_resolves_consumer_name_and_client_ip() -> None:
@@ -243,6 +246,7 @@ def test_django_resolves_consumer_name_and_client_ip() -> None:
243
246
 
244
247
  assert response.status_code == 200
245
248
  assert fake.ingest_calls[0]["consumer_name"] == "rapidapi-user"
249
+ assert fake.ingest_calls[0]["api_key"] == "prod_consumer"
246
250
  assert fake.ingest_calls[0]["client_ip"] == "203.0.113.30"
247
251
 
248
252
 
@@ -306,7 +310,7 @@ def test_django_reports_validation_and_ingestion_errors() -> None:
306
310
  assert ingest_events[0].request_id == "request_123"
307
311
 
308
312
 
309
- def test_django_query_key_is_redacted_from_analytics() -> None:
313
+ def test_django_query_key_is_redacted_from_path_but_ingested_as_identity() -> None:
310
314
  fake = FakeSyncReqKey(allowed_result())
311
315
 
312
316
  with override_settings(
@@ -327,7 +331,7 @@ def test_django_query_key_is_redacted_from_analytics() -> None:
327
331
  event = fake.ingest_calls[0]
328
332
  assert event["query_params"] == {"page": "2"}
329
333
  assert event["path"] == "/protected?page=2"
330
- assert "prod_secret" not in str(event)
334
+ assert event["api_key"] == "prod_secret"
331
335
 
332
336
 
333
337
  def test_django_fail_open_exposes_error_and_continues() -> None:
@@ -371,6 +375,7 @@ async def test_django_async_middleware_uses_async_client_without_adaptation() ->
371
375
  assert response.status_code == 200
372
376
  assert fake.verify_calls[0]["key"] == "prod_consumer"
373
377
  assert fake.ingest_calls[0]["request_id"] == "request_123"
378
+ assert fake.ingest_calls[0]["api_key"] == "prod_consumer"
374
379
 
375
380
 
376
381
  @pytest.mark.asyncio
@@ -410,6 +415,7 @@ async def test_django_async_denial_uses_async_resolvers_and_ingests() -> None:
410
415
  assert fake.ingest_calls[0]["request_id"] == "request_limited"
411
416
  assert fake.ingest_calls[0]["status_code"] == 429
412
417
  assert fake.ingest_calls[0]["consumer_name"] == "rapidapi-user"
418
+ assert fake.ingest_calls[0]["api_key"] == "prod_consumer"
413
419
  assert fake.ingest_calls[0]["client_ip"] == "203.0.113.40"
414
420
 
415
421
 
@@ -449,3 +455,4 @@ def test_django_ingest_only_mode_does_not_require_consumer_key() -> None:
449
455
  assert response.status_code == 200
450
456
  assert fake.verify_calls == []
451
457
  assert fake.ingest_calls[0]["api_id"] == "payments"
458
+ assert fake.ingest_calls[0]["api_key"] is None
@@ -129,6 +129,7 @@ def test_both_mode_gates_then_records_correlated_metadata() -> None:
129
129
  ]
130
130
  assert fake.ingest_calls[0]["request_id"] == "request_123"
131
131
  assert fake.ingest_calls[0]["api_id"] == "payments"
132
+ assert fake.ingest_calls[0]["api_key"] == "prod_consumer"
132
133
  assert fake.ingest_calls[0]["path"] == "/protected"
133
134
  assert fake.ingest_calls[0]["query_params"] is None
134
135
 
@@ -152,6 +153,7 @@ def test_ingest_mode_does_not_require_or_validate_a_consumer_key() -> None:
152
153
  assert fake.verify_calls == []
153
154
  assert fake.ingest_calls[0]["request_id"] is None
154
155
  assert fake.ingest_calls[0]["api_id"] == "payments"
156
+ assert fake.ingest_calls[0]["api_key"] is None
155
157
 
156
158
 
157
159
  def test_missing_key_is_denied_before_handler() -> None:
@@ -163,6 +165,7 @@ def test_missing_key_is_denied_before_handler() -> None:
163
165
  assert response.json()["error"] == "missing_api_key"
164
166
  assert fake.verify_calls == []
165
167
  assert fake.ingest_calls[0]["request_id"] is None
168
+ assert fake.ingest_calls[0]["api_key"] is None
166
169
  assert fake.ingest_calls[0]["status_code"] == 401
167
170
  assert fake.ingest_calls[0]["path"] == "/protected"
168
171
 
@@ -198,6 +201,7 @@ def test_rate_limit_preserves_retry_after() -> None:
198
201
  assert response.status_code == 429
199
202
  assert response.headers["Retry-After"] == "4"
200
203
  assert fake.ingest_calls[0]["status_code"] == 429
204
+ assert fake.ingest_calls[0]["api_key"] == "prod_consumer"
201
205
 
202
206
 
203
207
  def test_denied_ingestion_can_be_disabled() -> None:
@@ -268,7 +272,7 @@ def test_query_parameter_key_and_custom_header_name() -> None:
268
272
  assert fake.verify_calls[0]["key"] == "prod_consumer"
269
273
 
270
274
 
271
- def test_query_parameter_consumer_key_is_never_ingested() -> None:
275
+ def test_query_parameter_consumer_key_is_only_ingested_as_identity() -> None:
272
276
  fake = FakeReqKey(allowed_result())
273
277
  with TestClient(
274
278
  make_app(
@@ -283,7 +287,7 @@ def test_query_parameter_consumer_key_is_never_ingested() -> None:
283
287
  assert response.status_code == 200
284
288
  assert fake.ingest_calls[0]["query_params"] == {"page": "2"}
285
289
  assert fake.ingest_calls[0]["path"] == "/protected?page=2"
286
- assert "prod_secret" not in str(fake.ingest_calls[0])
290
+ assert fake.ingest_calls[0]["api_key"] == "prod_secret"
287
291
 
288
292
 
289
293
  def test_capture_options_redact_consumer_key_and_capture_response() -> None:
@@ -308,6 +312,7 @@ def test_capture_options_redact_consumer_key_and_capture_response() -> None:
308
312
  assert event["path"] == "/protected?page=2"
309
313
  assert event["request_headers"]["x-safe"] == "visible"
310
314
  assert "x-api-key" not in event["request_headers"]
315
+ assert event["api_key"] == "prod_consumer"
311
316
  assert event["response_headers"]["content-type"] == "application/json"
312
317
  assert json.loads(event["response_body"]) == {
313
318
  "ok": True,
@@ -336,6 +341,7 @@ def test_consumer_name_and_client_ip_resolvers_are_recorded() -> None:
336
341
 
337
342
  assert response.status_code == 200
338
343
  assert fake.ingest_calls[0]["consumer_name"] == "rapidapi-user"
344
+ assert fake.ingest_calls[0]["api_key"] == "prod_consumer"
339
345
  assert fake.ingest_calls[0]["client_ip"] == "203.0.113.10"
340
346
 
341
347
 
@@ -183,6 +183,7 @@ def test_wsgi_both_mode_validates_and_records_correlated_metadata() -> None:
183
183
  ]
184
184
  event = fake.ingest_calls[0]
185
185
  assert event["request_id"] == "request_123"
186
+ assert event["api_key"] == "prod_consumer"
186
187
  assert event["path"] == "/protected?page=2"
187
188
  assert event["query_params"] == {"page": "2"}
188
189
  assert event["client_ip"] == "127.0.0.1"
@@ -201,6 +202,7 @@ def test_wsgi_missing_key_is_denied_before_application() -> None:
201
202
  assert json.loads(body)["error"] == "missing_api_key"
202
203
  assert fake.verify_calls == []
203
204
  assert fake.ingest_calls[0]["request_id"] is None
205
+ assert fake.ingest_calls[0]["api_key"] is None
204
206
  assert fake.ingest_calls[0]["status_code"] == 401
205
207
  assert fake.ingest_calls[0]["path"] == "/protected"
206
208
 
@@ -217,6 +219,7 @@ def test_wsgi_rate_limit_preserves_retry_after_and_skips_application() -> None:
217
219
  assert json.loads(body)["error"] == "rate_limited"
218
220
  assert fake.ingest_calls[0]["request_id"] == "request_limited"
219
221
  assert fake.ingest_calls[0]["status_code"] == 429
222
+ assert fake.ingest_calls[0]["api_key"] == "prod_consumer"
220
223
 
221
224
 
222
225
  def test_wsgi_denied_ingestion_can_be_disabled() -> None:
@@ -259,6 +262,7 @@ def test_wsgi_resolves_consumer_name_and_client_ip() -> None:
259
262
 
260
263
  assert status == 200
261
264
  assert fake.ingest_calls[0]["consumer_name"] == "rapidapi-user"
265
+ assert fake.ingest_calls[0]["api_key"] == "prod_consumer"
262
266
  assert fake.ingest_calls[0]["client_ip"] == "203.0.113.10"
263
267
 
264
268
 
@@ -322,7 +326,7 @@ def test_wsgi_reports_validation_and_ingestion_errors() -> None:
322
326
  assert ingest_events[0].request_id == "request_123"
323
327
 
324
328
 
325
- def test_wsgi_query_key_is_validated_but_never_ingested() -> None:
329
+ def test_wsgi_query_key_is_redacted_from_path_but_ingested_as_identity() -> None:
326
330
  fake = FakeReqKey(allowed_result())
327
331
  middleware = ReqKeyMiddleware(
328
332
  app,
@@ -343,7 +347,7 @@ def test_wsgi_query_key_is_validated_but_never_ingested() -> None:
343
347
  event = fake.ingest_calls[0]
344
348
  assert event["query_params"] == {"page": "2"}
345
349
  assert event["path"] == "/protected?page=2"
346
- assert "prod_secret" not in str(event)
350
+ assert event["api_key"] == "prod_secret"
347
351
 
348
352
 
349
353
  def test_wsgi_failure_mode_open_runs_application_and_records_error() -> None:
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes