feat: integrate Rybbit analytics alongside OpenPanel
Some checks failed
Build and Deploy / build (push) Has been cancelled

- Add RybbitService for tracking e-commerce events
- Update useAnalytics hook to track with both OpenPanel and Rybbit
- Add Rybbit script to layout for page view tracking
- Track all applicable store events: product views, cart, checkout, orders, search, etc.
This commit is contained in:
Unchained
2026-03-31 00:38:38 +02:00
parent 044aefae94
commit b3efebd3e4
11 changed files with 1422 additions and 118 deletions

0
EOF Normal file
View File

3
features.md Normal file
View File

@@ -0,0 +1,3 @@
programmatic seo
pop up and exit pop to grow emaillist connected with resend and mautic. want to always have my list growing and owned by me on my server
abandoned cart setup with sequences to get people back

40
public/debug-op.js Normal file
View File

@@ -0,0 +1,40 @@
// OpenPanel Debug Script
// Run this in browser console to test OpenPanel
(function debugOpenPanel() {
console.log('=== OpenPanel Debug ===');
// Check if OpenPanel is loaded
if (typeof window.op === 'undefined') {
console.error('❌ OpenPanel SDK not loaded (window.op is undefined)');
console.log('Script URL should be:', 'https://op.nodecrew.me/op1.js');
return;
}
console.log('✅ OpenPanel SDK loaded');
console.log('window.op:', window.op);
// Check client ID
const clientId = window.op._clientId || 'not set';
console.log('Client ID:', clientId);
// Try to track an event
console.log('Attempting to track test event...');
window.op.track('debug_test', { source: 'console', timestamp: new Date().toISOString() })
.then(() => console.log('✅ Track successful'))
.catch(err => console.error('❌ Track failed:', err));
// Check network requests
console.log('');
console.log('Check Network tab for requests to:');
console.log('- https://manoonoils.com/api/op/track');
console.log('- https://op.nodecrew.me/api/track');
// Common issues
console.log('');
console.log('Common issues:');
console.log('1. Ad blockers (try disabling uBlock/AdBlock)');
console.log('2. CORS errors (check console for red errors)');
console.log('3. Do Not Track enabled in browser');
console.log('4. Private/Incognito mode (some blockers active)');
})();

View File

