Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,4 @@ DISABLE_SPRING=1
STOREDOG_URL=http://service-proxy:80 # base url for storedog service (default: 'http://service-proxy:80')
PUPPETEER_TIMEOUT=30000 # timeout for puppeteer (default: 30000)

SKIP_SESSION_CLOSE= # skip session close for puppeteer (default: ''). Note that the current puppeteer script doesn't make use of this environment variable but can easily be updated to do so
SKIP_SESSION_CLOSE= # skip session close for puppeteer (default: ''). Note that the current puppeteer script doesn't make use of this environment variable but can easily be updated to do so
122 changes: 113 additions & 9 deletions services/ads/java/src/main/java/adsjava/AdsJavaApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.http.MediaType;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.util.StreamUtils;
import java.io.*;
import java.net.URI;
import org.springframework.web.bind.annotation.ResponseBody;
import org.apache.commons.io.IOUtils;
import java.util.concurrent.ThreadLocalRandom;
import java.util.HashMap;
import java.util.Optional;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.concurrent.TimeoutException;
import org.springframework.web.bind.annotation.RequestHeader;
Expand Down Expand Up @@ -44,15 +49,67 @@ public String home() {
value = "/banners/{id}",
produces = MediaType.IMAGE_JPEG_VALUE
)
public @ResponseBody byte[] getImageWithMediaType() throws IOException {
logger.info("/banners/{id} called");
int randomNum = ThreadLocalRandom.current().nextInt(1, 3 + 1);
String imagePath = "/static/ads/ad" + randomNum + ".jpg";
InputStream in = getClass()
.getResourceAsStream(imagePath);
public @ResponseBody byte[] getImageWithMediaType(@PathVariable String id) throws IOException {
logger.info("/banners/{} called", id);

// Map the image path to the correct static file
String imagePath;
switch (id) {
case "1.jpg":
imagePath = "/static/ads/ad1.jpg";
break;
case "2.jpg":
imagePath = "/static/ads/ad2.jpg";
break;
case "3.jpg":
imagePath = "/static/ads/ad3.jpg";
break;
default:
// Fallback to random image if unknown
int randomNum = ThreadLocalRandom.current().nextInt(1, 3 + 1);
imagePath = "/static/ads/ad" + randomNum + ".jpg";
logger.warn("Unknown image id: {}, using random image", id);
}

InputStream in = getClass().getResourceAsStream(imagePath);
if (in == null) {
logger.error("Image not found: {}", imagePath);
throw new IOException("Image not found: " + imagePath);
}
return IOUtils.toByteArray(in);
}

@CrossOrigin(origins = {"*"})
@RequestMapping("/click/{id}")
public ResponseEntity<Void> handleAdClick(@PathVariable Long id) {
logger.info("Ad click for id: " + id);

Optional<Advertisement> adOptional = advertisementRepository.findById(id);
if (adOptional.isPresent()) {
Advertisement ad = adOptional.get();
String clickUrl = ad.getClickUrl();

// Log the click for analytics
logger.info("Redirecting ad '{}' (id: {}) to: {}", ad.getName(), id, clickUrl);

if (clickUrl != null && !clickUrl.isEmpty()) {
// Return a redirect response to the click URL
return ResponseEntity.status(HttpStatus.FOUND)
.location(URI.create(clickUrl))
.build();
} else {
// Default redirect if no clickUrl is set
logger.warn("No clickUrl set for ad id: " + id + ", redirecting to homepage");
return ResponseEntity.status(HttpStatus.FOUND)
.location(URI.create("/"))
.build();
}
} else {
logger.error("Ad not found for id: " + id);
return ResponseEntity.notFound().build();
}
}

@CrossOrigin(origins = {"*"})
@RequestMapping(
value = "/ads",
Expand Down Expand Up @@ -95,9 +152,56 @@ public static void main(String[] args) {
public CommandLineRunner initDb(AdvertisementRepository repository) {
return args -> {
if (repository.count() == 0) {
repository.save(new Advertisement("Discount Clothing", "1.jpg"));
repository.save(new Advertisement("Cool Hats", "2.jpg"));
repository.save(new Advertisement("Nice Bags", "3.jpg"));
// Create ads with meaningful click URLs that point to relevant frontend pages
// Based on actual image content, the files are mislabeled
// Image 1.jpg shows Discount Clothing content, 2.jpg shows Cool Hats content
repository.save(new Advertisement("Discount Clothing", "1.jpg", "/discount-clothing"));
repository.save(new Advertisement("Cool Hats", "2.jpg", "/cool-hats"));
repository.save(new Advertisement("Nice Bags", "3.jpg", "/nice-bags"));
logger.info("Initialized database with 3 advertisements with click URLs");
} else {
// Always update existing ads to ensure they have the correct click URLs
List<Advertisement> existingAds = repository.findAll();
boolean needsUpdate = false;

for (Advertisement ad : existingAds) {
String oldClickUrl = ad.getClickUrl();
switch (ad.getName()) {
case "Discount Clothing":
if (!"/discount-clothing".equals(oldClickUrl) || !"1.jpg".equals(ad.getPath())) {
ad.setClickUrl("/discount-clothing");
ad.setPath("1.jpg");
needsUpdate = true;
logger.info("Updated '{}' clickUrl from '{}' to '/discount-clothing' and path to '1.jpg'", ad.getName(), oldClickUrl);
}
break;
case "Cool Hats":
if (!"/cool-hats".equals(oldClickUrl) || !"2.jpg".equals(ad.getPath())) {
ad.setClickUrl("/cool-hats");
ad.setPath("2.jpg");
needsUpdate = true;
logger.info("Updated '{}' clickUrl from '{}' to '/cool-hats' and path to '2.jpg'", ad.getName(), oldClickUrl);
}
break;
case "Nice Bags":
if (!"/nice-bags".equals(oldClickUrl) || !"3.jpg".equals(ad.getPath())) {
ad.setClickUrl("/nice-bags");
ad.setPath("3.jpg");
needsUpdate = true;
logger.info("Updated '{}' clickUrl from '{}' to '/nice-bags' and path to '3.jpg'", ad.getName(), oldClickUrl);
}
break;
default:
logger.info("Unknown ad name: '{}', leaving clickUrl unchanged", ad.getName());
}
}

if (needsUpdate) {
repository.saveAll(existingAds);
logger.info("Successfully updated existing ads with correct click URLs");
} else {
logger.info("All ads already have correct click URLs, no update needed");
}
}
};
}
Expand Down
9 changes: 9 additions & 0 deletions services/ads/java/src/main/java/adsjava/Advertisement.java
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ public class Advertisement {
private Long id;
private String name;
private String path;
private String clickUrl;

public Advertisement() {}

Expand All @@ -17,10 +18,18 @@ public Advertisement(String name, String path) {
this.path = path;
}

public Advertisement(String name, String path, String clickUrl) {
this.name = name;
this.path = path;
this.clickUrl = clickUrl;
}

public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getPath() { return path; }
public void setPath(String path) { this.path = path; }
public String getClickUrl() { return clickUrl; }
public void setClickUrl(String clickUrl) { this.clickUrl = clickUrl; }
}
12 changes: 9 additions & 3 deletions services/backend/Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,9 @@ GEM
globalid (1.0.0)
activesupport (>= 5.0)
glyphicons (1.0.2)
google-protobuf (4.31.1-aarch64-linux-gnu)
bigdecimal
rake (>= 13)
google-protobuf (4.31.1-x86_64-linux-gnu)
bigdecimal
rake (>= 13)
Expand Down Expand Up @@ -301,7 +304,10 @@ GEM
addressable (~> 2.7)
letter_opener (1.7.0)
launchy (~> 2.2)
libdatadog (18.1.0.1.0-aarch64-linux)
libdatadog (18.1.0.1.0-x86_64-linux)
libddwaf (1.24.1.0.0-aarch64-linux)
ffi (~> 1.0)
libddwaf (1.24.1.0.0-x86_64-linux)
ffi (~> 1.0)
listen (3.7.1)
Expand Down Expand Up @@ -341,6 +347,8 @@ GEM
net-smtp (0.5.1)
net-protocol
nio4r (2.5.8)
nokogiri (1.13.3-aarch64-linux)
racc (~> 1.4)
nokogiri (1.13.3-x86_64-linux)
racc (~> 1.4)
octokit (4.22.0)
Expand Down Expand Up @@ -498,8 +506,6 @@ GEM
sendgrid-ruby (~> 6.4)
sendgrid-ruby (6.6.0)
ruby_http_client (~> 3.4)
sentry-raven (3.1.2)
faraday (>= 1.0)
sidekiq (6.4.0)
connection_pool (>= 2.2.2)
rack (~> 2.0)
Expand Down Expand Up @@ -683,6 +689,7 @@ GEM
zeitwerk (2.5.3)

PLATFORMS
aarch64-linux
x86_64-linux

DEPENDENCIES
Expand Down Expand Up @@ -714,7 +721,6 @@ DEPENDENCIES
sass-rails
sassc!
sendgrid-actionmailer
sentry-raven
sidekiq
spree (>= 4.4.0)
spree_auth_devise
Expand Down
56 changes: 46 additions & 10 deletions services/frontend/components/common/Ad/Ad.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@ import { useState, useEffect, useCallback } from 'react'
import { codeStash } from 'code-stash'
import config from '../../../featureFlags.config.json'

export interface AdDataResults {
data: object | null
// Proper TypeScript interface for Advertisement object from Java service
export interface Advertisement {
id: number
name: string
path: string
clickUrl: string
}

// Advertisement banner
function Ad() {
const [data, setData] = useState<AdDataResults | null>(null)
const [data, setData] = useState<Advertisement | null>(null)
const [isLoading, setLoading] = useState(false)
const adsPath = process.env.NEXT_PUBLIC_ADS_ROUTE || `/services/ads`

Expand All @@ -27,21 +31,45 @@ function Ad() {

try {
console.log('ads path', adsPath)
const res = await fetch(`${adsPath}/ads`, { headers })
// Add cache-busting parameter to ensure fresh data
const timestamp = Date.now()
const res = await fetch(`${adsPath}/ads?t=${timestamp}`, { headers })
if (!res.ok) {
throw new Error('Error fetching ad')
}
const data = await res.json()
console.log(data)
const index = getRandomArbitrary(0, data.length)
setData(data[index])
const data: Advertisement[] = await res.json()
console.log('Available ads:', data)
// Sort ads by ID to ensure consistent ordering
const sortedAds = data.sort((a, b) => a.id - b.id)
// Use a deterministic selection based on time to show different ads
// This ensures the visual ad matches the expected click behavior
const now = new Date()
const adIndex = Math.floor(now.getSeconds() / 5) % sortedAds.length // Change ad every 5 seconds
const selectedAd = sortedAds[adIndex]
console.log('Selected ad:', selectedAd)
setData(selectedAd)
setLoading(false)
} catch (e) {
console.error(e)
setLoading(false)
}
}, [adsPath, getRandomArbitrary, setData, setLoading])

const handleAdClick = useCallback(() => {
if (data?.id) {
console.log('Ad clicked!', {
adId: data.id,
adName: data.name,
clickUrl: data.clickUrl,
imagePath: data.path,
redirectUrl: `${adsPath}/click/${data.id}`
})
// Direct browser navigation to the click endpoint
// The Java service will handle the redirect to the appropriate URL
window.location.href = `${adsPath}/click/${data.id}`
}
}, [data, adsPath])

useEffect(() => {
if (!data) fetchAd()
}, [data, fetchAd])
Expand All @@ -61,9 +89,17 @@ function Ad() {

return (
<div className="flex flex-row justify-center py-4 advertisement-wrapper">
<picture className="advertisement-banner">
<picture
className="advertisement-banner cursor-pointer"
onClick={handleAdClick}
title={`Click to see ${data.name}`}
>
<source srcSet={`${adsPath}/banners/${data.path}`} type="image/webp" />
<img src={`${adsPath}/banners/${data.path}`} alt="Landscape picture" />
<img
src={`${adsPath}/banners/${data.path}`}
alt={data.name || "Advertisement"}
className="cursor-pointer"
/>
</picture>
</div>
)
Expand Down
Loading