Feature Flags en ABAP Cloud: Activacion controlada de funcionalidades

Kategorie
ABAP Cloud
Veröffentlicht
Autor
Johannes

Los Feature Flags (tambien llamados Feature Toggles) permiten activar o desactivar funcionalidades en tiempo de ejecucion sin cambiar codigo. Son esenciales para despliegue continuo, testing A/B y lanzamientos controlados.

Conceptos

TipoDescripcionDuracion
Release ToggleFuncionalidades en desarrolloDias/Semanas
Ops ToggleControl operacionalPermanente
Experiment ToggleA/B TestingDias/Semanas
Permission ToggleFuncionalidades premiumPermanente

Implementacion

1. Tabla de Feature Flags

@EndUserText.label : 'Feature Flags'
@AbapCatalog.enhancement.category : #NOT_EXTENSIBLE
define table zfeature_flags {
key client : abap.clnt not null;
key feature_id : abap.char(40) not null;
description : abap.char(100);
is_enabled : abap_boolean;
enabled_from : abap.dats;
enabled_to : abap.dats;
rollout_percent : abap.int1; // 0-100
created_by : abap.uname;
created_at : timestampl;
changed_by : abap.uname;
changed_at : timestampl;
}

2. Servicio basico de Feature Flags

CLASS zcl_feature_flags DEFINITION
PUBLIC FINAL CREATE PRIVATE.
PUBLIC SECTION.
CLASS-METHODS:
get_instance
RETURNING VALUE(ro_instance) TYPE REF TO zcl_feature_flags.
METHODS:
is_enabled
IMPORTING iv_feature_id TYPE zfeature_id
RETURNING VALUE(rv_enabled) TYPE abap_bool,
is_enabled_for_user
IMPORTING iv_feature_id TYPE zfeature_id
iv_user_id TYPE syuname DEFAULT sy-uname
RETURNING VALUE(rv_enabled) TYPE abap_bool.
PRIVATE SECTION.
CLASS-DATA: go_instance TYPE REF TO zcl_feature_flags.
DATA: mt_cache TYPE HASHED TABLE OF zfeature_flags
WITH UNIQUE KEY feature_id.
METHODS:
constructor,
load_flags,
get_flag
IMPORTING iv_feature_id TYPE zfeature_id
RETURNING VALUE(rs_flag) TYPE zfeature_flags.
ENDCLASS.
CLASS zcl_feature_flags IMPLEMENTATION.
METHOD get_instance.
IF go_instance IS NOT BOUND.
go_instance = NEW zcl_feature_flags( ).
ENDIF.
ro_instance = go_instance.
ENDMETHOD.
METHOD constructor.
load_flags( ).
ENDMETHOD.
METHOD load_flags.
SELECT * FROM zfeature_flags
INTO TABLE @mt_cache.
ENDMETHOD.
METHOD get_flag.
rs_flag = VALUE #( mt_cache[ feature_id = iv_feature_id ]
DEFAULT VALUE #( ) ).
ENDMETHOD.
METHOD is_enabled.
DATA(ls_flag) = get_flag( iv_feature_id ).
" No existe = deshabilitado
IF ls_flag IS INITIAL.
rv_enabled = abap_false.
RETURN.
ENDIF.
" Verificar flag basico
IF ls_flag-is_enabled = abap_false.
rv_enabled = abap_false.
RETURN.
ENDIF.
" Verificar rango de fechas
DATA(lv_today) = sy-datum.
IF ls_flag-enabled_from IS NOT INITIAL
AND lv_today < ls_flag-enabled_from.
rv_enabled = abap_false.
RETURN.
ENDIF.
IF ls_flag-enabled_to IS NOT INITIAL
AND lv_today > ls_flag-enabled_to.
rv_enabled = abap_false.
RETURN.
ENDIF.
rv_enabled = abap_true.
ENDMETHOD.
METHOD is_enabled_for_user.
" Primero verificar flag global
IF is_enabled( iv_feature_id ) = abap_false.
rv_enabled = abap_false.
RETURN.
ENDIF.
DATA(ls_flag) = get_flag( iv_feature_id ).
" Rollout progresivo basado en hash del usuario
IF ls_flag-rollout_percent < 100.
DATA(lv_hash) = get_user_hash( iv_user_id ).
DATA(lv_bucket) = lv_hash MOD 100.
rv_enabled = xsdbool( lv_bucket < ls_flag-rollout_percent ).
ELSE.
rv_enabled = abap_true.
ENDIF.
ENDMETHOD.
ENDCLASS.

