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 :
| Aspect | Sans Feature Flags | Avec Feature Flags |
|---|---|---|
| Release | Tout ou rien | Activation progressive |
| Rollback | Nouveau déploiement nécessaire | Immédiat via Flag |
| A/B Testing | Complexe | Facilement réalisable |
| Fonctionnalités Beta | Branche séparée | En production, mais cachée |
| Risque | Élevé pour les gros releases | Contrôlé et minimal |
| Feedback | Après release complet | Tô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 : #RESTRICTEDdefine 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 : #RESTRICTEDdefine 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 : #RESTRICTEDdefine 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: truedefine 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: #CUSTOMERannotate 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 FeatureFlagpersistent table zfeature_flaglock masterauthorization 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 ServiceCLASS 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éfixe | Signification | Exemple |
|---|---|---|
FF_ | Feature Flag (Standard) | FF_NEW_CHECKOUT |
EXP_ | Experiment (Test A/B) | EXP_BUTTON_COLOR |
OPS_ | Operations Toggle | OPS_CACHE_ENABLED |
KILL_ | Kill Switch | KILL_EXTERNAL_API |
BETA_ | Beta Feature | BETA_DASHBOARD_V2 |
Documentation par Flag
" Documentation recommandée pour chaque Flag"" Flag ID: FF_NEW_PRICING_ENGINE" Owner: [email protected]" 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 disponibleTest 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ème | Recommandation |
|---|---|
| Implémentation | Table personnalisée ou SAP Feature Flags Service |
| Caching | Mettre les flags en cache (5-10 min TTL) pour la performance |
| Granularité | Basé sur utilisateur, rôle ou pourcentage |
| Fail-Safe | Désactiver la fonctionnalité en cas d’erreur |
| Testing | Utiliser des mocks pour les tests unitaires |
| Lifecycle | Définir une date de cleanup à la création |
| Documentation | Documenter owner, purpose, rollout plan |
| Monitoring | Suivre l’utilisation et l’impact des flags |
Sujets connexes
- CI/CD avec ABAP Cloud - Intégration pipeline
- RAP Basics - Développement RAP
- SAP Destination Service - Connecter des services externes