Feature Flags en ABAP Cloud

Catégorie
DevOps
Publié
Auteur
Johannes

Les Feature Flags (également appelés Feature Toggles) permettent le déploiement progressif de nouvelles fonctionnalités sans déployer de nouveau code. En ABAP Cloud, vous pouvez implémenter vous-même les Feature Flags ou utiliser le SAP Feature Flags Service.

Que sont les Feature Flags ?

Les Feature Flags sont des commutateurs de configuration qui déterminent à l’exécution si une fonctionnalité est active ou non :

AspectSans Feature FlagsAvec Feature Flags
ReleaseTout ou rienActivation progressive
RollbackNouveau déploiement nécessaireImmédiat via Flag
A/B TestingComplexeFacilement réalisable
Fonctionnalités BetaBranche séparéeEn production, mais cachée
RisqueÉlevé pour les gros releasesContrôlé et minimal
FeedbackAprès release completTôt, de la part d’utilisateurs sélectionnés

Cas d’utilisation typiques

┌────────────────────────────────────────────────────────────────┐
│ Cas d'utilisation des Feature Flags │
├────────────────────────────────────────────────────────────────┤
│ │
│ 1. Release Toggles │
│ └─ Activer une nouvelle fonctionnalité pour tous quand prêt│
│ │
│ 2. Experiment Toggles │
│ └─ Tests A/B avec différentes variantes │
│ │
│ 3. Ops Toggles │
│ └─ Désactiver des fonctionnalités en cas de surcharge │
│ │
│ 4. Permission Toggles │
│ └─ Fonctionnalités uniquement pour certains utilisateurs │
│ │
│ 5. Kill Switches │
│ └─ Désactivation immédiate en cas de problème │
│ │
└────────────────────────────────────────────────────────────────┘

Implémentation personnalisée de Feature Flag

Modèle de données : Table Feature Flag

Créez d’abord une table pour les Feature Flags :

@EndUserText.label : 'Feature Flags"
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #C
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zfeature_flag {
key client : abap.clnt not null;
key flag_id : abap.char(40) not null;
flag_name : abap.char(100);
description : abap.char(255);
is_enabled : abap_boolean;
rollout_percent : abap.int2;
valid_from : abap.dats;
valid_to : abap.dats;
created_by : abap.uname;
created_at : timestampl;
changed_by : abap.uname;
changed_at : timestampl;
}

Flags spécifiques aux utilisateurs

Pour l’activation basée sur les utilisateurs ou les rôles :

@EndUserText.label : 'Feature Flag User Assignments"
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #C
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zfeature_flag_usr {
key client : abap.clnt not null;
key flag_id : abap.char(40) not null;
key user_id : abap.uname not null;
is_enabled : abap_boolean;
valid_from : abap.dats;
valid_to : abap.dats;
}

Flags basés sur les rôles

@EndUserText.label : 'Feature Flag Role Assignments"
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
@AbapCatalog.tableCategory : #TRANSPARENT
@AbapCatalog.deliveryClass : #C
@AbapCatalog.dataMaintenance : #RESTRICTED
define table zfeature_flag_rol {
key client : abap.clnt not null;
key flag_id : abap.char(40) not null;
key role_id : abap.char(40) not null;
is_enabled : abap_boolean;
valid_from : abap.dats;
valid_to : abap.dats;
}

Classe Service Feature Flag

Définition de l’interface

INTERFACE zif_feature_flag_service
PUBLIC.
TYPES:
BEGIN OF ty_flag_info,
flag_id TYPE zfeature_flag-flag_id,
flag_name TYPE zfeature_flag-flag_name,
is_enabled TYPE abap_bool,
rollout_percent TYPE zfeature_flag-rollout_percent,
source TYPE string,
END OF ty_flag_info.
METHODS:
"! Vérifie si une fonctionnalité est active pour l'utilisateur actuel
is_enabled
IMPORTING
iv_flag_id TYPE zfeature_flag-flag_id
iv_user_id TYPE sy-uname DEFAULT sy-uname
RETURNING
VALUE(rv_enabled) TYPE abap_bool,
"! Retourne tous les Feature Flags
get_all_flags
RETURNING
VALUE(rt_flags) TYPE STANDARD TABLE OF ty_flag_info WITH EMPTY KEY,
"! Active un Feature Flag
enable_flag
IMPORTING
iv_flag_id TYPE zfeature_flag-flag_id
RAISING
zcx_feature_flag,
"! Désactive un Feature Flag
disable_flag
IMPORTING
iv_flag_id TYPE zfeature_flag-flag_id
RAISING
zcx_feature_flag,
"! Définit le pourcentage de rollout
set_rollout_percentage
IMPORTING
iv_flag_id TYPE zfeature_flag-flag_id
iv_percent TYPE zfeature_flag-rollout_percent
RAISING
zcx_feature_flag.
ENDINTERFACE.

