-
Notifications
You must be signed in to change notification settings - Fork 727
Serp linkedin finder enrichment source #2664
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
2976359
e0f8860
604e9a8
d4f4611
0cf254b
b7ce392
9b7d434
93c416b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,133 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import axios from 'axios' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { Logger, LoggerBase } from '@crowd/logging' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { MemberEnrichmentSource, MemberIdentityType, PlatformType } from '@crowd/types' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| IEnrichmentService, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| IEnrichmentSourceInput, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| IMemberEnrichmentDataNormalized, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } from '../../types' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import { IMemberEnrichmentDataSerp, IMemberEnrichmentSerpApiResponse } from './types' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default class EnrichmentServiceSerpApi extends LoggerBase implements IEnrichmentService { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public source: MemberEnrichmentSource = MemberEnrichmentSource.SERP | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public platform = `enrichment-${this.source}` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public enrichMembersWithActivityMoreThan = 10 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public enrichableBySql = ` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ("activitySummary".total_count > ${this.enrichMembersWithActivityMoreThan}) AND | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (members."displayName" like '% %') AND | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the idea? People with a space in their name?
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah, people with multiple-word names. Single-word names are not good enough when searching in google. But now I'm thinking, since we're already using other stuff to distinguish (like website, location w/e), maybe we can remove this
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the test results with one-word display names aren't great.
I'm keeping this for now, since it's more important to have more correct answers than more enrichable members |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (members.attributes->'location'->>'default' is not null and members.attributes->'location'->>'default' <> '') AND | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ((members.attributes->'websiteUrl'->>'default' is not null and members.attributes->'websiteUrl'->>'default' <> '') OR | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (mi.verified AND mi.type = 'username' and mi.platform = 'github') OR | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (mi.verified AND mi.type = 'email') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| )` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
epipav marked this conversation as resolved.
Show resolved
Hide resolved
Comment on lines
+19
to
+26
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix undefined SQL alias 'mi' The SQL condition uses the Consider adding the necessary JOIN clause: public enrichableBySql = `
("activitySummary".total_count > ${this.enrichMembersWithActivityMoreThan}) AND
(members."displayName" like '% %') AND
(members.attributes->'location'->>'default' is not null and members.attributes->'location'->>'default' <> '') AND
((members.attributes->'websiteUrl'->>'default' is not null and members.attributes->'websiteUrl'->>'default' <> '') OR
+ EXISTS (SELECT 1 FROM member_identities mi WHERE mi.member_id = members.id AND
(mi.verified AND mi.type = 'username' and mi.platform = 'github') OR
(mi.verified AND mi.type = 'email')
+ )
)`📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // bust cache after 120 days | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| public cacheObsoleteAfterSeconds = 60 * 60 * 24 * 120 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| constructor(public readonly log: Logger) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| super(log) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| isEnrichableBySource(input: IEnrichmentSourceInput): boolean { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const displayNameSplit = input.displayName?.split(' ') | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| displayNameSplit?.length > 1 && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| !!input.location && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ((!!input.email && input.email.verified) || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (!!input.github && input.github.verified) || | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| !!input.website) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| async getData(input: IEnrichmentSourceInput): Promise<IMemberEnrichmentDataSerp | null> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let enriched: IMemberEnrichmentDataSerp = null | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (input.displayName && input.location && input.website) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| enriched = await this.querySerpApi(input.displayName, input.location, input.website) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!enriched && input.displayName && input.location && input.github && input.github.value) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| enriched = await this.querySerpApi(input.displayName, input.location, input.github.value) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!enriched && input.displayName && input.location && input.email && input.email.value) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| enriched = await this.querySerpApi(input.displayName, input.location, input.email.value) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return enriched | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private async querySerpApi( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| displayName: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| location: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| identifier: string, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ): Promise<IMemberEnrichmentDataSerp> { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const config = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| method: 'get', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| url: process.env['CROWD_ENRICHMENT_SERP_API_URL'], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| params: { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| api_key: process.env['CROWD_ENRICHMENT_SERP_API_KEY'], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| q: `"${displayName}" ${location} "${identifier}" site:linkedin.com/in`, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| num: 3, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| engine: 'google', | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const response: IMemberEnrichmentSerpApiResponse = (await axios(config)).data | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add error handling for Axios request to prevent unhandled exceptions The Axios request in Apply this diff to add error handling: try {
const response: IMemberEnrichmentSerpApiResponse = (await axios(config)).data
if (response.search_information.total_results > 0) {
// existing logic
}
return null
+ } catch (error) {
+ this.log.error('Error fetching data from SerpAPI', error)
+ return null
}
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (response.search_information.total_results > 0) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| response.organic_results.length > 0 && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| response.organic_results[0].link && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| !response.search_information.spelling_fix && | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| !response.search_information.spelling_fix_type | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| linkedinUrl: response.organic_results[0].link, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return null | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| normalize(data: IMemberEnrichmentDataSerp): IMemberEnrichmentDataNormalized { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const normalized: IMemberEnrichmentDataNormalized = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| identities: [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| platform: PlatformType.LINKEDIN, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type: MemberIdentityType.USERNAME, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| verified: false, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: this.normalizeLinkedUrl(data.linkedinUrl), | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ], | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return normalized | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+97
to
+109
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Add error handling in normalize method The Add error handling: normalize(data: IMemberEnrichmentDataSerp): IMemberEnrichmentDataNormalized {
+ let normalizedUrl: string;
+ try {
+ normalizedUrl = this.normalizeLinkedUrl(data.linkedinUrl);
+ } catch (error) {
+ this.log.warn('Failed to normalize LinkedIn URL, using original', { url: data.linkedinUrl });
+ normalizedUrl = data.linkedinUrl;
+ }
const normalized: IMemberEnrichmentDataNormalized = {
identities: [
{
platform: PlatformType.LINKEDIN,
type: MemberIdentityType.USERNAME,
verified: false,
- value: this.normalizeLinkedUrl(data.linkedinUrl),
+ value: normalizedUrl,
},
],
}
return normalized
}📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| private normalizeLinkedUrl(url: string): string { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const parsedUrl = new URL(url) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (parsedUrl.hostname.endsWith('linkedin.com')) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parsedUrl.hostname = 'linkedin.com' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| parsedUrl.search = '' | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let path = parsedUrl.pathname | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (path.endsWith('/')) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| path = path.slice(0, -1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return parsedUrl.origin + path | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return url | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } catch (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| this.log.error(`Error while normalizing linkedin url: ${url}`, error) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
epipav marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| throw error | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| /* eslint-disable @typescript-eslint/no-explicit-any */ | ||
| import axios from 'axios' | ||
|
|
||
| // import { writeFileSync } from 'fs' | ||
| // import { Parser } from 'json2csv' | ||
| import { timeout } from '@crowd/common' | ||
|
|
||
|
Comment on lines
+1
to
+7
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider removing eslint-disable directive. The eslint-disable directive for -/* eslint-disable @typescript-eslint/no-explicit-any */
+interface Member {
+ displayName: string;
+ location: string;
+ website: string;
+ linkedinFromSerp?: string;
+}
|
||
| const testSerpApi = async () => { | ||
| const members = [] as any[] | ||
|
|
||
| for (const mem of members) { | ||
|
Comment on lines
+8
to
+11
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Convert script to proper test suite. This appears to be a manual testing script rather than an automated test suite. Consider restructuring using a testing framework like Jest: -const testSerpApi = async () => {
+describe('SERP API LinkedIn Finder', () => {
+ it('should find LinkedIn profiles for members', async () => {
+ const members: Member[] = [];Would you like me to help create a proper test suite with mocked API responses and test cases?
|
||
| const url = `https://serpapi.com/search.json` | ||
| const config = { | ||
| method: 'get', | ||
| url, | ||
| params: { | ||
| api_key: process.env['CROWD_SERP_API_KEY'], | ||
| q: `"${mem.displayName}" ${mem.location} "${mem.website}" site:linkedin.com/in`, | ||
| num: 3, | ||
| engine: 'google', | ||
| }, | ||
|
Comment on lines
+17
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Secure API key handling and input validation needed. Several security and robustness concerns:
- api_key: process.env['CROWD_SERP_API_KEY'],
- q: `"${mem.displayName}" ${mem.location} "${mem.website}" site:linkedin.com/in`,
+ api_key: getRequiredEnvVar('CROWD_SERP_API_KEY'),
+ q: `"${escapeSearchString(mem.displayName)}" ${escapeSearchString(mem.location)} "${escapeSearchString(mem.website)}" site:linkedin.com/in`,
|
||
| } | ||
|
|
||
| const response = (await axios(config)).data | ||
|
|
||
| if (response.search_information.total_results > 0) { | ||
| if ( | ||
| response.organic_results.length > 0 && | ||
| response.organic_results[0].link && | ||
| !response.search_information.spelling_fix && | ||
| !response.search_information.spelling_fix_type | ||
| ) { | ||
| console.log(`Found LinkedIn for ${mem.displayName}: ${response.organic_results[0].link}`) | ||
| console.log(response.search_information) | ||
| mem.linkedinFromSerp = response.organic_results[0].link | ||
| } else { | ||
| console.log(`No LinkedIn found for ${mem.displayName}`) | ||
| } | ||
| } else { | ||
| console.log(`No LinkedIn found for ${mem.displayName}`) | ||
| } | ||
|
|
||
| await timeout(1000) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Consider implementing proper rate limiting. Using a fixed timeout isn't the best approach for rate limiting. Consider implementing a proper rate limiter that respects API limits. - await timeout(1000)
+ await rateLimiter.wait() // Implement a proper rate limiter class
|
||
| } | ||
|
Comment on lines
+24
to
+44
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Improve error handling and response validation. The response handling lacks proper error handling and validation:
- const response = (await axios(config)).data
+ try {
+ const response = await axios(config)
+ const data = validateSerpResponse(response.data)
+
+ if (!data.search_information?.total_results) {
+ logger.info(`No LinkedIn found for ${mem.displayName}`)
+ continue
+ }
+
+ const linkedInProfile = extractLinkedInProfile(data.organic_results)
+ if (linkedInProfile) {
+ logger.info(`Found LinkedIn for ${mem.displayName}: ${linkedInProfile}`)
+ mem.linkedinFromSerp = linkedInProfile
+ } else {
+ logger.info(`No valid LinkedIn profile found for ${mem.displayName}`)
+ }
+ } catch (error) {
+ logger.error(`Error fetching data for ${mem.displayName}:`, error)
+ }
|
||
|
|
||
| try { | ||
| // const fields = [ | ||
| // 'id', | ||
| // 'displayName', | ||
| // 'location', | ||
| // 'profileUrl', | ||
| // 'website', | ||
| // 'linkedinFromClearbit', | ||
| // 'linkedinFromProgai', | ||
| // 'linkedinFromSerp', | ||
| // ] | ||
| // const json2csvParser = new Parser({ fields }) | ||
| // const csv = json2csvParser.parse(members) | ||
| // writeFileSync('output.csv', csv) | ||
| // console.log('CSV file has been successfully written.') | ||
| } catch (err) { | ||
| console.error('Error writing CSV file:', err) | ||
| } | ||
|
Comment on lines
+46
to
+63
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Remove or implement CSV export functionality. The commented-out CSV export code creates confusion. Either implement it properly or remove it. The try-catch block is currently unreachable as noted by static analysis. If CSV export is needed, consider moving it to a separate utility function: async function exportToCSV(members: Member[], outputPath: string): Promise<void> {
const fields = ['id', 'displayName', /* ... */];
const parser = new Parser({ fields });
await writeFile(outputPath, parser.parse(members));
}🧰 Tools🪛 Biome[error] 61-63: This code is unreachable (lint/correctness/noUnreachable) |
||
| } | ||
|
|
||
| setImmediate(async () => { | ||
| await testSerpApi() | ||
| process.exit(0) | ||
| }) | ||
|
Comment on lines
+66
to
+69
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Avoid using setImmediate for test execution. Using setImmediate and process.exit() is not a proper way to run tests. This should be restructured to use a proper test runner. -setImmediate(async () => {
- await testSerpApi()
- process.exit(0)
-})
+if (require.main === module) {
+ describe('SERP API Tests', () => {
+ // ... test cases
+ });
+}
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| export interface IMemberEnrichmentDataSerp { | ||
| linkedinUrl: string | ||
| } | ||
epipav marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| export interface IMemberEnrichmentSerpApiResponse { | ||
| organic_results: IMemberEnrichmentSerpApiResponseOrganicResult[] | ||
| search_information: IMemberEnrichmentSerpApiResponseSearchInformation | ||
| } | ||
epipav marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| export interface IMemberEnrichmentSerpApiResponseSearchInformation { | ||
| query_displayed: string | ||
| total_results: number | ||
| time_taken_displayed: number | ||
| organic_results_state: string | ||
| spelling_fix?: string | ||
| spelling_fix_type?: string | ||
| } | ||
epipav marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| export interface IMemberEnrichmentSerpApiResponseOrganicResult { | ||
| position: number | ||
| title: string | ||
| link: string | ||
| redirect_link: string | ||
| displayed_link: string | ||
| favicon: string | ||
| snippet: string | ||
| snippet_highlighted_words: string[] | ||
| sitelinks: { | ||
| inline: IMemberEnrichmentSerpApiResponseOrganicResultSitelinkInline[] | ||
| } | ||
| source: string | ||
| } | ||
epipav marked this conversation as resolved.
Show resolved
Hide resolved
epipav marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| export interface IMemberEnrichmentSerpApiResponseOrganicResultSitelinkInline { | ||
| title: string | ||
| link: string | ||
| } | ||
epipav marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Uh oh!
There was an error while loading. Please reload this page.