@@ -0,0 +1,310 @@
#!/usr/bin/env node
/**
* Test script for checkout shipping cost calculation
* Creates a checkout via API and verifies totalPrice includes shipping
*/
const SALEOR_API_URL = process.env.NEXT_PUBLIC_SALEOR_API_URL || 'https://api.manoonoils.com/graphql/';
// Test data
const TEST_VARIANT_ID = 'UHJvZHVjdFZhcmlhbnQ6Mjk0'; // Replace with actual variant ID
const TEST_EMAIL = 'test@example.com';
const TEST_SHIPPING_ADDRESS = {
firstName: 'Test',
lastName: 'User',
streetAddress1: '123 Test Street',
city: 'Belgrade',
postalCode: '11000',
country: 'RS',
phone: '+38160123456'
};
async function saleorFetch(query, variables = {}, token = null) {
const headers = {
'Content-Type': 'application/json',
};
if (token) {
headers['Authorization'] = `JWT ${token}`;
}
const response = await fetch(SALEOR_API_URL, {
method: 'POST',
headers,
body: JSON.stringify({ query, variables }),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (result.errors) {
throw new Error(`GraphQL errors: ${JSON.stringify(result.errors)}`);
}
return result.data;
}
async function testCheckoutWithShipping() {
console.log('🧪 Testing checkout shipping cost calculation...\n');
try {
// Step 1: Create checkout
console.log('Step 1: Creating checkout...');
const checkoutCreateMutation = `
mutation CheckoutCreate($input: CheckoutCreateInput!) {
checkoutCreate(input: $input) {
checkout {
id
token
totalPrice {
gross {
amount
currency
}
}
subtotalPrice {
gross {
amount
currency
}
}
}
errors {
field
message
}
}
}
`;
const checkoutResult = await saleorFetch(checkoutCreateMutation, {
input: {
channel: 'default-channel',
email: TEST_EMAIL,
lines: [],
languageCode: 'SR'
}
});
if (checkoutResult.checkoutCreate.errors?.length > 0) {
throw new Error(`Checkout creation failed: ${checkoutResult.checkoutCreate.errors[0].message}`);
}
const checkout = checkoutResult.checkoutCreate.checkout;
console.log(`✅ Checkout created: ${checkout.id}`);
console.log(` Token: ${checkout.token}`);
console.log(` Initial total: ${checkout.totalPrice.gross.amount} ${checkout.totalPrice.gross.currency}\n`);
// Step 2: Add product to checkout
console.log('Step 2: Adding product to checkout...');
const linesAddMutation = `
mutation CheckoutLinesAdd($checkoutId: ID!, $lines: [CheckoutLineInput!]!) {
checkoutLinesAdd(checkoutId: $checkoutId, lines: $lines) {
checkout {
id
totalPrice {
gross {
amount
currency
}
}
subtotalPrice {
gross {
amount
currency
}
}
lines {
id
quantity
totalPrice {
gross {
amount
}
}
}
}
errors {
field
message
}
}
}
`;
// First, let's query for available products to get a real variant ID
console.log(' Querying available products...');
const productsQuery = `
query Products {
products(channel: "default-channel", first: 1) {
edges {
node {
id
name
variants {
id
name
}
}
}
}
}
`;
const productsResult = await saleorFetch(productsQuery);
const product = productsResult.products.edges[0]?.node;
if (!product || !product.variants?.[0]) {
throw new Error('No products found in store');
}
const variantId = product.variants[0].id;
console.log(` Product: ${product.name}, Variant: ${product.variants[0].name}`);
const linesResult = await saleorFetch(linesAddMutation, {
checkoutId: checkout.id,
lines: [{ variantId, quantity: 1 }]
});
if (linesResult.checkoutLinesAdd.errors?.length > 0) {
throw new Error(`Adding lines failed: ${linesResult.checkoutLinesAdd.errors[0].message}`);
}
const checkoutWithLines = linesResult.checkoutLinesAdd.checkout;
const productTotal = checkoutWithLines.totalPrice.gross.amount;
console.log(`✅ Product added (qty: 1)`);
console.log(` Product total: ${productTotal} RSD\n`);
// Step 3: Set shipping address
console.log('Step 3: Setting shipping address...');
const shippingAddressMutation = `
mutation CheckoutShippingAddressUpdate($checkoutId: ID!, $shippingAddress: AddressInput!) {
checkoutShippingAddressUpdate(checkoutId: $checkoutId, shippingAddress: $shippingAddress) {
checkout {
id
shippingMethods {
id
name
price {
amount
currency
}
}
}
errors {
field
message
}
}
}
`;
const shippingResult = await saleorFetch(shippingAddressMutation, {
checkoutId: checkout.id,
shippingAddress: TEST_SHIPPING_ADDRESS
});
if (shippingResult.checkoutShippingAddressUpdate.errors?.length > 0) {
throw new Error(`Setting shipping address failed: ${shippingResult.checkoutShippingAddressUpdate.errors[0].message}`);
}
const availableMethods = shippingResult.checkoutShippingAddressUpdate.checkout.shippingMethods;
console.log(`✅ Shipping address set`);
console.log(` Available shipping methods: ${availableMethods.length}`);
if (availableMethods.length === 0) {
console.log(' ⚠️ No shipping methods available for this address/region');
return;
}
availableMethods.forEach((method, i) => {
console.log(` [${i + 1}] ${method.name}: ${method.price.amount} ${method.price.currency}`);
});
console.log('');
// Step 4: Set shipping method
const selectedMethod = availableMethods[0];
console.log(`Step 4: Selecting shipping method: ${selectedMethod.name} (${selectedMethod.price.amount} RSD)...`);
const shippingMethodMutation = `
mutation CheckoutShippingMethodUpdate($checkoutId: ID!, $shippingMethodId: ID!) {
checkoutShippingMethodUpdate(checkoutId: $checkoutId, shippingMethodId: $shippingMethodId) {
checkout {
id
totalPrice {
gross {
amount
currency
}
}
subtotalPrice {
gross {
amount
currency
}
}
shippingPrice {
gross {
amount
currency
}
}
}
errors {
field
message
}
}
}
`;
const methodResult = await saleorFetch(shippingMethodMutation, {
checkoutId: checkout.id,
shippingMethodId: selectedMethod.id
});
if (methodResult.checkoutShippingMethodUpdate.errors?.length > 0) {
throw new Error(`Setting shipping method failed: ${methodResult.checkoutShippingMethodUpdate.errors[0].message}`);
}
const finalCheckout = methodResult.checkoutShippingMethodUpdate.checkout;
const subtotal = finalCheckout.subtotalPrice.gross.amount;
const shipping = finalCheckout.shippingPrice.gross.amount;
const finalTotal = finalCheckout.totalPrice.gross.amount;
const expectedTotal = subtotal + shipping;
console.log(`✅ Shipping method set`);
console.log(` Subtotal: ${subtotal} RSD`);
console.log(` Shipping: ${shipping} RSD`);
console.log(` Total: ${finalTotal} RSD`);
console.log(` Expected: ${expectedTotal} RSD`);
console.log('');
// Verification
console.log('📊 VERIFICATION:');
if (finalTotal === expectedTotal) {
console.log('✅ PASS: Total includes shipping cost correctly');
console.log(` ${subtotal} + ${shipping} = ${finalTotal}`);
} else {
console.log('❌ FAIL: Total does NOT include shipping cost');
console.log(` Expected: ${expectedTotal}, Got: ${finalTotal}`);
console.log(` Difference: ${expectedTotal - finalTotal}`);
}
// Cleanup - delete checkout
console.log('\n🧹 Cleaning up test checkout...');
// Note: Checkout deletion requires admin permissions
console.log(` Checkout ID for manual cleanup: ${checkout.id}`);
} catch (error) {
console.error('\n❌ Test failed:', error.message);
process.exit(1);
}
}
// Run the test
testCheckoutWithShipping();

View File

137
scripts/test-frontend.mjs Normal file
View File

@@ -0,0 +1,137 @@
const SALEOR_API_URL = 'https://api.manoonoils.com/graphql/';
async function saleorFetch(query, variables = {}) {
const response = await fetch(SALEOR_API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, variables }),
});
const result = await response.json();
if (result.errors) {
console.error('GraphQL Errors:', JSON.stringify(result.errors, null, 2));
throw new Error(JSON.stringify(result.errors));
}
return result.data;
}
async function test() {
// Create checkout
const createResult = await saleorFetch(`
mutation {
checkoutCreate(input: {
channel: "default-channel"
email: "test@test.com"
lines: [{ variantId: "UHJvZHVjdFZhcmlhbnQ6Mjk0", quantity: 1 }]
languageCode: SR
}) {
checkout {
id
token
totalPrice { gross { amount } }
subtotalPrice { gross { amount } }
}
errors {
field
message
code
}
}
}
`);
if (createResult.checkoutCreate.errors?.length > 0) {
console.error('Checkout creation errors:', createResult.checkoutCreate.errors);
throw new Error('Checkout creation failed');
}
if (!createResult.checkoutCreate.checkout) {
console.error('Create result:', createResult);
throw new Error('Checkout creation returned null');
}
const checkout = createResult.checkoutCreate.checkout;
const token = checkout.token;
console.log('Created checkout:');
console.log(' ID:', checkout.id);
console.log(' Token:', token);
console.log(' Initial Total:', checkout.totalPrice.gross.amount);
// Set address
await saleorFetch(`
mutation {
checkoutShippingAddressUpdate(
checkoutId: "${checkout.id}"
shippingAddress: {
firstName: "Test"
lastName: "User"
streetAddress1: "123 Street"
city: "Belgrade"
postalCode: "11000"
country: "RS"
phone: "+38160123456"
}
) {
checkout {
shippingMethods { id name price { amount } }
}
}
}
`);
// Query by token (what refreshCheckout does)
const tokenQuery = await saleorFetch(`
query {
checkout(token: "${token}") {
id
token
totalPrice { gross { amount } }
subtotalPrice { gross { amount } }
shippingPrice { gross { amount } }
shippingMethods { id name price { amount } }
}
}
`);
console.log('\nQuery by token (before shipping method):');
console.log(' Total:', tokenQuery.checkout.totalPrice.gross.amount);
console.log(' Subtotal:', tokenQuery.checkout.subtotalPrice.gross.amount);
console.log(' Shipping:', tokenQuery.checkout.shippingPrice.gross.amount);
console.log(' Methods:', tokenQuery.checkout.shippingMethods.length);
if (tokenQuery.checkout.shippingMethods.length > 0) {
const methodId = tokenQuery.checkout.shippingMethods[0].id;
// Set shipping method
await saleorFetch(`
mutation {
checkoutShippingMethodUpdate(
checkoutId: "${checkout.id}"
shippingMethodId: "${methodId}"
) {
checkout {
totalPrice { gross { amount } }
subtotalPrice { gross { amount } }
shippingPrice { gross { amount } }
}
}
}
`);
// Query by token again (what should happen after refreshCheckout)
const afterMethod = await saleorFetch(`
query {
checkout(token: "${token}") {
totalPrice { gross { amount } }
subtotalPrice { gross { amount } }
shippingPrice { gross { amount } }
}
}
`);
console.log('\nQuery by token (AFTER shipping method):');
console.log(' Total:', afterMethod.checkout.totalPrice.gross.amount);
console.log(' Subtotal:', afterMethod.checkout.subtotalPrice.gross.amount);
console.log(' Shipping:', afterMethod.checkout.shippingPrice.gross.amount);
}
}
test().catch(console.error);