Implémentation du service

CLASS zcl_feature_flag_service DEFINITION
PUBLIC
FINAL
CREATE PRIVATE.
PUBLIC SECTION.
INTERFACES zif_feature_flag_service.
CLASS-METHODS:
get_instance
RETURNING
VALUE(ro_instance) TYPE REF TO zcl_feature_flag_service.
PRIVATE SECTION.
CLASS-DATA:
go_instance TYPE REF TO zcl_feature_flag_service.
DATA:
mt_flag_cache TYPE HASHED TABLE OF zfeature_flag
WITH UNIQUE KEY flag_id,
mv_cache_timestamp TYPE timestampl.
CONSTANTS:
c_cache_ttl_seconds TYPE i VALUE 300. " Cache de 5 minutes
METHODS:
refresh_cache_if_needed,
check_user_override
IMPORTING
iv_flag_id TYPE zfeature_flag-flag_id
iv_user_id TYPE sy-uname
EXPORTING
ev_found TYPE abap_bool
ev_enabled TYPE abap_bool,
check_role_override
IMPORTING
iv_flag_id TYPE zfeature_flag-flag_id
iv_user_id TYPE sy-uname
EXPORTING
ev_found TYPE abap_bool
ev_enabled TYPE abap_bool,
is_in_rollout_group
IMPORTING
iv_flag_id TYPE zfeature_flag-flag_id
iv_user_id TYPE sy-uname
iv_rollout_percent TYPE zfeature_flag-rollout_percent
RETURNING
VALUE(rv_in_group) TYPE abap_bool,
is_within_validity
IMPORTING
iv_valid_from TYPE dats
iv_valid_to TYPE dats
RETURNING
VALUE(rv_valid) TYPE abap_bool.
ENDCLASS.
CLASS zcl_feature_flag_service IMPLEMENTATION.
METHOD get_instance.
IF go_instance IS NOT BOUND.
go_instance = NEW #( ).
ENDIF.
ro_instance = go_instance.
ENDMETHOD.
METHOD zif_feature_flag_service~is_enabled.
DATA: lv_found TYPE abap_bool,
lv_enabled TYPE abap_bool.
" Mettre à jour le cache si nécessaire
refresh_cache_if_needed( ).
" 1. Vérifier si le Flag existe
READ TABLE mt_flag_cache INTO DATA(ls_flag)
WITH TABLE KEY flag_id = iv_flag_id.
IF sy-subrc <> 0.
" Flag non trouvé = désactivé
rv_enabled = abap_false.
RETURN.
ENDIF.
" 2. Vérifier la validité temporelle
IF NOT is_within_validity(
iv_valid_from = ls_flag-valid_from
iv_valid_to = ls_flag-valid_to ).
rv_enabled = abap_false.
RETURN.
ENDIF.
" 3. Vérifier le remplacement spécifique utilisateur
check_user_override(
EXPORTING
iv_flag_id = iv_flag_id
iv_user_id = iv_user_id
IMPORTING
ev_found = lv_found
ev_enabled = lv_enabled ).
IF lv_found = abap_true.
rv_enabled = lv_enabled.
RETURN.
ENDIF.
" 4. Vérifier le remplacement basé sur les rôles
check_role_override(
EXPORTING
iv_flag_id = iv_flag_id
iv_user_id = iv_user_id
IMPORTING
ev_found = lv_found
ev_enabled = lv_enabled ).
IF lv_found = abap_true.
rv_enabled = lv_enabled.
RETURN.
ENDIF.
" 5. Statut global du Flag
IF ls_flag-is_enabled = abap_false.
rv_enabled = abap_false.
RETURN.
ENDIF.
" 6. Vérifier le pourcentage de rollout
IF ls_flag-rollout_percent > 0 AND ls_flag-rollout_percent < 100.
rv_enabled = is_in_rollout_group(
iv_flag_id = iv_flag_id
iv_user_id = iv_user_id
iv_rollout_percent = ls_flag-rollout_percent ).
ELSE.
rv_enabled = xsdbool( ls_flag-rollout_percent = 100 OR ls_flag-is_enabled = abap_true ).
ENDIF.
ENDMETHOD.
METHOD refresh_cache_if_needed.
DATA: lv_current_ts TYPE timestampl,
lv_diff TYPE i.
GET TIME STAMP FIELD lv_current_ts.
IF mv_cache_timestamp IS INITIAL.
lv_diff = c_cache_ttl_seconds + 1.
ELSE.
lv_diff = cl_abap_tstmp=>subtract(
tstmp1 = lv_current_ts
tstmp2 = mv_cache_timestamp ).
ENDIF.
IF lv_diff > c_cache_ttl_seconds.
" Recharger le cache
SELECT * FROM zfeature_flag
INTO TABLE @mt_flag_cache.
mv_cache_timestamp = lv_current_ts.
ENDIF.
ENDMETHOD.
METHOD check_user_override.
DATA: lv_today TYPE dats.
ev_found = abap_false.
lv_today = cl_abap_context_info=>get_system_date( ).
SELECT SINGLE is_enabled
FROM zfeature_flag_usr
WHERE flag_id = @iv_flag_id
AND user_id = @iv_user_id
AND ( valid_from IS INITIAL OR valid_from <= @lv_today )
AND ( valid_to IS INITIAL OR valid_to >= @lv_today )
INTO @ev_enabled.
IF sy-subrc = 0.
ev_found = abap_true.
ENDIF.
ENDMETHOD.
METHOD check_role_override.
DATA: lv_today TYPE dats,
lt_roles TYPE STANDARD TABLE OF agr_users-agr_name.
ev_found = abap_false.
lv_today = cl_abap_context_info=>get_system_date( ).
" Déterminer les rôles de l'utilisateur (représentation simplifiée)
" En pratique via les APIs d'autorisation
SELECT agr_name FROM agr_users
WHERE uname = @iv_user_id
AND from_dat <= @lv_today
AND to_dat >= @lv_today
INTO TABLE @lt_roles.
IF lt_roles IS INITIAL.
RETURN.
ENDIF.
" Vérifier si un rôle remplace le flag
SELECT SINGLE is_enabled
FROM zfeature_flag_rol
WHERE flag_id = @iv_flag_id
AND role_id IN @( SELECT agr_name FROM @lt_roles AS roles )
AND ( valid_from IS INITIAL OR valid_from <= @lv_today )
AND ( valid_to IS INITIAL OR valid_to >= @lv_today )
INTO @ev_enabled.
IF sy-subrc = 0.
ev_found = abap_true.
ENDIF.
ENDMETHOD.
METHOD is_in_rollout_group.
DATA: lv_hash TYPE i,
lv_bucket TYPE i.
" Hash déterministe basé sur Flag-ID et User-ID
" Le même utilisateur obtient toujours le même résultat pour le même flag
DATA(lv_combined) = iv_flag_id && iv_user_id.
lv_hash = cl_abap_message_digest=>calculate_hash_for_char(
if_algorithm = 'MD5"
if_data = lv_combined ).
" Convertir le hash en bucket 0-99
lv_bucket = lv_hash MOD 100.
" Vérifier si le bucket est dans la plage de rollout
rv_in_group = xsdbool( lv_bucket < iv_rollout_percent ).
ENDMETHOD.
METHOD is_within_validity.
DATA(lv_today) = cl_abap_context_info=>get_system_date( ).
rv_valid = abap_true.
IF iv_valid_from IS NOT INITIAL AND iv_valid_from > lv_today.
rv_valid = abap_false.
RETURN.
ENDIF.
IF iv_valid_to IS NOT INITIAL AND iv_valid_to < lv_today.
rv_valid = abap_false.
ENDIF.
ENDMETHOD.
METHOD zif_feature_flag_service~get_all_flags.
refresh_cache_if_needed( ).
LOOP AT mt_flag_cache INTO DATA(ls_flag).
APPEND VALUE #(
flag_id = ls_flag-flag_id
flag_name = ls_flag-flag_name
is_enabled = ls_flag-is_enabled
rollout_percent = ls_flag-rollout_percent
source = 'DATABASE"
) TO rt_flags.
ENDLOOP.
ENDMETHOD.
METHOD zif_feature_flag_service~enable_flag.
UPDATE zfeature_flag
SET is_enabled = @abap_true,
changed_by = @sy-uname,
changed_at = @( cl_abap_context_info=>get_system_time( ) )
WHERE flag_id = @iv_flag_id.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_feature_flag
MESSAGE e001(zfeature_flag) WITH iv_flag_id.
ENDIF.
" Invalider le cache
CLEAR: mt_flag_cache, mv_cache_timestamp.
ENDMETHOD.
METHOD zif_feature_flag_service~disable_flag.
UPDATE zfeature_flag
SET is_enabled = @abap_false,
changed_by = @sy-uname,
changed_at = @( cl_abap_context_info=>get_system_time( ) )
WHERE flag_id = @iv_flag_id.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_feature_flag
MESSAGE e001(zfeature_flag) WITH iv_flag_id.
ENDIF.
" Invalider le cache
CLEAR: mt_flag_cache, mv_cache_timestamp.
ENDMETHOD.
METHOD zif_feature_flag_service~set_rollout_percentage.
IF iv_percent < 0 OR iv_percent > 100.
RAISE EXCEPTION TYPE zcx_feature_flag
MESSAGE e002(zfeature_flag) WITH iv_percent.
ENDIF.
UPDATE zfeature_flag
SET rollout_percent = @iv_percent,
changed_by = @sy-uname,
changed_at = @( cl_abap_context_info=>get_system_time( ) )
WHERE flag_id = @iv_flag_id.
IF sy-subrc <> 0.
RAISE EXCEPTION TYPE zcx_feature_flag
MESSAGE e001(zfeature_flag) WITH iv_flag_id.
ENDIF.
" Invalider le cache
CLEAR: mt_flag_cache, mv_cache_timestamp.
ENDMETHOD.
ENDCLASS.

