datetime based feature flags

This commit is contained in:
Tom Hombergs
2022-01-15 09:44:52 +11:00
parent f935b37177
commit 3c85af86a9
9 changed files with 328 additions and 0 deletions

View File

@@ -1,5 +1,8 @@
package io.reflectoring.featureflags;
import java.time.LocalDateTime;
import java.util.Optional;
public interface FeatureFlagService {
/**
@@ -24,4 +27,21 @@ public interface FeatureFlagService {
Boolean isNewServiceEnabled();
/**
* Returns the current time to be used by the welcome message feature.
*/
Optional<LocalDateTime> currentDateForWelcomeMessage();
/**
* Returns the current time to be used by the welcome email feature.
*/
Optional<LocalDateTime> currentDateForWelcomeEmails();
Optional<LocalDateTime> currentDateForWelcomeMessage(String username);
/**
* Returns the current time to be used by the welcome email feature.
*/
Optional<LocalDateTime> currentDateForWelcomeEmails(String username);
}

View File

@@ -2,8 +2,10 @@ package io.reflectoring.featureflags;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class FeatureFlagsApplication {
public static void main(String[] args) {

View File

@@ -0,0 +1,49 @@
package io.reflectoring.featureflags.datebased;
import io.reflectoring.featureflags.FeatureFlagService;
import io.reflectoring.featureflags.web.UserSession;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.ModelAndView;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.Map;
import java.util.Optional;
@Controller
public class DateFeatureFlagController {
private final UserSession userSession;
private final FeatureFlagService featureFlagService;
DateFeatureFlagController(UserSession userSession, FeatureFlagService featureFlagService) {
this.userSession = userSession;
this.featureFlagService = featureFlagService;
}
@GetMapping(path = {"/welcome"})
ModelAndView welcome() {
userSession.login("ben");
Optional<LocalDateTime> date = featureFlagService.currentDateForWelcomeMessage();
if (date.isEmpty()) {
return new ModelAndView("/welcome-page-without-message.html");
}
LocalTime time = date.get().toLocalTime();
String welcomeMessage = "";
if (time.isBefore(LocalTime.NOON)) {
welcomeMessage = "Good Morning!";
} else if (time.isBefore(LocalTime.of(17, 0))) {
welcomeMessage = "Good Day!";
} else {
welcomeMessage = "Good Evening!";
}
return new ModelAndView("/welcome-page.html", Map.of("welcomeMessage", welcomeMessage));
}
}

View File

@@ -0,0 +1,70 @@
package io.reflectoring.featureflags.datebased;
import io.reflectoring.featureflags.FeatureFlagService;
import io.reflectoring.featureflags.web.UserSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
@Component
public class EmailSender {
private final Logger logger = LoggerFactory.getLogger(EmailSender.class);
private final FeatureFlagService featureFlagService;
public EmailSender(FeatureFlagService featureFlagService, UserSession userSession) {
this.featureFlagService = featureFlagService;
}
@Scheduled(fixedDelay = 10000)
public void sendWelcomeEmails() {
for (User user : getUsers()) {
Optional<LocalDateTime> now = featureFlagService.currentDateForWelcomeEmails(user.name);
if (now.isEmpty()) {
logger.info("not sending email to user {}", user.name);
continue;
}
if (user.registrationDate.isBefore(now.get().minusDays(14L).toLocalDate())) {
sendEmail(user, "Welcome email after 14 days");
} else if (user.registrationDate.isBefore(now.get().minusDays(7L).toLocalDate())) {
sendEmail(user, "Welcome email after 7 days");
} else if (user.registrationDate.isBefore(now.get().minusDays(1L).toLocalDate())) {
sendEmail(user, "Welcome email after 1 day");
}
}
}
private void sendEmail(User user, String message) {
logger.info("sending email to user {} with message {}", user.name, message);
}
private List<User> getUsers() {
return Arrays.asList(
new User("alice", LocalDate.now().minusDays(1)),
new User("bob", LocalDate.now().minusDays(2)),
new User("charlie", LocalDate.now().minusDays(3)),
new User("ben", LocalDate.now().minusDays(4)),
new User("tom", LocalDate.now().minusDays(5)),
new User("hugo", LocalDate.now().minusDays(6))
);
}
static class User {
private final String name;
private final LocalDate registrationDate;
User(String name, LocalDate registrationDate) {
this.name = name;
this.registrationDate = registrationDate;
}
}
}

View File

@@ -4,6 +4,9 @@ import io.reflectoring.featureflags.FeatureFlagService;
import org.ff4j.FF4j;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Optional;
@Component("ff4j")
public class FF4JFeatureFlagService implements FeatureFlagService {
@@ -38,4 +41,28 @@ public class FF4JFeatureFlagService implements FeatureFlagService {
return null;
}
@Override
public Optional<LocalDateTime> currentDateForWelcomeMessage() {
// not supported
return Optional.of(LocalDateTime.now());
}
@Override
public Optional<LocalDateTime> currentDateForWelcomeEmails() {
// not supported
return Optional.of(LocalDateTime.now());
}
@Override
public Optional<LocalDateTime> currentDateForWelcomeMessage(String username) {
// not supported
return Optional.of(LocalDateTime.now());
}
@Override
public Optional<LocalDateTime> currentDateForWelcomeEmails(String username) {
// not supported
return Optional.of(LocalDateTime.now());
}
}

View File

@@ -4,17 +4,25 @@ import com.launchdarkly.sdk.LDUser;
import com.launchdarkly.sdk.server.LDClient;
import io.reflectoring.featureflags.FeatureFlagService;
import io.reflectoring.featureflags.web.UserSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.Optional;
@Component("launchdarkly")
@Primary
public class LaunchDarklyFeatureFlagService implements FeatureFlagService {
private final Logger logger = LoggerFactory.getLogger(LaunchDarklyFeatureFlagService.class);
private final LDClient launchdarklyClient;
private final UserSession userSession;
private final DateTimeFormatter dateFormatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME;
public LaunchDarklyFeatureFlagService(LDClient launchdarklyClient, UserSession userSession) {
this.launchdarklyClient = launchdarklyClient;
@@ -63,9 +71,91 @@ public class LaunchDarklyFeatureFlagService implements FeatureFlagService {
return true;
}
@Override
public Optional<LocalDateTime> currentDateForWelcomeMessage() {
String stringValue = launchdarklyClient.stringVariation("current-date-for-welcome-message", getLaunchdarklyUserFromSession(), "false");
if ("false".equals(stringValue)) {
return Optional.empty();
}
if ("now".equals(stringValue)) {
return Optional.of(LocalDateTime.now());
}
try {
return Optional.of(LocalDateTime.parse(stringValue, dateFormatter));
} catch (DateTimeParseException e) {
logger.warn("could not parse date ... falling back to current date", e);
return Optional.of(LocalDateTime.now());
}
}
@Override
public Optional<LocalDateTime> currentDateForWelcomeEmails() {
String stringValue = launchdarklyClient.stringVariation("current-date-for-welcome-email", getLaunchdarklyUserFromSession(), "false");
if ("false".equals(stringValue)) {
return Optional.empty();
}
if ("now".equals(stringValue)) {
return Optional.of(LocalDateTime.now());
}
try {
return Optional.of(LocalDateTime.parse(stringValue, dateFormatter));
} catch (DateTimeParseException e) {
logger.warn("could not parse date ... falling back to current date", e);
return Optional.of(LocalDateTime.now());
}
}
@Override
public Optional<LocalDateTime> currentDateForWelcomeMessage(String username) {
String stringValue = launchdarklyClient.stringVariation("current-date-for-welcome-message", new LDUser.Builder(username).build(), "false");
if ("false".equals(stringValue)) {
return Optional.empty();
}
if ("now".equals(stringValue)) {
return Optional.of(LocalDateTime.now());
}
try {
return Optional.of(LocalDateTime.parse(stringValue, dateFormatter));
} catch (DateTimeParseException e) {
logger.warn("could not parse date ... falling back to current date", e);
return Optional.of(LocalDateTime.now());
}
}
@Override
public Optional<LocalDateTime> currentDateForWelcomeEmails(String username) {
String stringValue = launchdarklyClient.stringVariation("current-date-for-welcome-email", new LDUser.Builder(username).build(), "false");
if ("false".equals(stringValue)) {
return Optional.empty();
}
if ("now".equals(stringValue)) {
return Optional.of(LocalDateTime.now());
}
try {
return Optional.of(LocalDateTime.parse(stringValue, dateFormatter));
} catch (DateTimeParseException e) {
logger.warn("could not parse date ... falling back to current date", e);
return Optional.of(LocalDateTime.now());
}
}
private LDUser getLaunchdarklyUserFromSession() {
return new LDUser.Builder(userSession.getUsername())
.custom("clicked", userSession.hasClicked())
.build();
}
}

View File

@@ -3,6 +3,9 @@ package io.reflectoring.featureflags.togglz;
import io.reflectoring.featureflags.FeatureFlagService;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Optional;
@Component("togglz")
public class TooglzFeatureFlagService implements FeatureFlagService {
@@ -31,4 +34,29 @@ public class TooglzFeatureFlagService implements FeatureFlagService {
return false;
}
@Override
public Optional<LocalDateTime> currentDateForWelcomeMessage() {
// not supported
return Optional.of(LocalDateTime.now());
}
@Override
public Optional<LocalDateTime> currentDateForWelcomeEmails() {
// not supported
return Optional.of(LocalDateTime.now());
}
@Override
public Optional<LocalDateTime> currentDateForWelcomeMessage(String username) {
// not supported
return Optional.of(LocalDateTime.now());
}
@Override
public Optional<LocalDateTime> currentDateForWelcomeEmails(String username) {
// not supported
return Optional.of(LocalDateTime.now());
}
}

View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Welcome</title>
<style>
td {
padding:10px;
}
</style>
</head>
<body>
This is the version of the welcome page without a welcome message.
</body>
</html>

View File

@@ -0,0 +1,21 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Your features</title>
<style>
td {
padding:10px;
}
</style>
</head>
<body>
<h1 th:text="${welcomeMessage}"></h1>
</body>
</html>