View File

@@ -0,0 +1,254 @@
#!/usr/bin/env node
/**
* Complete API test simulating frontend checkout flow
* Tests every step the frontend takes
*/
const SALEOR_API_URL = 'https://api.manoonoils.com/graphql/';
async function saleorFetch(query, variables = {}) {
const response = await fetch(SALEOR_API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: query.replace(/\n\s*/g, ' '), variables }),
});
const result = await response.json();
if (result.errors) {
console.error('GraphQL Error:', JSON.stringify(result.errors, null, 2));
throw new Error(result.errors[0].message);
}
return result.data;
}
async function runTest() {
console.log('🧪 TESTING FRONTEND CHECKOUT FLOW\n');
console.log('=' .repeat(50));
let checkoutId = null;
let checkoutToken = null;
let shippingMethodId = null;
try {
// STEP 1: Create checkout (like frontend does on first cart add)
console.log('\n📦 STEP 1: Create Checkout');
console.log('-'.repeat(50));
const createResult = await saleorFetch(`
mutation CheckoutCreate($input: CheckoutCreateInput!) {
checkoutCreate(input: $input) {
checkout {
id
token
totalPrice { gross { amount currency } }
subtotalPrice { gross { amount } }
}
errors { field message }
}
}
`, {
input: {
channel: "default-channel",
email: "test@test.com",
lines: [],
languageCode: "SR"
}
});
checkoutId = createResult.checkoutCreate.checkout.id;
checkoutToken = createResult.checkoutCreate.checkout.token;
console.log('✅ Checkout created');
console.log(' ID:', checkoutId);
console.log(' Token:', checkoutToken);
console.log(' Initial Total:', createResult.checkoutCreate.checkout.totalPrice.gross.amount, 'RSD');
// STEP 2: Add product (like frontend does)
console.log('\n🛒 STEP 2: Add Product to Cart');
console.log('-'.repeat(50));
// Get a valid variant first
const productsResult = await saleorFetch(`
query {
products(channel: "default-channel", first: 1) {
edges {
node {
variants { id name }
}
}
}
}
`);
const variantId = productsResult.products.edges[0].node.variants[0].id;
const addLineResult = await saleorFetch(`
mutation CheckoutLinesAdd($checkoutId: ID!, $lines: [CheckoutLineInput!]!) {
checkoutLinesAdd(checkoutId: $checkoutId, lines: $lines) {
checkout {
id
token
totalPrice { gross { amount currency } }
subtotalPrice { gross { amount } }
}
errors { field message }
}
}
`, {
checkoutId: checkoutId,
lines: [{ variantId: variantId, quantity: 1 }]
});
const afterAdd = addLineResult.checkoutLinesAdd.checkout;
console.log('✅ Product added');
console.log(' Product Total:', afterAdd.totalPrice.gross.amount, 'RSD');
console.log(' Subtotal:', afterAdd.subtotalPrice.gross.amount, 'RSD');
// STEP 3: Refresh checkout by token (what refreshCheckout() does)
console.log('\n🔄 STEP 3: Refresh Checkout by Token');
console.log('-'.repeat(50));
console.log(' (This simulates what refreshCheckout() does in the store)');
const refreshResult = await saleorFetch(`
query GetCheckout($token: UUID!) {
checkout(token: $token) {
id
token
totalPrice { gross { amount currency } }
subtotalPrice { gross { amount } }
}
}
`, { token: checkoutToken });
console.log('✅ Refreshed checkout');
console.log(' Total from refresh:', refreshResult.checkout.totalPrice.gross.amount, 'RSD');
// STEP 4: Set shipping address
console.log('\n📍 STEP 4: Set Shipping Address');
console.log('-'.repeat(50));
const addressResult = await saleorFetch(`
mutation CheckoutShippingAddressUpdate($checkoutId: ID!, $shippingAddress: AddressInput!) {
checkoutShippingAddressUpdate(checkoutId: $checkoutId, shippingAddress: $shippingAddress) {
checkout {
id
shippingMethods { id name price { amount currency } }
}
errors { field message }
}
}
`, {
checkoutId: checkoutId,
shippingAddress: {
firstName: "Test",
lastName: "User",
streetAddress1: "123 Test Street",
city: "Belgrade",
postalCode: "11000",
country: "RS",
phone: "+38160123456"
}
});
const methods = addressResult.checkoutShippingAddressUpdate.checkout.shippingMethods;
console.log('✅ Address set');
console.log(' Available shipping methods:', methods.length);
if (methods.length === 0) {
console.log('❌ No shipping methods available!');
return;
}
methods.forEach((m, i) => {
console.log(` [${i+1}] ${m.name}: ${m.price.amount} ${m.price.currency}`);
});
shippingMethodId = methods[0].id;
const shippingPrice = methods[0].price.amount;
// STEP 5: Select shipping method (what happens when user clicks radio button)
console.log('\n🚚 STEP 5: Select Shipping Method');
console.log('-'.repeat(50));
console.log(` Selecting: ${methods[0].name} (${shippingPrice} RSD)`);
const methodResult = await saleorFetch(`
mutation CheckoutShippingMethodUpdate($checkoutId: ID!, $shippingMethodId: ID!) {
checkoutShippingMethodUpdate(checkoutId: $checkoutId, shippingMethodId: $shippingMethodId) {
checkout {
id
totalPrice { gross { amount currency } }
subtotalPrice { gross { amount } }
shippingPrice { gross { amount } }
}
errors { field message }
}
}
`, {
checkoutId: checkoutId,
shippingMethodId: shippingMethodId
});
const afterMethod = methodResult.checkoutShippingMethodUpdate.checkout;
console.log('✅ Shipping method set');
console.log(' Total:', afterMethod.totalPrice.gross.amount, 'RSD');
console.log(' Subtotal:', afterMethod.subtotalPrice.gross.amount, 'RSD');
console.log(' Shipping:', afterMethod.shippingPrice.gross.amount, 'RSD');
// STEP 6: Refresh checkout again (what refreshCheckout() does after setting method)
console.log('\n🔄 STEP 6: Refresh Checkout Again');
console.log('-'.repeat(50));
console.log(' (Simulating refreshCheckout() call in handleShippingMethodSelect)');
const finalRefresh = await saleorFetch(`
query GetCheckout($token: UUID!) {
checkout(token: $token) {
id
token
totalPrice { gross { amount currency } }
subtotalPrice { gross { amount } }
shippingPrice { gross { amount } }
}
}
`, { token: checkoutToken });
const final = finalRefresh.checkout;
console.log('✅ Final checkout state after refresh:');
console.log(' Total:', final.totalPrice.gross.amount, 'RSD');
console.log(' Subtotal:', final.subtotalPrice.gross.amount, 'RSD');
console.log(' Shipping:', final.shippingPrice.gross.amount, 'RSD');
// VERIFICATION
console.log('\n📊 VERIFICATION');
console.log('=' .repeat(50));
const expectedTotal = final.subtotalPrice.gross.amount + final.shippingPrice.gross.amount;
const actualTotal = final.totalPrice.gross.amount;
if (actualTotal === expectedTotal) {
console.log('✅ PASS: API returns correct total with shipping');
console.log(` ${final.subtotalPrice.gross.amount} + ${final.shippingPrice.gross.amount} = ${actualTotal}`);
} else {
console.log('❌ FAIL: API total does not include shipping');
console.log(` Expected: ${expectedTotal}, Got: ${actualTotal}`);
}
console.log('\n🔍 FRONTEND ISSUE ANALYSIS');
console.log('=' .repeat(50));
console.log('The API works correctly. The bug is in the frontend.');
console.log('');
console.log('What should happen:');
console.log(' 1. User selects shipping method → handleShippingMethodSelect()');
console.log(' 2. Calls checkoutService.updateShippingMethod() → API updates');
console.log(' 3. Calls refreshCheckout() → store updates with new checkout');
console.log(' 4. Component re-renders with new checkout.totalPrice');
console.log('');
console.log('Check browser console for:');
console.log(' - [Checkout Debug] logs showing totalPrice values');
console.log(' - Network tab showing the GraphQL mutation/refresh calls');
console.log(' - React DevTools showing if checkout object updates');
} catch (error) {
console.error('\n❌ Test failed:', error.message);
process.exit(1);
}
}
runTest();