Vérification des Feature Flags dans le code

Utilisation simple

CLASS zcl_order_service DEFINITION
PUBLIC
FINAL.
PUBLIC SECTION.
METHODS:
create_order
IMPORTING
is_order TYPE zs_order_create
RETURNING
VALUE(rs_result) TYPE zs_order_result
RAISING
zcx_order_error.
ENDCLASS.
CLASS zcl_order_service IMPLEMENTATION.
METHOD create_order.
DATA(lo_feature_flags) = zcl_feature_flag_service=>get_instance( ).
" Vérifier la nouvelle fonctionnalité de validation
IF lo_feature_flags->zif_feature_flag_service~is_enabled( 'ORDER_EXTENDED_VALIDATION' ).
" Nouvelle validation étendue
validate_order_extended( is_order ).
ELSE.
" Validation actuelle
validate_order_basic( is_order ).
ENDIF.
" Vérifier la nouvelle fonctionnalité de remise
IF lo_feature_flags->zif_feature_flag_service~is_enabled( 'ORDER_AUTO_DISCOUNT' ).
rs_result-discount = calculate_auto_discount( is_order ).
ENDIF.
" Créer la commande
rs_result = create_order_internal( is_order ).
ENDMETHOD.
ENDCLASS.

Pattern Feature Wrapper

