@graphcommerce/algolia-insights 9.0.0-canary.84
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/README.md +3 -0
- package/algolia-insights-spec.yaml +994 -0
- package/graphql/AlgoliaSendEvent.graphql +6 -0
- package/hooks/useSendAlgoliaEvent.ts +378 -0
- package/index.ts +2 -0
- package/package.json +34 -0
- package/plugins/getSearchResultsInputInsights.ts +16 -0
- package/plugins/getSearchSuggestionsInputInsights.ts +14 -0
- package/plugins/meshConfigAlgoliaInsights.ts +59 -0
- package/plugins/useSendEventAlgolia.ts +22 -0
- package/scripts/generate-insights-spec.mts +72 -0
- package/tsconfig.json +5 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
/* eslint-disable arrow-body-style */
|
|
2
|
+
import { useAlgoliaIndexName, useAlgoliaQuery } from '@graphcommerce/algolia-products'
|
|
3
|
+
import { GoogleEventTypes, sendEvent } from '@graphcommerce/google-datalayer'
|
|
4
|
+
import { useApolloClient } from '@graphcommerce/graphql'
|
|
5
|
+
import type { AlgoliaEventsItems_Input } from '@graphcommerce/graphql-mesh'
|
|
6
|
+
import { CustomerDocument } from '@graphcommerce/magento-customer/hooks/Customer.gql'
|
|
7
|
+
import { isFilterTypeEqual, ProductFilterParams } from '@graphcommerce/magento-product'
|
|
8
|
+
import { cookie } from '@graphcommerce/next-ui'
|
|
9
|
+
import { useDebounce } from '@graphcommerce/react-hook-form'
|
|
10
|
+
import { useEventCallback } from '@mui/material'
|
|
11
|
+
import { useRef } from 'react'
|
|
12
|
+
import { AlgoliaSendEventDocument } from '../graphql/AlgoliaSendEvent.gql'
|
|
13
|
+
|
|
14
|
+
const getSHA256Hash = async (input: string) => {
|
|
15
|
+
const textAsBuffer = new TextEncoder().encode(input)
|
|
16
|
+
const hashBuffer = await window.crypto.subtle.digest('SHA-256', textAsBuffer)
|
|
17
|
+
const hashArray = Array.from(new Uint8Array(hashBuffer))
|
|
18
|
+
const hash = hashArray.map((item) => item.toString(16).padStart(2, '0')).join('')
|
|
19
|
+
return hash
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const ALGOLIA_USER_TOKEN_COOKIE_NAME = '_algolia_userToken'
|
|
23
|
+
|
|
24
|
+
function mapSelectedFiltersToAlgoliaEvent(filters: ProductFilterParams['filters']) {
|
|
25
|
+
const flattenedFilters: string[] = []
|
|
26
|
+
|
|
27
|
+
Object.entries(filters).forEach(([key, filter]) => {
|
|
28
|
+
if (isFilterTypeEqual(filter)) {
|
|
29
|
+
const valueArray = (filter.eq ? [filter.eq] : (filter.in ?? [])) as string[]
|
|
30
|
+
valueArray.forEach((value) => {
|
|
31
|
+
if (key === 'category_uid') {
|
|
32
|
+
flattenedFilters.push(`categoryIds:${atob(value)}`)
|
|
33
|
+
} else {
|
|
34
|
+
flattenedFilters.push(`${key}:${encodeURIComponent(value)}`)
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// if (isFilterTypeMatch(value)) return null
|
|
40
|
+
// if (isFilterTypeRange(value)) return null
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
return flattenedFilters
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const LOCAL_STORAGE_KEY = '_algolia_conversion'
|
|
47
|
+
function getObjectIDToQuery(): Record<string, { queryID: string; filters: string[] }> {
|
|
48
|
+
return JSON.parse(localStorage.getItem(LOCAL_STORAGE_KEY) || 'null') ?? {}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function clearAlgoliaIdToQuery() {
|
|
52
|
+
window.localStorage.removeItem(LOCAL_STORAGE_KEY)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function saveAlgoliaIdToQuery(
|
|
56
|
+
objectIDs: string[],
|
|
57
|
+
queryID: string,
|
|
58
|
+
incomingFilters: ProductFilterParams['filters'],
|
|
59
|
+
) {
|
|
60
|
+
const current = getObjectIDToQuery()
|
|
61
|
+
const filters = mapSelectedFiltersToAlgoliaEvent(incomingFilters)
|
|
62
|
+
|
|
63
|
+
objectIDs.forEach((objectID) => {
|
|
64
|
+
current[objectID] = { queryID, filters }
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
window.localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(current))
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
type AlgoliaEventCommon = {
|
|
71
|
+
index: string
|
|
72
|
+
userToken: string
|
|
73
|
+
authenticatedUserToken?: string
|
|
74
|
+
// timestamp: bigint
|
|
75
|
+
queryID?: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const prevFilters: Record<string, string[]> = {}
|
|
79
|
+
|
|
80
|
+
const dataLayerToAlgoliaMap: {
|
|
81
|
+
[K in keyof Partial<GoogleEventTypes>]: (
|
|
82
|
+
eventName: K,
|
|
83
|
+
eventData: GoogleEventTypes[K],
|
|
84
|
+
common: AlgoliaEventCommon,
|
|
85
|
+
) => AlgoliaEventsItems_Input[]
|
|
86
|
+
} = {
|
|
87
|
+
// todo should we use view_item or view_item_list?
|
|
88
|
+
view_item_list: (eventName, eventData, { queryID, ...common }) => {
|
|
89
|
+
const objectIDs = eventData.items.map((item) => atob(item.item_uid))
|
|
90
|
+
|
|
91
|
+
const events: AlgoliaEventsItems_Input[] = []
|
|
92
|
+
|
|
93
|
+
if (
|
|
94
|
+
// Values of filters are different when using Algolia vs Magento, thus it does not make sense to send the filters
|
|
95
|
+
queryID &&
|
|
96
|
+
eventData.filter_params?.filters &&
|
|
97
|
+
Object.keys(eventData.filter_params?.filters).length > 0
|
|
98
|
+
) {
|
|
99
|
+
const filters = mapSelectedFiltersToAlgoliaEvent(eventData.filter_params.filters)
|
|
100
|
+
|
|
101
|
+
const newlyAppliedFilters = filters.filter(
|
|
102
|
+
(filter) => !prevFilters[eventData.item_list_name]?.includes(filter),
|
|
103
|
+
)
|
|
104
|
+
prevFilters[eventData.item_list_name] = filters
|
|
105
|
+
|
|
106
|
+
if (newlyAppliedFilters.length > 0) {
|
|
107
|
+
events.push({
|
|
108
|
+
Clicked_filters_Input: {
|
|
109
|
+
eventName: `${eventName}_filter_diff`,
|
|
110
|
+
eventType: 'click',
|
|
111
|
+
filters: newlyAppliedFilters,
|
|
112
|
+
...common,
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// There is a max of 10 filters per event, if there are more than 10 items
|
|
118
|
+
// we need to split the event into multiple events
|
|
119
|
+
for (let i = 0; i < filters.length; i += 10) {
|
|
120
|
+
events.push({
|
|
121
|
+
Viewed_filters_Input: {
|
|
122
|
+
eventName: `${eventName}_filters`,
|
|
123
|
+
eventType: 'view',
|
|
124
|
+
filters: filters.slice(i, i + 10),
|
|
125
|
+
...common,
|
|
126
|
+
},
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// There is a max of 20 ObjectIDs per event, if there are more than 20 items
|
|
132
|
+
// we need to split the event into multiple events
|
|
133
|
+
for (let i = 0; i < objectIDs.length; i += 20) {
|
|
134
|
+
events.push({
|
|
135
|
+
Viewed_object_IDs_Input: {
|
|
136
|
+
objectIDs: objectIDs.slice(i, i + 20),
|
|
137
|
+
eventName,
|
|
138
|
+
eventType: 'view',
|
|
139
|
+
...common,
|
|
140
|
+
},
|
|
141
|
+
})
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return events
|
|
145
|
+
},
|
|
146
|
+
|
|
147
|
+
select_item: (eventName, eventData, { queryID, ...common }) => {
|
|
148
|
+
const objectIDs = eventData.items.map((item) => atob(item.item_uid))
|
|
149
|
+
if (queryID) saveAlgoliaIdToQuery(objectIDs, queryID, eventData.filter_params?.filters ?? {})
|
|
150
|
+
|
|
151
|
+
return queryID
|
|
152
|
+
? [
|
|
153
|
+
{
|
|
154
|
+
Clicked_object_IDs_after_search_Input: {
|
|
155
|
+
eventName,
|
|
156
|
+
eventType: 'click',
|
|
157
|
+
objectIDs,
|
|
158
|
+
positions: eventData.items.map((item) => item.index + 1),
|
|
159
|
+
queryID,
|
|
160
|
+
...common,
|
|
161
|
+
},
|
|
162
|
+
} satisfies AlgoliaEventsItems_Input,
|
|
163
|
+
]
|
|
164
|
+
: [
|
|
165
|
+
{
|
|
166
|
+
Clicked_object_IDs_Input: {
|
|
167
|
+
eventName,
|
|
168
|
+
eventType: 'click',
|
|
169
|
+
objectIDs: eventData.items.map((item) => atob(item.item_uid)),
|
|
170
|
+
...common,
|
|
171
|
+
},
|
|
172
|
+
} satisfies AlgoliaEventsItems_Input,
|
|
173
|
+
]
|
|
174
|
+
},
|
|
175
|
+
add_to_cart: (eventName, eventData, { queryID: _, ...common }) => {
|
|
176
|
+
// It seems that these two events are 'simplified' versions of the Add_to_cart events.
|
|
177
|
+
// - Converted_object_IDs_after_search_Input
|
|
178
|
+
// - Converted_object_IDs_Input
|
|
179
|
+
|
|
180
|
+
const events: AlgoliaEventsItems_Input[] = []
|
|
181
|
+
|
|
182
|
+
const mapping = getObjectIDToQuery()
|
|
183
|
+
const objectIDs = eventData.items.map((item) => atob(item.item_uid))
|
|
184
|
+
|
|
185
|
+
const relevant = objectIDs.map((objectID) => mapping[objectID])
|
|
186
|
+
const queryID = relevant?.[0]?.queryID
|
|
187
|
+
const filters = [...new Set(...relevant.map((item) => item.filters))]
|
|
188
|
+
|
|
189
|
+
if (filters.length > 0) {
|
|
190
|
+
// There is a max of 10 filters per event, if there are more than 10 items
|
|
191
|
+
// we need to split the event into multiple events
|
|
192
|
+
for (let i = 0; i < filters.length; i += 10) {
|
|
193
|
+
events.push({
|
|
194
|
+
Converted_filters_Input: {
|
|
195
|
+
eventName: `${eventName}_filters`,
|
|
196
|
+
eventType: 'conversion',
|
|
197
|
+
filters: filters.slice(i, i + 10),
|
|
198
|
+
...common,
|
|
199
|
+
},
|
|
200
|
+
})
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (queryID) {
|
|
205
|
+
events.push({
|
|
206
|
+
Added_to_cart_object_IDs_after_search_Input: {
|
|
207
|
+
queryID,
|
|
208
|
+
eventName,
|
|
209
|
+
eventType: 'conversion',
|
|
210
|
+
eventSubtype: 'addToCart',
|
|
211
|
+
objectIDs: eventData.items.map((item) => atob(item.item_uid)),
|
|
212
|
+
objectData: eventData.items.map((item) => ({
|
|
213
|
+
discount: { Float: Number(item.discount?.toFixed(14)) ?? 0 },
|
|
214
|
+
price: { Float: Number(item.price.toFixed(14)) },
|
|
215
|
+
quantity: item.quantity,
|
|
216
|
+
})),
|
|
217
|
+
currency: eventData.currency,
|
|
218
|
+
value: { Float: Number(eventData.value.toFixed(14)) },
|
|
219
|
+
...common,
|
|
220
|
+
},
|
|
221
|
+
} satisfies AlgoliaEventsItems_Input)
|
|
222
|
+
} else {
|
|
223
|
+
events.push({
|
|
224
|
+
Added_to_cart_object_IDs_Input: {
|
|
225
|
+
eventName,
|
|
226
|
+
eventType: 'conversion',
|
|
227
|
+
eventSubtype: 'addToCart',
|
|
228
|
+
objectIDs: eventData.items.map((item) => atob(item.item_uid)),
|
|
229
|
+
objectData: eventData.items.map((item) => ({
|
|
230
|
+
discount: { Float: item.discount ?? 0 },
|
|
231
|
+
price: { Float: Number(item.price.toFixed(14)) },
|
|
232
|
+
quantity: item.quantity,
|
|
233
|
+
})),
|
|
234
|
+
currency: eventData.currency,
|
|
235
|
+
value: { Float: Number(eventData.value.toFixed(14)) },
|
|
236
|
+
...common,
|
|
237
|
+
},
|
|
238
|
+
} satisfies AlgoliaEventsItems_Input)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return events
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
purchase: (eventName, eventData, common) => {
|
|
245
|
+
const mapping = getObjectIDToQuery()
|
|
246
|
+
const isAfterSearch = !!eventData.items.find((item) => mapping[atob(item.item_uid)]?.queryID)
|
|
247
|
+
|
|
248
|
+
const events: AlgoliaEventsItems_Input[] = []
|
|
249
|
+
|
|
250
|
+
const objectIDs = eventData.items.map((item) => atob(item.item_uid))
|
|
251
|
+
const relevant = objectIDs.map((objectID) => mapping[objectID])
|
|
252
|
+
const filters = [...new Set(...relevant.map((item) => item.filters))]
|
|
253
|
+
|
|
254
|
+
if (filters.length > 0) {
|
|
255
|
+
// There is a max of 10 filters per event, if there are more than 10 items
|
|
256
|
+
// we need to split the event into multiple events
|
|
257
|
+
for (let i = 0; i < filters.length; i += 10) {
|
|
258
|
+
events.push({
|
|
259
|
+
Converted_filters_Input: {
|
|
260
|
+
eventName: `${eventName}_filters`,
|
|
261
|
+
eventType: 'conversion',
|
|
262
|
+
filters: filters.slice(i, i + 10),
|
|
263
|
+
...common,
|
|
264
|
+
},
|
|
265
|
+
})
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (isAfterSearch) {
|
|
270
|
+
events.push({
|
|
271
|
+
Purchased_object_IDs_after_search_Input: {
|
|
272
|
+
eventName,
|
|
273
|
+
eventType: 'conversion',
|
|
274
|
+
eventSubtype: 'purchase',
|
|
275
|
+
objectIDs: eventData.items.map((item) => atob(item.item_uid)),
|
|
276
|
+
objectData: eventData.items.map((item) => ({
|
|
277
|
+
discount: { Float: Number(item.discount?.toFixed(14)) ?? 0 },
|
|
278
|
+
price: { Float: Number(item.price.toFixed(14)) },
|
|
279
|
+
quantity: item.quantity,
|
|
280
|
+
queryID: mapping[atob(item.item_uid)]?.queryID,
|
|
281
|
+
})),
|
|
282
|
+
currency: eventData.currency,
|
|
283
|
+
value: { Float: Number(eventData.value.toFixed(13)) },
|
|
284
|
+
...common,
|
|
285
|
+
},
|
|
286
|
+
} satisfies AlgoliaEventsItems_Input)
|
|
287
|
+
} else {
|
|
288
|
+
events.push({
|
|
289
|
+
Purchased_object_IDs_Input: {
|
|
290
|
+
eventName,
|
|
291
|
+
eventType: 'conversion',
|
|
292
|
+
eventSubtype: 'purchase',
|
|
293
|
+
objectIDs: eventData.items.map((item) => atob(item.item_uid)),
|
|
294
|
+
objectData: eventData.items.map((item) => ({
|
|
295
|
+
discount: { Float: Number(item.discount?.toFixed(14)) ?? 0 },
|
|
296
|
+
price: { Float: Number(item.price.toFixed(14)) },
|
|
297
|
+
quantity: item.quantity,
|
|
298
|
+
})),
|
|
299
|
+
currency: eventData.currency,
|
|
300
|
+
value: { Float: Number(eventData.value.toFixed(13)) },
|
|
301
|
+
...common,
|
|
302
|
+
},
|
|
303
|
+
} satisfies AlgoliaEventsItems_Input)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
clearAlgoliaIdToQuery()
|
|
307
|
+
return events
|
|
308
|
+
},
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
export function useSendAlgoliaEvent() {
|
|
312
|
+
const client = useApolloClient()
|
|
313
|
+
const index = useAlgoliaIndexName()
|
|
314
|
+
const algoliaQuery = useAlgoliaQuery()
|
|
315
|
+
|
|
316
|
+
const eventsBuffer = useRef<AlgoliaEventsItems_Input[]>([])
|
|
317
|
+
const submit = useDebounce(
|
|
318
|
+
() => {
|
|
319
|
+
if (eventsBuffer.current.length === 0) return
|
|
320
|
+
|
|
321
|
+
const events = eventsBuffer.current
|
|
322
|
+
eventsBuffer.current = []
|
|
323
|
+
|
|
324
|
+
client
|
|
325
|
+
.mutate({
|
|
326
|
+
mutation: AlgoliaSendEventDocument,
|
|
327
|
+
variables: { events },
|
|
328
|
+
})
|
|
329
|
+
.then(({ data, errors }) => {
|
|
330
|
+
const errorMessage = (errors ?? []).map((e) => e.message)
|
|
331
|
+
if (errorMessage.length > 0) {
|
|
332
|
+
console.log('There was a problem sending the Algolia event to the server', errorMessage)
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const response = data?.algolia_pushEvents
|
|
336
|
+
|
|
337
|
+
if (response && response.status !== 200) {
|
|
338
|
+
console.log(
|
|
339
|
+
'There was a problem sending the Algolia event to the server Y',
|
|
340
|
+
response.message,
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
})
|
|
344
|
+
.catch((e) => {
|
|
345
|
+
console.error('There was a problem sending the Algolia event to the server Z', e)
|
|
346
|
+
})
|
|
347
|
+
},
|
|
348
|
+
2000,
|
|
349
|
+
{ trailing: true },
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
return useEventCallback<typeof sendEvent>(async (eventName, eventData) => {
|
|
353
|
+
const email = client.cache.readQuery({ query: CustomerDocument })?.customer?.email
|
|
354
|
+
const authenticatedUserToken = email ? await getSHA256Hash(email) : undefined
|
|
355
|
+
let userToken = cookie(ALGOLIA_USER_TOKEN_COOKIE_NAME)
|
|
356
|
+
if (!userToken) {
|
|
357
|
+
userToken = (Math.random() + 1).toString(36).substring(2)
|
|
358
|
+
cookie(ALGOLIA_USER_TOKEN_COOKIE_NAME, userToken, { sameSite: true })
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// if (authenticatedUserToken) {
|
|
362
|
+
// userToken = authenticatedUserToken
|
|
363
|
+
// }
|
|
364
|
+
|
|
365
|
+
const events = dataLayerToAlgoliaMap[eventName]?.(eventName, eventData, {
|
|
366
|
+
index,
|
|
367
|
+
userToken,
|
|
368
|
+
authenticatedUserToken,
|
|
369
|
+
queryID: algoliaQuery.queryID ?? undefined,
|
|
370
|
+
// timestamp: (Math.floor(Date.now() / 1000)),
|
|
371
|
+
})
|
|
372
|
+
|
|
373
|
+
if (events) {
|
|
374
|
+
eventsBuffer.current.push(...events)
|
|
375
|
+
submit()
|
|
376
|
+
}
|
|
377
|
+
})
|
|
378
|
+
}
|
package/index.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@graphcommerce/algolia-insights",
|
|
3
|
+
"homepage": "https://www.graphcommerce.org/",
|
|
4
|
+
"repository": "github:graphcommerce-org/graphcommerce",
|
|
5
|
+
"version": "9.0.0-canary.84",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"prettier": "@graphcommerce/prettier-config-pwa",
|
|
8
|
+
"eslintConfig": {
|
|
9
|
+
"extends": "@graphcommerce/eslint-config-pwa",
|
|
10
|
+
"parserOptions": {
|
|
11
|
+
"project": "./tsconfig.json"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"generate": "tsx scripts/generate-insights-spec.mts"
|
|
16
|
+
},
|
|
17
|
+
"peerDependencies": {
|
|
18
|
+
"@graphcommerce/algolia-products": "^9.0.0-canary.84",
|
|
19
|
+
"@graphcommerce/google-datalayer": "^9.0.0-canary.84",
|
|
20
|
+
"@graphcommerce/graphql": "^9.0.0-canary.84",
|
|
21
|
+
"@graphcommerce/graphql-mesh": "^9.0.0-canary.84",
|
|
22
|
+
"@graphcommerce/magento-customer": "^9.0.0-canary.84",
|
|
23
|
+
"@graphcommerce/magento-product": "^9.0.0-canary.84",
|
|
24
|
+
"@graphcommerce/next-config": "^9.0.0-canary.84",
|
|
25
|
+
"@graphcommerce/next-ui": "^9.0.0-canary.84",
|
|
26
|
+
"@graphcommerce/react-hook-form": "^9.0.0-canary.84",
|
|
27
|
+
"@mui/material": "*",
|
|
28
|
+
"react": "^18.2.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"graphql": "^16.0.0",
|
|
32
|
+
"tsx": "^4.16.2"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { getSearchResultsInput as getSearchResultsInputType } from '@graphcommerce/algolia-products'
|
|
2
|
+
import { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config'
|
|
3
|
+
|
|
4
|
+
export const config: PluginConfig = {
|
|
5
|
+
type: 'function',
|
|
6
|
+
module: '@graphcommerce/algolia-products',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const getSearchResultsInput: FunctionPlugin<typeof getSearchResultsInputType> = async (
|
|
10
|
+
prev,
|
|
11
|
+
args,
|
|
12
|
+
context,
|
|
13
|
+
) => ({
|
|
14
|
+
...(await prev(args, context)),
|
|
15
|
+
analytics: true,
|
|
16
|
+
})
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { getSearchSuggestionsInput as getSearchSuggestionsInputType } from '@graphcommerce/algolia-products'
|
|
2
|
+
import { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config'
|
|
3
|
+
|
|
4
|
+
export const config: PluginConfig = {
|
|
5
|
+
type: 'function',
|
|
6
|
+
module: '@graphcommerce/algolia-products',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const getSearchSuggestionsInput: FunctionPlugin<
|
|
10
|
+
typeof getSearchSuggestionsInputType
|
|
11
|
+
> = async (prev, search, context) => ({
|
|
12
|
+
...(await prev(search, context)),
|
|
13
|
+
analytics: true,
|
|
14
|
+
})
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { meshConfig as meshConfigBase } from '@graphcommerce/graphql-mesh/meshConfig'
|
|
2
|
+
import type { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config'
|
|
3
|
+
|
|
4
|
+
export const config: PluginConfig = {
|
|
5
|
+
module: '@graphcommerce/graphql-mesh/meshConfig',
|
|
6
|
+
type: 'function',
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const meshConfig: FunctionPlugin<typeof meshConfigBase> = (
|
|
10
|
+
prev,
|
|
11
|
+
baseConfig,
|
|
12
|
+
graphCommerceConfig,
|
|
13
|
+
) =>
|
|
14
|
+
prev(
|
|
15
|
+
{
|
|
16
|
+
...baseConfig,
|
|
17
|
+
sources: [
|
|
18
|
+
...baseConfig.sources,
|
|
19
|
+
{
|
|
20
|
+
name: 'algoliaInsights',
|
|
21
|
+
handler: {
|
|
22
|
+
openapi: {
|
|
23
|
+
endpoint: `https://insights.algolia.io/`,
|
|
24
|
+
source: '@graphcommerce/algolia-insights/algolia-insights-spec.yaml',
|
|
25
|
+
ignoreErrorResponses: true,
|
|
26
|
+
schemaHeaders: {
|
|
27
|
+
'X-Algolia-Application-Id': graphCommerceConfig.algolia.applicationId,
|
|
28
|
+
'X-Algolia-API-Key': graphCommerceConfig.algolia.searchOnlyApiKey,
|
|
29
|
+
},
|
|
30
|
+
operationHeaders: {
|
|
31
|
+
'X-Algolia-Application-Id': graphCommerceConfig.algolia.applicationId,
|
|
32
|
+
'X-Algolia-API-Key': graphCommerceConfig.algolia.searchOnlyApiKey,
|
|
33
|
+
},
|
|
34
|
+
selectQueryOrMutationField: [{ type: 'Query', fieldName: 'sendEvent' }],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
transforms: [
|
|
38
|
+
{
|
|
39
|
+
prefix: {
|
|
40
|
+
value: 'algolia_',
|
|
41
|
+
includeRootOperations: true,
|
|
42
|
+
includeTypes: false,
|
|
43
|
+
mode: 'bare',
|
|
44
|
+
},
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
prefix: {
|
|
48
|
+
value: 'Algolia',
|
|
49
|
+
includeRootOperations: false,
|
|
50
|
+
includeTypes: true,
|
|
51
|
+
mode: 'bare',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
},
|
|
58
|
+
graphCommerceConfig,
|
|
59
|
+
)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
sendEvent,
|
|
3
|
+
useSendEvent as useSendEventBase,
|
|
4
|
+
} from '@graphcommerce/google-datalayer/api/sendEvent'
|
|
5
|
+
import type { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config'
|
|
6
|
+
import { useEventCallback } from '@mui/material'
|
|
7
|
+
import { useSendAlgoliaEvent } from '../hooks/useSendAlgoliaEvent'
|
|
8
|
+
|
|
9
|
+
export const config: PluginConfig = {
|
|
10
|
+
module: '@graphcommerce/google-datalayer',
|
|
11
|
+
type: 'function',
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const useSendEvent: FunctionPlugin<typeof useSendEventBase> = (prev) => {
|
|
15
|
+
const originalSendEvent = prev()
|
|
16
|
+
const sendAlgoliaEvent = useSendAlgoliaEvent()
|
|
17
|
+
|
|
18
|
+
return useEventCallback<typeof sendEvent>((eventName, eventData) => {
|
|
19
|
+
originalSendEvent(eventName, eventData)
|
|
20
|
+
sendAlgoliaEvent(eventName, eventData)
|
|
21
|
+
})
|
|
22
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import yaml from 'js-yaml'
|
|
2
|
+
import { writeFile, readFile } from 'node:fs/promises'
|
|
3
|
+
import { OpenAPIV3 } from 'openapi-types'
|
|
4
|
+
import prettier from 'prettier'
|
|
5
|
+
import conf from '@graphcommerce/prettier-config-pwa'
|
|
6
|
+
|
|
7
|
+
const response = await fetch(
|
|
8
|
+
'https://raw.githubusercontent.com/algolia/api-clients-automation/main/specs/bundled/insights.yml',
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
const openApiSchema = yaml.load(await response.text()) as OpenAPIV3.Document
|
|
12
|
+
|
|
13
|
+
const allMethods = [
|
|
14
|
+
OpenAPIV3.HttpMethods.TRACE,
|
|
15
|
+
OpenAPIV3.HttpMethods.POST,
|
|
16
|
+
OpenAPIV3.HttpMethods.PUT,
|
|
17
|
+
OpenAPIV3.HttpMethods.GET,
|
|
18
|
+
OpenAPIV3.HttpMethods.DELETE,
|
|
19
|
+
OpenAPIV3.HttpMethods.PATCH,
|
|
20
|
+
OpenAPIV3.HttpMethods.OPTIONS,
|
|
21
|
+
OpenAPIV3.HttpMethods.HEAD,
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
const { info, openapi, components, tags, ...rest } = openApiSchema
|
|
25
|
+
|
|
26
|
+
function filterPaths(
|
|
27
|
+
paths: OpenAPIV3.PathsObject,
|
|
28
|
+
allow: Record<string, OpenAPIV3.HttpMethods[]>,
|
|
29
|
+
): OpenAPIV3.PathsObject {
|
|
30
|
+
const allowedEntries = Object.entries(allow)
|
|
31
|
+
|
|
32
|
+
return Object.fromEntries(
|
|
33
|
+
Object.entries(paths)
|
|
34
|
+
.map(([path, pathItem]) => {
|
|
35
|
+
if (!pathItem) return [path, pathItem]
|
|
36
|
+
const newValue = pathItem
|
|
37
|
+
|
|
38
|
+
const [allowedPath, allowedMethods] =
|
|
39
|
+
allowedEntries.find(([allowedPath]) => allowedPath === path) ?? []
|
|
40
|
+
|
|
41
|
+
if (!allowedPath || !allowedMethods) return [path, undefined]
|
|
42
|
+
|
|
43
|
+
allMethods
|
|
44
|
+
.filter((method) => !allowedMethods.includes(method))
|
|
45
|
+
.forEach((method) => {
|
|
46
|
+
newValue[method] = undefined
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
return [path, newValue]
|
|
50
|
+
})
|
|
51
|
+
.filter(([path, pathItem]) => {
|
|
52
|
+
if (!pathItem) return false
|
|
53
|
+
if (allMethods.every((key) => !pathItem[key])) return false
|
|
54
|
+
return true
|
|
55
|
+
}),
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const newSchema: OpenAPIV3.Document = {
|
|
60
|
+
openapi,
|
|
61
|
+
info,
|
|
62
|
+
paths: filterPaths(openApiSchema.paths, { '/1/events': [OpenAPIV3.HttpMethods.POST] }),
|
|
63
|
+
components,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await writeFile(
|
|
67
|
+
'./algolia-insights-spec.yaml',
|
|
68
|
+
await prettier.format(JSON.stringify(newSchema), {
|
|
69
|
+
parser: 'json',
|
|
70
|
+
...conf,
|
|
71
|
+
}),
|
|
72
|
+
)
|