Skip to content
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

Fetch sponsored products #2

Merged
merged 4 commits into from
Nov 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## [Unreleased]

### Added

- Fetch sponsored products.

## [0.0.3] - 2024-11-05

### Feature
Expand Down
2 changes: 1 addition & 1 deletion node/clients/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { IOClients } from '@vtex/api'

import Synerise from './synerise'
import { IntelligentSearch } from './intelligent-search'
import { IntelligentSearch } from './intelligentSearch'

// Extend the default IOClients implementation with our own custom clients.
export class Clients extends IOClients {
Expand Down
53 changes: 0 additions & 53 deletions node/clients/intelligent-search.ts

This file was deleted.

115 changes: 115 additions & 0 deletions node/clients/intelligentSearch/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
import type { InstanceOptions, IOContext } from '@vtex/api'
import { ExternalClient } from '@vtex/api'

import type {
SearchResultArgs,
SearchProductsResponse,
SponsoredProductsResponse,
IIntelligentSearchClient,
} from './types'

export const parseState = (state?: string): { [key: string]: string } => {
if (!state) {
return {}
}

try {
const parsed = JSON.parse(decodeURI(state))

if (typeof parsed === 'object') {
return parsed
}
} catch (err) {
/* ignore parsing errors */
}

return {}
}

const isPathTraversal = (str: string) => str.indexOf('..') >= 0

const decodeQuery = (query: string) => {
try {
return decodeURIComponent(query)
} catch (e) {
return query
}
}

export class IntelligentSearch
extends ExternalClient
implements IIntelligentSearchClient {
private locale: string | undefined

constructor(context: IOContext, options?: InstanceOptions) {
super(
`http://${context.workspace}--${context.account}.myvtex.com/_v/api/intelligent-search`,
context,
{
...options,
headers: {
...options?.headers,
},
}
)

const { locale, tenant } = context

this.locale = locale ?? tenant?.locale
}

public async searchProducts(
params: SearchResultArgs,
path: string,
shippingHeader?: string[]
) {
const { query, leap, searchState } = params

if (isPathTraversal(path)) {
throw new Error('Malformed URL')
}

return this.http.get<SearchProductsResponse>(`/product_search/${path}`, {
params: {
query: query && decodeQuery(query),
locale: this.locale,
bgy_leap: leap ? true : undefined,
...parseState(searchState),
...params,
},
metric: 'product-search',
headers: {
'x-vtex-shipping-options': shippingHeader ?? '',
},
})
}

public async searchSponsoredProducts(
params: SearchResultArgs,
path: string,
shippingHeader?: string[]
) {
const { query, leap, searchState } = params

if (isPathTraversal(path)) {
throw new Error('Malformed URL')
}

return this.http.get<SponsoredProductsResponse>(
`/sponsored_products/${path}`,
{
params: {
query: query && decodeQuery(query),
locale: this.locale,
bgy_leap: leap ? true : undefined,
...parseState(searchState),
...params,
},
metric: 'product-search',
headers: {
'x-vtex-shipping-options': shippingHeader ?? '',
},
}
)
}
}
81 changes: 81 additions & 0 deletions node/clients/intelligentSearch/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
type SegmentData = {
channel: number
utm_campaign: string
regionId?: string
utm_source: string
utmi_campaign: string
currencyCode: string
currencySymbol: string
countryCode: string
cultureInfo: string
}

type IndexingType = 'API' | 'XML'

type RegionSeller = {
id: string
name: string
}

type Options = {
allowRedirect?: boolean
}

type AdvertisementOptions = {
showSponsored?: boolean
sponsoredCount?: number
repeatSponsoredProducts?: boolean
advertisementPlacement?: string
}

export type SearchResultArgs = {
attributePath?: string
query?: string
page?: number
count?: number
sort?: string
operator?: string
fuzzy?: string
leap?: boolean
tradePolicy?: number
segment?: SegmentData
indexingType?: IndexingType
searchState?: string
sellers?: RegionSeller[]
hideUnavailableItems?: boolean | null
removeHiddenFacets?: boolean | null
options?: Options
initialAttributes?: string
workspaceSearchParams?: Record<string, string>
regionId?: string | null
from?: number | null
to?: number | null
showSponsored?: boolean
} & AdvertisementOptions

// We have more fields but they are not relevant for this codebase.

export type Product = {
productId: string
}

export type SearchProductsResponse = {
products: Product[]
}

export type SponsoredProductsResponse = Product[] // Same product but with ADS info...

//

export interface IIntelligentSearchClient {
searchProducts(
params: SearchResultArgs,
path: string,
shippingHeader?: string[]
): Promise<SearchProductsResponse>
searchSponsoredProducts(
params: SearchResultArgs,
path: string,
shippingHeader?: string[]
): Promise<SponsoredProductsResponse>
}
91 changes: 83 additions & 8 deletions node/middlewares/campaign.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,91 @@
const convertProducts = (products: SynProductData[], ctx: Context) => {
import type { Product } from '../clients/intelligentSearch/types'

export async function fetchHydratedProductsWithAds(
ctx: Context,
products: SynProductData[]
) {
const {
clients: { intelligentSearch },
} = ctx

return products.map((product) =>
if (products.length === 0) {
return []
}

const query = products.reduce((acc, product) => {
return `${acc}${product.itemId};`
}, 'product:')

const [sponsoredProducts, searchProducts] = await Promise.all([
intelligentSearch
.productById(product.itemId)
.then((result) => result)
.catch(() => {
return undefined
.searchSponsoredProducts(
{
query,
},
''
)
.catch((e) => {
ctx.vtex.logger.error(e)

return [] as Product[]
}),
intelligentSearch
.searchProducts(
{
query,
},
''
)
.then((res) => {
// This is a type error, but we can't fix it because we don't have the type definition.
// We can't import the type definition because it's not exported.
return res.products
})
)
.catch((e) => {
ctx.vtex.logger.error(e)

return [] as Product[]
}),
])

const setOfProducts = new Map<string, Product>()
const productsOrdered: string[] = []

sponsoredProducts.forEach((product) => {
setOfProducts.set(product.productId, product)
productsOrdered.push(product.productId)
})

searchProducts.forEach((product) => {
if (!setOfProducts.has(product.productId)) {
setOfProducts.set(product.productId, product)
productsOrdered.push(product.productId)
}
})

products.forEach((product) => {
const hydratedProduct = setOfProducts.get(product.itemId)

if (!hydratedProduct) {
ctx.vtex.logger.error(`Product not found: ${product.itemId}`)
}
})

const result = productsOrdered
.map((productId) => {
const hydratedProduct = setOfProducts.get(productId)

if (!hydratedProduct) {
ctx.vtex.logger.error(`Product not found: ${productId}`)

return null
}

return hydratedProduct
})
.filter((product) => product !== null)

return result
}

export async function campaign(ctx: Context, next: () => Promise<void>) {
Expand All @@ -34,7 +109,7 @@ export async function campaign(ctx: Context, next: () => Promise<void>) {
return
}

const products = await Promise.all(convertProducts(recs.data, ctx))
const products = await fetchHydratedProductsWithAds(ctx, recs.data)

ctx.body = { products: products.filter((product) => !!product), jwt: userJWT }

Expand Down
2 changes: 1 addition & 1 deletion node/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5616,7 +5616,7 @@ static-extend@^0.1.1:
define-property "^0.2.5"
object-copy "^0.1.0"

"stats-lite@github:vtex/node-stats-lite#dist":
stats-lite@vtex/node-stats-lite#dist:
version "2.2.0"
resolved "https://codeload.github.com/vtex/node-stats-lite/tar.gz/1b0d39cc41ef7aaecfd541191f877887a2044797"
dependencies:
Expand Down
Loading