Pour un code plus propre, un pattern wrapper est recommandé :

CLASS zcl_features DEFINITION
PUBLIC
FINAL.
PUBLIC SECTION.
CLASS-METHODS:
is_extended_validation_enabled
RETURNING VALUE(rv_enabled) TYPE abap_bool,
is_auto_discount_enabled
RETURNING VALUE(rv_enabled) TYPE abap_bool,
is_new_pricing_engine_enabled
RETURNING VALUE(rv_enabled) TYPE abap_bool,
is_beta_dashboard_enabled
RETURNING VALUE(rv_enabled) TYPE abap_bool.
PRIVATE SECTION.
CLASS-DATA:
go_service TYPE REF TO zif_feature_flag_service.
CLASS-METHODS:
get_service
RETURNING VALUE(ro_service) TYPE REF TO zif_feature_flag_service.
ENDCLASS.
CLASS zcl_features IMPLEMENTATION.
METHOD get_service.
IF go_service IS NOT BOUND.
go_service = zcl_feature_flag_service=>get_instance( ).
ENDIF.
ro_service = go_service.
ENDMETHOD.
METHOD is_extended_validation_enabled.
rv_enabled = get_service( )->is_enabled( 'ORDER_EXTENDED_VALIDATION' ).
ENDMETHOD.
METHOD is_auto_discount_enabled.
rv_enabled = get_service( )->is_enabled( 'ORDER_AUTO_DISCOUNT' ).
ENDMETHOD.
METHOD is_new_pricing_engine_enabled.
rv_enabled = get_service( )->is_enabled( 'NEW_PRICING_ENGINE' ).
ENDMETHOD.
METHOD is_beta_dashboard_enabled.
rv_enabled = get_service( )->is_enabled( 'BETA_DASHBOARD' ).
ENDMETHOD.
ENDCLASS.

Utilisation dans le code :

METHOD calculate_price.
IF zcl_features=>is_new_pricing_engine_enabled( ).
rv_price = calculate_price_new( is_item ).
ELSE.
rv_price = calculate_price_legacy( is_item ).
ENDIF.
ENDMETHOD.

UI d’administration pour la gestion des Flags

RAP Business Object pour Feature Flags

@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Feature Flags"
define root view entity ZI_FeatureFlag
as select from zfeature_flag
{
key flag_id as FlagId,
flag_name as FlagName,
description as Description,
is_enabled as IsEnabled,
rollout_percent as RolloutPercent,
valid_from as ValidFrom,
valid_to as ValidTo,
created_by as CreatedBy,
created_at as CreatedAt,
changed_by as ChangedBy,
changed_at as ChangedAt
}

Vue Projection avec annotations UI

