Last active
November 11, 2024 19:47
-
-
Save VariableVic/dd3ff891cc68414c87ab54cd67e0bf8f to your computer and use it in GitHub Desktop.
Medusa -> Google Analytics 4 server side tracking example
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { TransactionBaseService } from "@medusajs/medusa"; | |
import { | |
ConsentSettings, | |
EventItem, | |
JSONPostBody, | |
} from "../types/google-analytics"; | |
const { subtle } = require("crypto").webcrypto; | |
// Set to true to enable debug mode. In debug mode, your events will not be processed in the Google Analytics UI, but will be validated as if they were. | |
const DEBUG = false; | |
class GoogleAnalyticsService extends TransactionBaseService { | |
protected measurementId_: string; | |
protected apiSecret_: string; | |
protected endpoint_: string; | |
protected debugPath_: string; | |
constructor() { | |
super(arguments); | |
this.measurementId_ = process.env.GA_MEASUREMENT_ID || ""; | |
this.apiSecret_ = process.env.GA_API_SECRET || ""; | |
this.debugPath_ = DEBUG ? "debug/mp" : "mp"; | |
this.endpoint_ = `https://www.google-analytics.com/${this.debugPath_}/collect?measurement_id=${this.measurementId_}&api_secret=${this.apiSecret_}`; | |
} | |
async track({ | |
clientId, | |
userId, | |
events, | |
userData, | |
consentSettings, | |
}: { | |
clientId: string; | |
userId?: string; | |
events: EventItem[]; | |
userData?: Record<string, any>; | |
consentSettings?: ConsentSettings; | |
}): Promise<Response> { | |
const body: JSONPostBody = { | |
client_id: clientId, | |
user_id: userId, | |
events, | |
user_data: userData, | |
consent: consentSettings, | |
timestamp_micros: Date.now() * 1000, | |
}; | |
const response = await fetch(this.endpoint_, { | |
method: "POST", | |
headers: { | |
"Content-Type": "application/json", | |
}, | |
body: JSON.stringify(body), | |
}).then((res) => (DEBUG ? res.json() : res)); | |
DEBUG && console.log("Response", response); | |
return response; | |
} | |
async populateSensitiveUserData(value: string): Promise<string> { | |
const encoder = new TextEncoder(); | |
// Convert a string value to UTF-8 encoded text. | |
const value_utf8 = encoder.encode(value); | |
// Compute the hash (digest) using the SHA-256 algorithm. | |
const hash_sha256 = await subtle.digest("SHA-256", value_utf8); | |
// Convert buffer to byte array. | |
const hash_array = Array.from(new Uint8Array(hash_sha256)); | |
// Return a hex-encoded string. | |
return hash_array.map((b) => b.toString(16).padStart(2, "0")).join(""); | |
} | |
} | |
export default GoogleAnalyticsService; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
OrderService, | |
type SubscriberConfig, | |
type SubscriberArgs, | |
} from "@medusajs/medusa"; | |
import GoogleAnalyticsService from "../../services/google-analytics"; | |
export default async function orderPlacedHandler({ | |
data, | |
eventName, | |
container, | |
pluginOptions, | |
}: SubscriberArgs<Record<string, any>>) { | |
// Retrieve order service and GA service from the container | |
const orderService: OrderService = container.resolve("orderService"); | |
const ga: GoogleAnalyticsService = container.resolve( | |
"googleAnalyticsService" | |
); | |
// Retrieve order with totals using the order id from the event payload | |
const order = await orderService.retrieveWithTotals(data.id, { | |
relations: [ | |
"shipping_address", | |
"items", | |
"items.variant", | |
"items.variant.product.categories", | |
"customer", | |
], | |
}); | |
// Create items array | |
const gaItems = order.items.map((item) => ({ | |
item_id: item.variant_id, | |
item_name: item.title, | |
item_category: item.variant.product.categories?.[0]?.name, | |
price: item.unit_price / 100, | |
quantity: item.quantity, | |
})); | |
// Create events array | |
const events = [ | |
{ | |
name: "purchase", | |
params: { | |
transaction_id: order.id, | |
session_id: order.metadata.ga_session_id, | |
value: order.total / 100, | |
tax: order.tax_total / 100, | |
shipping: order.shipping_total / 100, | |
currency: order.currency_code, | |
coupon: order.discounts?.[0], | |
items: gaItems, | |
}, | |
}, | |
]; | |
// Hash sensitive user data | |
const hashedEmail = await ga.populateSensitiveUserData(order.customer.email); | |
const hashedPhone = await ga.populateSensitiveUserData(order.customer.phone); | |
const hashedFirstName = await ga.populateSensitiveUserData( | |
order.customer.first_name | |
); | |
const hashedLastName = await ga.populateSensitiveUserData( | |
order.customer.last_name | |
); | |
const hashedStreet = await ga.populateSensitiveUserData( | |
order.shipping_address?.address_1 | |
); | |
// Create user data object | |
const userData = { | |
sha256_email_address: [hashedEmail], | |
sha256_phone_number: [hashedPhone], | |
address: [ | |
{ | |
sha256_first_name: hashedFirstName, | |
sha256_last_name: hashedLastName, | |
sha256_street: hashedStreet, | |
city: order.shipping_address?.city, | |
region: order.shipping_address?.province, | |
postal_code: order.shipping_address?.postal_code, | |
country: order.shipping_address?.country_code, | |
}, | |
], | |
}; | |
// Send data to GA | |
await ga.track({ | |
clientId: order.metadata.ga_client_id, | |
userId: order.customer_id, | |
userData, | |
events, | |
}); | |
} | |
export const config: SubscriberConfig = { | |
event: OrderService.Events.PLACED, | |
context: { | |
subscriberId: "ga4-order-placed-handler", | |
}, | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { MedusaRequest, MedusaResponse } from "@medusajs/medusa"; | |
import GoogleAnalyticsService from "../../../../../services/google-analytics"; | |
export async function POST( | |
req: MedusaRequest, | |
res: MedusaResponse | |
): Promise<void> { | |
const { user } = req; | |
const { name } = req.params; | |
let { params, consentSettings, clientId } = req.body; | |
const ga: GoogleAnalyticsService = req.scope.resolve( | |
"googleAnalyticsService" | |
); | |
clientId = clientId || "anonymous_" + new Date().getTime(); | |
const userId = user?.customer_id || user?.id; | |
const events = [ | |
{ | |
name, | |
params, | |
}, | |
]; | |
const response = await ga.track({ | |
clientId, | |
userId, | |
events, | |
consentSettings, | |
}); | |
res.status(200).json({ response }); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export type JSONPostBody = { | |
client_id: string; // Required. Uniquely identifies a user instance of a web client. | |
user_id?: string; // Optional. A unique identifier for a user. | |
timestamp_micros?: number; // Optional. A Unix timestamp (in microseconds) for the time to associate with the event. | |
user_data?: Record<string, any>; // Optional. The user data for the measurement. | |
consent?: ConsentSettings; // Optional. Sets the consent settings for events. | |
non_personalized_ads?: boolean; // Optional. Set to true to indicate these events should not be used for personalized ads. | |
events: EventItem[]; // Required. An array of event items. Up to 25 events can be sent per request. | |
}; | |
export type ConsentSettings = { | |
ad_user_data?: "GRANTED" | "DENIED"; // Optional. The consent value for ad user data. | |
ad_personalization?: "GRANTED" | "DENIED"; // Optional. The consent value for ad personalization. | |
}; | |
export type EventItem = { | |
name: string; // Required. The name for the event. | |
params?: Record<string, any>; // Optional. The parameters for the event. | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Thank you for this script! Quick question, where do you retrieve the order.metadata.ga_session_id from? I assume you add it from the front end at the checkout, I'm using your Next.js starter for Medusa.