3. Uso en codigo de negocio

METHOD process_order.
DATA(lo_flags) = zcl_feature_flags=>get_instance( ).
" Verificar feature flag antes de usar nueva funcionalidad
IF lo_flags->is_enabled( 'NEW_PRICING_ENGINE' ).
" Nuevo algoritmo de precios
calculate_price_v2( ).
ELSE.
" Algoritmo antiguo
calculate_price_v1( ).
ENDIF.
" Feature flag con rollout por usuario
IF lo_flags->is_enabled_for_user( 'ENHANCED_UI' ).
enable_enhanced_ui( ).
ENDIF.
ENDMETHOD.

4. Feature Flags con contexto

" Tabla adicional para contexto
@EndUserText.label : 'Feature Flag Context'
define table zfeature_context {
key client : abap.clnt not null;
key feature_id : abap.char(40) not null;
key context_type : abap.char(20) not null; // USER, ROLE, COMPANY, etc.
key context_value : abap.char(100) not null;
is_enabled : abap_boolean;
}
" Servicio extendido
METHOD is_enabled_with_context.
" Verificar contextos en orden de prioridad
" 1. Usuario especifico
DATA(lv_user_enabled) = check_context(
iv_feature_id = iv_feature_id
iv_context_type = 'USER'
iv_context_value = sy-uname
).
IF lv_user_enabled IS NOT INITIAL.
rv_enabled = lv_user_enabled.
RETURN.
ENDIF.
" 2. Rol del usuario
SELECT role_name FROM agr_users
WHERE uname = @sy-uname
INTO TABLE @DATA(lt_roles).
LOOP AT lt_roles INTO DATA(lv_role).
DATA(lv_role_enabled) = check_context(
iv_feature_id = iv_feature_id
iv_context_type = 'ROLE'
iv_context_value = lv_role
).
IF lv_role_enabled = abap_true.
rv_enabled = abap_true.
RETURN.
ENDIF.
ENDLOOP.
" 3. Flag global
rv_enabled = is_enabled( iv_feature_id ).
ENDMETHOD.

5. API RAP para administracion

" CDS View
@EndUserText.label: 'Feature Flags'
define root view entity ZI_FeatureFlag
as select from zfeature_flags
{
key feature_id as FeatureId,
description as Description,
is_enabled as IsEnabled,
enabled_from as EnabledFrom,
enabled_to as EnabledTo,
rollout_percent as RolloutPercent,
@Semantics.user.createdBy: true
created_by as CreatedBy,
@Semantics.systemDateTime.createdAt: true
created_at as CreatedAt,
@Semantics.user.lastChangedBy: true
changed_by as ChangedBy,
@Semantics.systemDateTime.lastChangedAt: true
changed_at as ChangedAt
}
" Behavior Definition
managed implementation in class zbp_i_featureflag unique;
strict ( 2 );
define behavior for ZI_FeatureFlag alias FeatureFlag
persistent table zfeature_flags
lock master
authorization master ( instance )
{
create;
update;
delete;
field ( readonly ) CreatedBy, CreatedAt, ChangedBy, ChangedAt;
field ( mandatory ) FeatureId, Description;
action enable;
action disable;
action setRollout parameter ZA_RolloutParams;
}

6. Patron: Feature Flag Guard