@AccessControl.authorizationCheck: #NOT_REQUIRED
@EndUserText.label: 'Feature Flags Admin"
@Metadata.allowExtensions: true
@Search.searchable: true
define root view entity ZC_FeatureFlag
provider contract transactional_query
as projection on ZI_FeatureFlag
{
@Search.defaultSearchElement: true
key FlagId,
@Search.defaultSearchElement: true
FlagName,
Description,
@UI.hidden: false
IsEnabled,
RolloutPercent,
ValidFrom,
ValidTo,
@UI.hidden: true
CreatedBy,
@UI.hidden: true
CreatedAt,
@UI.hidden: true
ChangedBy,
@UI.hidden: true
ChangedAt
}

Metadata Extension

@Metadata.layer: #CUSTOMER
annotate view ZC_FeatureFlag with
{
@UI.facet: [
{
id: 'GeneralInfo',
type: #IDENTIFICATION_REFERENCE,
label: 'Feature Flag',
position: 10
},
{
id: 'Validity',
type: #FIELDGROUP_REFERENCE,
targetQualifier: 'Validity',
label: 'Validité',
position: 20
}
]
@UI.lineItem: [{ position: 10 }]
@UI.identification: [{ position: 10 }]
FlagId;
@UI.lineItem: [{ position: 20 }]
@UI.identification: [{ position: 20 }]
FlagName;
@UI.lineItem: [{ position: 30 }]
@UI.identification: [{ position: 30 }]
Description;
@UI.lineItem: [{ position: 40, criticality: 'EnabledCriticality' }]
@UI.identification: [{ position: 40 }]
IsEnabled;
@UI.lineItem: [{ position: 50 }]
@UI.identification: [{ position: 50 }]
@EndUserText.label: 'Rollout %"
RolloutPercent;
@UI.fieldGroup: [{ qualifier: 'Validity', position: 10 }]
ValidFrom;
@UI.fieldGroup: [{ qualifier: 'Validity', position: 20 }]
ValidTo;
}

RAP Behavior avec Actions

managed implementation in class zbp_i_featureflag unique;
strict ( 2 );
define behavior for ZI_FeatureFlag alias FeatureFlag
persistent table zfeature_flag
lock master
authorization master ( global )
{
field ( readonly ) CreatedBy, CreatedAt, ChangedBy, ChangedAt;
field ( mandatory ) FlagId, FlagName;
create;
update;
delete;
action EnableFlag result [1] $self;
action DisableFlag result [1] $self;
action SetRollout parameter zs_rollout_param result [1] $self;
determination SetDefaults on save { create; }
determination SetTimestamps on save { create; update; }
mapping for zfeature_flag
{
FlagId = flag_id;
FlagName = flag_name;
Description = description;
IsEnabled = is_enabled;
RolloutPercent = rollout_percent;
ValidFrom = valid_from;
ValidTo = valid_to;
CreatedBy = created_by;
CreatedAt = created_at;
ChangedBy = changed_by;
ChangedAt = changed_at;
}
}

Implémentation du Behavior