View File

@@ -0,0 +1,232 @@
#!/usr/bin/env node
/**
* Full order creation test via API
* Tests complete checkout flow including order completion
*/
const SALEOR_API_URL = 'https://api.manoonoils.com/graphql/';
async function saleorFetch(query, variables = {}) {
const response = await fetch(SALEOR_API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query: query.replace(/\n\s*/g, ' '), variables }),
});
const result = await response.json();
if (result.errors) {
console.error('GraphQL Error:', JSON.stringify(result.errors, null, 2));
throw new Error(result.errors[0].message);
}
return result.data;
}
async function runOrderTest() {
console.log('🧪 FULL ORDER CREATION TEST ON DEV BRANCH\n');
console.log('=' .repeat(60));
try {
// STEP 1: Create checkout
console.log('\n📦 STEP 1: Create Checkout');
const createResult = await saleorFetch(`
mutation CheckoutCreate($input: CheckoutCreateInput!) {
checkoutCreate(input: $input) {
checkout {
id
token
totalPrice { gross { amount currency } }
}
errors { field message }
}
}
`, {
input: {
channel: "default-channel",
email: "test-order@example.com",
lines: [],
languageCode: "SR"
}
});
const checkoutId = createResult.checkoutCreate.checkout.id;
console.log('✅ Checkout created:', checkoutId);
// STEP 2: Get product and add to cart
console.log('\n🛒 STEP 2: Add Product');
const productsResult = await saleorFetch(`
query {
products(channel: "default-channel", first: 1) {
edges { node { variants { id name } } }
}
}
`);
const variantId = productsResult.products.edges[0].node.variants[0].id;
await saleorFetch(`
mutation CheckoutLinesAdd($checkoutId: ID!, $lines: [CheckoutLineInput!]!) {
checkoutLinesAdd(checkoutId: $checkoutId, lines: $lines) {
checkout { id }
errors { field message }
}
}
`, {
checkoutId: checkoutId,
lines: [{ variantId: variantId, quantity: 1 }]
});
console.log('✅ Product added');
// STEP 3: Update email
console.log('\n📧 STEP 3: Update Email');
await saleorFetch(`
mutation CheckoutEmailUpdate($checkoutId: ID!, $email: String!) {
checkoutEmailUpdate(checkoutId: $checkoutId, email: $email) {
checkout { id }
errors { field message }
}
}
`, { checkoutId: checkoutId, email: "test-order@example.com" });
console.log('✅ Email updated');
// STEP 4: Set shipping address
console.log('\n📍 STEP 4: Set Shipping Address');
await saleorFetch(`
mutation CheckoutShippingAddressUpdate($checkoutId: ID!, $shippingAddress: AddressInput!) {
checkoutShippingAddressUpdate(checkoutId: $checkoutId, shippingAddress: $shippingAddress) {
checkout {
id
shippingMethods { id name price { amount } }
}
errors { field message }
}
}
`, {
checkoutId: checkoutId,
shippingAddress: {
firstName: "Test",
lastName: "User",
streetAddress1: "123 Test Street",
city: "Belgrade",
postalCode: "11000",
country: "RS",
phone: "+38160123456"
}
});
// Get shipping methods
const methodsResult = await saleorFetch(`
query GetCheckout($token: UUID!) {
checkout(token: $token) {
shippingMethods { id name price { amount } }
}
}
`, { token: createResult.checkoutCreate.checkout.token });
const shippingMethodId = methodsResult.checkout.shippingMethods[0].id;
console.log('✅ Address set, shipping method available:', methodsResult.checkout.shippingMethods[0].name);
// STEP 5: Set billing address
console.log('\n💳 STEP 5: Set Billing Address');
await saleorFetch(`
mutation CheckoutBillingAddressUpdate($checkoutId: ID!, $billingAddress: AddressInput!) {
checkoutBillingAddressUpdate(checkoutId: $checkoutId, billingAddress: $billingAddress) {
checkout { id }
errors { field message }
}
}
`, {
checkoutId: checkoutId,
billingAddress: {
firstName: "Test",
lastName: "User",
streetAddress1: "123 Test Street",
city: "Belgrade",
postalCode: "11000",
country: "RS",
phone: "+38160123456"
}
});
console.log('✅ Billing address set');
// STEP 6: Select shipping method
console.log('\n🚚 STEP 6: Select Shipping Method');
await saleorFetch(`
mutation CheckoutShippingMethodUpdate($checkoutId: ID!, $shippingMethodId: ID!) {
checkoutShippingMethodUpdate(checkoutId: $checkoutId, shippingMethodId: $shippingMethodId) {
checkout {
id
totalPrice { gross { amount } }
subtotalPrice { gross { amount } }
shippingPrice { gross { amount } }
}
errors { field message }
}
}
`, { checkoutId: checkoutId, shippingMethodId: shippingMethodId });
console.log('✅ Shipping method selected');
// STEP 7: Complete checkout (create order)
console.log('\n✅ STEP 7: Complete Checkout (Create Order)');
console.log('-'.repeat(60));
const completeResult = await saleorFetch(`
mutation CheckoutComplete($checkoutId: ID!) {
checkoutComplete(checkoutId: $checkoutId) {
order {
id
number
status
created
total {
gross { amount currency }
}
subtotal {
gross { amount }
}
shippingPrice {
gross { amount }
}
}
errors { field message }
}
}
`, { checkoutId: checkoutId });
if (completeResult.checkoutComplete.errors?.length > 0) {
throw new Error(`Order creation failed: ${completeResult.checkoutComplete.errors[0].message}`);
}
const order = completeResult.checkoutComplete.order;
console.log('✅ ORDER CREATED SUCCESSFULLY!');
console.log('');
console.log('Order Details:');
console.log(' Order ID:', order.id);
console.log(' Order Number:', order.number);
console.log(' Status:', order.status);
console.log(' Created:', order.created);
console.log('');
console.log('Pricing:');
console.log(' Subtotal:', order.subtotal.gross.amount, 'RSD');
console.log(' Shipping:', order.shippingPrice.gross.amount, 'RSD');
console.log(' Total:', order.total.gross.amount, 'RSD');
// Verification
const expectedTotal = order.subtotal.gross.amount + order.shippingPrice.gross.amount;
console.log('');
console.log('📊 VERIFICATION:');
if (order.total.gross.amount === expectedTotal) {
console.log('✅ PASS: Order total includes shipping correctly');
console.log(` ${order.subtotal.gross.amount} + ${order.shippingPrice.gross.amount} = ${order.total.gross.amount}`);
} else {
console.log('❌ FAIL: Order total does not match expected');
}
console.log('');
console.log('🎉 DEV BRANCH TEST COMPLETE - ALL SYSTEMS GO!');
} catch (error) {
console.error('\n❌ Test failed:', error.message);
process.exit(1);
}
}
runOrderTest();