" Clase guard para encapsular verificacion
CLASS zcl_feature_guard DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
constructor
IMPORTING iv_feature_id TYPE zfeature_id,
when_enabled
IMPORTING io_action TYPE REF TO zif_action
RETURNING VALUE(ro_self) TYPE REF TO zcl_feature_guard,
when_disabled
IMPORTING io_action TYPE REF TO zif_action
RETURNING VALUE(ro_self) TYPE REF TO zcl_feature_guard,
execute.
PRIVATE SECTION.
DATA: mv_feature_id TYPE zfeature_id,
mo_enabled_action TYPE REF TO zif_action,
mo_disabled_action TYPE REF TO zif_action.
ENDCLASS.
CLASS zcl_feature_guard IMPLEMENTATION.
METHOD constructor.
mv_feature_id = iv_feature_id.
ENDMETHOD.
METHOD when_enabled.
mo_enabled_action = io_action.
ro_self = me.
ENDMETHOD.
METHOD when_disabled.
mo_disabled_action = io_action.
ro_self = me.
ENDMETHOD.
METHOD execute.
IF zcl_feature_flags=>get_instance( )->is_enabled( mv_feature_id ).
IF mo_enabled_action IS BOUND.
mo_enabled_action->execute( ).
ENDIF.
ELSE.
IF mo_disabled_action IS BOUND.
mo_disabled_action->execute( ).
ENDIF.
ENDIF.
ENDMETHOD.
ENDCLASS.
" Uso fluido
NEW zcl_feature_guard( 'NEW_CHECKOUT' )
->when_enabled( NEW zcl_new_checkout_action( ) )
->when_disabled( NEW zcl_old_checkout_action( ) )
->execute( ).

7. A/B Testing con Feature Flags

" Tabla para experimentos
@EndUserText.label : 'Feature Experiments'
define table zfeature_experiment {
key client : abap.clnt not null;
key experiment_id : abap.char(40) not null;
key user_id : abap.uname not null;
variant : abap.char(10); // A, B, C...
assigned_at : timestampl;
converted : abap_boolean;
converted_at : timestampl;
}
" Servicio de experimentos
CLASS zcl_ab_testing DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
METHODS:
get_variant
IMPORTING iv_experiment_id TYPE string
RETURNING VALUE(rv_variant) TYPE char10,
record_conversion
IMPORTING iv_experiment_id TYPE string.
ENDCLASS.
CLASS zcl_ab_testing IMPLEMENTATION.
METHOD get_variant.
" Verificar asignacion existente
SELECT SINGLE variant FROM zfeature_experiment
WHERE experiment_id = @iv_experiment_id
AND user_id = @sy-uname
INTO @rv_variant.
IF sy-subrc <> 0.
" Asignar variante aleatoriamente
DATA(lv_random) = cl_abap_random_int=>create(
seed = CONV i( sy-uzeit )
min = 1
max = 2
)->get_next( ).
rv_variant = COND #( WHEN lv_random = 1 THEN 'A' ELSE 'B' ).
" Guardar asignacion
INSERT zfeature_experiment FROM @(
VALUE #(
experiment_id = iv_experiment_id
user_id = sy-uname
variant = rv_variant
assigned_at = utclong_current( )
)
).
ENDIF.
ENDMETHOD.
METHOD record_conversion.
UPDATE zfeature_experiment
SET converted = @abap_true
converted_at = @( utclong_current( ) )
WHERE experiment_id = @iv_experiment_id
AND user_id = @sy-uname.
ENDMETHOD.
ENDCLASS.
" Uso
DATA(lo_ab) = NEW zcl_ab_testing( ).
DATA(lv_variant) = lo_ab->get_variant( 'CHECKOUT_FLOW_2024' ).
CASE lv_variant.
WHEN 'A'.
" Flujo de checkout original
show_original_checkout( ).
WHEN 'B'.
" Flujo de checkout nuevo
show_new_checkout( ).
ENDCASE.
" Si el usuario completa la compra
lo_ab->record_conversion( 'CHECKOUT_FLOW_2024' ).

8. Feature Flags en CDS/RAP

" Extender CDS con logica de feature flag
define view entity ZC_SalesOrder
as projection on ZI_SalesOrder
{
key OrderId,
CustomerName,
OrderDate,
TotalAmount,
// Mostrar campo solo si feature esta habilitado
@UI.hidden: #( FeatureFlagHidden )
NewField,
// Campo calculado para visibilidad
case when ( select single is_enabled
from zfeature_flags
where feature_id = 'SHOW_NEW_FIELD' ) = 'X'
then ' '
else 'X'
end as FeatureFlagHidden
}

9. Cache y rendimiento

