Skip to content

Commit 242ef0c

Browse files
fix: extract LinkedIn activity URN from tracking scope for valid post URLs
The "View" button was generating invalid URLs using componentkey IDs instead of numeric activity URNs. LinkedIn now stores the actual URN in data-view-tracking-scope as encoded JSON byte arrays. - Add extractActivityUrn() to parse tracking scope data - Add parseTrackingScope() to decode byte array to JSON - Use activity URN for URL generation when available - Fix console.debug format string patterns
1 parent bed0015 commit 242ef0c

4 files changed

Lines changed: 89 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [1.1.3] - 2026-02-04
11+
12+
### Fixed
13+
14+
- Fixed "View" button generating invalid LinkedIn URLs (was using componentkey IDs instead of activity URNs)
15+
- Post URLs now correctly extracted from LinkedIn's tracking scope data for proper deep linking
16+
1017
## [1.1.2] - 2026-02-04
1118

1219
### Fixed
@@ -68,7 +75,8 @@ Initial release of ReplyQueue, a Chrome extension that helps content creators fi
6875
- Reply suggestion generation with customizable writing style
6976
- Side panel UI with post cards, filtering, and settings
7077

71-
[Unreleased]: https://github.com/charlesjones-dev/replyqueue/compare/v1.1.2...HEAD
78+
[Unreleased]: https://github.com/charlesjones-dev/replyqueue/compare/v1.1.3...HEAD
79+
[1.1.3]: https://github.com/charlesjones-dev/replyqueue/compare/v1.1.2...v1.1.3
7280
[1.1.2]: https://github.com/charlesjones-dev/replyqueue/compare/v1.1.1...v1.1.2
7381
[1.1.1]: https://github.com/charlesjones-dev/replyqueue/compare/v1.1.0...v1.1.1
7482
[1.1.0]: https://github.com/charlesjones-dev/replyqueue/compare/v1.0.0...v1.1.0

manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"manifest_version": 3,
33
"name": "ReplyQueue",
44
"description": "Side panel for finding social media posts relevant to your blog content and get AI-powered reply suggestions.",
5-
"version": "1.1.2",
5+
"version": "1.1.3",
66
"icons": {
77
"16": "icons/icon16.png",
88
"48": "icons/icon48.png",

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "replyqueue",
3-
"version": "1.1.2",
3+
"version": "1.1.3",
44
"type": "module",
55
"homepage": "https://chromewebstore.google.com/detail/replyqueue/lkdecdgjijicaehjngnehdhoahjipnpg",
66
"scripts": {

src/platforms/linkedin/adapter.ts

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -99,45 +99,116 @@ export class LinkedInAdapter implements PlatformAdapter {
9999

100100
/**
101101
* Generate the permalink URL for a post
102+
* If the postId is already a numeric activity ID, use it directly
103+
* Otherwise, it's a componentKey and we can't generate a valid URL
102104
*/
103105
getPostUrl(postId: string): string {
106+
// Check if this is a numeric activity ID
107+
if (/^\d+$/.test(postId)) {
108+
return `${linkedInUrlPatterns.postPermalink}urn:li:activity:${postId}`;
109+
}
110+
// For componentKey-based IDs, return a search URL as fallback
111+
// The actual URL will be set during extraction if we find the tracking scope
104112
return `${linkedInUrlPatterns.postPermalink}urn:li:activity:${postId}`;
105113
}
106114

115+
/**
116+
* Extract the actual activity URN from tracking scope data
117+
* LinkedIn embeds the URN in a data-view-tracking-scope attribute as encoded JSON
118+
*/
119+
private extractActivityUrn(element: Element): string | null {
120+
// Find the parent with tracking scope data
121+
const trackingParent = element.closest('[data-view-tracking-scope]');
122+
if (!trackingParent) {
123+
// Try checking if the element itself has tracking scope
124+
const parentContainer = element.parentElement?.closest('[data-view-tracking-scope]');
125+
if (!parentContainer) {
126+
return null;
127+
}
128+
return this.parseTrackingScope(parentContainer);
129+
}
130+
return this.parseTrackingScope(trackingParent);
131+
}
132+
133+
/**
134+
* Parse the tracking scope attribute to extract the updateUrn
135+
*/
136+
private parseTrackingScope(element: Element): string | null {
137+
const trackingScope = element.getAttribute('data-view-tracking-scope');
138+
if (!trackingScope) return null;
139+
140+
try {
141+
const parsed = JSON.parse(trackingScope);
142+
if (!Array.isArray(parsed) || parsed.length === 0) return null;
143+
144+
// Look for FeedUpdateServedEvent which contains the post URN
145+
for (const item of parsed) {
146+
if (item.breadcrumb?.content?.data) {
147+
// The data is an array of byte values that form a JSON string
148+
const bytes = item.breadcrumb.content.data;
149+
if (Array.isArray(bytes)) {
150+
const jsonString = bytes.map((b: number) => String.fromCharCode(b)).join('');
151+
try {
152+
const contentData = JSON.parse(jsonString);
153+
if (contentData.updateUrn) {
154+
// Extract the activity ID from "urn:li:activity:1234567890"
155+
const match = contentData.updateUrn.match(/urn:li:activity:(\d+)/);
156+
if (match) {
157+
return match[1];
158+
}
159+
}
160+
} catch {
161+
// JSON parse failed for content data
162+
continue;
163+
}
164+
}
165+
}
166+
}
167+
} catch {
168+
// JSON parse failed for tracking scope
169+
console.debug(LOG_PREFIX, 'Failed to parse tracking scope');
170+
}
171+
return null;
172+
}
173+
107174
/**
108175
* Extract a post from a DOM element
109176
*/
110177
extractPost(element: Element): ExtractedPost | null {
111178
try {
112179
const postId = this.getPostId(element);
113180
if (!postId) {
114-
console.debug(`${LOG_PREFIX} Could not extract post ID from element`);
181+
console.debug(LOG_PREFIX, 'Could not extract post ID from element');
115182
return null;
116183
}
117184

118185
// Check if this is a sponsored post and skip it
119186
if (this.isSponsored(element)) {
120-
console.debug(`${LOG_PREFIX} Skipping sponsored post: ${postId}`);
187+
console.debug(LOG_PREFIX, 'Skipping sponsored post:', postId);
121188
return null;
122189
}
123190

124191
const authorName = this.extractAuthorName(element);
125192
if (!authorName) {
126-
console.debug(`${LOG_PREFIX} Could not extract author name for post: ${postId}`);
193+
console.debug(LOG_PREFIX, 'Could not extract author name for post:', postId);
127194
return null;
128195
}
129196

130197
const content = this.extractPostContent(element);
131198
if (!content) {
132-
console.debug(`${LOG_PREFIX} Could not extract content for post: ${postId}`);
199+
console.debug(LOG_PREFIX, 'Could not extract content for post:', postId);
133200
return null;
134201
}
135202

136203
const isRepost = this.isRepost(element);
137204

205+
// Try to extract the actual activity URN for proper URL generation
206+
const activityUrn = this.extractActivityUrn(element);
207+
const postUrl = activityUrn ? this.getPostUrl(activityUrn) : this.getPostUrl(postId);
208+
138209
const post: ExtractedPost = {
139210
id: postId,
140-
url: this.getPostUrl(postId),
211+
url: postUrl,
141212
authorName,
142213
authorHeadline: this.extractAuthorHeadline(element),
143214
authorProfileUrl: this.extractAuthorProfileUrl(element),
@@ -153,10 +224,10 @@ export class LinkedInAdapter implements PlatformAdapter {
153224
extractedAt: Date.now(),
154225
};
155226

156-
console.debug(`${LOG_PREFIX} Extracted post:`, post.id, post.authorName);
227+
console.debug(LOG_PREFIX, 'Extracted post:', post.id, post.authorName);
157228
return post;
158229
} catch (error) {
159-
console.error(`${LOG_PREFIX} Error extracting post:`, error);
230+
console.error(LOG_PREFIX, 'Error extracting post:', error);
160231
return null;
161232
}
162233
}

0 commit comments

Comments
 (0)