CLASS lhc_featureflag DEFINITION INHERITING FROM cl_abap_behavior_handler.
PRIVATE SECTION.
METHODS:
get_global_authorizations FOR GLOBAL AUTHORIZATION
IMPORTING REQUEST requested_authorizations
FOR FeatureFlag
RESULT result,
enableflag FOR MODIFY
IMPORTING keys FOR ACTION FeatureFlag~EnableFlag
RESULT result,
disableflag FOR MODIFY
IMPORTING keys FOR ACTION FeatureFlag~DisableFlag
RESULT result,
setrollout FOR MODIFY
IMPORTING keys FOR ACTION FeatureFlag~SetRollout
RESULT result,
setdefaults FOR DETERMINE ON SAVE
IMPORTING keys FOR FeatureFlag~SetDefaults,
settimestamps FOR DETERMINE ON SAVE
IMPORTING keys FOR FeatureFlag~SetTimestamps.
ENDCLASS.
CLASS lhc_featureflag IMPLEMENTATION.
METHOD get_global_authorizations.
" Vérifier l'autorisation pour la gestion des Feature Flags
AUTHORITY-CHECK OBJECT 'Z_FF_ADM"
ID 'ACTVT' FIELD '02'.
IF sy-subrc = 0.
result = VALUE #( ( %create = if_abap_behv=>auth-allowed
%update = if_abap_behv=>auth-allowed
%delete = if_abap_behv=>auth-allowed
%action-EnableFlag = if_abap_behv=>auth-allowed
%action-DisableFlag = if_abap_behv=>auth-allowed
%action-SetRollout = if_abap_behv=>auth-allowed ) ).
ELSE.
result = VALUE #( ( %create = if_abap_behv=>auth-unauthorized
%update = if_abap_behv=>auth-unauthorized
%delete = if_abap_behv=>auth-unauthorized
%action-EnableFlag = if_abap_behv=>auth-unauthorized
%action-DisableFlag = if_abap_behv=>auth-unauthorized
%action-SetRollout = if_abap_behv=>auth-unauthorized ) ).
ENDIF.
ENDMETHOD.
METHOD enableflag.
" Activer tous les flags sélectionnés
MODIFY ENTITIES OF zi_featureflag IN LOCAL MODE
ENTITY FeatureFlag
UPDATE FIELDS ( IsEnabled )
WITH VALUE #( FOR key IN keys
( %tky = key-%tky
IsEnabled = abap_true ) )
FAILED failed
REPORTED reported.
" Retourner le résultat
READ ENTITIES OF zi_featureflag IN LOCAL MODE
ENTITY FeatureFlag
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(lt_flags).
result = VALUE #( FOR flag IN lt_flags
( %tky = flag-%tky
%param = flag ) ).
ENDMETHOD.
METHOD disableflag.
MODIFY ENTITIES OF zi_featureflag IN LOCAL MODE
ENTITY FeatureFlag
UPDATE FIELDS ( IsEnabled )
WITH VALUE #( FOR key IN keys
( %tky = key-%tky
IsEnabled = abap_false ) )
FAILED failed
REPORTED reported.
READ ENTITIES OF zi_featureflag IN LOCAL MODE
ENTITY FeatureFlag
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(lt_flags).
result = VALUE #( FOR flag IN lt_flags
( %tky = flag-%tky
%param = flag ) ).
ENDMETHOD.
METHOD setrollout.
" Définir le pourcentage de rollout
MODIFY ENTITIES OF zi_featureflag IN LOCAL MODE
ENTITY FeatureFlag
UPDATE FIELDS ( RolloutPercent )
WITH VALUE #( FOR key IN keys
( %tky = key-%tky
RolloutPercent = key-%param-rollout_percent ) )
FAILED failed
REPORTED reported.
READ ENTITIES OF zi_featureflag IN LOCAL MODE
ENTITY FeatureFlag
ALL FIELDS WITH CORRESPONDING #( keys )
RESULT DATA(lt_flags).
result = VALUE #( FOR flag IN lt_flags
( %tky = flag-%tky
%param = flag ) ).
ENDMETHOD.
METHOD setdefaults.
READ ENTITIES OF zi_featureflag IN LOCAL MODE
ENTITY FeatureFlag
FIELDS ( IsEnabled RolloutPercent )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_flags).
MODIFY ENTITIES OF zi_featureflag IN LOCAL MODE
ENTITY FeatureFlag
UPDATE FIELDS ( IsEnabled RolloutPercent )
WITH VALUE #( FOR flag IN lt_flags
( %tky = flag-%tky
IsEnabled = COND #( WHEN flag-IsEnabled IS INITIAL
THEN abap_false
ELSE flag-IsEnabled )
RolloutPercent = COND #( WHEN flag-RolloutPercent IS INITIAL
THEN 0
ELSE flag-RolloutPercent ) ) )
REPORTED reported.
ENDMETHOD.
METHOD settimestamps.
DATA(lv_timestamp) = cl_abap_context_info=>get_system_time( ).
DATA(lv_user) = cl_abap_context_info=>get_user_technical_name( ).
READ ENTITIES OF zi_featureflag IN LOCAL MODE
ENTITY FeatureFlag
FIELDS ( CreatedAt )
WITH CORRESPONDING #( keys )
RESULT DATA(lt_flags).
MODIFY ENTITIES OF zi_featureflag IN LOCAL MODE
ENTITY FeatureFlag
UPDATE FIELDS ( CreatedBy CreatedAt ChangedBy ChangedAt )
WITH VALUE #( FOR flag IN lt_flags
( %tky = flag-%tky
CreatedBy = COND #( WHEN flag-CreatedAt IS INITIAL
THEN lv_user
ELSE flag-CreatedBy )
CreatedAt = COND #( WHEN flag-CreatedAt IS INITIAL
THEN lv_timestamp
ELSE flag-CreatedAt )
ChangedBy = lv_user
ChangedAt = lv_timestamp ) )
REPORTED reported.
ENDMETHOD.
ENDCLASS.

Intégration SAP Feature Flags Service

Le SAP Feature Flags Service est un service BTP pour la gestion des fonctionnalités d’entreprise.

Souscription au service

┌────────────────────────────────────────────────────────────────┐
│ SAP BTP Cockpit > Services > Instances and Subscriptions │
├────────────────────────────────────────────────────────────────┤
│ │
│ Service: Feature Flags Service │
│ Plan: lite (free) / standard │
│ │
│ Instance Name: feature-flags-instance │
│ │
│ Binding: │
│ - Name: ff-binding │
│ - Type: Service Key │
│ │
└────────────────────────────────────────────────────────────────┘

Communication Arrangement