CLASS zcl_feature_flags_cached DEFINITION
PUBLIC FINAL CREATE PRIVATE.
PUBLIC SECTION.
CONSTANTS: c_cache_ttl_seconds TYPE i VALUE 300. " 5 minutos
CLASS-METHODS:
get_instance
RETURNING VALUE(ro_instance) TYPE REF TO zcl_feature_flags_cached.
METHODS:
is_enabled
IMPORTING iv_feature_id TYPE zfeature_id
RETURNING VALUE(rv_enabled) TYPE abap_bool,
invalidate_cache.
PRIVATE SECTION.
TYPES: BEGIN OF ty_cache_entry,
feature_id TYPE zfeature_id,
is_enabled TYPE abap_bool,
cached_at TYPE timestampl,
END OF ty_cache_entry.
CLASS-DATA: go_instance TYPE REF TO zcl_feature_flags_cached.
DATA: mt_cache TYPE HASHED TABLE OF ty_cache_entry
WITH UNIQUE KEY feature_id.
METHODS:
is_cache_valid
IMPORTING is_entry TYPE ty_cache_entry
RETURNING VALUE(rv_valid) TYPE abap_bool,
load_from_db
IMPORTING iv_feature_id TYPE zfeature_id
RETURNING VALUE(rv_enabled) TYPE abap_bool.
ENDCLASS.
CLASS zcl_feature_flags_cached IMPLEMENTATION.
METHOD is_enabled.
" Buscar en cache
DATA(ls_entry) = VALUE #( mt_cache[ feature_id = iv_feature_id ]
DEFAULT VALUE #( ) ).
IF ls_entry IS NOT INITIAL AND is_cache_valid( ls_entry ).
rv_enabled = ls_entry-is_enabled.
RETURN.
ENDIF.
" Cargar de BD y cachear
rv_enabled = load_from_db( iv_feature_id ).
ls_entry = VALUE #(
feature_id = iv_feature_id
is_enabled = rv_enabled
cached_at = utclong_current( )
).
INSERT ls_entry INTO TABLE mt_cache.
ENDMETHOD.
METHOD is_cache_valid.
DATA(lv_now) = utclong_current( ).
DATA(lv_age) = cl_abap_tstmp=>subtract(
tstmp1 = lv_now
tstmp2 = is_entry-cached_at
).
rv_valid = xsdbool( lv_age < c_cache_ttl_seconds ).
ENDMETHOD.
METHOD invalidate_cache.
CLEAR mt_cache.
ENDMETHOD.
ENDCLASS.

10. Logging y metricas

METHOD is_enabled.
DATA(lv_start) = utclong_current( ).
" Obtener valor
DATA(lv_enabled) = get_flag_value( iv_feature_id ).
" Log de acceso
INSERT zfeature_log FROM @(
VALUE #(
feature_id = iv_feature_id
user_id = sy-uname
is_enabled = lv_enabled
accessed_at = lv_start
)
).
" Metricas (si esta disponible)
TRY.
DATA(lo_metrics) = cl_abap_metrics=>get_instance( ).
lo_metrics->increment(
iv_metric_name = |feature_flag.access.{ iv_feature_id }|
).
CATCH cx_root.
" Metricas no disponibles
ENDTRY.
rv_enabled = lv_enabled.
ENDMETHOD.

Mejores Practicas

PracticaDescripcion
Nombre descriptivoENABLE_NEW_CHECKOUT no FLAG_123
DocumentarAgregar descripcion del proposito
Fecha de expiracionEstablecer enabled_to para limpieza
Limpieza regularEliminar flags obsoletos
Valores por defectoComportamiento si flag no existe
TestingProbar ambos estados del flag

Resumen

ComponenteProposito
Tabla de flagsAlmacenamiento persistente
Servicio singletonAcceso centralizado
CacheRendimiento
ContextoActivacion granular
LoggingAuditoria y metricas

Notas importantes

  • Los Feature Flags no son configuracion - son temporales.
  • Eliminar flags despues de rollout completo.
  • Usar nombres claros y descriptivos.
  • Implementar fallback para flags no existentes.
  • Considerar rendimiento con caching.
  • Documentar el ciclo de vida de cada flag.
  • En ABAP Cloud: aprovechar RAP para administracion.