{"record":{"id":"0a3122b92eb605d7","repo":"zeroclaw-labs/zeroclaw","slug":"linkedin-get-engagement-failed","errorCode":null,"errorMessage":"LinkedIn get_engagement failed ({}): {}","messagePattern":"LinkedIn get_engagement failed \\((.+?)\\): (.+?)","errorType":"http","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/zeroclaw-tools/src/linkedin_client.rs","lineNumber":457,"sourceCode":"            let body_text = response.text().await.unwrap_or_default();\n            anyhow::bail!(\"LinkedIn delete_post failed ({}): {}\", status, body_text);\n        }\n\n        Ok(())\n    }\n\n    pub async fn get_engagement(&self, post_id: &str) -> anyhow::Result<EngagementSummary> {\n        let creds = self.get_credentials().await?;\n        let url = format!(\"{}/rest/socialActions/{}\", LINKEDIN_API_BASE, post_id);\n\n        let response = self\n            .api_request(Method::GET, &url, &creds.access_token, None)\n            .await?;\n\n        let status = response.status();\n        if !status.is_success() {\n            let body_text = response.text().await.unwrap_or_default();\n            anyhow::bail!(\"LinkedIn get_engagement failed ({}): {}\", status, body_text);\n        }\n\n        let json: serde_json::Value = response\n            .json()\n            .await\n            .context(\"Failed to parse get_engagement response\")?;\n\n        let likes = json\n            .get(\"likesSummary\")\n            .and_then(|v| v.get(\"totalLikes\"))\n            .and_then(|v| v.as_u64())\n            .unwrap_or(0);\n\n        let comments = json\n            .get(\"commentsSummary\")\n            .and_then(|v| v.get(\"totalFirstLevelComments\"))\n            .and_then(|v| v.as_u64())\n            .unwrap_or(0);","sourceCodeStart":439,"sourceCodeEnd":475,"githubUrl":"https://github.com/zeroclaw-labs/zeroclaw/blob/88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc/crates/zeroclaw-tools/src/linkedin_client.rs#L439-L475","documentation":"Thrown when GET of a post's engagement statistics returns non-2xx, body included. Engagement endpoints are restricted: tokens typically need the correct product/scope, and organizational analytics require additional permissions. The parsed JSON is then shaped into an EngagementSummary, so shape problems surface after this check.","triggerScenarios":"Fetching engagement for a post the token's member does not own; missing analytics scope (403); expired token (401); deleted post (404); using share statistics endpoints on ugcPost URNs or vice versa.","commonSituations":"Dashboards polling metrics with tokens granted only for posting; URN kind mismatches after LinkedIn's share/ugcPost migration; rate limits on frequent polling.","solutions":["For 403, request the analytics product/scope for the app and re-authorize.","Ensure the URN matches the post type you created (share vs ugcPost vs posts API URN).","For 401, refresh the token; for 429, lengthen the polling interval.","Only query posts owned by the authorized member or organization."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"match client.get_engagement(&token, &post_urn).await {\n    Ok(stats) => record(stats),\n    Err(e) if e.to_string().contains(\"(403\") => { /* analytics not granted; degrade silently or alert once */ }\n    Err(e) if e.to_string().contains(\"(429\") => { /* back off polling */ }\n    Err(e) => return Err(e),\n}","preventionTips":["Poll engagement at a low fixed cadence to avoid 429s.","Request analytics products/scopes up front if dashboards need them.","Store the URN kind (share vs ugcPost) with the post so stats queries use the right one."],"tags":["linkedin","http-status","engagement","analytics"],"backgroundTag":"http-error-response","analyzedSha":"88bb9c8533fc57ed7a03e36ca7c9ed2bf8336dcc","analyzedAt":"2026-08-23T01:07:41.857Z","schemaVersion":2},"datasetVersion":"2026-08-23T08:06:27.607Z"}