11import json
22import logging
33import re
4+ import socket
45from dataclasses import dataclass
56from datetime import date , datetime
67from gzip import GzipFile
78from io import BytesIO
8- from typing import Any , Optional , Union
9+ from typing import Any , List , Optional , Tuple , Union
10+
911
1012import requests
1113from dateutil .tz import tzutc
14+ from requests .adapters import HTTPAdapter
15+ from urllib3 .connection import HTTPConnection
1216from urllib3 .util .retry import Retry
1317
1418from posthog .utils import remove_trailing_slash
1519from posthog .version import VERSION
1620
21+ SocketOptions = List [Tuple [int , int , Union [int , bytes ]]]
22+
23+ KEEPALIVE_IDLE_SECONDS = 60
24+ KEEPALIVE_INTERVAL_SECONDS = 60
25+ KEEPALIVE_PROBE_COUNT = 3
26+
27+ # TCP keepalive probes idle connections to prevent them from being dropped.
28+ # SO_KEEPALIVE is cross-platform, but timing options vary:
29+ # - Linux: TCP_KEEPIDLE, TCP_KEEPINTVL, TCP_KEEPCNT
30+ # - macOS: only SO_KEEPALIVE (uses system defaults)
31+ # - Windows: TCP_KEEPIDLE, TCP_KEEPINTVL (since Windows 10 1709)
32+ KEEP_ALIVE_SOCKET_OPTIONS : SocketOptions = list (
33+ HTTPConnection .default_socket_options
34+ ) + [
35+ (socket .SOL_SOCKET , socket .SO_KEEPALIVE , 1 ),
36+ ]
37+ for attr , value in [
38+ ("TCP_KEEPIDLE" , KEEPALIVE_IDLE_SECONDS ),
39+ ("TCP_KEEPINTVL" , KEEPALIVE_INTERVAL_SECONDS ),
40+ ("TCP_KEEPCNT" , KEEPALIVE_PROBE_COUNT ),
41+ ]:
42+ if hasattr (socket , attr ):
43+ KEEP_ALIVE_SOCKET_OPTIONS .append ((socket .SOL_TCP , getattr (socket , attr ), value ))
44+
1745
1846def _mask_tokens_in_url (url : str ) -> str :
1947 """Mask token values in URLs for safe logging, keeping first 10 chars visible."""
@@ -29,17 +57,53 @@ class GetResponse:
2957 not_modified : bool = False
3058
3159
32- # Retry on both connect and read errors
33- # by default read errors will only retry idempotent HTTP methods (so not POST)
34- adapter = requests .adapters .HTTPAdapter (
35- max_retries = Retry (
36- total = 2 ,
37- connect = 2 ,
38- read = 2 ,
60+ class HTTPAdapterWithSocketOptions (HTTPAdapter ):
61+ """HTTPAdapter with configurable socket options."""
62+
63+ def __init__ (self , * args , socket_options : Optional [SocketOptions ] = None , ** kwargs ):
64+ self .socket_options = socket_options
65+ super ().__init__ (* args , ** kwargs )
66+
67+ def init_poolmanager (self , * args , ** kwargs ):
68+ if self .socket_options is not None :
69+ kwargs ["socket_options" ] = self .socket_options
70+ super ().init_poolmanager (* args , ** kwargs )
71+
72+
73+ def _build_session (socket_options : Optional [SocketOptions ] = None ) -> requests .Session :
74+ adapter = HTTPAdapterWithSocketOptions (
75+ max_retries = Retry (
76+ total = 2 ,
77+ connect = 2 ,
78+ read = 2 ,
79+ ),
80+ socket_options = socket_options ,
3981 )
40- )
41- _session = requests .sessions .Session ()
42- _session .mount ("https://" , adapter )
82+ session = requests .sessions .Session ()
83+ session .mount ("https://" , adapter )
84+ return session
85+
86+
87+ # Default session uses urllib3 defaults (no custom socket options)
88+ _session = _build_session ()
89+
90+
91+ def set_socket_options (socket_options : Optional [SocketOptions ]) -> None :
92+ """
93+ Configure socket options for all HTTP connections.
94+
95+ Example:
96+ from posthog import set_socket_options
97+ set_socket_options([(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)])
98+ """
99+ global _session
100+ _session = _build_session (socket_options )
101+
102+
103+ def enable_keep_alive () -> None :
104+ """Enable TCP keepalive to prevent idle connections from being dropped."""
105+ set_socket_options (KEEP_ALIVE_SOCKET_OPTIONS )
106+
43107
44108US_INGESTION_ENDPOINT = "https://us.i.posthog.com"
45109EU_INGESTION_ENDPOINT = "https://eu.i.posthog.com"
0 commit comments