View File

@@ -3,6 +3,8 @@ import { NextIntlClientProvider } from "next-intl";
import { getMessages, setRequestLocale } from "next-intl/server"; import { getMessages, setRequestLocale } from "next-intl/server";
import { SUPPORTED_LOCALES, DEFAULT_LOCALE, isValidLocale } from "@/lib/i18n/locales"; import { SUPPORTED_LOCALES, DEFAULT_LOCALE, isValidLocale } from "@/lib/i18n/locales";
import { OpenPanelComponent } from "@openpanel/nextjs"; import { OpenPanelComponent } from "@openpanel/nextjs";
import Script from "next/script";
import { RYBBIT_HOST, RYBBIT_SITE_ID } from "@/lib/services/RybbitService";
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://dev.manoonoils.com"; const baseUrl = process.env.NEXT_PUBLIC_SITE_URL || "https://dev.manoonoils.com";
@@ -46,13 +48,18 @@ export default async function LocaleLayout({
return ( return (
<> <>
<OpenPanelComponent <OpenPanelComponent
clientId={process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID || ""} clientId={process.env.NEXT_PUBLIC_OPENPANEL_CLIENT_ID || ""}
trackScreenViews={true} trackScreenViews={true}
trackOutgoingLinks={true} trackOutgoingLinks={true}
apiUrl="https://op.nodecrew.me/api" apiUrl="https://op.nodecrew.me/api"
scriptUrl="https://op.nodecrew.me/op1.js" scriptUrl="https://op.nodecrew.me/op1.js"
/> />
<Script
src={`${RYBBIT_HOST}/api/script.js`}
data-site-id={RYBBIT_SITE_ID}
strategy="afterInteractive"
/>
<NextIntlClientProvider messages={messages}> <NextIntlClientProvider messages={messages}>
{children} {children}
</NextIntlClientProvider> </NextIntlClientProvider>

View File

@@ -2,11 +2,119 @@
import { useOpenPanel } from "@openpanel/nextjs"; import { useOpenPanel } from "@openpanel/nextjs";
import { useCallback } from "react"; import { useCallback } from "react";
import {
trackRybbitProductView,
trackRybbitAddToCart,
trackRybbitRemoveFromCart,
trackRybbitCheckoutStarted,
trackRybbitCheckoutStep,
trackRybbitOrderCompleted,
trackRybbitSearch,
trackRybbitExternalLink,
trackRybbitCartView,
trackRybbitWishlistAdd,
trackRybbitUserLogin,
trackRybbitUserRegister,
} from "@/lib/services/RybbitService";
export function useAnalytics() { export function useAnalytics() {
const op = useOpenPanel(); const op = useOpenPanel();
// Client-side tracking for user behavior // Helper to track with both OpenPanel and Rybbit
const trackDual = useCallback((
eventName: string,
openPanelData: Record<string, any>
) => {
// OpenPanel tracking
try {
op.track(eventName, openPanelData);
} catch (e) {
console.error("[OpenPanel] Tracking error:", e);
}
// Rybbit tracking (fire-and-forget)
try {
switch (eventName) {
case "product_viewed":
trackRybbitProductView({
id: openPanelData.product_id,
name: openPanelData.product_name,
price: openPanelData.price,
currency: openPanelData.currency,
category: openPanelData.category,
});
break;
case "add_to_cart":
trackRybbitAddToCart({
id: openPanelData.product_id,
name: openPanelData.product_name,
price: openPanelData.price,
currency: openPanelData.currency,
quantity: openPanelData.quantity,
variant: openPanelData.variant,
});
break;
case "remove_from_cart":
trackRybbitRemoveFromCart({
id: openPanelData.product_id,
name: openPanelData.product_name,
quantity: openPanelData.quantity,
});
break;
case "cart_view":
trackRybbitCartView({
total: openPanelData.cart_total,
currency: openPanelData.currency,
item_count: openPanelData.item_count,
});
break;
case "checkout_started":
trackRybbitCheckoutStarted({
total: openPanelData.cart_total,
currency: openPanelData.currency,
item_count: openPanelData.item_count,
items: openPanelData.items,
});
break;
case "checkout_step":
trackRybbitCheckoutStep(openPanelData.step, openPanelData);
break;
case "order_completed":
trackRybbitOrderCompleted({
order_id: openPanelData.order_id,
order_number: openPanelData.order_number,
total: openPanelData.total,
currency: openPanelData.currency,
item_count: openPanelData.item_count,
shipping_cost: openPanelData.shipping_cost,
customer_email: openPanelData.customer_email,
payment_method: openPanelData.payment_method,
});
break;
case "search":
trackRybbitSearch(openPanelData.query, openPanelData.results_count);
break;
case "external_link_click":
trackRybbitExternalLink(openPanelData.url, openPanelData.label);
break;
case "wishlist_add":
trackRybbitWishlistAdd({
id: openPanelData.product_id,
name: openPanelData.product_name,
});
break;
case "user_login":
trackRybbitUserLogin(openPanelData.method);
break;
case "user_register":
trackRybbitUserRegister(openPanelData.method);
break;
}
} catch (e) {
console.warn("[Rybbit] Tracking error:", e);
}
}, [op]);
const trackProductView = useCallback((product: { const trackProductView = useCallback((product: {
id: string; id: string;
name: string; name: string;
@@ -14,19 +122,15 @@ export function useAnalytics() {
currency: string; currency: string;
category?: string; category?: string;
}) => { }) => {
try { trackDual("product_viewed", {
op.track("product_viewed", { product_id: product.id,
product_id: product.id, product_name: product.name,
product_name: product.name, price: product.price,
price: product.price, currency: product.currency,
currency: product.currency, category: product.category,
category: product.category, source: "client",
source: "client", });
}); }, [trackDual]);
} catch (e) {
console.error("[Client Analytics] Product view error:", e);
}
}, [op]);
const trackAddToCart = useCallback((product: { const trackAddToCart = useCallback((product: {
id: string; id: string;
@@ -36,37 +140,42 @@ export function useAnalytics() {
quantity: number; quantity: number;
variant?: string; variant?: string;
}) => { }) => {
try { trackDual("add_to_cart", {
op.track("add_to_cart", { product_id: product.id,
product_id: product.id, product_name: product.name,
product_name: product.name, price: product.price,
price: product.price, currency: product.currency,
currency: product.currency, quantity: product.quantity,
quantity: product.quantity, variant: product.variant,
variant: product.variant, source: "client",
source: "client", });
}); }, [trackDual]);
} catch (e) {
console.error("[Client Analytics] Add to cart error:", e);
}
}, [op]);
const trackRemoveFromCart = useCallback((product: { const trackRemoveFromCart = useCallback((product: {
id: string; id: string;
name: string; name: string;
quantity: number; quantity: number;
}) => { }) => {
try { trackDual("remove_from_cart", {
op.track("remove_from_cart", { product_id: product.id,
product_id: product.id, product_name: product.name,
product_name: product.name, quantity: product.quantity,
quantity: product.quantity, source: "client",
source: "client", });
}); }, [trackDual]);
} catch (e) {
console.error("[Client Analytics] Remove from cart error:", e); const trackCartView = useCallback((cart: {
} total: number;
}, [op]); currency: string;
item_count: number;
}) => {
trackDual("cart_view", {
cart_total: cart.total,
currency: cart.currency,
item_count: cart.item_count,
source: "client",
});
}, [trackDual]);
const trackCheckoutStarted = useCallback((cart: { const trackCheckoutStarted = useCallback((cart: {
total: number; total: number;
@@ -79,36 +188,23 @@ export function useAnalytics() {
price: number; price: number;
}>; }>;
}) => { }) => {
try { trackDual("checkout_started", {
op.track("checkout_started", { cart_total: cart.total,
cart_total: cart.total, currency: cart.currency,
currency: cart.currency, item_count: cart.item_count,
item_count: cart.item_count, items: cart.items,
items: cart.items, source: "client",
source: "client", });
}); }, [trackDual]);
} catch (e) {
console.error("[Client Analytics] Checkout started error:", e);
}
}, [op]);
const trackCheckoutStep = useCallback((step: string, data?: Record<string, unknown>) => { const trackCheckoutStep = useCallback((step: string, data?: Record<string, unknown>) => {
try { trackDual("checkout_step", {
op.track("checkout_step", { step,
step, ...data,
...data, source: "client",
source: "client", });
}); }, [trackDual]);
} catch (e) {
console.error("[Client Analytics] Checkout step error:", e);
}
}, [op]);
/**
* DUAL TRACKING: Order completion
* 1. Track client-side (immediate, captures user session)
* 2. Call server-side API (reliable, can't be blocked)
*/
const trackOrderCompleted = useCallback(async (order: { const trackOrderCompleted = useCallback(async (order: {
order_id: string; order_id: string;
order_number: string; order_number: string;
@@ -119,37 +215,34 @@ export function useAnalytics() {
customer_email?: string; customer_email?: string;
payment_method?: string; payment_method?: string;
}) => { }) => {
console.log("[Dual Analytics] Tracking order:", order.order_number, "Total:", order.total); console.log("[Analytics] Tracking order:", order.order_number);
// CLIENT-SIDE: Track immediately for user session data // Track with both OpenPanel and Rybbit
trackDual("order_completed", {
order_id: order.order_id,
order_number: order.order_number,
total: order.total,
currency: order.currency,
item_count: order.item_count,
shipping_cost: order.shipping_cost,
customer_email: order.customer_email,
payment_method: order.payment_method,
source: "client",
});
// OpenPanel revenue tracking
try { try {
op.track("order_completed", {
order_id: order.order_id,
order_number: order.order_number,
total: order.total,
currency: order.currency,
item_count: order.item_count,
shipping_cost: order.shipping_cost,
customer_email: order.customer_email,
payment_method: order.payment_method,
source: "client",
});
op.revenue(order.total, { op.revenue(order.total, {
currency: order.currency, currency: order.currency,
transaction_id: order.order_number, transaction_id: order.order_number,
source: "client", source: "client",
}); });
console.log("[Client Analytics] Order tracked");
} catch (e) { } catch (e) {
console.error("[Client Analytics] Order tracking error:", e); console.error("[OpenPanel] Revenue tracking error:", e);
} }
// SERVER-SIDE: Call API for reliable tracking // Server-side tracking for reliability
try { try {
console.log("[Server Analytics] Calling server-side tracking API...");
const response = await fetch("/api/analytics/track-order", { const response = await fetch("/api/analytics/track-order", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: { "Content-Type": "application/json" },
@@ -165,39 +258,54 @@ export function useAnalytics() {
}), }),
}); });
if (response.ok) { if (!response.ok) {
console.log("[Server Analytics] Order tracked successfully");
} else {
console.error("[Server Analytics] Failed:", await response.text()); console.error("[Server Analytics] Failed:", await response.text());
} }
} catch (e) { } catch (e) {
console.error("[Server Analytics] API call failed:", e); console.error("[Server Analytics] API call failed:", e);
} }
}, [op]); }, [op, trackDual]);
const trackSearch = useCallback((query: string, results_count: number) => { const trackSearch = useCallback((query: string, results_count: number) => {
try { trackDual("search", {
op.track("search", { query,
query, results_count,
results_count, source: "client",
source: "client", });
}); }, [trackDual]);
} catch (e) {
console.error("[Client Analytics] Search error:", e);
}
}, [op]);
const trackExternalLink = useCallback((url: string, label?: string) => { const trackExternalLink = useCallback((url: string, label?: string) => {
try { trackDual("external_link_click", {
op.track("external_link_click", { url,
url, label,
label, source: "client",
source: "client", });
}); }, [trackDual]);
} catch (e) {
console.error("[Client Analytics] External link error:", e); const trackWishlistAdd = useCallback((product: {
} id: string;
}, [op]); name: string;
}) => {
trackDual("wishlist_add", {
product_id: product.id,
product_name: product.name,
source: "client",
});
}, [trackDual]);
const trackUserLogin = useCallback((method: string) => {
trackDual("user_login", {
method,
source: "client",
});
}, [trackDual]);
const trackUserRegister = useCallback((method: string) => {
trackDual("user_register", {
method,
source: "client",
});
}, [trackDual]);
const identifyUser = useCallback((user: { const identifyUser = useCallback((user: {
profileId: string; profileId: string;
@@ -213,7 +321,7 @@ export function useAnalytics() {
email: user.email, email: user.email,
}); });
} catch (e) { } catch (e) {
console.error("[Client Analytics] Identify error:", e); console.error("[OpenPanel] Identify error:", e);
} }
}, [op]); }, [op]);
@@ -221,11 +329,15 @@ export function useAnalytics() {
trackProductView, trackProductView,
trackAddToCart, trackAddToCart,
trackRemoveFromCart, trackRemoveFromCart,
trackCartView,
trackCheckoutStarted, trackCheckoutStarted,
trackCheckoutStep, trackCheckoutStep,
trackOrderCompleted, trackOrderCompleted,
trackSearch, trackSearch,
trackExternalLink, trackExternalLink,
trackWishlistAdd,
trackUserLogin,
trackUserRegister,
identifyUser, identifyUser,
}; };
} }

