@graphcommerce/algolia-personalization 9.0.0-canary.82 → 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.
@@ -1,377 +0,0 @@
1
- /* eslint-disable arrow-body-style */
2
- import { useAlgoliaIndexName, useAlgoliaQuery } from '@graphcommerce/algolia-mesh'
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 { cookie } from '@graphcommerce/next-ui'
8
- import { useDebounce } from '@graphcommerce/react-hook-form'
9
- import { useEventCallback } from '@mui/material'
10
- import { useRef } from 'react'
11
- import { AlgoliaSendEventDocument } from '../mutations/AlgoliaSendEvent.gql'
12
- import { isFilterTypeEqual, ProductFilterParams } from '@graphcommerce/magento-product'
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
- let prevFilters: 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((filter) => !prevFilters.includes(filter))
102
- prevFilters = filters
103
-
104
- if (newlyAppliedFilters.length > 0) {
105
- events.push({
106
- Clicked_filters_Input: {
107
- eventName: `${eventName}_filter_diff`,
108
- eventType: 'click',
109
- filters: newlyAppliedFilters,
110
- ...common,
111
- },
112
- })
113
- }
114
-
115
- // There is a max of 10 filters per event, if there are more than 10 items
116
- // we need to split the event into multiple events
117
- for (let i = 0; i < filters.length; i += 10) {
118
- events.push({
119
- Viewed_filters_Input: {
120
- eventName: `${eventName}_filters`,
121
- eventType: 'view',
122
- filters: filters.slice(i, i + 10),
123
- ...common,
124
- },
125
- })
126
- }
127
- }
128
-
129
- // There is a max of 20 ObjectIDs per event, if there are more than 20 items
130
- // we need to split the event into multiple events
131
- for (let i = 0; i < objectIDs.length; i += 20) {
132
- events.push({
133
- Viewed_object_IDs_Input: {
134
- objectIDs: objectIDs.slice(i, i + 20),
135
- eventName,
136
- eventType: 'view',
137
- ...common,
138
- },
139
- })
140
- }
141
-
142
- return events
143
- },
144
-
145
- select_item: (eventName, eventData, { queryID, ...common }) => {
146
- const objectIDs = eventData.items.map((item) => atob(item.item_uid))
147
- if (queryID) saveAlgoliaIdToQuery(objectIDs, queryID, eventData.filter_params?.filters ?? {})
148
-
149
- return queryID
150
- ? [
151
- {
152
- Clicked_object_IDs_after_search_Input: {
153
- eventName,
154
- eventType: 'click',
155
- objectIDs,
156
- positions: eventData.items.map((item) => item.index + 1),
157
- queryID,
158
- ...common,
159
- },
160
- } satisfies AlgoliaEventsItems_Input,
161
- ]
162
- : [
163
- {
164
- Clicked_object_IDs_Input: {
165
- eventName,
166
- eventType: 'click',
167
- objectIDs: eventData.items.map((item) => atob(item.item_uid)),
168
- ...common,
169
- },
170
- } satisfies AlgoliaEventsItems_Input,
171
- ]
172
- },
173
- add_to_cart: (eventName, eventData, { queryID: _, ...common }) => {
174
- // It seems that these two events are 'simplified' versions of the Add_to_cart events.
175
- // - Converted_object_IDs_after_search_Input
176
- // - Converted_object_IDs_Input
177
-
178
- const events: AlgoliaEventsItems_Input[] = []
179
-
180
- const mapping = getObjectIDToQuery()
181
- const objectIDs = eventData.items.map((item) => atob(item.item_uid))
182
-
183
- const relevant = objectIDs.map((objectID) => mapping[objectID])
184
- const queryID = relevant?.[0]?.queryID
185
- const filters = [...new Set(...relevant.map((item) => item.filters))]
186
-
187
- if (filters.length) {
188
- // There is a max of 10 filters per event, if there are more than 10 items
189
- // we need to split the event into multiple events
190
- for (let i = 0; i < filters.length; i += 10) {
191
- events.push({
192
- Converted_filters_Input: {
193
- eventName: `${eventName}_filters`,
194
- eventType: 'conversion',
195
- filters: filters.slice(i, i + 10),
196
- ...common,
197
- },
198
- })
199
- }
200
- }
201
-
202
- if (queryID) {
203
- events.push({
204
- Added_to_cart_object_IDs_after_search_Input: {
205
- queryID,
206
- eventName,
207
- eventType: 'conversion',
208
- eventSubtype: 'addToCart',
209
- objectIDs: eventData.items.map((item) => atob(item.item_uid)),
210
- objectData: eventData.items.map((item) => ({
211
- discount: { Float: item.discount ?? 0 },
212
- price: { Float: Number(item.price.toFixed(15)) },
213
- quantity: item.quantity,
214
- })),
215
- currency: eventData.currency,
216
- value: { Float: Number(eventData.value.toFixed(15)) },
217
- ...common,
218
- },
219
- } satisfies AlgoliaEventsItems_Input)
220
- } else {
221
- events.push({
222
- Added_to_cart_object_IDs_Input: {
223
- eventName,
224
- eventType: 'conversion',
225
- eventSubtype: 'addToCart',
226
- objectIDs: eventData.items.map((item) => atob(item.item_uid)),
227
- objectData: eventData.items.map((item) => ({
228
- discount: { Float: item.discount ?? 0 },
229
- price: { Float: Number(item.price.toFixed(15)) },
230
- quantity: item.quantity,
231
- })),
232
- currency: eventData.currency,
233
- value: { Float: Number(eventData.value.toFixed(15)) },
234
- ...common,
235
- },
236
- } satisfies AlgoliaEventsItems_Input)
237
- }
238
-
239
- return events
240
- },
241
-
242
- purchase: (eventName, eventData, common) => {
243
- const mapping = getObjectIDToQuery()
244
- const isAfterSearch = !!eventData.items.find((item) => mapping[atob(item.item_uid)]?.queryID)
245
-
246
- const events: AlgoliaEventsItems_Input[] = []
247
-
248
- const objectIDs = eventData.items.map((item) => atob(item.item_uid))
249
- const relevant = objectIDs.map((objectID) => mapping[objectID])
250
- const filters = [...new Set(...relevant.map((item) => item.filters))]
251
-
252
- if (filters.length) {
253
- // There is a max of 10 filters per event, if there are more than 10 items
254
- // we need to split the event into multiple events
255
- for (let i = 0; i < filters.length; i += 10) {
256
- events.push({
257
- Converted_filters_Input: {
258
- eventName: `${eventName}_filters`,
259
- eventType: 'conversion',
260
- filters: filters.slice(i, i + 10),
261
- ...common,
262
- },
263
- })
264
- }
265
- }
266
-
267
- if (isAfterSearch) {
268
- events.push({
269
- Purchased_object_IDs_after_search_Input: {
270
- eventName,
271
- eventType: 'conversion',
272
- eventSubtype: 'purchase',
273
- objectIDs: eventData.items.map((item) => atob(item.item_uid)),
274
- objectData: eventData.items.map((item) => ({
275
- discount: { Float: item.discount ?? 0 },
276
- price: { Float: Number(item.price.toFixed(15)) },
277
- quantity: item.quantity,
278
- queryID: mapping[atob(item.item_uid)]?.queryID,
279
- })),
280
- currency: eventData.currency,
281
- value: { Float: Number(eventData.value.toFixed(15)) },
282
- ...common,
283
- },
284
- } satisfies AlgoliaEventsItems_Input)
285
- } else {
286
- events.push({
287
- Purchased_object_IDs_Input: {
288
- eventName,
289
- eventType: 'conversion',
290
- eventSubtype: 'purchase',
291
- objectIDs: eventData.items.map((item) => atob(item.item_uid)),
292
- objectData: eventData.items.map((item) => ({
293
- discount: { Float: item.discount ?? 0 },
294
- price: { Float: Number(item.price.toFixed(15)) },
295
- quantity: item.quantity,
296
- })),
297
- currency: eventData.currency,
298
- value: { Float: Number(eventData.value.toFixed(15)) },
299
- ...common,
300
- },
301
- } satisfies AlgoliaEventsItems_Input)
302
- }
303
-
304
- clearAlgoliaIdToQuery()
305
- return events
306
- },
307
- }
308
-
309
- export function useSendAlgoliaEvent() {
310
- const client = useApolloClient()
311
- const index = useAlgoliaIndexName()
312
- const algoliaQuery = useAlgoliaQuery()
313
-
314
- const eventsBuffer = useRef<AlgoliaEventsItems_Input[]>([])
315
- const submit = useDebounce(
316
- () => {
317
- if (eventsBuffer.current.length === 0) return
318
-
319
- const events = eventsBuffer.current
320
- eventsBuffer.current = []
321
-
322
- client
323
- .mutate({
324
- mutation: AlgoliaSendEventDocument,
325
- variables: { events },
326
- })
327
- .then(({ data, errors }) => {
328
- const errorMessage = (errors ?? []).map((e) => e.message)
329
- if (errorMessage.length > 0) {
330
- console.log('There was a problem sending the Algolia event to the server', errorMessage)
331
- }
332
-
333
- const response = data?.algolia_pushEvents
334
-
335
- if (response && response.status !== 200) {
336
- console.log(
337
- 'There was a problem sending the Algolia event to the server Y',
338
- response.message,
339
- )
340
- }
341
- })
342
- .catch((e) => {
343
- console.error('There was a problem sending the Algolia event to the server Z', e)
344
- })
345
- },
346
- 2000,
347
- { trailing: true },
348
- )
349
-
350
- return useEventCallback<typeof sendEvent>(async (eventName, eventData) => {
351
- const email = client.cache.readQuery({ query: CustomerDocument })?.customer?.email
352
- const authenticatedUserToken = email ? await getSHA256Hash(email) : undefined
353
- let userToken = cookie(ALGOLIA_USER_TOKEN_COOKIE_NAME)
354
- if (!userToken) {
355
- userToken = (Math.random() + 1).toString(36).substring(2)
356
- cookie(ALGOLIA_USER_TOKEN_COOKIE_NAME, userToken, { sameSite: true })
357
- }
358
-
359
- // todo check if valid
360
- // if (authenticatedUserToken) {
361
- // userToken = authenticatedUserToken
362
- // }
363
-
364
- const events = dataLayerToAlgoliaMap[eventName]?.(eventName, eventData, {
365
- index,
366
- userToken,
367
- authenticatedUserToken,
368
- queryID: algoliaQuery.queryID ?? undefined,
369
- // timestamp: (Math.floor(Date.now() / 1000)),
370
- })
371
-
372
- if (events) {
373
- eventsBuffer.current.push(...events)
374
- submit()
375
- }
376
- })
377
- }
@@ -1,6 +0,0 @@
1
- mutation AlgoliaSendEvent($events: [AlgoliaEventsItems_Input]!) {
2
- algolia_pushEvents(input: { events: $events }) {
3
- message
4
- status
5
- }
6
- }
@@ -1,18 +0,0 @@
1
- import type { getSearchResultsInput as getSearchResultsInputType } from '@graphcommerce/algolia-mesh'
2
- import { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config'
3
-
4
- export const config: PluginConfig = {
5
- type: 'function',
6
- module: '@graphcommerce/algolia-mesh',
7
- ifConfig: 'algolia.personalizationEnabled',
8
- }
9
-
10
- export const getSearchResultsInput: FunctionPlugin<typeof getSearchResultsInputType> = async (
11
- prev,
12
- args,
13
- context,
14
- ) => ({
15
- ...(await prev(args, context)),
16
- // enablePersonalization: true,
17
- // personalizationImpact: 50,
18
- })
@@ -1,16 +0,0 @@
1
- import type { getSearchSuggestionsInput as getSearchSuggestionsInputType } from '@graphcommerce/algolia-mesh'
2
- import { FunctionPlugin, PluginConfig } from '@graphcommerce/next-config'
3
-
4
- export const config: PluginConfig = {
5
- type: 'function',
6
- module: '@graphcommerce/algolia-mesh',
7
- ifConfig: 'algolia.personalizationEnabled',
8
- }
9
-
10
- export const getSearchSuggestionsInput: FunctionPlugin<
11
- typeof getSearchSuggestionsInputType
12
- > = async (prev, search, context) => ({
13
- ...(await prev(search, context)),
14
- // enablePersonalization: true,
15
- // personalizationImpact: 50,
16
- })
@@ -1,66 +0,0 @@
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-personalization/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: [
35
- { type: 'Query', fieldName: 'searchSingleIndex' },
36
- { type: 'Query', fieldName: 'searchForFacetValues' },
37
- ],
38
- },
39
- },
40
- transforms: [
41
- {
42
- prefix: {
43
- value: 'algolia_',
44
- includeRootOperations: true,
45
- includeTypes: false,
46
- mode: 'bare',
47
- },
48
- },
49
- {
50
- prefix: {
51
- value: 'Algolia',
52
- includeRootOperations: false,
53
- includeTypes: true,
54
- mode: 'bare',
55
- },
56
- },
57
- ],
58
- },
59
- ],
60
- // additionalResolvers: [
61
- // ...(baseConfig.additionalResolvers ?? []),
62
- // '@graphcommerce/algolia-mesh/mesh/resolvers.ts',
63
- // ],
64
- },
65
- graphCommerceConfig,
66
- )
@@ -1,23 +0,0 @@
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
- ifConfig: 'algolia.analyticsEnabled',
13
- }
14
-
15
- export const useSendEvent: FunctionPlugin<typeof useSendEventBase> = (prev) => {
16
- const originalSendEvent = prev()
17
- const sendAlgoliaEvent = useSendAlgoliaEvent()
18
-
19
- return useEventCallback<typeof sendEvent>((eventName, eventData) => {
20
- originalSendEvent(eventName, eventData)
21
- sendAlgoliaEvent(eventName, eventData)
22
- })
23
- }
@@ -1,72 +0,0 @@
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
- )