" Destination pour Feature Flags Service
CLASS zcl_feature_flags_btp DEFINITION
PUBLIC
FINAL.
PUBLIC SECTION.
INTERFACES zif_feature_flag_service.
METHODS:
constructor
IMPORTING
iv_destination TYPE string DEFAULT 'FEATURE_FLAGS_SERVICE'.
PRIVATE SECTION.
DATA:
mv_destination TYPE string,
mo_http_client TYPE REF TO if_web_http_client.
METHODS:
get_http_client
RETURNING VALUE(ro_client) TYPE REF TO if_web_http_client
RAISING cx_web_http_client_error.
ENDCLASS.
CLASS zcl_feature_flags_btp IMPLEMENTATION.
METHOD constructor.
mv_destination = iv_destination.
ENDMETHOD.
METHOD get_http_client.
IF mo_http_client IS NOT BOUND.
DATA(lo_destination) = cl_http_destination_provider=>create_by_cloud_destination(
i_name = mv_destination
i_authn_mode = if_a4c_cp_service=>service_specific
).
mo_http_client = cl_web_http_client_manager=>create_by_http_destination(
lo_destination
).
ENDIF.
ro_client = mo_http_client.
ENDMETHOD.
METHOD zif_feature_flag_service~is_enabled.
TRY.
DATA(lo_client) = get_http_client( ).
DATA(lo_request) = lo_client->get_http_request( ).
lo_request->set_uri_path( |/api/v2/evaluate/{ iv_flag_id }| ).
lo_request->set_header_field(
i_name = 'X-User-Id"
i_value = CONV #( iv_user_id )
).
DATA(lo_response) = lo_client->execute( if_web_http_client=>get ).
IF lo_response->get_status( )-code = 200.
DATA(lv_json) = lo_response->get_text( ).
" Parser le JSON
/ui2/cl_json=>deserialize(
EXPORTING json = lv_json
CHANGING data = rv_enabled
).
ELSE.
rv_enabled = abap_false.
ENDIF.
CATCH cx_web_http_client_error cx_root.
" En cas d'erreur : Fonctionnalité désactivée (Fail-Safe)
rv_enabled = abap_false.
ENDTRY.
ENDMETHOD.
METHOD zif_feature_flag_service~get_all_flags.
TRY.
DATA(lo_client) = get_http_client( ).
DATA(lo_request) = lo_client->get_http_request( ).
lo_request->set_uri_path( '/api/v2/flags' ).
DATA(lo_response) = lo_client->execute( if_web_http_client=>get ).
IF lo_response->get_status( )-code = 200.
DATA(lv_json) = lo_response->get_text( ).
/ui2/cl_json=>deserialize(
EXPORTING json = lv_json
CHANGING data = rt_flags
).
ENDIF.
CATCH cx_web_http_client_error cx_root.
" Liste vide en cas d'erreur
ENDTRY.
ENDMETHOD.
METHOD zif_feature_flag_service~enable_flag.
" Non supporté via l'API BTP - uniquement via Cockpit/Dashboard
RAISE EXCEPTION TYPE zcx_feature_flag
MESSAGE e003(zfeature_flag).
ENDMETHOD.
METHOD zif_feature_flag_service~disable_flag.
RAISE EXCEPTION TYPE zcx_feature_flag
MESSAGE e003(zfeature_flag).
ENDMETHOD.
METHOD zif_feature_flag_service~set_rollout_percentage.
RAISE EXCEPTION TYPE zcx_feature_flag
MESSAGE e003(zfeature_flag).
ENDMETHOD.
ENDCLASS.

Factory pour choix flexible du backend

CLASS zcl_feature_flag_factory DEFINITION
PUBLIC
FINAL.
PUBLIC SECTION.
CLASS-METHODS:
get_service
IMPORTING
iv_use_btp_service TYPE abap_bool DEFAULT abap_false
RETURNING
VALUE(ro_service) TYPE REF TO zif_feature_flag_service.
ENDCLASS.
CLASS zcl_feature_flag_factory IMPLEMENTATION.
METHOD get_service.
IF iv_use_btp_service = abap_true.
ro_service = NEW zcl_feature_flags_btp( ).
ELSE.
ro_service = zcl_feature_flag_service=>get_instance( ).
ENDIF.
ENDMETHOD.
ENDCLASS.

Bonnes pratiques pour les Feature Flags

Gestion du cycle de vie des Flags