View File

@@ -0,0 +1,209 @@
"use client";
// Rybbit Analytics Service
// Self-hosted instance at rybbit.nodecrew.me
declare global {
interface Window {
rybbit?: {
event: (eventName: string, eventData?: Record<string, any>) => void;
pageview: () => void;
};
}
}
export const RYBBIT_HOST = "https://rybbit.nodecrew.me";
export const RYBBIT_SITE_ID = "1";
/**
* Check if Rybbit is loaded and available
*/
export function isRybbitAvailable(): boolean {
return typeof window !== "undefined" &&
!!window.rybbit &&
typeof window.rybbit.event === "function";
}
/**
* Track a custom event with Rybbit
*/
export function trackRybbitEvent(
eventName: string,
eventData?: Record<string, any>
): void {
if (isRybbitAvailable()) {
try {
window.rybbit!.event(eventName, eventData);
} catch (e) {
console.warn("[Rybbit] Event tracking error:", e);
}
} else {
console.warn("[Rybbit] Not available for event:", eventName);
}
}
/**
* Track page view manually (usually auto-tracked by Rybbit script)
*/
export function trackRybbitPageview(): void {
if (isRybbitAvailable()) {
try {
window.rybbit!.pageview();
} catch (e) {
console.warn("[Rybbit] Pageview error:", e);
}
}
}
// E-commerce Event Tracking Functions
export function trackRybbitProductView(product: {
id: string;
name: string;
price: number;
currency: string;
category?: string;
variant?: string;
}): void {
trackRybbitEvent("product_view", {
product_id: product.id,
product_name: product.name,
price: product.price,
currency: product.currency,
category: product.category,
variant: product.variant,
});
}
export function trackRybbitAddToCart(product: {
id: string;
name: string;
price: number;
currency: string;
quantity: number;
variant?: string;
}): void {
trackRybbitEvent("add_to_cart", {
product_id: product.id,
product_name: product.name,
price: product.price,
currency: product.currency,
quantity: product.quantity,
variant: product.variant,
});
}
export function trackRybbitRemoveFromCart(product: {
id: string;
name: string;
quantity: number;
}): void {
trackRybbitEvent("remove_from_cart", {
product_id: product.id,
product_name: product.name,
quantity: product.quantity,
});
}
export function trackRybbitCartView(cart: {
total: number;
currency: string;
item_count: number;
}): void {
trackRybbitEvent("cart_view", {
cart_total: cart.total,
currency: cart.currency,
item_count: cart.item_count,
});
}
export function trackRybbitCheckoutStarted(cart: {
total: number;
currency: string;
item_count: number;
items: Array<{
id: string;
name: string;
quantity: number;
price: number;
}>;
}): void {
trackRybbitEvent("checkout_started", {
cart_total: cart.total,
currency: cart.currency,
item_count: cart.item_count,
items: cart.items,
});
}
export function trackRybbitCheckoutStep(step: string, data?: Record<string, unknown>): void {
trackRybbitEvent("checkout_step", {
step,
...data,
});
}
export function trackRybbitOrderCompleted(order: {
order_id: string;
order_number: string;
total: number;
currency: string;
item_count: number;
shipping_cost?: number;
customer_email?: string;
payment_method?: string;
}): void {
trackRybbitEvent("order_completed", {
order_id: order.order_id,
order_number: order.order_number,
total: order.total,
currency: order.currency,
item_count: order.item_count,
shipping_cost: order.shipping_cost,
customer_email: order.customer_email,
payment_method: order.payment_method,
});
}
export function trackRybbitSearch(query: string, results_count: number): void {
trackRybbitEvent("search", {
query,
results_count,
});
}
export function trackRybbitExternalLink(url: string, label?: string): void {
trackRybbitEvent("external_link_click", {
url,
label,
});
}
export function trackRybbitNewsletterSignup(email: string, source: string): void {
trackRybbitEvent("newsletter_signup", {
email,
source,
});
}
export function trackRybbitWishlistAdd(product: {
id: string;
name: string;
}): void {
trackRybbitEvent("wishlist_add", {
product_id: product.id,
product_name: product.name,
});
}
export function trackRybbitUserLogin(method: string): void {
trackRybbitEvent("user_login", {
method,
});
}
export function trackRybbitUserRegister(method: string): void {
trackRybbitEvent("user_register", {
method,
});
}