Setting the file. One moment.
Client · Operate On AWS · aws/agent-toolkit-for-aws · Skills Docs
ContentsBack to the top of the page 93
Creating Amazon Aurora Db Cluster With Instances
Shieldadvanced
Resilience Program Design
Connecting Lambda To DynamoDB
Number 32.16
Position 16 of 17
Type Python
Size 11 KB
Lines 291 references/vendored/telemetry/metric_emission/ client.py
Python · 291 lines · 11 KB
16
17 Fire and forget. `post_event` swallows everything and reports failure to nobody: a
18 telemetry POST is not something the user asked for and must not be able to fail an
19 operation they did ask for. Every status is treated the same way — a 403 or 400 is
20 dropped exactly like a timeout, so nothing here depends on which the service sends.
21
22 The consent gate is re-checked in `post_event`, so a new call site cannot emit by
23 forgetting to ask.
24 """
25
26 import json
27 import os
28 import socket
29 import sys
30 import time
31 import urllib.error
32 import urllib.request
33 from pathlib import Path
34
35 _TELEMETRY = Path( __file__ ).resolve().parents[ 1 ]
36 sys.path[: 0 ] = [ str ( _TELEMETRY / "consent" ), str ( _TELEMETRY / "metric_emission" )]
37
38 import record # noqa: E402 (paths set above so this resolves in-plugin)
39
40 def _plugin_root ():
41 """Recognize canonical/plugin-bundled layouts; never treat references/ as a plugin."""
42 if _TELEMETRY .parent.name == "scripts" :
43 return _TELEMETRY .parent.parent
44 if ( len ( _TELEMETRY .parents) > 4
45 and _TELEMETRY .parent.name == "vendored"
46 and _TELEMETRY .parents[ 1 ].name == "references"
47 and _TELEMETRY .parents[ 3 ].name == "skills" ):
48 return _TELEMETRY .parents[ 4 ]
49 return None
50
51
52 PLUGIN_ROOT = _plugin_root()
53
54 # Prod is the only stage a customer should reach; beta and gamma are the service
55 # team's, which is what the override is for. It chooses where an event goes, never
56 # whether one is sent.
57 DEFAULT_ENDPOINT = (
58 "https://us-east-1.prod.startup-advisor-extension.saws.activate.aws.dev"
59 "/v1/plugin-telemetry-event"
60 )
61 ENDPOINT_ENV = "AWS_STARTUP_ADVISOR_PLUGIN_TELEMETRY_ENDPOINT"
62
63 # Short, because these run inline in an agent turn with a human waiting.
64 TIMEOUT_SECONDS = 3.0
65
66 # post_event_status's answer when no connection could be made at all (offline,
67 # DNS, refused): unlike a timeout, nothing can have reached the service.
68 UNREACHABLE = 0
69
70 USER_AGENT = "aws-startup-advisor-plugin"
71
72 SOURCE_CLAUDE_CODE = "CLAUDE_CODE"
73 SOURCE_CODEX = "CODEX"
74 SOURCE_CURSOR = "CURSOR"
75 SOURCE_KIRO = "KIRO"
76 SOURCE_OTHER = "OTHER"
77
78 # First marker present wins. Claude Code and Cursor Agent CLI markers have been
79 # verified against running hosts; an unmatched host reports OTHER rather than a plausible guess,
80 # because a wrong attribution silently moves one host's numbers into another's.
81 # TODO (StartupEngBlend-3621): confirm the other three.
82 # Cursor's hook-specific markers precede Claude compatibility variables.
83 # CURSOR_AGENT can be inherited by a nested CLI, so explicit Codex/Kiro markers
84 # precede that weaker marker.
85 _HOST_MARKERS = (
86 ( "CURSOR_VERSION" , SOURCE_CURSOR ), # verified in Cursor's hook environment
87 ( "CURSOR_PROJECT_DIR" , SOURCE_CURSOR ), # verified in Cursor's hook environment
88 ( "CURSOR_TRACE_ID" , SOURCE_CURSOR ), # unverified
89 ( "CODEX_SANDBOX" , SOURCE_CODEX ), # unverified
90 ( "KIRO_IDE" , SOURCE_KIRO ), # unverified
91 ( "CURSOR_AGENT" , SOURCE_CURSOR ), # verified in Cursor Agent CLI
92 ( "CLAUDECODE" , SOURCE_CLAUDE_CODE ), # verified
93 ( "CLAUDE_CODE_ENTRYPOINT" , SOURCE_CLAUDE_CODE ), # verified
94 )
95
96 # The PluginSkillId enum, mirrored from model/types/plugin-telemetry.smithy. An
97 # unmodelled member is a 400 for the whole request, so it is checked here, last,
98 # before anything goes on the wire.
99 PLUGIN_SKILL_IDS = frozenset (
100 {
101 "ARCHITECT_FOR_STARTUPS" ,
102 "KNOWLEDGE_BASE_FOR_STARTUPS" ,
103 "PROMPT_LIBRARY_FOR_STARTUPS" ,
104 "START_BUILDING_FOR_STARTUPS" ,
105 "AZURE_TO_AWS" ,
106 "GCP_TO_AWS" ,
107 "HEROKU_TO_AWS" ,
108 "LLM_TO_BEDROCK" ,
109 "AGENT_ADVISOR" ,
110 "TF_BEST_PRACTICES" ,
111 "CONTEXTUAL_OFFERS_FOR_STARTUPS" ,
112 "OPERATE_ON_AWS" ,
113 }
114 )
115
116 EVENT_SKILL_INVOKED = "SKILL_INVOKED"
117
118 # A bundle reports its own generated version. Only recognized plugin layouts
119 # may fall back to plugin manifests; unrelated references/plugin.json is ignored.
120 MANIFEST_PATHS = ( _TELEMETRY / "version.json" ,)
121 if PLUGIN_ROOT is not None :
122 MANIFEST_PATHS += (
123 PLUGIN_ROOT / ".claude-plugin" / "plugin.json" ,
124 PLUGIN_ROOT / ".cursor-plugin" / "plugin.json" ,
125 PLUGIN_ROOT / "plugin.json" ,
126 )
127
128
129 def endpoint ():
130 """The URL to POST to, honoring the override."""
131 return os.environ.get( ENDPOINT_ENV , "" ).strip() or DEFAULT_ENDPOINT
132
133
134 def detect_source ():
135 """Best-effort PluginSource for the host, or OTHER."""
136 for name, source in _HOST_MARKERS :
137 if os.environ.get(name, "" ).strip():
138 return source
139 return SOURCE_OTHER
140
141
142 def migration_reporting_mode ():
143 """Use configured plugin hooks when reachable, otherwise the bundled CLI.
144
145 This checks the install layout, not whether a host enabled or ran its hooks.
146 """
147 source = detect_source()
148 manifest_name = {
149 SOURCE_CLAUDE_CODE : ".claude-plugin/plugin.json" ,
150 SOURCE_CURSOR : ".cursor-plugin/plugin.json" ,
151 }.get(source)
152 if not manifest_name or PLUGIN_ROOT is None :
153 return "cli"
154 try :
155 manifest = json.loads(( PLUGIN_ROOT / manifest_name).read_text( encoding = "utf-8" ))
156 if manifest.get( "name" ) != "aws-startup-advisor" :
157 return "cli"
158 hook_file = ( PLUGIN_ROOT / manifest[ "hooks" ]).resolve()
159 hook_file.relative_to( PLUGIN_ROOT .resolve())
160 hooks = json.loads(hook_file.read_text( encoding = "utf-8" ))[ "hooks" ]
161 script = "scripts/telemetry/metric_emission/migration.py"
162 if not ( PLUGIN_ROOT / script).is_file():
163 return "cli"
164 events = ( "PostToolUse" , "Stop" ) if source == SOURCE_CLAUDE_CODE else (
165 "afterFileEdit" , "afterShellExecution" , "stop" ,
166 )
167 for event in events:
168 for entry in hooks.get(event, []):
169 commands = entry.get( "hooks" , []) if source == SOURCE_CLAUDE_CODE else [entry]
170 if any (script in command.get( "command" , "" ) for command in commands):
171 return "hook"
172 except ( OSError , ValueError , KeyError , TypeError , AttributeError ):
173 pass
174 return "cli"
175
176
177 def plugin_version ():
178 """The plugin's version, or None.
179
180 None is a refusal to send, not a default: a made-up "0.0.0" would pass the
181 semver pattern and misattribute the event to a version that does not exist.
182 """
183 for manifest in MANIFEST_PATHS :
184 try :
185 with open (manifest, "r" , encoding = "utf-8" ) as handle:
186 version = json.load(handle).get( "version" )
187 except ( OSError , ValueError ):
188 continue
189 if isinstance (version, str ) and version.strip():
190 return version.strip()
191 return None
192
193
194 def build_payload (event, install_id, now_ms = None ):
195 """The request body, or None if a required field is missing."""
196 version = plugin_version()
197 if not install_id or not version:
198 return None
199
200 return {
201 "installId" : install_id,
202 "source" : detect_source(),
203 "pluginVersion" : version,
204 # The service drops timestamps more than five minutes ahead, silently and
205 # with a 200, so a fast clock loses its telemetry with no signal here.
206 "occurredAt" : int ((now_ms if now_ms is not None else time.time() * 1000 )),
207 "pluginTelemetryEvent" : event,
208 }
209
210
211 def post_event_status (event, install_id, url, timeout = TIMEOUT_SECONDS ):
212 """POST one event and report the HTTP status; UNREACHABLE when no connection
213 could be made; None when nothing was sent or the outcome is unknown (a
214 timeout, a connection dropped mid-request); never raises.
215
216 `install_id` and `url` are passed in so a caller that has already read and
217 validated the record is not making this re-derive it. The status lets a
218 caller tell a refusal it may retry (403, 429, 5xx) from one it may not (400).
219
220 Everything is inside the try, not just the request: a malformed URL, an
221 unserializable event or an unreadable manifest must fail the same silent way
222 an unreachable network does.
223 """
224 try :
225 if not record.is_accepted():
226 return None
227
228 payload = build_payload(event, install_id)
229 if payload is None :
230 return None
231
232 request = urllib.request.Request(
233 url,
234 data = json.dumps(payload).encode( "utf-8" ),
235 headers = { "Content-Type" : "application/json" , "User-Agent" : USER_AGENT },
236 method = "POST" ,
237 )
238 try :
239 with urllib.request.urlopen(request, timeout = timeout) as response:
240 return response.status
241 except urllib.error.HTTPError as error:
242 return error.code
243 except urllib.error.URLError as error:
244 # Raised before any response: the connection was never made, unless
245 # the reason is a timeout, which may have struck after the send.
246 if isinstance (error.reason, (socket.timeout, TimeoutError )):
247 return None
248 return UNREACHABLE
249 except BaseException :
250 return None
251
252
253 def post_event (event, install_id, url):
254 """POST one event. True only if the service accepted it; never raises."""
255 status = post_event_status(event, install_id, url)
256 return status is not None and 200 <= status < 300
257
258
259 def send_event (event):
260 """POST one event, reading the install ID and endpoint for the caller."""
261 # .get: an opted-out record is valid without an installId, and post_event
262 # re-checks consent anyway, so None here simply means nothing is sent.
263 state = record.read_state() or {}
264 return post_event(event, state.get( "installId" ), endpoint())
265
266
267 def emit_skill_invocation_metric (install_id, skill_id, url):
268 """Report that `skill_id` was invoked.
269
270 A skill id that is not a PluginSkillId member is dropped here rather than
271 sent. The caller maps a host's skill name onto the enum and may legitimately
272 have no match, so that is an ordinary outcome, not an error.
273 """
274 if skill_id not in PLUGIN_SKILL_IDS :
275 return False
276
277 return post_event(
278 { "skillInvoked" : { "skill" : skill_id, "eventName" : EVENT_SKILL_INVOKED }},
279 install_id,
280 url,
281 )
282
283
284 def send_consent_recorded ():
285 """Report that consent was recorded. Only ever sent on acknowledgement.
286
287 `ConsentRecordedDetails` is empty and cannot say which way the user answered,
288 so a request from `opt_out.py` would be indistinguishable from a yes — besides
289 being a request made on behalf of someone who just said no.
290 """
291 return send_event({ "consentRecorded" : {}})