Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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