┌────────────────────────────────────────────────────────────────┐
│ Cycle de vie des Feature Flags │
├────────────────────────────────────────────────────────────────┤
│ │
│ 1. CREATED │
│ └─ Flag créé, désactivé │
│ └─ Code implémenté avec vérification du Flag │
│ │
│ 2. TESTING │
│ └─ Activé pour les utilisateurs de test │
│ └─ Validation QA │
│ │
│ 3. ROLLOUT │
│ └─ Progressif : 5% → 25% → 50% → 100% │
│ └─ Monitoring des métriques │
│ │
│ 4. RELEASED │
│ └─ 100% activé, fonctionnalité standard │
│ └─ Vérification du Flag reste temporairement │
│ │
│ 5. CLEANUP │
│ └─ Supprimer la vérification du Flag du code │
│ └─ Supprimer le Flag de la base de données │
│ └─ Supprimer l'ancien chemin de code │
│ │
│ ⚠️ IMPORTANT : Ne pas oublier le Cleanup ! │
│ Éviter la dette technique due aux anciens Flags │
│ │
└────────────────────────────────────────────────────────────────┘

Conventions de nommage

PréfixeSignificationExemple
FF_Feature Flag (Standard)FF_NEW_CHECKOUT
EXP_Experiment (Test A/B)EXP_BUTTON_COLOR
OPS_Operations ToggleOPS_CACHE_ENABLED
KILL_Kill SwitchKILL_EXTERNAL_API
BETA_Beta FeatureBETA_DASHBOARD_V2

Documentation par Flag

" Documentation recommandée pour chaque Flag
"
" Flag ID: FF_NEW_PRICING_ENGINE
" Created: 2026-01-15
" Purpose: Nouvel algorithme de calcul de prix avec support ML
" Rollout Plan:
" - 2026-02-01: 5% (Groupe pilote)
" - 2026-02-15: 25%
" - 2026-03-01: 50%
" - 2026-03-15: 100%
" Cleanup Date: 2026-04-30
" Dependencies: None
" Rollback: Safe - ancien algorithme reste disponible

Test des Feature Flags

CLASS zcl_order_service_test DEFINITION
FOR TESTING
RISK LEVEL HARMLESS
DURATION SHORT.
PRIVATE SECTION.
DATA:
mo_cut TYPE REF TO zcl_order_service,
mo_ff_mock TYPE REF TO ltd_feature_flag_mock.
METHODS:
setup,
test_with_feature_enabled FOR TESTING,
test_with_feature_disabled FOR TESTING.
CLASS-DATA:
go_ff_injector TYPE REF TO zif_feature_flag_service.
ENDCLASS.
CLASS ltd_feature_flag_mock DEFINITION FOR TESTING.
PUBLIC SECTION.
INTERFACES zif_feature_flag_service.
DATA:
mt_enabled_flags TYPE STANDARD TABLE OF zfeature_flag-flag_id WITH EMPTY KEY.
ENDCLASS.
CLASS ltd_feature_flag_mock IMPLEMENTATION.
METHOD zif_feature_flag_service~is_enabled.
rv_enabled = xsdbool( line_exists( mt_enabled_flags[ table_line = iv_flag_id ] ) ).
ENDMETHOD.
METHOD zif_feature_flag_service~get_all_flags.
ENDMETHOD.
METHOD zif_feature_flag_service~enable_flag.
ENDMETHOD.
METHOD zif_feature_flag_service~disable_flag.
ENDMETHOD.
METHOD zif_feature_flag_service~set_rollout_percentage.
ENDMETHOD.
ENDCLASS.
CLASS zcl_order_service_test IMPLEMENTATION.
METHOD setup.
mo_ff_mock = NEW ltd_feature_flag_mock( ).
mo_cut = NEW zcl_order_service( io_feature_flags = mo_ff_mock ).
ENDMETHOD.
METHOD test_with_feature_enabled.
" Given: Fonctionnalité activée
mo_ff_mock->mt_enabled_flags = VALUE #( ( 'FF_NEW_PRICING' ) ).
" When
DATA(ls_result) = mo_cut->calculate_price( VALUE #( product_id = 'PROD01' ) ).
" Then: Nouveau prix
cl_abap_unit_assert=>assert_equals(
act = ls_result-calculation_method
exp = 'NEW"
).
ENDMETHOD.
METHOD test_with_feature_disabled.
" Given: Fonctionnalité désactivée
CLEAR mo_ff_mock->mt_enabled_flags.
" When
DATA(ls_result) = mo_cut->calculate_price( VALUE #( product_id = 'PROD01' ) ).
" Then: Prix legacy
cl_abap_unit_assert=>assert_equals(
act = ls_result-calculation_method
exp = 'LEGACY"
).
ENDMETHOD.
ENDCLASS.

Résumé

ThèmeRecommandation
ImplémentationTable personnalisée ou SAP Feature Flags Service
CachingMettre les flags en cache (5-10 min TTL) pour la performance
GranularitéBasé sur utilisateur, rôle ou pourcentage
Fail-SafeDésactiver la fonctionnalité en cas d’erreur
TestingUtiliser des mocks pour les tests unitaires
LifecycleDéfinir une date de cleanup à la création
DocumentationDocumenter owner, purpose, rollout plan
MonitoringSuivre l’utilisation et l’impact des flags

Sujets connexes